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