getaura 0.1.5 → 0.1.7

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 existsSync16, rmSync as rmSync6 } from "fs";
13
+ import path18 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, path21) {
994
- if (!path21)
1012
+ function getElementAtPath(obj, path25) {
1013
+ if (!path25)
995
1014
  return obj;
996
- return path21.reduce((acc, key) => acc?.[key], obj);
1015
+ return path25.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(path21, issues) {
1355
+ function prefixIssues(path25, issues) {
1337
1356
  return issues.map((iss) => {
1338
1357
  var _a3;
1339
1358
  (_a3 = iss).path ?? (_a3.path = []);
1340
- iss.path.unshift(path21);
1359
+ iss.path.unshift(path25);
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, path21 = []) => {
1809
+ const processError = (error63, path25 = []) => {
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 }, [...path21, ...issue2.path]));
1812
+ issue2.errors.map((issues) => processError({ issues }, [...path25, ...issue2.path]));
1794
1813
  } else if (issue2.code === "invalid_key") {
1795
- processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
1814
+ processError({ issues: issue2.issues }, [...path25, ...issue2.path]);
1796
1815
  } else if (issue2.code === "invalid_element") {
1797
- processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
1816
+ processError({ issues: issue2.issues }, [...path25, ...issue2.path]);
1798
1817
  } else {
1799
- const fullpath = [...path21, ...issue2.path];
1818
+ const fullpath = [...path25, ...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, path21 = []) => {
1857
+ const processError = (error63, path25 = []) => {
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 }, [...path21, ...issue2.path]));
1861
+ issue2.errors.map((issues) => processError({ issues }, [...path25, ...issue2.path]));
1843
1862
  } else if (issue2.code === "invalid_key") {
1844
- processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
1863
+ processError({ issues: issue2.issues }, [...path25, ...issue2.path]);
1845
1864
  } else if (issue2.code === "invalid_element") {
1846
- processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
1865
+ processError({ issues: issue2.issues }, [...path25, ...issue2.path]);
1847
1866
  } else {
1848
- const fullpath = [...path21, ...issue2.path];
1867
+ const fullpath = [...path25, ...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 path21 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1888
- for (const seg of path21) {
1906
+ const path25 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1907
+ for (const seg of path25) {
1889
1908
  if (typeof seg === "number")
1890
1909
  segs.push(`[${seg}]`);
1891
1910
  else if (typeof seg === "symbol")
@@ -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 path21 = ref.slice(1).split("/").filter(Boolean);
18991
- if (path21.length === 0) {
19009
+ const path25 = ref.slice(1).split("/").filter(Boolean);
19010
+ if (path25.length === 0) {
18992
19011
  return ctx.rootSchema;
18993
19012
  }
18994
19013
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
18995
- if (path21[0] === defsKey) {
18996
- const key = path21[1] === void 0 ? void 0 : decodeJSONPointerSegment(path21[1]);
19014
+ if (path25[0] === defsKey) {
19015
+ const key = path25[1] === void 0 ? void 0 : decodeJSONPointerSegment(path25[1]);
18997
19016
  if (!key || !ctx.defs[key]) {
18998
19017
  throw new Error(`Reference not found: ${ref}`);
18999
19018
  }
@@ -19939,14 +19958,58 @@ 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
+ "merged",
19979
+ "closed",
19980
+ "resolved",
19981
+ "not_resolved"
19982
+ ];
19942
19983
  var TelemetryRequest = external_exports.object({
19943
- event: external_exports.enum(["scan", "action"]),
19984
+ event: external_exports.enum(["scan", "action", "setup", "fix_result", "error"]),
19944
19985
  auraVersion: external_exports.string().max(40),
19945
19986
  /** Anonymous, stable per-repo id (hash of the first commit), never the repo name. */
19946
19987
  repoId: external_exports.string().max(80),
19988
+ /** Who ran Aura: a person at a terminal, a coding agent or CI. */
19989
+ source: external_exports.enum(["terminal", "agent", "ci"]).optional(),
19990
+ /** The coding agent that ran this command, when it could be told. */
19991
+ agent: external_exports.enum(["claude", "codex", "cursor", "pi"]).optional(),
19992
+ /** The coding agents set up in this repo. */
19993
+ agents: external_exports.array(external_exports.enum(["claude", "codex", "cursor", "pi"])).max(4).optional(),
19994
+ os: external_exports.enum(["darwin", "win32", "linux", "other"]).optional(),
19995
+ nodeVersion: external_exports.string().regex(/^v?\d+\.\d+\.\d+$/).optional(),
19947
19996
  score: external_exports.number().optional(),
19948
19997
  categories: external_exports.record(external_exports.string(), external_exports.number().nullable()).optional(),
19949
- actionId: external_exports.string().max(80).optional()
19998
+ /** Findings by severity, and the ids of checks that didn't fully pass (never file names). */
19999
+ findings: external_exports.record(severity, external_exports.number().int().nonnegative()).optional(),
20000
+ failingChecks: external_exports.array(external_exports.string().regex(/^[a-z-]+\.[a-z-]+$/)).max(40).optional(),
20001
+ actionId: external_exports.string().regex(/^[a-z0-9-]{1,80}$/).optional(),
20002
+ step: external_exports.enum(SETUP_STEPS).optional(),
20003
+ outcome: external_exports.enum(FIX_OUTCOMES).optional(),
20004
+ delivery: external_exports.enum(["pr", "agent-fix", "agent-task", "guide", "checklist"]).optional(),
20005
+ /** Whether the user picked it from the next-steps checklist or ran the command. */
20006
+ via: external_exports.enum(["checklist", "command"]).optional(),
20007
+ durationMs: external_exports.number().int().nonnegative().max(864e5).optional(),
20008
+ agentSteps: external_exports.number().int().nonnegative().max(1e3).optional(),
20009
+ scoreChange: external_exports.number().optional(),
20010
+ command: external_exports.string().regex(/^[a-z-]{1,30}$/).optional(),
20011
+ /** The kind of error (its class or code), never its message, which can contain paths. */
20012
+ errorType: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,59}$/).optional()
19950
20013
  });
19951
20014
  var MeResponse = external_exports.object({
19952
20015
  name: external_exports.string(),
@@ -19969,7 +20032,6 @@ var ConnectStartResponse = external_exports.object({
19969
20032
  var ConnectPollRequest = external_exports.object({ deviceSecret: external_exports.string().min(20).max(200) });
19970
20033
  var ConnectPollResponse = external_exports.discriminatedUnion("status", [
19971
20034
  external_exports.object({ status: external_exports.literal("pending") }),
19972
- external_exports.object({ status: external_exports.literal("skipped") }),
19973
20035
  external_exports.object({ status: external_exports.literal("expired") }),
19974
20036
  /** `encryptedKey` is base64 RSA-OAEP (SHA-256) ciphertext of the pilot key. */
19975
20037
  external_exports.object({ status: external_exports.literal("complete"), encryptedKey: external_exports.string(), name: external_exports.string() })
@@ -20036,15 +20098,17 @@ function quoteForCmd(arg) {
20036
20098
  return /^[\w@%+=:,./\\-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '\\"')}"`;
20037
20099
  }
20038
20100
  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 };
20101
+ const isPath = cmd.includes("/") || cmd.includes("\\");
20102
+ const script = /\.(?:cmd|bat)$/i.test(cmd);
20103
+ const direct = platform !== "win32" || isPath && !script || WINDOWS_PROGRAMS.has(cmd.toLowerCase());
20104
+ return direct ? { command: cmd, args, shell: false } : { command: quoteForCmd(cmd), args: args.map(quoteForCmd), shell: true };
20041
20105
  }
20042
20106
  function run(cmd, args, opts = {}) {
20043
20107
  return new Promise((resolve) => {
20044
20108
  let child;
20045
20109
  try {
20046
20110
  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 });
20111
+ child = spawn(plan.command, plan.args, { cwd: opts.cwd, env: opts.env ?? process.env, stdio: ["pipe", "pipe", "pipe"], shell: plan.shell });
20048
20112
  } catch (err) {
20049
20113
  resolve({ code: 127, stdout: "", stderr: String(err) });
20050
20114
  return;
@@ -20203,6 +20267,7 @@ var AURA_GITIGNORE = `# Aura local files: they can describe security weaknesses,
20203
20267
  report.md
20204
20268
  scan.json
20205
20269
  history.json
20270
+ fixes.json
20206
20271
  cache/
20207
20272
  tasks/
20208
20273
  `;
@@ -20267,6 +20332,12 @@ var ENV_SERVICES = [
20267
20332
  [/^SLACK_/, "Slack"]
20268
20333
  ];
20269
20334
  var BUILTIN_ENV = /* @__PURE__ */ new Set([
20335
+ "USERPROFILE",
20336
+ "APPDATA",
20337
+ "LOCALAPPDATA",
20338
+ "SYSTEMROOT",
20339
+ "COMSPEC",
20340
+ "PROGRAMFILES",
20270
20341
  "NODE_ENV",
20271
20342
  "CI",
20272
20343
  "PORT",
@@ -20288,7 +20359,7 @@ var BUILTIN_ENV = /* @__PURE__ */ new Set([
20288
20359
  "TMPDIR",
20289
20360
  "HOSTNAME"
20290
20361
  ]);
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;
20362
+ 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
20363
  function serviceForEnv(name) {
20293
20364
  return ENV_SERVICES.find(([re]) => re.test(name))?.[1];
20294
20365
  }
@@ -20320,7 +20391,7 @@ function findEnvRefs(files, read2) {
20320
20391
  if (!text) continue;
20321
20392
  for (const m of text.matchAll(ENV_REF)) {
20322
20393
  const name = m[1] ?? m[2] ?? m[3] ?? m[4];
20323
- if (!name || BUILTIN_ENV.has(name) || inStringLiteral(text, m.index)) continue;
20394
+ if (!name || /[a-z]/.test(name) || BUILTIN_ENV.has(name) || inStringLiteral(text, m.index)) continue;
20324
20395
  if (!map2.has(name)) map2.set(name, /* @__PURE__ */ new Set());
20325
20396
  map2.get(name).add(f);
20326
20397
  }
@@ -20429,7 +20500,7 @@ function pmCommands(pm) {
20429
20500
  // package.json
20430
20501
  var package_default = {
20431
20502
  name: "getaura",
20432
- version: "0.1.5",
20503
+ version: "0.1.7",
20433
20504
  description: "Aura helps you and your coding agent follow best practices while you vibe code.",
20434
20505
  type: "module",
20435
20506
  bin: {
@@ -20438,7 +20509,9 @@ var package_default = {
20438
20509
  files: [
20439
20510
  "dist",
20440
20511
  "templates",
20441
- "README.md"
20512
+ "README.md",
20513
+ ".claude-plugin",
20514
+ "plugin"
20442
20515
  ],
20443
20516
  engines: {
20444
20517
  node: ">=22"
@@ -20553,7 +20626,7 @@ function templateVars(root, scan, pkg, extra = {}) {
20553
20626
  scriptLine(pkg, cmds.run, "lint", "lint the code"),
20554
20627
  scriptLine(pkg, cmds.run, "typecheck", "check types") ?? (scan.stack.typescript ? `- \`${cmds.exec} tsc --noEmit\`: check types` : null),
20555
20628
  scriptLine(pkg, cmds.run, "build", "production build"),
20556
- "- `aura scan`: check the Aura score"
20629
+ "- `aura scan`: check the Aura score (coding agents: `aura scan --json`, which never waits for input)"
20557
20630
  ].filter(Boolean);
20558
20631
  const nodeMajor = pkg?.engines?.node?.match(/(\d{2})/)?.[1];
20559
20632
  return {
@@ -20826,8 +20899,8 @@ function nextSteps(scan, opts = {}) {
20826
20899
  }
20827
20900
 
20828
20901
  // src/actions/generators.ts
20829
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
20830
- import path8 from "path";
20902
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
20903
+ import path9 from "path";
20831
20904
 
20832
20905
  // src/checks/agent-setup.ts
20833
20906
  import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
@@ -20883,16 +20956,13 @@ function skillsIn(ctx, dir) {
20883
20956
  }
20884
20957
  function targetAgents(ctx) {
20885
20958
  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;
20959
+ return detectAgentsFrom((rel) => exists(ctx, rel));
20892
20960
  }
20893
20961
  function rulesFileFor(ctx, agent) {
20894
20962
  const preferred = AGENT_LAYOUT[agent].rulesFile;
20895
20963
  if (exists(ctx, preferred)) return preferred;
20964
+ if (agent === "claude" && exists(ctx, ".claude/CLAUDE.md")) return ".claude/CLAUDE.md";
20965
+ if (agent === "pi" && exists(ctx, "CLAUDE.md")) return "CLAUDE.md";
20896
20966
  if (agent === "cursor") {
20897
20967
  if (exists(ctx, ".cursorrules")) return ".cursorrules";
20898
20968
  try {
@@ -20973,7 +21043,7 @@ var skills = {
20973
21043
  const agents = targetAgents(ctx);
20974
21044
  const dirs = agents.length ? uniqueSkillDirs(agents) : ALL_SKILLS_DIRS;
20975
21045
  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") }));
21046
+ const perDir = dirs.map((d) => ({ dir: d, skills: skillsIn(ctx, d).filter((s) => s !== "aura" && !s.startsWith("aura-")) }));
20977
21047
  const best = Math.max(0, ...perDir.map((p2) => p2.skills.length));
20978
21048
  const weakest = agents.length ? Math.min(...perDir.map((p2) => p2.skills.length)) : best;
20979
21049
  if (weakest >= 3) return { status: "pass", summary: `${weakest}+ skills installed.` };
@@ -21023,13 +21093,13 @@ var brief = {
21023
21093
  weight: 5,
21024
21094
  run(ctx) {
21025
21095
  const file2 = BRIEF_PATHS.find((f) => exists(ctx, f));
21026
- const words = file2 ? readDisk(ctx, file2).split(/\s+/).filter(Boolean).length : 0;
21096
+ const words2 = file2 ? readDisk(ctx, file2).split(/\s+/).filter(Boolean).length : 0;
21027
21097
  const committed = file2 && ctx.tracked ? ctx.tracked.has(file2) : true;
21028
- if (file2 && words >= 80 && committed) return { status: "pass", summary: `${file2} (${words} words).` };
21098
+ if (file2 && words2 >= 80 && committed) return { status: "pass", summary: `${file2} (${words2} words).` };
21029
21099
  const finding = makeFinding(this, {
21030
21100
  severity: "medium",
21031
21101
  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.`,
21102
+ 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
21103
  why: "The brief grounds every agent session in what you\u2019re actually building, and becomes page one of any handoff to engineers.",
21034
21104
  locations: file2 ? [{ file: file2 }] : [],
21035
21105
  actionId: "add-brief",
@@ -21128,12 +21198,64 @@ function unreachableFiles(files, read2, pkgScripts) {
21128
21198
  return source.filter((f) => !seen.has(f));
21129
21199
  }
21130
21200
 
21131
- // src/actions/agent-files.ts
21132
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
21201
+ // src/actions/agent-commands.ts
21202
+ import { readFileSync as readFileSync5 } from "fs";
21133
21203
  import path7 from "path";
21204
+ var AURA_COMMANDS = ["scan", "next", "fix", "setup"];
21205
+ function invocation(agent, command) {
21206
+ return agent === "claude" ? `/aura:${command}` : agent === "codex" ? `$aura-${command}` : `/aura-${command}`;
21207
+ }
21208
+ function pluginCommand(command) {
21209
+ const text = readFileSync5(path7.join(path7.dirname(templatesDir()), "plugin", "skills", command, "SKILL.md"), "utf8").replace(/\r\n/g, "\n");
21210
+ const [, frontmatter = "", body = ""] = text.match(/^---\n([\s\S]*?)\n---\n+([\s\S]*)$/) ?? [];
21211
+ const description = frontmatter.match(/^description: (.+)$/m)?.[1] ?? "";
21212
+ return { description, body };
21213
+ }
21214
+ function adapt(agent, body) {
21215
+ const args = agent === "pi" ? "Fix: $@\n\n" : "Fix the action or finding the user named after the command, if any.\n\n";
21216
+ 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}`);
21217
+ }
21218
+ function commandChanges(agents) {
21219
+ const changes = [];
21220
+ for (const agent of new Set(agents)) {
21221
+ for (const command of AURA_COMMANDS) {
21222
+ const { description, body } = pluginCommand(command);
21223
+ const text = adapt(agent, body);
21224
+ if (agent === "cursor") changes.push({ path: `.cursor/commands/aura-${command}.md`, content: `# Aura: ${description}
21225
+
21226
+ ${text}` });
21227
+ if (agent === "pi") changes.push({ path: `.pi/prompts/aura-${command}.md`, content: `---
21228
+ description: ${description}
21229
+ ---
21230
+
21231
+ ${text}` });
21232
+ if (agent === "codex") {
21233
+ const dir = `.agents/skills/aura-${command}`;
21234
+ changes.push({
21235
+ path: `${dir}/SKILL.md`,
21236
+ content: `---
21237
+ name: aura-${command}
21238
+ description: ${description} Only when the user runs ${invocation(agent, command)}.
21239
+ ---
21240
+
21241
+ ${text}`
21242
+ });
21243
+ changes.push({ path: `${dir}/agents/openai.yaml`, content: "policy:\n allow_implicit_invocation: false\n" });
21244
+ }
21245
+ }
21246
+ }
21247
+ return changes;
21248
+ }
21249
+ function commandList(agent) {
21250
+ return AURA_COMMANDS.map((c) => invocation(agent, c)).join(", ");
21251
+ }
21252
+
21253
+ // src/actions/agent-files.ts
21254
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
21255
+ import path8 from "path";
21134
21256
  function current(root, file2) {
21135
- const full = path7.join(root, file2);
21136
- return existsSync6(full) ? readFileSync5(full, "utf8") : null;
21257
+ const full = path8.join(root, file2);
21258
+ return existsSync6(full) ? readFileSync6(full, "utf8") : null;
21137
21259
  }
21138
21260
  function upsertBlock(existing, body) {
21139
21261
  const block = `${AURA_BLOCK_START}
@@ -21177,8 +21299,8 @@ function skillChanges(agents, names) {
21177
21299
 
21178
21300
  // src/actions/generators.ts
21179
21301
  function read(root, file2) {
21180
- const full = path8.join(root, file2);
21181
- return existsSync7(full) ? readFileSync6(full, "utf8") : null;
21302
+ const full = path9.join(root, file2);
21303
+ return existsSync7(full) ? readFileSync7(full, "utf8") : null;
21182
21304
  }
21183
21305
  function withScripts(root, scripts, overwrite = false) {
21184
21306
  const text = read(root, "package.json");
@@ -21199,7 +21321,7 @@ function findingMeta(scan, checkId, key) {
21199
21321
  return scan.checks.find((c) => c.checkId === checkId)?.findings[0]?.meta?.[key];
21200
21322
  }
21201
21323
  var addBrief = ({ root }) => {
21202
- const brief2 = BRIEF_PATHS.find((f) => existsSync7(path8.join(root, f)));
21324
+ const brief2 = BRIEF_PATHS.find((f) => existsSync7(path9.join(root, f)));
21203
21325
  if (!brief2) return { skip: "There is no product brief yet. Run `aura init` to draft and confirm it first." };
21204
21326
  const changes = [
21205
21327
  { path: brief2, content: read(root, brief2) },
@@ -21214,22 +21336,24 @@ var addAgentRules = ({ root, agents, vars }) => {
21214
21336
  };
21215
21337
  var installSkills = ({ agents, skills: skills2 }) => {
21216
21338
  const names = [.../* @__PURE__ */ new Set([...skills2 ?? STARTER_SKILLS.map((s) => s.name), "aura"])];
21217
- const changes = skillChanges(agents, names);
21218
21339
  return {
21219
- changes,
21220
- details: names.map((n) => `- \`${n}\`: ${STARTER_SKILLS.find((s) => s.name === n)?.description ?? "check the Aura score and fix findings."}`)
21340
+ changes: [...skillChanges(agents, names), ...commandChanges(agents)],
21341
+ details: [
21342
+ ...names.map((n) => `- \`${n}\`: ${STARTER_SKILLS.find((s) => s.name === n)?.description ?? "check the Aura score and fix findings."}`),
21343
+ ...agents.filter((a) => a !== "claude").map((a) => `- Aura's commands for ${AGENT_LABELS[a]}: ${commandList(a)}`)
21344
+ ]
21221
21345
  };
21222
21346
  };
21223
21347
  var tokenEfficiency = (g) => {
21224
21348
  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) });
21349
+ if (!existsSync7(path9.join(g.root, "docs/index.md"))) changes.push({ path: "docs/index.md", content: render("docs/index.md", g.vars) });
21226
21350
  changes.push(...rulesChanges(g.root, g.agents, g.vars));
21227
21351
  return { changes, details: ["- `docs/index.md`: a map of the repo your agent reads first", "- Lean-context rules in the agent rules file"] };
21228
21352
  };
21229
21353
  var addCi = ({ root, ctx, vars }) => {
21230
21354
  const existing = ctx.files.filter((f) => /^\.github\/workflows\//.test(f));
21231
21355
  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.` };
21356
+ if (existsSync7(path9.join(root, file2))) return { skip: `${file2} already exists.` };
21233
21357
  return {
21234
21358
  changes: [{ path: file2, content: render("github/workflows/ci.yml", vars) }],
21235
21359
  details: [`- \`${file2}\`: lint, type check, tests and build on every pull request`]
@@ -21253,7 +21377,7 @@ var setupTesting = ({ root, ctx, vars }) => {
21253
21377
  };
21254
21378
  };
21255
21379
  var addEnvExample = ({ root, scan }) => {
21256
- const file2 = [".env.example", ".env.sample", ".env.template"].find((f) => existsSync7(path8.join(root, f))) ?? ".env.example";
21380
+ const file2 = [".env.example", ".env.sample", ".env.template"].find((f) => existsSync7(path9.join(root, f))) ?? ".env.example";
21257
21381
  const existing = read(root, file2);
21258
21382
  const declared = new Set(
21259
21383
  (existing ?? "").split(/\r?\n/).map((l) => l.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)?.[1]).filter(Boolean)
@@ -21310,7 +21434,7 @@ var addLinting = ({ root, ctx }) => {
21310
21434
  return { changes, devDeps, details };
21311
21435
  };
21312
21436
  var addPreCommit = ({ root, ctx, vars }) => {
21313
- if (existsSync7(path8.join(root, ".husky/pre-commit"))) return { skip: "A husky pre-commit hook already exists." };
21437
+ if (existsSync7(path9.join(root, ".husky/pre-commit"))) return { skip: "A husky pre-commit hook already exists." };
21314
21438
  const changes = [{ path: ".husky/pre-commit", content: render("configs/husky-pre-commit", vars) }];
21315
21439
  const scripts = withScripts(root, { prepare: "husky" }, false);
21316
21440
  if (scripts) changes.push(scripts);
@@ -21325,10 +21449,10 @@ var addReadme = ({ root, ctx, vars }) => {
21325
21449
  const file2 = ctx.files.find((f) => /^readme\.mdx?$/i.test(f)) ?? "README.md";
21326
21450
  const current2 = read(root, file2);
21327
21451
  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.` };
21452
+ const words2 = current2?.split(/\s+/).filter(Boolean).length ?? 0;
21453
+ if (current2 && !boilerplate && words2 >= 80) return { skip: `${file2} already describes the app.` };
21330
21454
  let content = render("readme/README.md", vars);
21331
- if (current2 && !boilerplate && words > 0) content = `${content.trimEnd()}
21455
+ if (current2 && !boilerplate && words2 > 0) content = `${content.trimEnd()}
21332
21456
 
21333
21457
  ## Notes
21334
21458
 
@@ -21340,7 +21464,7 @@ ${current2.trim()}
21340
21464
  };
21341
21465
  };
21342
21466
  var enableDependencyUpdates = ({ root, vars }) => {
21343
- if ([".github/dependabot.yml", ".github/dependabot.yaml", "renovate.json", ".github/renovate.json"].some((f) => existsSync7(path8.join(root, f)))) {
21467
+ if ([".github/dependabot.yml", ".github/dependabot.yaml", "renovate.json", ".github/renovate.json"].some((f) => existsSync7(path9.join(root, f)))) {
21344
21468
  return { skip: "Dependabot or Renovate is already configured." };
21345
21469
  }
21346
21470
  return {
@@ -21420,7 +21544,7 @@ function planMoves(root, ctx, moves) {
21420
21544
  const alias = aliasFor(toFile);
21421
21545
  if (alias) return alias[0] + noExt.slice(alias[1].length);
21422
21546
  }
21423
- let rel = path8.posix.relative(path8.posix.dirname(fromFile), noExt);
21547
+ let rel = path9.posix.relative(path9.posix.dirname(fromFile), noExt);
21424
21548
  if (!rel.startsWith(".")) rel = `./${rel}`;
21425
21549
  return rel;
21426
21550
  };
@@ -21454,7 +21578,7 @@ function planMoves(root, ctx, moves) {
21454
21578
  }
21455
21579
  var moveMisplacedFiles = ({ root, ctx, scan }) => {
21456
21580
  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);
21581
+ const moves = raw.map((m) => m.split("=>")).filter(([, to]) => !existsSync7(path9.join(root, to))).slice(0, MAX_MOVES);
21458
21582
  if (moves.length === 0) return { skip: "No files to move (or their targets already exist)." };
21459
21583
  return { changes: planMoves(root, ctx, moves), details: moves.map(([from, to]) => `- \`${from}\` \u2192 \`${to}\``), verifyTypes: true };
21460
21584
  };
@@ -21545,7 +21669,7 @@ var AuraClient = class _AuraClient {
21545
21669
  get hasKey() {
21546
21670
  return Boolean(this.key);
21547
21671
  }
21548
- async request(method, path21, body, timeoutMs = 6e4) {
21672
+ async request(method, path25, body, timeoutMs = 6e4) {
21549
21673
  if (!this.key) throw new AuraApiError("no_key", "No pilot key");
21550
21674
  let payload;
21551
21675
  if (body !== void 0) {
@@ -21555,7 +21679,7 @@ var AuraClient = class _AuraClient {
21555
21679
  }
21556
21680
  let res;
21557
21681
  try {
21558
- res = await fetch(`${this.baseUrl}/v1/${path21}`, {
21682
+ res = await fetch(`${this.baseUrl}/v1/${path25}`, {
21559
21683
  method,
21560
21684
  headers: { authorization: `Bearer ${this.key}`, "content-type": "application/json", "user-agent": `getaura/${AURA_VERSION}` },
21561
21685
  body: payload,
@@ -21580,22 +21704,162 @@ var AuraClient = class _AuraClient {
21580
21704
 
21581
21705
  // src/api/telemetry.ts
21582
21706
  import { createHash as createHash2 } from "crypto";
21707
+
21708
+ // src/ui.ts
21709
+ import * as p from "@clack/prompts";
21710
+ import pc from "picocolors";
21711
+ function interactive() {
21712
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && !inCI() && !runningAgent());
21713
+ }
21714
+ var UserCancelled = class extends Error {
21715
+ };
21716
+ function unwrap(value) {
21717
+ if (p.isCancel(value)) {
21718
+ p.cancel("Cancelled.");
21719
+ throw new UserCancelled();
21720
+ }
21721
+ return value;
21722
+ }
21723
+ function spin(enabled) {
21724
+ if (!enabled || !process.stdout.isTTY) {
21725
+ return { start: (_m) => {
21726
+ }, message: (_m) => {
21727
+ }, stop: (_m) => {
21728
+ }, error: (_m) => {
21729
+ } };
21730
+ }
21731
+ const s = p.spinner();
21732
+ return { start: (m) => s.start(m), message: (m) => s.message(m), stop: (m) => s.stop(m), error: (m) => s.error(m) };
21733
+ }
21734
+ function printJson(value) {
21735
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
21736
+ }
21737
+ var say = {
21738
+ info: (m) => console.log(m),
21739
+ ok: (m) => console.log(`${pc.green("\u2714")} ${m}`),
21740
+ warn: (m) => console.error(`${pc.yellow("!")} ${m}`),
21741
+ error: (m) => console.error(`${pc.red("\u2716")} ${m}`),
21742
+ dim: (m) => console.log(pc.dim(m))
21743
+ };
21744
+
21745
+ // src/fix-tracking.ts
21746
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
21747
+ var FILE = "fixes.json";
21748
+ var MAX_AGE_MS = 60 * 24 * 60 * 60 * 1e3;
21749
+ var MAX_CHECKS_PER_SCAN = 5;
21750
+ function loadTrackedFixes(root) {
21751
+ try {
21752
+ return existsSync8(auraPath(root, FILE)) ? JSON.parse(readFileSync8(auraPath(root, FILE), "utf8")) : [];
21753
+ } catch {
21754
+ return [];
21755
+ }
21756
+ }
21757
+ function save(root, fixes) {
21758
+ writeAuraFile(root, FILE, JSON.stringify(fixes, null, 2));
21759
+ }
21760
+ function trackFix(root, config2, fix) {
21761
+ if (!sharesUsage(config2)) return;
21762
+ save(root, [...loadTrackedFixes(root).filter((f) => f.branch !== fix.branch), { ...fix, openedAt: (/* @__PURE__ */ new Date()).toISOString() }]);
21763
+ }
21764
+ async function prState(root, branch) {
21765
+ const r = await run("gh", ["pr", "view", branch, "--json", "state,mergeCommit", "--jq", '[.state, .mergeCommit.oid // ""] | join(" ")'], {
21766
+ cwd: root,
21767
+ timeoutMs: 15e3
21768
+ });
21769
+ const [state, sha] = r.stdout.trim().split(" ");
21770
+ if (r.code !== 0) return null;
21771
+ if (state === "OPEN" || state === "CLOSED") return { state };
21772
+ if (state !== "MERGED") return null;
21773
+ const inCheckout = Boolean(sha) && (await run("git", ["merge-base", "--is-ancestor", sha, "HEAD"], { cwd: root })).code === 0;
21774
+ return { state, inCheckout };
21775
+ }
21776
+ async function followUpFixes(root, config2, scan, state = prState) {
21777
+ const tracked = loadTrackedFixes(root);
21778
+ if (tracked.length === 0 || !sharesUsage(config2)) return;
21779
+ const current2 = new Set(scan.checks.flatMap((c) => c.findings.map((f) => f.id)));
21780
+ const keep = [];
21781
+ for (const [i, fix] of tracked.entries()) {
21782
+ if (Date.now() - Date.parse(fix.openedAt) > MAX_AGE_MS) continue;
21783
+ const s = i < MAX_CHECKS_PER_SCAN ? await state(root, fix.branch) : null;
21784
+ if (s?.state === "MERGED") {
21785
+ if (!fix.merged) await recordUsage(root, config2, { event: "fix_result", actionId: fix.actionId, outcome: "merged" });
21786
+ if (!s.inCheckout) {
21787
+ keep.push({ ...fix, merged: true });
21788
+ continue;
21789
+ }
21790
+ const stillThere = fix.findingIds.some((id) => current2.has(id));
21791
+ await recordUsage(root, config2, {
21792
+ event: "fix_result",
21793
+ actionId: fix.actionId,
21794
+ outcome: stillThere ? "not_resolved" : "resolved",
21795
+ scoreChange: scan.score - fix.scoreBefore
21796
+ });
21797
+ } else if (s?.state === "CLOSED") {
21798
+ await recordUsage(root, config2, { event: "fix_result", actionId: fix.actionId, outcome: "closed" });
21799
+ } else keep.push(fix);
21800
+ }
21801
+ save(root, keep);
21802
+ }
21803
+
21804
+ // src/api/telemetry.ts
21805
+ function sharesUsage(config2, key = getApiKey()) {
21806
+ if (!key) return false;
21807
+ if (config2.shareUsage !== void 0) return config2.shareUsage;
21808
+ return key.startsWith("aura_live_") || config2.telemetry === true;
21809
+ }
21810
+ function runSource() {
21811
+ return inCI() ? "ci" : interactive() ? "terminal" : "agent";
21812
+ }
21813
+ function osName() {
21814
+ return process.platform === "darwin" || process.platform === "win32" || process.platform === "linux" ? process.platform : "other";
21815
+ }
21583
21816
  async function repoId(root) {
21584
21817
  const r = await run("git", ["rev-list", "--max-parents=0", "HEAD"], { cwd: root });
21585
21818
  const seed = r.code === 0 ? r.stdout.trim().split("\n")[0] : root;
21586
21819
  return createHash2("sha256").update(seed).digest("hex").slice(0, 24);
21587
21820
  }
21588
- async function sendTelemetry(root, client, event) {
21589
- if (!client.hasKey) return;
21821
+ async function recordUsage(root, config2, event) {
21822
+ if (!sharesUsage(config2)) return;
21590
21823
  try {
21591
- await client.request("POST", "telemetry", { ...event, auraVersion: AURA_VERSION, repoId: await repoId(root) }, 5e3);
21824
+ const body = {
21825
+ ...event,
21826
+ auraVersion: AURA_VERSION,
21827
+ repoId: await repoId(root),
21828
+ source: runSource(),
21829
+ agent: runningAgent() ?? void 0,
21830
+ agents: config2.agents.length ? config2.agents : void 0,
21831
+ os: osName(),
21832
+ nodeVersion: process.version
21833
+ };
21834
+ await AuraClient.from(config2).request("POST", "telemetry", body, 5e3);
21592
21835
  } catch {
21593
21836
  }
21594
21837
  }
21838
+ function errorKind(err) {
21839
+ const e = err;
21840
+ const kind = typeof e?.code === "string" ? e.code : typeof e?.name === "string" ? e.name : "Error";
21841
+ return /^[A-Za-z_][A-Za-z0-9_]{0,59}$/.test(kind) ? kind : "Error";
21842
+ }
21843
+ async function recordAction(root, config2, actionId, report, startedAt, via) {
21844
+ if (!report.outcome) return;
21845
+ await recordUsage(root, config2, {
21846
+ event: "action",
21847
+ actionId,
21848
+ outcome: report.outcome,
21849
+ delivery: report.delivery,
21850
+ via,
21851
+ durationMs: Date.now() - startedAt,
21852
+ agentSteps: report.agentSteps,
21853
+ errorType: report.errorType
21854
+ });
21855
+ if (report.outcome === "pr_opened" && report.branch) {
21856
+ trackFix(root, config2, { actionId, branch: report.branch, findingIds: report.findingIds ?? [], scoreBefore: report.scoreBefore ?? 0 });
21857
+ }
21858
+ }
21595
21859
 
21596
21860
  // src/core/git.ts
21597
21861
  import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
21598
- import path9 from "path";
21862
+ import path10 from "path";
21599
21863
  async function git(root, ...args) {
21600
21864
  return run("git", args, { cwd: root });
21601
21865
  }
@@ -21610,7 +21874,7 @@ async function ghStatus(root) {
21610
21874
  const remote = await git(root, "remote", "get-url", "origin");
21611
21875
  return { installed: true, authed: auth.code === 0, hasRemote: remote.code === 0 && /github\.com/.test(remote.stdout) };
21612
21876
  }
21613
- var AURA_OWNED = /^(?:\.aura\/|\.claude\/skills\/aura\/|\.agents\/skills\/aura\/)/;
21877
+ var AURA_OWNED = /^(?:\.aura\/|\.claude\/skills\/aura\/|\.agents\/skills\/aura(?:-(?:scan|next|fix|setup))?\/|\.cursor\/commands\/aura-[a-z]+\.md$|\.pi\/prompts\/aura-[a-z]+\.md$|\.claude\/settings\.json$)/;
21614
21878
  async function dirtyFiles(root) {
21615
21879
  const r = await git(root, "status", "--porcelain", "-uall");
21616
21880
  return r.stdout.split("\n").filter(Boolean).map((l) => l.slice(3).replace(/^"|"$/g, "").split(" -> ").pop()).filter((f) => !AURA_OWNED.test(f));
@@ -21629,11 +21893,11 @@ async function freeBranchName(root, base) {
21629
21893
  }
21630
21894
  function writeChanges(root, changes) {
21631
21895
  for (const c of changes) {
21632
- const full = path9.join(root, c.path);
21896
+ const full = path10.join(root, c.path);
21633
21897
  if (!isInside(root, full)) throw new Error(`Refusing to write outside the repo: ${c.path}`);
21634
21898
  if (c.content === null) rmSync(full, { force: true });
21635
21899
  else {
21636
- mkdirSync2(path9.dirname(full), { recursive: true });
21900
+ mkdirSync2(path10.dirname(full), { recursive: true });
21637
21901
  writeFileSync2(full, c.content);
21638
21902
  }
21639
21903
  }
@@ -21643,7 +21907,7 @@ async function commitPushPr(root, opts) {
21643
21907
  const base = { branch: opts.branch, url: null };
21644
21908
  if (commit.code !== 0) {
21645
21909
  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;
21910
+ const error62 = /tell me who you are|user\.email/i.test(out) ? 'git doesn\u2019t know your name and email yet. Run these two commands:\n git config --global user.name "Your Name"\n git config --global user.email "you@example.com"' : out;
21647
21911
  return { ...base, committed: false, pushed: false, error: error62 };
21648
21912
  }
21649
21913
  if (!opts.openPr) return { ...base, committed: true, pushed: false };
@@ -21747,13 +22011,13 @@ var readme = {
21747
22011
  run(ctx) {
21748
22012
  const file2 = ctx.files.find((f) => /^readme(\.md|\.mdx|\.txt)?$/i.test(f));
21749
22013
  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).` };
22014
+ const words2 = text.split(/\s+/).filter(Boolean).length;
22015
+ if (file2 && !BOILERPLATE.test(text) && words2 >= 80) return { status: "pass", summary: `${file2} (${words2} words).` };
21752
22016
  const boilerplate = Boolean(file2) && BOILERPLATE.test(text);
21753
22017
  const finding = makeFinding(this, {
21754
22018
  severity: "low",
21755
22019
  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.`,
22020
+ 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
22021
  why: "A good README is the first thing a new engineer or your coding agent reads; without it they guess.",
21758
22022
  locations: file2 ? [{ file: file2 }] : [],
21759
22023
  actionId: "add-readme",
@@ -21856,10 +22120,10 @@ var typecheck = {
21856
22120
  applies: (ctx) => ctx.stack.typescript,
21857
22121
  async run(ctx) {
21858
22122
  if (ctx.options.fast) return { status: "unknown", summary: "Skipped in fast mode." };
21859
- const { existsSync: existsSync17 } = await import("fs");
21860
- const path21 = await import("path");
21861
- const tsc = path21.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." };
22123
+ const { existsSync: existsSync18 } = await import("fs");
22124
+ const path25 = await import("path");
22125
+ const tsc = path25.join(ctx.root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
22126
+ if (!existsSync18(tsc)) return { status: "unknown", summary: "Dependencies not installed, so the type check was skipped." };
21863
22127
  const r = await run(tsc, ["--noEmit", "--pretty", "false"], { cwd: ctx.root, timeoutMs: 18e4 });
21864
22128
  if (r.timedOut) return { status: "unknown", summary: "Type check timed out." };
21865
22129
  if (r.code === 0) return { status: "pass", summary: "No type errors.", engine: "tsc" };
@@ -21985,9 +22249,9 @@ var apiRoutes = {
21985
22249
 
21986
22250
  // src/core/tools.ts
21987
22251
  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";
22252
+ import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync3, mkdtempSync, readdirSync as readdirSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
21989
22253
  import os2 from "os";
21990
- import path10 from "path";
22254
+ import path11 from "path";
21991
22255
  var GITLEAKS_SHA = {
21992
22256
  darwin_arm64: "b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5",
21993
22257
  darwin_x64: "dfe101a4db2255fc85120ac7f3d25e4342c3c20cf749f2c20a18081af1952709",
@@ -22004,13 +22268,16 @@ var OSV_SHA = {
22004
22268
  windows_amd64: "e0ed7644118b717b028c249ee9d3515024e55e8510747ca08906eb96765354d6",
22005
22269
  windows_arm64: "ca1379da0e408279ef2c85cde0846a903495738a91c7b065a208048437c1686c"
22006
22270
  };
22271
+ function tarCommand(platform = process.platform, systemRoot = process.env.SystemRoot) {
22272
+ return platform === "win32" ? path11.win32.join(systemRoot || "C:\\Windows", "System32", "tar.exe") : "tar";
22273
+ }
22007
22274
  var TOOLS = {
22008
22275
  gitleaks: {
22009
22276
  version: "8.30.1",
22010
22277
  asset(platform, arch) {
22011
22278
  const p2 = platform === "win32" ? "windows" : platform;
22012
- const a = arch === "arm64" ? "arm64" : "x64";
22013
- const sha = GITLEAKS_SHA[`${p2}_${a}`];
22279
+ const a = arch === "arm64" ? "arm64" : arch === "x64" ? "x64" : null;
22280
+ const sha = a && GITLEAKS_SHA[`${p2}_${a}`];
22014
22281
  if (!sha) return null;
22015
22282
  const archive = p2 === "windows" ? "zip" : "tar.gz";
22016
22283
  return { file: `gitleaks_8.30.1_${p2}_${a}.${archive}`, sha256: sha, archive };
@@ -22021,8 +22288,8 @@ var TOOLS = {
22021
22288
  version: "2.6.0",
22022
22289
  asset(platform, arch) {
22023
22290
  const p2 = platform === "win32" ? "windows" : platform;
22024
- const a = arch === "arm64" ? "arm64" : "amd64";
22025
- const sha = OSV_SHA[`${p2}_${a}`];
22291
+ const a = arch === "arm64" ? "arm64" : arch === "x64" ? "amd64" : null;
22292
+ const sha = a && OSV_SHA[`${p2}_${a}`];
22026
22293
  if (!sha) return null;
22027
22294
  return { file: `osv-scanner_${p2}_${a}${p2 === "windows" ? ".exe" : ""}`, sha256: sha, archive: null };
22028
22295
  },
@@ -22038,8 +22305,8 @@ function ensureTool(name, noDownload = false) {
22038
22305
  async function resolveTool(name, noDownload) {
22039
22306
  const spec = TOOLS[name];
22040
22307
  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 };
22308
+ const cached2 = path11.join(auraBinDir(), `${name}-${spec.version}`, exe);
22309
+ if (existsSync9(cached2)) return { path: cached2, version: spec.version };
22043
22310
  if (await commandExists(name)) {
22044
22311
  const v = await run(name, ["version"]);
22045
22312
  return { path: name, version: (v.stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? "system").trim() };
@@ -22059,20 +22326,20 @@ async function download(name, spec, target) {
22059
22326
  const buf = Buffer.from(await res.arrayBuffer());
22060
22327
  const digest = createHash3("sha256").update(buf).digest("hex");
22061
22328
  if (digest !== asset.sha256) throw new Error(`Checksum mismatch for ${asset.file}`);
22062
- const dir = path10.dirname(target);
22329
+ const dir = path11.dirname(target);
22063
22330
  mkdirSync3(dir, { recursive: true });
22064
- const tmp = mkdtempSync(path10.join(os2.tmpdir(), `aura-${name}-`));
22331
+ const tmp = mkdtempSync(path11.join(os2.tmpdir(), `aura-${name}-`));
22065
22332
  try {
22066
22333
  if (asset.archive === null) {
22067
22334
  writeFileSync3(target, buf);
22068
22335
  } else {
22069
- const archivePath = path10.join(tmp, asset.file);
22336
+ const archivePath = path11.join(tmp, asset.file);
22070
22337
  writeFileSync3(archivePath, buf);
22071
- const r = await run("tar", ["-xf", archivePath, "-C", tmp]);
22338
+ const r = await run(tarCommand(), ["-xf", archivePath, "-C", tmp]);
22072
22339
  if (r.code !== 0) return null;
22073
22340
  const exe = readdirSync3(tmp).find((f) => f === name || f === `${name}.exe`);
22074
22341
  if (!exe) return null;
22075
- renameSync(path10.join(tmp, exe), target);
22342
+ renameSync(path11.join(tmp, exe), target);
22076
22343
  }
22077
22344
  chmodSync2(target, 493);
22078
22345
  return { path: target, version: spec.version };
@@ -22343,9 +22610,9 @@ var rls = {
22343
22610
  };
22344
22611
 
22345
22612
  // src/checks/security/secrets.ts
22346
- import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
22613
+ import { existsSync as existsSync10, mkdtempSync as mkdtempSync2, readFileSync as readFileSync9, rmSync as rmSync3 } from "fs";
22347
22614
  import os3 from "os";
22348
- import path11 from "path";
22615
+ import path12 from "path";
22349
22616
 
22350
22617
  // src/core/ignore.ts
22351
22618
  function globToRegex(glob) {
@@ -22383,15 +22650,15 @@ async function gitleaksHistory(ctx) {
22383
22650
  if (!ctx.tracked) return null;
22384
22651
  const bin = await ensureTool("gitleaks", ctx.options.noDownload);
22385
22652
  if (!bin) return null;
22386
- const dir = mkdtempSync2(path11.join(os3.tmpdir(), "aura-gitleaks-"));
22387
- const report = path11.join(dir, "report.json");
22653
+ const dir = mkdtempSync2(path12.join(os3.tmpdir(), "aura-gitleaks-"));
22654
+ const report = path12.join(dir, "report.json");
22388
22655
  try {
22389
22656
  const r = await run(bin.path, ["git", "--no-banner", "--redact", "--report-format", "json", "--report-path", report, "--exit-code", "0", "."], {
22390
22657
  cwd: ctx.root,
22391
22658
  timeoutMs: 18e4
22392
22659
  });
22393
- if (r.code !== 0 || !existsSync9(report)) return null;
22394
- return { hits: JSON.parse(readFileSync7(report, "utf8")), engine: `gitleaks ${bin.version}` };
22660
+ if (r.code !== 0 || !existsSync10(report)) return null;
22661
+ return { hits: JSON.parse(readFileSync9(report, "utf8")), engine: `gitleaks ${bin.version}` };
22395
22662
  } catch {
22396
22663
  return null;
22397
22664
  } finally {
@@ -22506,7 +22773,7 @@ var envFiles = {
22506
22773
  var securityChecks = [secrets, envFiles, deps, rls, apiRoutes, headers];
22507
22774
 
22508
22775
  // src/checks/structure.ts
22509
- import path12 from "path";
22776
+ import path13 from "path";
22510
22777
  var GENERATED = /(?:^|\/)(?:supabase\/migrations|generated|__generated__|\.generated)\/|\.(?:d\.ts|min\.js)$|(?:^|\/)(?:database|supabase)\.types\.ts$|(?:^|\/)components\/ui\//;
22511
22778
  function appSource(ctx) {
22512
22779
  return ctx.files.filter((f) => isSourceFile(f) && !GENERATED.test(f));
@@ -22595,7 +22862,7 @@ var duplicates = {
22595
22862
  const findings = pairs.map(
22596
22863
  ([a, b, sim]) => makeFinding(this, {
22597
22864
  severity: "low",
22598
- title: `${path12.basename(a)} and ${path12.basename(b)} are near-duplicates`,
22865
+ title: `${path13.basename(a)} and ${path13.basename(b)} are near-duplicates`,
22599
22866
  detail: `${a} and ${b} are ${Math.round(sim * 100)}% the same.`,
22600
22867
  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
22868
  locations: [{ file: a }, { file: b }],
@@ -22609,7 +22876,7 @@ var duplicates = {
22609
22876
  };
22610
22877
  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
22878
  function suggestTarget(file2, srcDir) {
22612
- const base = path12.posix.basename(file2);
22879
+ const base = path13.posix.basename(file2);
22613
22880
  const prefix = srcDir ? "src/" : "";
22614
22881
  const isComponent = /^[A-Z][A-Za-z0-9]*\.(?:tsx|jsx)$/.test(base);
22615
22882
  if (isComponent) return `${prefix}components/${base}`;
@@ -22721,59 +22988,22 @@ async function runScan(root, options, onProgress) {
22721
22988
  };
22722
22989
  }
22723
22990
 
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
22991
  // src/commands/fix.ts
22762
- import { existsSync as existsSync14, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
22763
- import path16 from "path";
22992
+ import { existsSync as existsSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync6 } from "fs";
22993
+ import path17 from "path";
22764
22994
 
22765
22995
  // 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";
22996
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync10, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
22997
+ import path14 from "path";
22768
22998
  var MAX_OUTPUT = 12e3;
22769
22999
  var MAX_FILE_LINES = 800;
22770
23000
  var FORBIDDEN = /^(?:\.git|node_modules|\.next|\.vercel)(?:\/|$)|(?:^|\/)\.env(?:\.(?!example$|sample$|template$)[^/]+)?$/;
22771
23001
  function safePath(root, rel) {
22772
23002
  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}`);
23003
+ const normalized = path14.posix.normalize(rel.replace(/\\/g, "/").replace(/^\.\//, ""));
23004
+ if (normalized.startsWith("../") || normalized === ".." || path14.isAbsolute(normalized)) throw new Error(`Path is outside the repo: ${rel}`);
22775
23005
  if (FORBIDDEN.test(normalized)) throw new Error(`Aura doesn\u2019t let the agent touch ${normalized}.`);
22776
- const full = path13.join(root, normalized);
23006
+ const full = path14.join(root, normalized);
22777
23007
  if (!isInside(root, full)) throw new Error(`Path is outside the repo: ${rel}`);
22778
23008
  return normalized;
22779
23009
  }
@@ -22794,10 +23024,10 @@ async function runTool(root, call, allowedCommands2) {
22794
23024
  }
22795
23025
  case "read_file": {
22796
23026
  const rel = safePath(root, input2.path);
22797
- const full = path13.join(root, rel);
22798
- if (!existsSync10(full)) return { output: `File not found: ${rel}` };
23027
+ const full = path14.join(root, rel);
23028
+ if (!existsSync11(full)) return { output: `File not found: ${rel}` };
22799
23029
  if (statSync2(full).size > 1e6) return { output: `${rel} is too large to read.` };
22800
- const lines = readFileSync8(full, "utf8").split("\n");
23030
+ const lines = readFileSync10(full, "utf8").split("\n");
22801
23031
  const start = Math.max(1, Number(input2.startLine) || 1);
22802
23032
  const end = Math.min(lines.length, Number(input2.endLine) || start + MAX_FILE_LINES - 1);
22803
23033
  const body = lines.slice(start - 1, end).map((l, i) => `${start + i}: ${l}`).join("\n");
@@ -22814,7 +23044,7 @@ async function runTool(root, call, allowedCommands2) {
22814
23044
  if (FORBIDDEN.test(f) || glob && !glob.test(f)) continue;
22815
23045
  let text;
22816
23046
  try {
22817
- text = readFileSync8(path13.join(root, f), "utf8");
23047
+ text = readFileSync10(path14.join(root, f), "utf8");
22818
23048
  } catch {
22819
23049
  continue;
22820
23050
  }
@@ -22833,14 +23063,14 @@ async function runTool(root, call, allowedCommands2) {
22833
23063
  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
23064
  };
22835
23065
  }
22836
- mkdirSync4(path13.dirname(path13.join(root, rel)), { recursive: true });
22837
- writeFileSync4(path13.join(root, rel), content);
23066
+ mkdirSync4(path14.dirname(path14.join(root, rel)), { recursive: true });
23067
+ writeFileSync4(path14.join(root, rel), content);
22838
23068
  return { output: `Wrote ${rel} (${content.split("\n").length} lines).`, touched: rel };
22839
23069
  }
22840
23070
  case "delete_file": {
22841
23071
  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));
23072
+ if (!existsSync11(path14.join(root, rel))) return { output: `File not found: ${rel}` };
23073
+ rmSync4(path14.join(root, rel));
22844
23074
  return { output: `Deleted ${rel}.`, touched: rel };
22845
23075
  }
22846
23076
  case "run_command": {
@@ -22862,7 +23092,7 @@ ${(r.stdout + "\n" + r.stderr).trim().slice(-6e3)}`;
22862
23092
 
22863
23093
  // src/api/explain.ts
22864
23094
  import { createHash as createHash4 } from "crypto";
22865
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
23095
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
22866
23096
 
22867
23097
  // src/report.ts
22868
23098
  import pc2 from "picocolors";
@@ -23051,7 +23281,7 @@ var CACHE_FILE = "cache/explanations.json";
23051
23281
  var MAX_EXPLAIN = 25;
23052
23282
  function loadCache(root) {
23053
23283
  try {
23054
- return JSON.parse(readFileSync9(auraPath(root, CACHE_FILE), "utf8"));
23284
+ return JSON.parse(readFileSync11(auraPath(root, CACHE_FILE), "utf8"));
23055
23285
  } catch {
23056
23286
  return {};
23057
23287
  }
@@ -23072,8 +23302,8 @@ function excerptFor(root, f, context = 4) {
23072
23302
  }
23073
23303
  function productContext(root) {
23074
23304
  const file2 = auraPath(root, "brief.md");
23075
- if (!existsSync11(file2)) return void 0;
23076
- return redact(readFileSync9(file2, "utf8")).slice(0, 1800);
23305
+ if (!existsSync12(file2)) return void 0;
23306
+ return redact(readFileSync11(file2, "utf8")).slice(0, 1800);
23077
23307
  }
23078
23308
  async function addExplanations(root, scan, client) {
23079
23309
  const cache = loadCache(root);
@@ -23117,19 +23347,19 @@ async function addExplanations(root, scan, client) {
23117
23347
 
23118
23348
  // src/commands/simple.ts
23119
23349
  import { execFileSync } from "child_process";
23120
- import { existsSync as existsSync13, readdirSync as readdirSync4, readFileSync as readFileSync13 } from "fs";
23121
- import path15 from "path";
23350
+ import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync15 } from "fs";
23351
+ import path16 from "path";
23122
23352
  import YAML2 from "yaml";
23123
23353
 
23124
23354
  // src/commands/inventory.ts
23125
- import { readFileSync as readFileSync10 } from "fs";
23355
+ import { readFileSync as readFileSync12 } from "fs";
23126
23356
  function accountState(root, services) {
23127
23357
  const inv = loadInventory(root);
23128
23358
  return { inventoryComplete: services.every((s) => inv.services[s]?.owner) && Boolean(inv.githubOrg), githubSecurity: Boolean(inv.githubSecurity) };
23129
23359
  }
23130
23360
  function loadInventory(root) {
23131
23361
  try {
23132
- return JSON.parse(readFileSync10(auraPath(root, "inventory.json"), "utf8"));
23362
+ return JSON.parse(readFileSync12(auraPath(root, "inventory.json"), "utf8"));
23133
23363
  } catch {
23134
23364
  return { version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), services: {} };
23135
23365
  }
@@ -23216,10 +23446,10 @@ async function inventoryCommand(flags) {
23216
23446
  }
23217
23447
 
23218
23448
  // src/history.ts
23219
- import { readFileSync as readFileSync11 } from "fs";
23449
+ import { readFileSync as readFileSync13 } from "fs";
23220
23450
  function loadHistory(root) {
23221
23451
  try {
23222
- const data = JSON.parse(readFileSync11(auraPath(root, "history.json"), "utf8"));
23452
+ const data = JSON.parse(readFileSync13(auraPath(root, "history.json"), "utf8"));
23223
23453
  return Array.isArray(data) ? data : [];
23224
23454
  } catch {
23225
23455
  return [];
@@ -23267,10 +23497,10 @@ function banner(colors = pc3.isColorSupported) {
23267
23497
  // src/api/connect.ts
23268
23498
  import { spawn as spawn2 } from "child_process";
23269
23499
  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";
23500
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync14, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
23271
23501
  import os4 from "os";
23272
- import path14 from "path";
23273
- var pendingFile = () => path14.join(os4.homedir(), ".aura", "pending-connect.json");
23502
+ import path15 from "path";
23503
+ var pendingFile = () => path15.join(os4.homedir(), ".aura", "pending-connect.json");
23274
23504
  async function postJson(apiUrl, route, body) {
23275
23505
  let res;
23276
23506
  try {
@@ -23289,14 +23519,14 @@ async function postJson(apiUrl, route, body) {
23289
23519
  }
23290
23520
  function loadPending(apiUrl, now) {
23291
23521
  try {
23292
- const p2 = JSON.parse(readFileSync12(pendingFile(), "utf8"));
23522
+ const p2 = JSON.parse(readFileSync14(pendingFile(), "utf8"));
23293
23523
  if (p2.apiUrl === apiUrl && Date.parse(p2.expiresAt) - now > 6e4) return p2;
23294
23524
  } catch {
23295
23525
  }
23296
23526
  return null;
23297
23527
  }
23298
23528
  function clearPendingConnect() {
23299
- if (existsSync12(pendingFile())) rmSync5(pendingFile(), { force: true });
23529
+ if (existsSync13(pendingFile())) rmSync5(pendingFile(), { force: true });
23300
23530
  }
23301
23531
  async function startOrResumeConnect(apiUrl, now = Date.now()) {
23302
23532
  const existing = loadPending(apiUrl, now);
@@ -23308,7 +23538,7 @@ async function startOrResumeConnect(apiUrl, now = Date.now()) {
23308
23538
  });
23309
23539
  const res = ConnectStartResponse.parse(await postJson(apiUrl, "connect", { publicKey }));
23310
23540
  const pending = { apiUrl, privateKey, ...res };
23311
- mkdirSync5(path14.dirname(pendingFile()), { recursive: true });
23541
+ mkdirSync5(path15.dirname(pendingFile()), { recursive: true });
23312
23542
  writeFileSync5(pendingFile(), JSON.stringify(pending, null, 2) + "\n", { mode: 384 });
23313
23543
  return { ...pending, resumed: false };
23314
23544
  }
@@ -23333,7 +23563,7 @@ async function waitForConnect(pending, waitSeconds, sleep = (ms) => new Promise(
23333
23563
  }
23334
23564
  }
23335
23565
  function openInBrowser(url2) {
23336
- if (process.env.AURA_NO_BROWSER || process.env.CI) return;
23566
+ if (process.env.AURA_NO_BROWSER || inCI()) return;
23337
23567
  const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : ["xdg-open", [url2]];
23338
23568
  try {
23339
23569
  const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
@@ -23362,7 +23592,7 @@ ${pc.magenta("\u2726")} ${pc.bold("Connect your Aura pilot key")}`);
23362
23592
  console.log(pc.dim(` Check the page shows the code ${pending.code}. Press Ctrl+C to stop.
23363
23593
  `));
23364
23594
  } else {
23365
- console.log(` Ask the user to open this link and paste their pilot key there (or choose to continue without one):`);
23595
+ console.log(" Ask the user to open this link and paste their pilot key there:");
23366
23596
  console.log(` ${pending.url}`);
23367
23597
  console.log(` Don\u2019t ask for the key in chat. The page sends it to this computer encrypted. Code: ${pending.code}`);
23368
23598
  console.log(` Waiting up to ${Math.round(wait)} seconds\u2026
@@ -23371,7 +23601,7 @@ ${pc.magenta("\u2726")} ${pc.bold("Connect your Aura pilot key")}`);
23371
23601
  const s = spin(opts.person);
23372
23602
  s.start("Waiting for you to finish in the browser");
23373
23603
  const outcome = await waitForConnect(pending, wait);
23374
- s.stop(outcome.status === "complete" ? "Key received" : outcome.status === "skipped" ? "Continuing without a key" : "Stopped waiting");
23604
+ s.stop(outcome.status === "complete" ? "Key received" : "Stopped waiting");
23375
23605
  return outcome;
23376
23606
  } catch (err) {
23377
23607
  return { status: "error", message: friendlyApiError(err) };
@@ -23379,18 +23609,15 @@ ${pc.magenta("\u2726")} ${pc.bold("Connect your Aura pilot key")}`);
23379
23609
  }
23380
23610
  function explainSignIn(outcome) {
23381
23611
  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
23612
  case "pending":
23386
23613
  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;
23614
+ break;
23388
23615
  case "expired":
23389
23616
  say.warn("The sign-in link expired. Run the same command again for a new one.");
23390
- return false;
23617
+ break;
23391
23618
  case "error":
23392
23619
  say.warn(`Couldn\u2019t start sign-in: ${outcome.message}`);
23393
- return false;
23620
+ break;
23394
23621
  }
23395
23622
  }
23396
23623
 
@@ -23430,7 +23657,17 @@ async function pickAndRunSteps(steps, limit = 10) {
23430
23657
  console.log(`
23431
23658
  ${pc.magenta("\u2726")} ${pc.bold(step.title)}`);
23432
23659
  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({});
23660
+ const code = run2.kind === "apply" ? await applyCommand(run2.actionId, { yes: true, via: "checklist" }) : run2.kind === "fix" ? await fixCommand(run2.actionId, run2.withOwnAgent ? { agent: true, via: "checklist" } : { yes: true, via: "checklist" }) : run2.kind === "guide" ? await guideCommand(run2.topic, {}) : await inventoryCommand({});
23661
+ if (run2.kind === "guide" || run2.kind === "inventory") {
23662
+ const root = await findRepoRoot();
23663
+ await recordUsage(root, loadConfig(root), {
23664
+ event: "action",
23665
+ actionId: step.actionId,
23666
+ delivery: run2.kind === "guide" ? "guide" : "checklist",
23667
+ via: "checklist",
23668
+ outcome: "shown"
23669
+ });
23670
+ }
23434
23671
  results.push({ title: step.title, ok: code === 0 });
23435
23672
  }
23436
23673
  if (results.length > 1) {
@@ -23444,9 +23681,9 @@ ${pc.bold(" Done")}`);
23444
23681
  // src/commands/simple.ts
23445
23682
  async function latestScan(root) {
23446
23683
  const file2 = auraPath(root, "scan.json");
23447
- if (existsSync13(file2)) {
23684
+ if (existsSync14(file2)) {
23448
23685
  try {
23449
- const scan = JSON.parse(readFileSync13(file2, "utf8"));
23686
+ const scan = JSON.parse(readFileSync15(file2, "utf8"));
23450
23687
  const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
23451
23688
  const dirty = execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
23452
23689
  if (scan.commit === head && !dirty && Date.now() - Date.parse(scan.scannedAt) < 24 * 36e5) return scan;
@@ -23481,14 +23718,14 @@ async function loginCommand(flags) {
23481
23718
  let key = flags.key ?? process.env.AURA_KEY;
23482
23719
  if (!key && flags.paste && person) key = unwrap(await p.password({ message: "Paste your Aura pilot key", mask: "\u2022" }));
23483
23720
  if (!key) {
23484
- if (process.env.CI) {
23721
+ if (inCI()) {
23485
23722
  say.error("Pass your key with --key or set AURA_KEY.");
23486
23723
  return 1;
23487
23724
  }
23488
23725
  const outcome = await signInWithBrowser(apiUrl, { person, waitSeconds: parseWait(flags.wait) });
23489
23726
  if (outcome.status !== "complete") {
23490
23727
  explainSignIn(outcome);
23491
- return outcome.status === "skipped" ? 0 : 1;
23728
+ return 1;
23492
23729
  }
23493
23730
  key = outcome.key;
23494
23731
  }
@@ -23534,8 +23771,8 @@ async function configCommand(key, value) {
23534
23771
  const bool = (v) => ["on", "true", "yes", "1"].includes(v.toLowerCase());
23535
23772
  switch (key) {
23536
23773
  case "telemetry":
23537
- if (value === void 0) return say.info(config2.telemetry ? "on" : "off"), 0;
23538
- config2.telemetry = bool(value);
23774
+ if (value === void 0) return say.info(sharesUsage(config2) ? "on" : "off"), 0;
23775
+ config2.shareUsage = bool(value);
23539
23776
  break;
23540
23777
  case "ciMaxDrop":
23541
23778
  case "ci-max-drop":
@@ -23561,9 +23798,9 @@ async function configCommand(key, value) {
23561
23798
  return 0;
23562
23799
  }
23563
23800
  function loadGuides() {
23564
- const dir = path15.join(templatesDir(), "guides");
23801
+ const dir = path16.join(templatesDir(), "guides");
23565
23802
  return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((f) => {
23566
- const text = readFileSync13(path15.join(dir, f), "utf8");
23803
+ const text = readFileSync15(path16.join(dir, f), "utf8");
23567
23804
  const fm = text.match(/^---\n([\s\S]*?)\n---\n?/);
23568
23805
  const meta3 = fm ? YAML2.parse(fm[1]) : {};
23569
23806
  return { id: f.replace(/\.md$/, ""), title: meta3.title ?? f, summary: meta3.summary ?? "", body: fm ? text.slice(fm[0].length) : text };
@@ -23631,7 +23868,7 @@ function allowedCommands(root, pm, typescript) {
23631
23868
  const pkg = readJson(root, "package.json");
23632
23869
  const run2 = pmCommands(pm).run;
23633
23870
  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`);
23871
+ if (typescript && !pkg?.scripts?.typecheck && existsSync15(path17.join(root, "node_modules/.bin/tsc"))) cmds.push(`${pmCommands(pm).exec} tsc --noEmit`);
23635
23872
  return cmds;
23636
23873
  }
23637
23874
  function taskFor(root, scan, action2, findings) {
@@ -23682,8 +23919,8 @@ async function untrackEnvFiles(root, findings) {
23682
23919
  const files = findings.filter((f) => f.checkId === "security.env-files").map((f) => f.locations[0].file);
23683
23920
  for (const f of files) await git(root, "rm", "-q", "--cached", "--", f);
23684
23921
  if (files.length) {
23685
- const gi = path16.join(root, ".gitignore");
23686
- const current2 = existsSync14(gi) ? readFileSync14(gi, "utf8") : "";
23922
+ const gi = path17.join(root, ".gitignore");
23923
+ const current2 = existsSync15(gi) ? readFileSync16(gi, "utf8") : "";
23687
23924
  if (!/^\.env\*?$/m.test(current2)) writeFileSync6(gi, `${current2.trimEnd()}
23688
23925
 
23689
23926
  # Environment files hold secrets
@@ -23695,6 +23932,15 @@ async function untrackEnvFiles(root, findings) {
23695
23932
  return files;
23696
23933
  }
23697
23934
  async function fixCommand(target, flags) {
23935
+ const started = Date.now();
23936
+ const report = { delivery: flags.agent ? "agent-task" : "agent-fix" };
23937
+ const actionId = { value: target };
23938
+ const code = await fixAction(target, flags, report, actionId);
23939
+ const root = await findRepoRoot();
23940
+ await recordAction(root, loadConfig(root), actionId.value, report, started, flags.via ?? "command");
23941
+ return code;
23942
+ }
23943
+ async function fixAction(target, flags, report, resolvedId) {
23698
23944
  const root = await findRepoRoot();
23699
23945
  const config2 = loadConfig(root);
23700
23946
  const scan = await latestScan(root);
@@ -23704,8 +23950,10 @@ async function fixCommand(target, flags) {
23704
23950
  return 1;
23705
23951
  }
23706
23952
  const { action: action2, findings } = resolved;
23953
+ resolvedId.value = action2.id;
23707
23954
  if (findings.length === 0 && !["write-critical-tests"].includes(action2.id)) {
23708
23955
  say.info(`Aura found nothing to fix for "${action2.title}".`);
23956
+ report.outcome = "no_changes";
23709
23957
  return 0;
23710
23958
  }
23711
23959
  const task = taskFor(root, scan, action2, findings);
@@ -23715,11 +23963,13 @@ async function fixCommand(target, flags) {
23715
23963
  console.log(`
23716
23964
  Ask your coding agent:
23717
23965
  ${pc.cyan(`Complete the Aura task in ${file2}`)}`);
23966
+ report.outcome = "task_written";
23718
23967
  return 0;
23719
23968
  }
23720
23969
  const client = AuraClient.from(config2);
23721
23970
  if (!client.hasKey) {
23722
23971
  say.error("Aura\u2019s agent needs a pilot key (aura login). Or run with --agent to hand this to your own coding agent.");
23972
+ Object.assign(report, { outcome: "failed", errorType: "NoPilotKey" });
23723
23973
  return 1;
23724
23974
  }
23725
23975
  console.log(`
@@ -23732,6 +23982,7 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
23732
23982
  const dirty = await dirtyFiles(root);
23733
23983
  if (dirty.length) {
23734
23984
  say.error(`You have uncommitted changes (${dirty.slice(0, 5).join(", ")}). Commit or stash them first.`);
23985
+ Object.assign(report, { outcome: "failed", errorType: "UncommittedChanges" });
23735
23986
  return 1;
23736
23987
  }
23737
23988
  const openPr = flags.pr !== false;
@@ -23739,15 +23990,19 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
23739
23990
  const gh = await ghStatus(root);
23740
23991
  if (!gh.installed || !gh.authed || !gh.hasRemote) {
23741
23992
  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.");
23993
+ Object.assign(report, { outcome: "failed", errorType: gh.installed ? "GhNotReady" : "GhNotInstalled" });
23742
23994
  return 1;
23743
23995
  }
23744
23996
  }
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." })))
23997
+ 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." }))) {
23998
+ report.outcome = "cancelled";
23746
23999
  return 0;
24000
+ }
23747
24001
  const original = await currentBranch(root);
23748
24002
  const branch = await freeBranchName(root, `aura/fix-${action2.id}`);
23749
24003
  if ((await git(root, "switch", "-c", branch)).code !== 0) {
23750
24004
  say.error(`Couldn\u2019t create branch ${branch}.`);
24005
+ Object.assign(report, { outcome: "failed", errorType: "BranchFailed" });
23751
24006
  return 1;
23752
24007
  }
23753
24008
  const s = spin(true);
@@ -23757,6 +24012,7 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
23757
24012
  try {
23758
24013
  s.start("Aura\u2019s agent is working");
23759
24014
  for (let step = 0; step <= MAX_STEPS && !finish; step++) {
24015
+ report.agentSteps = step + 1;
23760
24016
  const res = await client.request("POST", "agent/step", { task, messages, step }, 18e4);
23761
24017
  messages = [...messages, ...res.responseMessages];
23762
24018
  if (res.toolCalls.length === 0) break;
@@ -23785,6 +24041,7 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
23785
24041
  say.warn("Aura\u2019s agent didn\u2019t change anything. " + (finish?.summary ?? ""));
23786
24042
  await git(root, "switch", original);
23787
24043
  await git(root, "branch", "-D", branch);
24044
+ report.outcome = "no_changes";
23788
24045
  return 1;
23789
24046
  }
23790
24047
  const guides = [
@@ -23819,11 +24076,12 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
23819
24076
  else if (result.error) say.warn(`Committed on ${branch}, but couldn\u2019t open the PR: ${result.error.trim()}`);
23820
24077
  else say.ok(`Committed on branch ${branch}.`);
23821
24078
  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 });
24079
+ Object.assign(report, { outcome: result.url ? "pr_opened" : "committed", branch, scoreBefore: scan.score, findingIds: findings.map((f) => f.id) });
23823
24080
  return 0;
23824
24081
  } catch (err) {
23825
24082
  s.error("Aura\u2019s agent hit a problem");
23826
24083
  say.error(friendlyApiError(err));
24084
+ Object.assign(report, { outcome: "failed", errorType: errorKind(err) });
23827
24085
  await git(root, "reset", "-q");
23828
24086
  for (const f of touched) {
23829
24087
  const inHead = (await git(root, "cat-file", "-e", `HEAD:${f}`)).code === 0;
@@ -23842,6 +24100,14 @@ ${pc.bold(action2.title)} ${pc.dim(`${findings.length} finding(s)`)}`);
23842
24100
  // src/commands/apply.ts
23843
24101
  var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
23844
24102
  async function applyCommand(actionId, flags) {
24103
+ const started = Date.now();
24104
+ const report = { delivery: "pr" };
24105
+ const code = await applyAction(actionId, flags, report);
24106
+ const root = await findRepoRoot();
24107
+ await recordAction(root, loadConfig(root), actionId, report, started, flags.via ?? "command");
24108
+ return code;
24109
+ }
24110
+ async function applyAction(actionId, flags, report) {
23845
24111
  const root = await findRepoRoot();
23846
24112
  const action2 = getAction(actionId);
23847
24113
  if (!action2) {
@@ -23853,7 +24119,7 @@ async function applyCommand(actionId, flags) {
23853
24119
  if (delivery === "guide") return guideCommand(actionId, {});
23854
24120
  if (delivery === "agent-fix") {
23855
24121
  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 });
24122
+ return fixCommand(actionId, { yes: flags.yes, dryRun: flags.dryRun, pr: flags.pr, via: flags.via });
23857
24123
  }
23858
24124
  if (delivery !== "pr") {
23859
24125
  say.error(`"${actionId}" isn\u2019t a PR action. Try aura ${delivery === "checklist" ? "inventory" : "next"}.`);
@@ -23875,18 +24141,15 @@ async function applyCommand(actionId, flags) {
23875
24141
  }
23876
24142
  const g = { root, scan, ctx, agents: [...agents], vars: templateVars(root, scan, ctx.pkg), skills: skills2 };
23877
24143
  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.");
24144
+ if ("skip" in plan || plan.changes.length === 0 && !plan.commands?.length && !plan.devDeps?.length) {
24145
+ say.info("skip" in plan ? plan.skip : "Nothing to change.");
24146
+ report.outcome = "no_changes";
23884
24147
  return 0;
23885
24148
  }
23886
24149
  console.log(`
23887
24150
  ${pc.bold(action2.title)}`);
23888
24151
  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}`);
24152
+ console.log(` ${c.content === null ? pc.red("delete") : existsSync16(path18.join(root, c.path)) ? pc.yellow("update") : pc.green("add ")} ${c.path}`);
23890
24153
  const pm = pmCommands(scan.stack.packageManager);
23891
24154
  if (plan.devDeps?.length) console.log(` ${pc.cyan("install")} ${plan.devDeps.join(", ")} ${pc.dim(`(${pm.addDev})`)}`);
23892
24155
  for (const cmd of plan.commands ?? []) console.log(` ${pc.cyan("run ")} ${cmd.join(" ")}`);
@@ -23899,6 +24162,7 @@ ${pc.bold(action2.title)}`);
23899
24162
  say.error(
23900
24163
  `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
24164
  );
24165
+ Object.assign(report, { outcome: "failed", errorType: "UncommittedChanges" });
23902
24166
  return 1;
23903
24167
  }
23904
24168
  const openPr = flags.pr !== false;
@@ -23909,13 +24173,16 @@ ${pc.bold(action2.title)}`);
23909
24173
  !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
24174
  );
23911
24175
  say.dim("Or run with --no-pr to commit the change on a local branch.");
24176
+ Object.assign(report, { outcome: "failed", errorType: gh.installed ? "GhNotReady" : "GhNotInstalled" });
23912
24177
  return 1;
23913
24178
  }
23914
24179
  }
23915
- if (interactive() && !flags.yes && !unwrap(await p.confirm({ message: openPr ? "Create a branch and open a pull request?" : "Create a branch and commit?" })))
24180
+ if (interactive() && !flags.yes && !unwrap(await p.confirm({ message: openPr ? "Create a branch and open a pull request?" : "Create a branch and commit?" }))) {
24181
+ report.outcome = "cancelled";
23916
24182
  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);
24183
+ }
24184
+ const tsc = path18.join(root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
24185
+ const checkTypes = plan.verifyTypes && scan.stack.typescript && existsSync16(tsc);
23919
24186
  const typeErrors = async () => {
23920
24187
  const r = await run(tsc, ["--noEmit", "--pretty", "false"], { cwd: root, timeoutMs: 3e5 });
23921
24188
  return (r.stdout.match(/error TS\d+/g) ?? []).length;
@@ -23926,6 +24193,7 @@ ${pc.bold(action2.title)}`);
23926
24193
  const sw = await git(root, "switch", "-c", branch);
23927
24194
  if (sw.code !== 0) {
23928
24195
  say.error(`Couldn\u2019t create branch ${branch}: ${sw.stderr.trim()}`);
24196
+ Object.assign(report, { outcome: "failed", errorType: "BranchFailed" });
23929
24197
  return 1;
23930
24198
  }
23931
24199
  try {
@@ -23953,6 +24221,7 @@ ${r.stderr.slice(-1500)}`);
23953
24221
  say.info("Nothing changed, so there\u2019s no PR to open.");
23954
24222
  await git(root, "switch", original);
23955
24223
  await git(root, "branch", "-D", branch);
24224
+ report.outcome = "no_changes";
23956
24225
  return 0;
23957
24226
  }
23958
24227
  const prTemplate = `pr/${actionId}.md`;
@@ -23963,22 +24232,31 @@ ${plan.details.join("\n")}`;
23963
24232
  const result = await commitPushPr(root, { branch, base: original, title: `Aura: ${action2.title}`, body, openPr });
23964
24233
  if (result.error && result.committed) {
23965
24234
  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}`);
24235
+ say.dim(`Finish by hand with these two commands:
24236
+ git push -u origin ${branch}
24237
+ gh pr create --head ${branch}`);
24238
+ Object.assign(report, { outcome: "failed", errorType: result.pushed ? "PrFailed" : "PushFailed" });
23967
24239
  return 1;
23968
24240
  }
23969
24241
  if (result.error) throw new Error(result.error.trim());
23970
24242
  if (result.url) say.ok(`Opened ${result.url}`);
23971
24243
  else say.ok(`Committed on branch ${branch}.`);
24244
+ Object.assign(report, {
24245
+ outcome: result.url ? "pr_opened" : "committed",
24246
+ branch,
24247
+ scoreBefore: scan.score,
24248
+ findingIds: nextSteps(scan, { includeFoundationBundle: true }).find((s) => s.actionId === actionId)?.findingIds ?? []
24249
+ });
23972
24250
  if (plan.changes.some((c) => /^\.aura\/|\/skills\/aura\//.test(c.path))) {
23973
24251
  say.dim(
23974
24252
  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
24253
  );
23976
24254
  }
23977
24255
  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
24256
  return 0;
23980
24257
  } catch (err) {
23981
24258
  say.error(err.message);
24259
+ Object.assign(report, { outcome: "failed", errorType: errorKind(err) });
23982
24260
  await undoPlan(root, plan);
23983
24261
  await git(root, "switch", original);
23984
24262
  await git(root, "branch", "-D", branch);
@@ -24013,16 +24291,17 @@ async function undoPlan(root, plan) {
24013
24291
  for (const file2 of touched) {
24014
24292
  const inHead = (await git(root, "cat-file", "-e", `HEAD:${file2}`)).code === 0;
24015
24293
  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 });
24294
+ else if (plan.changes.some((c) => c.path === file2)) rmSync6(path18.join(root, file2), { force: true });
24017
24295
  }
24018
24296
  }
24019
24297
 
24020
24298
  // src/commands/init.ts
24021
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
24022
- import { spawnSync as spawnSync2 } from "child_process";
24299
+ import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
24300
+ import { spawnSync as spawnSync3 } from "child_process";
24301
+ import path22 from "path";
24023
24302
 
24024
24303
  // src/brief.ts
24025
- import path18 from "path";
24304
+ import path19 from "path";
24026
24305
  function repoSummary(ctx) {
24027
24306
  const routes = ctx.files.filter((f) => /^(?:src\/)?app\/(?:.+\/)?(?:page|route)\.[cm]?[jt]sx?$/.test(f) || /^(?:src\/)?pages\/.+\.[cm]?[jt]sx?$/.test(f)).map(
24028
24307
  (f) => "/" + f.replace(/^(?:src\/)?(?:app|pages)\//, "").replace(/(?:^|\/)(?:page|route|index)\.[cm]?[jt]sx?$|\.[cm]?[jt]sx?$/, "").replace(/\([^)]+\)\//g, "")
@@ -24038,7 +24317,7 @@ function repoSummary(ctx) {
24038
24317
  const readme2 = ctx.files.find((f) => /^readme\.mdx?$/i.test(f));
24039
24318
  const readmeText = readme2 ? ctx.read(readme2) ?? "" : "";
24040
24319
  return {
24041
- name: ctx.pkg?.name ?? path18.basename(ctx.root),
24320
+ name: ctx.pkg?.name ?? path19.basename(ctx.root),
24042
24321
  packageJson: ctx.pkg ? {
24043
24322
  name: ctx.pkg.name,
24044
24323
  description: ctx.pkg.description,
@@ -24110,6 +24389,7 @@ async function draftBrief(client, answers, summary) {
24110
24389
  }
24111
24390
 
24112
24391
  // src/core/aura-command.ts
24392
+ import { spawnSync as spawnSync2 } from "child_process";
24113
24393
  function hasAuraCommand() {
24114
24394
  return commandExists("aura");
24115
24395
  }
@@ -24119,18 +24399,144 @@ async function installAuraCommand() {
24119
24399
  if (r.code !== 0) return { ok: false, needsAdmin: /EACCES|EPERM|permission denied/i.test(r.stderr + r.stdout) };
24120
24400
  return { ok: true, onPath: await hasAuraCommand() };
24121
24401
  }
24402
+ async function installWithSudo() {
24403
+ const r = spawnSync2("sudo", ["npm", "install", "-g", `getaura@${AURA_VERSION}`], { stdio: "inherit" });
24404
+ return r.status === 0 && await hasAuraCommand();
24405
+ }
24122
24406
  function installFallback(result, platform = process.platform) {
24123
24407
  const admin = result.needsAdmin && platform !== "win32" ? `Run \`sudo ${INSTALL_AURA}\` and enter your computer password, or use` : "Use";
24124
24408
  return `${admin} \`npx getaura <command>\` in place of \`aura <command>\` (for example \`npx getaura scan\`).`;
24125
24409
  }
24126
24410
 
24127
- // src/commands/scan.ts
24128
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync7 } from "fs";
24411
+ // src/core/claude-plugin.ts
24412
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
24129
24413
  import os5 from "os";
24130
- import path19 from "path";
24414
+ import path20 from "path";
24415
+ var CLAUDE_PLUGIN_ID = "aura@aura";
24416
+ var marketplaceDir = () => path20.join(os5.homedir(), ".aura", "claude-marketplace");
24417
+ var MARKETPLACE = {
24418
+ name: "aura",
24419
+ description: "Aura for Claude Code",
24420
+ owner: { name: "Aura" },
24421
+ plugins: [
24422
+ {
24423
+ name: "aura",
24424
+ description: "Slash commands for Aura: /aura:scan, /aura:next, /aura:fix and /aura:setup.",
24425
+ source: { source: "npm", package: "getaura" }
24426
+ }
24427
+ ]
24428
+ };
24429
+ function writeMarketplace() {
24430
+ const dir = marketplaceDir();
24431
+ mkdirSync6(path20.join(dir, ".claude-plugin"), { recursive: true });
24432
+ writeFileSync7(path20.join(dir, ".claude-plugin", "marketplace.json"), `${JSON.stringify(MARKETPLACE, null, 2)}
24433
+ `);
24434
+ return dir;
24435
+ }
24436
+ function manualInstall() {
24437
+ return [`claude plugin marketplace add "${writeMarketplace()}"`, `claude plugin install ${CLAUDE_PLUGIN_ID} --scope project`];
24438
+ }
24439
+ async function installClaudePlugin(root) {
24440
+ if (!await commandExists("claude")) return "no-claude";
24441
+ const dir = writeMarketplace();
24442
+ const add = await run("claude", ["plugin", "marketplace", "add", dir], { cwd: root, timeoutMs: 12e4 });
24443
+ if (add.code !== 0) return "failed";
24444
+ const install = await run("claude", ["plugin", "install", CLAUDE_PLUGIN_ID, "--scope", "project"], { cwd: root, timeoutMs: 18e4 });
24445
+ return install.code === 0 ? "installed" : "failed";
24446
+ }
24447
+
24448
+ // src/onboarding.ts
24449
+ var DATA_LABELS2 = {
24450
+ personal: "personal info (names, emails)",
24451
+ payments: "payments",
24452
+ health: "health",
24453
+ legal: "legal",
24454
+ children: "children\u2019s data",
24455
+ none: "none of these"
24456
+ };
24457
+ var BOILERPLATE2 = /bootstrapped with|create-next-app|getting started|this template|vite \+|turborepo|starter kit/i;
24458
+ function whatItDoes(ctx) {
24459
+ const description = ctx.pkg?.description?.trim();
24460
+ if (description && description.length >= 15 && !BOILERPLATE2.test(description)) return { value: description, from: "package.json" };
24461
+ const readme2 = ctx.files.find((f) => /^readme\.mdx?$/i.test(f));
24462
+ const readmeText = readme2 ? ctx.read(readme2) ?? "" : "";
24463
+ if (/create-next-app|create-react-app|create-vite|create-t3-app|bootstrapped with/i.test(readmeText)) return null;
24464
+ const paragraphs = readmeText.replace(/\r\n/g, "\n").split(/\n\s*\n/);
24465
+ for (const raw of paragraphs) {
24466
+ const text = raw.replace(/\s+/g, " ").trim();
24467
+ if (!text || /^(?:#|!\[|\[|<|```|[-*|>]|\d+\.)/.test(text) || text.length < 30 || BOILERPLATE2.test(text)) continue;
24468
+ return { value: text.length > 300 ? `${text.slice(0, 297).replace(/\s+\S*$/, "")}\u2026` : text, from: "the README" };
24469
+ }
24470
+ return null;
24471
+ }
24472
+ var PAYMENT_PACKAGES = [
24473
+ [/^stripe$|^@stripe\//, "Stripe"],
24474
+ [/^@lemonsqueezy\//, "Lemon Squeezy"],
24475
+ [/^@paddle\//, "Paddle"],
24476
+ [/^@polar-sh\//, "Polar"],
24477
+ [/^@revenuecat\/|^react-native-purchases$/, "RevenueCat"]
24478
+ ];
24479
+ var AUTH_PACKAGES = [
24480
+ [/^@supabase\//, "Supabase"],
24481
+ [/^@clerk\//, "Clerk"],
24482
+ [/^next-auth$|^@auth\//, "Auth.js"],
24483
+ [/^firebase$|^firebase-admin$/, "Firebase"],
24484
+ [/^better-auth$/, "Better Auth"],
24485
+ [/^@kinde-oss\//, "Kinde"],
24486
+ [/^@auth0\//, "Auth0"]
24487
+ ];
24488
+ var words = (list) => new RegExp(`(?<![a-z])(?:${list})(?![a-z])`, "i");
24489
+ var SENSITIVE = [
24490
+ ["health", words("patients?|diagnos[a-z]*|medical|medications?|prescriptions?|symptoms?|health_records?"), "health"],
24491
+ ["legal", words("case_number|lawsuits?|attorneys?|legal_matters?|court[a-z]*"), "legal"],
24492
+ ["children", words("child|children|kids?|minors?|guardians?|parental_consent"), "children"]
24493
+ ];
24494
+ function schemaText(ctx) {
24495
+ const files = ctx.files.filter((f) => /\.sql$|schema\.prisma$|(?:^|\/)schema\.[cm]?[jt]s$|(?:^|\/)db\/schema\//.test(f) && !/node_modules\//.test(f));
24496
+ return files.slice(0, 200).map((f) => ctx.read(f) ?? "").join("\n");
24497
+ }
24498
+ var matchPackage = (deps2, list) => list.find(([re]) => deps2.some((d) => re.test(d)))?.[1];
24499
+ function inferAnswers(ctx) {
24500
+ const deps2 = Object.keys(allDeps(ctx.pkg));
24501
+ const payment = matchPackage(deps2, PAYMENT_PACKAGES);
24502
+ const paymentPage = ctx.files.find((f) => /(?:^|\/)(?:pricing|checkout|billing|subscribe)(?:\/|\.|$)/i.test(f));
24503
+ 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" };
24504
+ const kinds = [];
24505
+ const reasons = [];
24506
+ const auth = matchPackage(deps2, AUTH_PACKAGES);
24507
+ const schema = schemaText(ctx);
24508
+ if (auth || /\b(?:users?|profiles?|accounts?)\b[\s\S]{0,400}\bemail\b/i.test(schema)) {
24509
+ kinds.push("personal");
24510
+ reasons.push(auth ? `sign-in with ${auth}` : "a users table with emails");
24511
+ }
24512
+ if (chargesMoney.value) {
24513
+ kinds.push("payments");
24514
+ reasons.push("it charges money");
24515
+ }
24516
+ for (const [kind, re, label] of SENSITIVE) {
24517
+ if (re.test(schema)) {
24518
+ kinds.push(kind);
24519
+ reasons.push(`${label} words in the database tables`);
24520
+ }
24521
+ }
24522
+ const dataHandled = kinds.length ? { value: kinds, from: reasons.join("; ") } : { value: ["none"], from: "no sign-in, payments or sensitive tables found" };
24523
+ return { whatItDoes: whatItDoes(ctx), chargesMoney, dataHandled };
24524
+ }
24525
+ function describeInferred(inferred) {
24526
+ return [
24527
+ `What it does: ${inferred.whatItDoes ? `${inferred.whatItDoes.value} (from ${inferred.whatItDoes.from})` : "not found, please describe it"}`,
24528
+ `Charges money: ${inferred.chargesMoney.value ? "yes" : "no"} (${inferred.chargesMoney.from})`,
24529
+ `Data it handles: ${inferred.dataHandled.value.map((k) => DATA_LABELS2[k]).join(", ")} (${inferred.dataHandled.from})`
24530
+ ];
24531
+ }
24532
+
24533
+ // src/commands/scan.ts
24534
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "fs";
24535
+ import os6 from "os";
24536
+ import path21 from "path";
24131
24537
  async function scanRef(root, ref, opts) {
24132
- const dir = mkdtempSync3(path19.join(os5.tmpdir(), "aura-base-"));
24133
- const wt = path19.join(dir, "tree");
24538
+ const dir = mkdtempSync3(path21.join(os6.tmpdir(), "aura-base-"));
24539
+ const wt = path21.join(dir, "tree");
24134
24540
  const add = await run("git", ["worktree", "add", "--detach", wt, ref], { cwd: root });
24135
24541
  if (add.code !== 0) {
24136
24542
  rmSync7(dir, { recursive: true, force: true });
@@ -24144,6 +24550,7 @@ async function scanRef(root, ref, opts) {
24144
24550
  }
24145
24551
  }
24146
24552
  async function scanCommand(flags) {
24553
+ const started = Date.now();
24147
24554
  const root = await findRepoRoot();
24148
24555
  const config2 = loadConfig(root);
24149
24556
  const quiet = Boolean(flags.json);
@@ -24171,8 +24578,22 @@ async function scanCommand(flags) {
24171
24578
  writeAuraFile(root, "scan.json", JSON.stringify(scan, null, 2));
24172
24579
  writeAuraFile(root, "report.md", renderMarkdown(scan, steps));
24173
24580
  appendHistory(root, scan);
24174
- if (config2.telemetry)
24175
- await sendTelemetry(root, client, { event: "scan", score: scan.score, categories: Object.fromEntries(scan.categories.map((c) => [c.category, c.score])) });
24581
+ const findings = scan.checks.flatMap((c) => c.findings);
24582
+ await recordUsage(root, config2, {
24583
+ event: "scan",
24584
+ score: scan.score,
24585
+ categories: Object.fromEntries(scan.categories.map((c) => [c.category, c.score])),
24586
+ findings: {
24587
+ critical: findings.filter((f) => f.severity === "critical").length,
24588
+ high: findings.filter((f) => f.severity === "high").length,
24589
+ medium: findings.filter((f) => f.severity === "medium").length,
24590
+ low: findings.filter((f) => f.severity === "low").length,
24591
+ info: findings.filter((f) => f.severity === "info").length
24592
+ },
24593
+ failingChecks: scan.checks.filter((c) => c.status === "fail" || c.status === "partial").map((c) => c.checkId),
24594
+ durationMs: Date.now() - started
24595
+ });
24596
+ if (!flags.ci) await followUpFixes(root, config2, scan);
24176
24597
  let exitCode = 0;
24177
24598
  let base = null;
24178
24599
  if (flags.base) {
@@ -24183,7 +24604,7 @@ async function scanCommand(flags) {
24183
24604
  const baseIds = new Set(base ? base.checks.flatMap((c) => c.findings.map((f) => f.id)) : []);
24184
24605
  const newCritical = base ? scan.checks.flatMap((c) => c.findings).filter((f) => f.severity === "critical" && !baseIds.has(f.id)) : [];
24185
24606
  if (base && (base.score - scan.score > maxDrop || newCritical.length > 0)) exitCode = 1;
24186
- if (flags.commentFile) writeFileSync7(flags.commentFile, renderPrComment(scan, base, maxDrop));
24607
+ if (flags.commentFile) writeFileSync8(flags.commentFile, renderPrComment(scan, base, maxDrop));
24187
24608
  if (quiet) {
24188
24609
  printJson({ ...scan, nextSteps: steps, base: base ? { score: base.score, categories: base.categories } : void 0 });
24189
24610
  return exitCode;
@@ -24204,31 +24625,34 @@ async function scanCommand(flags) {
24204
24625
  // src/commands/init.ts
24205
24626
  var AGENT_IDS = ["claude", "codex", "cursor", "pi"];
24206
24627
  function detectAgents(root) {
24207
- const found = [];
24208
- if (existsSync16(`${root}/CLAUDE.md`) || existsSync16(`${root}/.claude`)) found.push("claude");
24209
- if (existsSync16(`${root}/AGENTS.md`)) found.push("codex");
24210
- if (existsSync16(`${root}/.cursor`) || existsSync16(`${root}/.cursorrules`)) found.push("cursor");
24211
- if (existsSync16(`${root}/.pi`)) found.push("pi");
24212
- return found;
24628
+ return detectAgentsFrom((rel) => existsSync17(path22.join(root, rel)));
24213
24629
  }
24214
24630
  function openEditor(file2) {
24215
- const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "nano");
24216
- const r = spawnSync2(editor, [file2], { stdio: "inherit", shell: process.platform === "win32" });
24217
- return r.status === 0;
24631
+ const fallback = process.platform === "win32" ? "notepad" : spawnSync3("sh", ["-c", "command -v nano"]).status === 0 ? "nano" : "vi";
24632
+ const editor = process.env.VISUAL || process.env.EDITOR || fallback;
24633
+ return spawnSync3(`${editor} "${file2}"`, { stdio: "inherit", shell: true }).status === 0;
24634
+ }
24635
+ function answersTemplate(inferred) {
24636
+ return JSON.stringify({
24637
+ whatItDoes: inferred.whatItDoes?.value ?? "\u2026",
24638
+ audience: "\u2026",
24639
+ chargesMoney: inferred.chargesMoney.value,
24640
+ dataHandled: inferred.dataHandled.value,
24641
+ userRegions: ["\u2026"]
24642
+ });
24218
24643
  }
24219
- var EXAMPLE_ANSWERS = '{"whatItDoes": "\u2026", "audience": "\u2026", "chargesMoney": false, "dataHandled": ["personal"], "userRegions": ["Canada"]}';
24220
- function briefHandoff() {
24644
+ function briefHandoff(inferred) {
24221
24645
  return [
24222
24646
  "",
24223
- `${pc.magenta("\u2726")} ${pc.bold("Next: write the product brief")}`,
24224
- " Ask the user these questions. If you are a coding agent, suggest answers from the code and let the user confirm or correct them:",
24225
- " 1. What does the app do?",
24226
- " 2. Who is it for?",
24227
- " 3. Does it charge money?",
24228
- " 4. What data does it handle? Any of: personal, payments, health, legal, children, none",
24229
- " 5. Where are its users? (countries or regions)",
24230
- " Save the answers to .aura/answers.json in this shape:",
24231
- ` ${EXAMPLE_ANSWERS}`,
24647
+ `${pc.magenta("\u2726")} ${pc.bold("Next: check the product brief with the user")}`,
24648
+ " Aura worked these out from the code. Show them to the user and correct anything they say is wrong:",
24649
+ ...describeInferred(inferred).map((l) => ` - ${l}`),
24650
+ " Then ask the user:",
24651
+ " - Who is the app for?",
24652
+ " - Where are its users? (countries or regions)",
24653
+ ' Save the answers to .aura/answers.json. Start from this and replace each "\u2026":',
24654
+ ` ${answersTemplate(inferred)}`,
24655
+ " dataHandled can include: personal, payments, health, legal, children, none.",
24232
24656
  " Then run: npx getaura@latest init --yes --answers .aura/answers.json",
24233
24657
  ""
24234
24658
  ].join("\n");
@@ -24257,6 +24681,8 @@ async function initCommand(flags) {
24257
24681
  else if (!gh.hasRemote) say.warn("This repo has no GitHub remote yet. Push it to GitHub before applying fixes (`gh repo create`).");
24258
24682
  else say.ok("git and the GitHub CLI are ready.");
24259
24683
  const config2 = loadConfig(root);
24684
+ if (flags.telemetry !== void 0) config2.shareUsage = flags.telemetry;
24685
+ const setupStep = (step) => recordUsage(root, config2, { event: "setup", step });
24260
24686
  let agents = (flags.agent ?? []).flatMap((a) => a.split(",")).map((a) => a.trim().toLowerCase());
24261
24687
  const unknown2 = agents.filter((a) => !AGENT_IDS.includes(a));
24262
24688
  if (unknown2.length) {
@@ -24278,20 +24704,15 @@ async function initCommand(flags) {
24278
24704
  }
24279
24705
  config2.agents = agents;
24280
24706
  let key = flags.key ?? getApiKey();
24281
- let explained = false;
24282
- if (!key && !process.env.CI && !config2.noKey) {
24707
+ let connectedNow = false;
24708
+ if (!key && !inCI()) {
24283
24709
  const outcome = await signInWithBrowser(getApiUrl(config2), { person: interactive(), waitSeconds: parseWait(flags.wait) });
24284
- if (outcome.status === "complete") {
24285
- key = outcome.key;
24286
- delete config2.noKey;
24287
- } else {
24288
- if (outcome.status === "skipped") config2.noKey = true;
24289
- if (!explainSignIn(outcome)) {
24290
- if (outcome.status === "pending") return 1;
24291
- say.dim("Continuing without a pilot key for now. Connect one later with `aura login`.");
24292
- }
24293
- explained = true;
24710
+ if (outcome.status !== "complete") {
24711
+ explainSignIn(outcome);
24712
+ return 1;
24294
24713
  }
24714
+ key = outcome.key;
24715
+ connectedNow = true;
24295
24716
  }
24296
24717
  let client = AuraClient.from(config2);
24297
24718
  if (key) {
@@ -24300,11 +24721,18 @@ async function initCommand(flags) {
24300
24721
  const me = await client.request("GET", "me");
24301
24722
  saveApiKey(key);
24302
24723
  say.ok(`Signed in as ${me.name}.`);
24724
+ if (sharesUsage(config2, key)) {
24725
+ say.dim(
24726
+ "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`."
24727
+ );
24728
+ await setupStep("started");
24729
+ if (connectedNow) await setupStep("key_connected");
24730
+ }
24303
24731
  } catch (err) {
24304
24732
  say.warn(`${friendlyApiError(err)} Continuing without Aura\u2019s agent.`);
24305
24733
  client = new AuraClient(getApiUrl(config2), null);
24306
24734
  }
24307
- } else if (!explained) {
24735
+ } else {
24308
24736
  say.dim("No pilot key: Aura will still score your repo, with standard explanations.");
24309
24737
  }
24310
24738
  const ctx = await buildContext(root, { fast: true, noDownload: true }, config2);
@@ -24312,35 +24740,49 @@ async function initCommand(flags) {
24312
24740
  let brief2 = null;
24313
24741
  let keptBrief = false;
24314
24742
  if (flags.briefFile) {
24315
- brief2 = readFileSync15(flags.briefFile, "utf8");
24316
- } else if (!flags.answers && existsSync16(briefPath) && !(tty && unwrap(await p.confirm({ message: "A product brief already exists. Draft a new one?", initialValue: false })))) {
24743
+ brief2 = readFileSync17(flags.briefFile, "utf8");
24744
+ } else if (!flags.answers && existsSync17(briefPath) && !(tty && unwrap(await p.confirm({ message: "A product brief already exists. Draft a new one?", initialValue: false })))) {
24317
24745
  keptBrief = true;
24318
24746
  } else {
24319
24747
  let answers = null;
24320
24748
  if (flags.answers) {
24321
- answers = OnboardingAnswersSchema.parse(JSON.parse(readFileSync15(flags.answers, "utf8")));
24749
+ answers = OnboardingAnswersSchema.parse(JSON.parse(readFileSync17(flags.answers, "utf8")));
24322
24750
  } else if (tty) {
24323
- p.note("A few questions so Aura understands your app. Your answers go into the product brief.", "About your app");
24324
- const whatItDoes = unwrap(await p.text({ message: "What does your app do?", placeholder: "e.g. Lets dog owners book and pay for walks" }));
24325
- const audience = unwrap(await p.text({ message: "Who is it for?", placeholder: "e.g. Busy dog owners in Toronto" }));
24326
- const chargesMoney = unwrap(await p.confirm({ message: "Does it charge money?", initialValue: ctx.stack.services.includes("Stripe") }));
24327
- const dataHandled = unwrap(
24751
+ const inferred = inferAnswers(ctx);
24752
+ p.note(describeInferred(inferred).join("\n"), "Here\u2019s what Aura found about your app");
24753
+ const fix = new Set(
24754
+ unwrap(
24755
+ await p.multiselect({
24756
+ message: "Anything Aura got wrong? Select it to correct it, or press enter if it\u2019s all right.",
24757
+ options: [
24758
+ { value: "whatItDoes", label: "What it does" },
24759
+ { value: "chargesMoney", label: "Charges money" },
24760
+ { value: "dataHandled", label: "Data it handles" }
24761
+ ],
24762
+ required: false
24763
+ })
24764
+ )
24765
+ );
24766
+ const whatItDoes2 = inferred.whatItDoes && !fix.has("whatItDoes") ? inferred.whatItDoes.value : unwrap(
24767
+ await p.text({
24768
+ message: "What does your app do?",
24769
+ placeholder: "e.g. Lets dog owners book and pay for walks",
24770
+ initialValue: inferred.whatItDoes?.value
24771
+ })
24772
+ );
24773
+ const chargesMoney = fix.has("chargesMoney") ? unwrap(await p.confirm({ message: "Does it charge money?", initialValue: !inferred.chargesMoney.value })) : inferred.chargesMoney.value;
24774
+ const dataHandled = fix.has("dataHandled") ? unwrap(
24328
24775
  await p.multiselect({
24329
24776
  message: "What data does it handle?",
24330
- options: [
24331
- { value: "personal", label: "Personal info (names, emails)" },
24332
- { value: "payments", label: "Payments" },
24333
- { value: "health", label: "Health" },
24334
- { value: "legal", label: "Legal" },
24335
- { value: "children", label: "Children\u2019s data" },
24336
- { value: "none", label: "None of these" }
24337
- ],
24777
+ options: Object.entries(DATA_LABELS2).map(([value, label]) => ({ value, label: label[0].toUpperCase() + label.slice(1) })),
24778
+ initialValues: inferred.dataHandled.value,
24338
24779
  required: true
24339
24780
  })
24340
- );
24781
+ ) : inferred.dataHandled.value;
24782
+ const audience = unwrap(await p.text({ message: "Who is it for?", placeholder: "e.g. Busy dog owners in Toronto" }));
24341
24783
  const regions = unwrap(await p.text({ message: "Where are your users?", placeholder: "e.g. Canada, United States" }));
24342
24784
  answers = {
24343
- whatItDoes,
24785
+ whatItDoes: whatItDoes2,
24344
24786
  audience,
24345
24787
  chargesMoney,
24346
24788
  dataHandled,
@@ -24363,13 +24805,14 @@ async function initCommand(flags) {
24363
24805
  }
24364
24806
  brief2 ??= offlineBrief(answers, summary, ctx);
24365
24807
  } else {
24366
- console.log(briefHandoff());
24808
+ console.log(briefHandoff(inferAnswers(ctx)));
24367
24809
  }
24368
24810
  }
24369
24811
  if (keptBrief) {
24370
24812
  say.ok("Kept your existing product brief (.aura/brief.md).");
24371
24813
  } else if (brief2) {
24372
24814
  writeAuraFile(root, "brief.md", brief2);
24815
+ await setupStep("brief_written");
24373
24816
  if (tty) {
24374
24817
  p.note(brief2.split("\n").slice(0, 24).join("\n") + (brief2.split("\n").length > 24 ? "\n\u2026" : ""), "Product brief draft");
24375
24818
  const choice = unwrap(
@@ -24385,27 +24828,36 @@ async function initCommand(flags) {
24385
24828
  }
24386
24829
  say.ok("Saved the product brief to .aura/brief.md. It gets committed in the first-run PR (aura apply foundation).");
24387
24830
  }
24388
- writeChanges(root, skillChanges(agents, ["aura"]));
24831
+ writeChanges(root, [...skillChanges(agents, ["aura"]), ...commandChanges(agents)]);
24389
24832
  say.ok(`Installed the Aura skill for ${agents.map((a) => AGENT_LABELS[a]).join(", ")}.`);
24390
- if (flags.telemetry !== void 0) config2.telemetry = flags.telemetry;
24391
- else if (tty) {
24392
- config2.telemetry = unwrap(
24393
- await p.confirm({ message: "Share your Aura score and which actions you take with the Aura team? (Never code. Off by default.)", initialValue: false })
24394
- );
24833
+ await setupStep("skill_installed");
24834
+ if (agents.includes("claude") && !inCI() && !process.env.AURA_NO_CLAUDE_PLUGIN) {
24835
+ const plugin = await installClaudePlugin(root);
24836
+ if (plugin === "installed")
24837
+ say.ok(
24838
+ "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."
24839
+ );
24840
+ else say.dim(`To add Aura\u2019s slash commands (/aura:scan, /aura:fix\u2026) to Claude Code, run these two commands:
24841
+ ${manualInstall().join("\n ")}`);
24395
24842
  }
24843
+ for (const agent of agents.filter((a) => a !== "claude")) say.ok(`Added Aura\u2019s commands for ${AGENT_LABELS[agent]}: ${commandList(agent)}.`);
24396
24844
  saveConfig(root, config2);
24397
24845
  ensureGitignore(root);
24398
- if (!process.env.CI && !await hasAuraCommand()) {
24846
+ if (!inCI() && !await hasAuraCommand()) {
24399
24847
  if (tty) {
24400
24848
  if (unwrap(await p.confirm({ message: "Install the `aura` command so you can run `aura scan` any time?", initialValue: true }))) {
24401
24849
  const s = spin(true);
24402
24850
  s.start("Installing the aura command");
24403
24851
  const installed = await installAuraCommand();
24852
+ await setupStep(installed.ok ? "command_installed" : "command_install_failed");
24404
24853
  if (installed.ok)
24405
24854
  s.stop(installed.onPath ? "Installed. Run `aura scan` from any folder." : "Installed. Open a new terminal window, then run `aura scan`.");
24406
24855
  else {
24407
24856
  s.stop("Couldn\u2019t install the aura command");
24408
- say.warn(installFallback(installed));
24857
+ const useSudo = installed.needsAdmin && process.platform !== "win32" && unwrap(await p.confirm({ message: "Your computer needs your password to install it. Enter your password now?", initialValue: true }));
24858
+ if (!useSudo) say.warn(installFallback(installed));
24859
+ else if (await installWithSudo()) say.ok("Installed. Run `aura scan` from any folder.");
24860
+ else say.warn(installFallback({ needsAdmin: false }));
24409
24861
  }
24410
24862
  } else say.dim(`Use \`npx getaura <command>\` instead, or install it later with \`${INSTALL_AURA}\`.`);
24411
24863
  } else {
@@ -24420,11 +24872,13 @@ ${pc.magenta("\u2726")} ${pc.bold("Give the user the aura command")}`);
24420
24872
  return 0;
24421
24873
  }
24422
24874
  if (tty) p.outro("Running your first scan\u2026");
24423
- return scanCommand({});
24875
+ const scanned = await scanCommand({});
24876
+ await setupStep("first_scan");
24877
+ return scanned;
24424
24878
  }
24425
24879
 
24426
24880
  // src/commands/summary.ts
24427
- import path20 from "path";
24881
+ import path23 from "path";
24428
24882
  async function activity(root, since) {
24429
24883
  const log = await git(root, "log", `--since=${since.toISOString()}`, "--no-merges", "--format=%H%x09%an%x09%s");
24430
24884
  const commits = log.stdout.split("\n").filter(Boolean).slice(0, 300).map((l) => {
@@ -24481,7 +24935,7 @@ async function summaryCommand(flags) {
24481
24935
  const beforeIds = new Set(before ? allFindings(before).map((f) => f.id) : []);
24482
24936
  const nowIds = new Set(allFindings(now).map((f) => f.id));
24483
24937
  const req = {
24484
- repo: path20.basename(root),
24938
+ repo: path23.basename(root),
24485
24939
  since: since.toISOString(),
24486
24940
  until: until.toISOString(),
24487
24941
  product: productContext(root),
@@ -24525,10 +24979,62 @@ ${body}`);
24525
24979
  return 0;
24526
24980
  }
24527
24981
 
24982
+ // src/index.ts
24983
+ import pc4 from "picocolors";
24984
+
24985
+ // src/core/update-check.ts
24986
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
24987
+ import os7 from "os";
24988
+ import path24 from "path";
24989
+ var DAY_MS = 24 * 60 * 60 * 1e3;
24990
+ var cacheFile = () => path24.join(os7.homedir(), ".aura", "update-check.json");
24991
+ function isNewer(a, b) {
24992
+ if (!/^\d+\.\d+\.\d+$/.test(a) || !/^\d+\.\d+\.\d+$/.test(b)) return false;
24993
+ const [x, y] = [a, b].map((v) => v.split(".").map(Number));
24994
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i];
24995
+ return false;
24996
+ }
24997
+ async function fetchLatest() {
24998
+ try {
24999
+ const res = await fetch("https://registry.npmjs.org/getaura/latest", { signal: AbortSignal.timeout(1500) });
25000
+ const body = await res.json();
25001
+ return res.ok && typeof body.version === "string" ? body.version : null;
25002
+ } catch {
25003
+ return null;
25004
+ }
25005
+ }
25006
+ async function newerVersion(opts = {}) {
25007
+ const now = opts.now ?? Date.now();
25008
+ let latest = null;
25009
+ try {
25010
+ const cached2 = JSON.parse(readFileSync18(cacheFile(), "utf8"));
25011
+ if (now - cached2.checkedAt < DAY_MS) latest = cached2.latest;
25012
+ else throw new Error("stale");
25013
+ } catch {
25014
+ latest = await (opts.fetchLatest ?? fetchLatest)();
25015
+ try {
25016
+ mkdirSync7(path24.dirname(cacheFile()), { recursive: true });
25017
+ writeFileSync9(cacheFile(), JSON.stringify({ checkedAt: now, latest }));
25018
+ } catch {
25019
+ }
25020
+ }
25021
+ return latest && isNewer(latest, opts.current ?? AURA_VERSION) ? latest : null;
25022
+ }
25023
+ function startUpdateCheck() {
25024
+ if (!process.stdout.isTTY || inCI() || runningAgent() || process.env.AURA_NO_UPDATE_CHECK) return Promise.resolve(null);
25025
+ return newerVersion().catch(() => null);
25026
+ }
25027
+ function updateNotice(latest, argv1 = process.argv[1] ?? "") {
25028
+ const viaNpx = /[\\/]_npx[\\/]/.test(argv1);
25029
+ const how = viaNpx ? "use `npx getaura@latest`" : "update with `npm install -g getaura@latest`";
25030
+ return `Aura ${latest} is available (you have ${AURA_VERSION}): ${how}.`;
25031
+ }
25032
+
24528
25033
  // src/index.ts
24529
25034
  var program = new Command();
24530
25035
  program.name("aura").description("Aura helps you and your coding agent follow best practices while you vibe code.").version(AURA_VERSION).showHelpAfterError();
24531
25036
  program.addHelpText("before", () => interactive() ? banner() : "");
25037
+ var update = startUpdateCheck();
24532
25038
  function action(fn) {
24533
25039
  return async (...args) => {
24534
25040
  try {
@@ -24541,7 +25047,16 @@ function action(fn) {
24541
25047
  say.error(err.message);
24542
25048
  if (process.env.AURA_DEBUG) console.error(err);
24543
25049
  process.exitCode = 1;
25050
+ const command = args.at(-1) instanceof Command ? args.at(-1).name() : "unknown";
25051
+ try {
25052
+ const root = await findRepoRoot();
25053
+ await recordUsage(root, loadConfig(root), { event: "error", command, errorType: errorKind(err) });
25054
+ } catch {
25055
+ }
24544
25056
  }
25057
+ const latest = await update;
25058
+ if (latest) console.error(pc4.dim(`
25059
+ ${updateNotice(latest)}`));
24545
25060
  };
24546
25061
  }
24547
25062
  program.command("init").description("Set up Aura: pick your agent, write the product brief, install the Aura skill and run the first scan").option("--agent <id...>", "coding agent(s): claude, codex, cursor, pi").option("-y, --yes", "no prompts; use detected defaults").option("--key <key>", "Aura pilot key, for CI and scripts (otherwise a browser page asks for it)").option("--wait <seconds>", "how long to wait for the browser sign-in (default: until the link expires; 90 for agents)").option("--brief-file <path>", "use this file as the product brief").option("--answers <path>", "JSON file with the setup answers").option("--no-scan", "skip the first scan").addOption(new Option("--telemetry", "share scores and action names with the Aura team")).addOption(new Option("--no-telemetry", "don\u2019t share anything")).action(action((flags) => initCommand(flags)));