peer-ai 1.0.0-next.0

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +403 -0
  3. package/dist/assess.d.ts +102 -0
  4. package/dist/assess.js +545 -0
  5. package/dist/check.d.ts +30 -0
  6. package/dist/check.js +253 -0
  7. package/dist/checks.d.ts +15 -0
  8. package/dist/checks.js +15 -0
  9. package/dist/cli.d.ts +10 -0
  10. package/dist/cli.js +231 -0
  11. package/dist/detect.d.ts +42 -0
  12. package/dist/detect.js +459 -0
  13. package/dist/doctor.d.ts +36 -0
  14. package/dist/doctor.js +297 -0
  15. package/dist/document.d.ts +30 -0
  16. package/dist/document.js +72 -0
  17. package/dist/enforcers.d.ts +6 -0
  18. package/dist/enforcers.js +307 -0
  19. package/dist/feedback.d.ts +67 -0
  20. package/dist/feedback.js +209 -0
  21. package/dist/files.d.ts +1 -0
  22. package/dist/files.js +72 -0
  23. package/dist/init.d.ts +31 -0
  24. package/dist/init.js +158 -0
  25. package/dist/mcp.d.ts +13 -0
  26. package/dist/mcp.js +247 -0
  27. package/dist/package-info.d.ts +4 -0
  28. package/dist/package-info.js +6 -0
  29. package/dist/pipeline.d.ts +82 -0
  30. package/dist/pipeline.js +265 -0
  31. package/dist/prompter.d.ts +23 -0
  32. package/dist/prompter.js +56 -0
  33. package/dist/render.d.ts +58 -0
  34. package/dist/render.js +557 -0
  35. package/dist/report.d.ts +3 -0
  36. package/dist/report.js +93 -0
  37. package/dist/routing.d.ts +24 -0
  38. package/dist/routing.js +121 -0
  39. package/dist/ruff.d.ts +19 -0
  40. package/dist/ruff.js +64 -0
  41. package/dist/standards.d.ts +46 -0
  42. package/dist/standards.js +130 -0
  43. package/dist/state.d.ts +22 -0
  44. package/dist/state.js +56 -0
  45. package/dist/test-helpers.d.ts +16 -0
  46. package/dist/test-helpers.js +62 -0
  47. package/dist/work.d.ts +147 -0
  48. package/dist/work.js +357 -0
  49. package/package.json +45 -0
package/dist/check.js ADDED
@@ -0,0 +1,253 @@
1
+ // peer-ai check: the gate CI runs. It fails when the setup is broken, or when a work item claims
2
+ // more than its record shows: an item at ship or done without a passing verify or reviews, or a
3
+ // gap marked done that a fresh assessment still finds. Gaps on the map never fail it; they are
4
+ // reported, so they become work items instead of blockers. The project's stage sets how strict
5
+ // it is.
6
+ import { renderedName } from "peer-ai-skills";
7
+ import { MAP_ITEM_IDS } from "peer-ai-workflow";
8
+ import { MAP_FILE, assess, gaps, loadConfig } from "./assess.js";
9
+ import { count, fail, formatChecks, ok, plural, warn } from "./checks.js";
10
+ import { CONFIG_FILE } from "./detect.js";
11
+ import { checkTracks, checkWorkItems, diagnose } from "./doctor.js";
12
+ import { mapChanges, readMap, readWorkItems } from "./state.js";
13
+ /** Doctor's checks that check already makes itself. */
14
+ const CHECKED_HERE = new Set(["config", "tracks", "map", "work-items"]);
15
+ /**
16
+ * The setup checks doctor makes: every failure, and one line counting the warnings. The skills
17
+ * are left out, since CI never has them.
18
+ */
19
+ export function setupChecks(root, nodeVersion, today) {
20
+ const diagnosis = diagnose(root, nodeVersion, today, { skills: false });
21
+ const others = diagnosis.checks.filter((check) => !CHECKED_HERE.has(check.id));
22
+ const warnings = count(others, "warn");
23
+ return [
24
+ ...others.filter((check) => check.status === "fail"),
25
+ ...(warnings === 0
26
+ ? []
27
+ : [warn("setup", `${plural(warnings, "setup warning")}.`, "Run npx peer-ai doctor for the details.")]),
28
+ ];
29
+ }
30
+ /** Work at these stages says it has been verified and reviewed. */
31
+ const CLAIMS_VERIFIED = ["ship", "done"];
32
+ const OPEN = ["prepare", "build", "verify", "ship"];
33
+ const COMMIT_MAP = `Run peer-ai assess, and commit ${MAP_FILE}.`;
34
+ const isKnownItem = (id) => MAP_ITEM_IDS.includes(id);
35
+ /** The most recent review from each skill: a later pass supersedes an earlier failure. */
36
+ function latestReviews(item) {
37
+ const latest = new Map();
38
+ for (const review of item.reviews ?? []) {
39
+ const seen = latest.get(review.skill);
40
+ if (seen === undefined || Date.parse(review.at) >= Date.parse(seen.at))
41
+ latest.set(review.skill, review);
42
+ }
43
+ return [...latest.values()];
44
+ }
45
+ /**
46
+ * Whether a work item may be at ship or done. `items` are the project's other work items, for its
47
+ * dependencies (RFC 0005): an item can't ship before the items it depends on have.
48
+ */
49
+ export function gateWorkItem(item, config, stage, assessment, items = []) {
50
+ const checks = [];
51
+ const claim = `${item.id} is at ${item.stage}`;
52
+ const backToBuild = "or move the item back to build.";
53
+ const verifyCommand = config.commands?.verify ?? undefined;
54
+ for (const id of item.dependsOn ?? []) {
55
+ const dependency = items.find((other) => other.id === id);
56
+ if (dependency !== undefined && CLAIMS_VERIFIED.includes(dependency.stage))
57
+ continue;
58
+ checks.push(fail("gates", dependency === undefined
59
+ ? `${claim}, but it depends on ${id}, which isn't a work item.`
60
+ : dependency.stage === "cancelled"
61
+ ? `${claim}, but it depends on ${id}, which was cancelled.`
62
+ : `${claim}, but it depends on ${id}, which is only at ${dependency.stage}.`, dependency?.stage === "cancelled" || dependency === undefined
63
+ ? `Remove ${id} from its dependsOn, ${backToBuild}`
64
+ : `Ship ${id} first, ${backToBuild}`));
65
+ }
66
+ if (item.lastVerify?.result === "fail") {
67
+ checks.push(fail("gates", `${claim}, but its last verify failed.`, `Fix what failed and verify again, ${backToBuild}`));
68
+ }
69
+ else if (item.lastVerify === undefined && verifyCommand !== undefined) {
70
+ checks.push(fail("gates", `${claim}, but it has no recorded verify.`, `Run ${verifyCommand} and record the result on the work item, ${backToBuild}`));
71
+ }
72
+ // The reviews the change needs (RFC 0004): a warning for an MVP, a failure in production. A
73
+ // prototype is only told, by next_work.
74
+ const recorded = new Set((item.reviews ?? []).map((review) => review.skill));
75
+ for (const required of item.requiredReviews ?? []) {
76
+ if (recorded.has(required.skill) || stage === "prototype")
77
+ continue;
78
+ const message = `${claim}, but it has no ${required.skill}, which it needs because ${required.reason}.`;
79
+ const fix = `Use the ${renderedName(required.skill)} skill and record its review, ${backToBuild}`;
80
+ checks.push(stage === "production" ? fail("gates", message, fix) : warn("gates", message, fix));
81
+ }
82
+ for (const review of latestReviews(item)) {
83
+ // A prototype accepts a review that didn't cover every rule; a failed one still stops it.
84
+ const allowed = review.result === "pass" || (review.result === "incomplete" && stage === "prototype");
85
+ if (!allowed) {
86
+ checks.push(review.result === "fail"
87
+ ? fail("gates", `${claim}, but its latest ${review.skill} failed.`, `Fix the findings and review again, ${backToBuild}`)
88
+ : fail("gates", `${claim}, but its latest ${review.skill} is incomplete: it didn't check every rule.`, `Review again until every rule is checked, ${backToBuild}`));
89
+ }
90
+ else if (review.report === undefined && stage !== "prototype") {
91
+ // Without a report, the result is the agent's word (RFC 0002): a warning for an MVP, a
92
+ // failure in production.
93
+ const message = `${claim}, but its latest ${review.skill} has no report, so its result is unproven.`;
94
+ const fix = "Write the review's report, and record the review again with it.";
95
+ checks.push(stage === "production" ? fail("gates", message, fix) : warn("gates", message, fix));
96
+ }
97
+ }
98
+ if (item.kind === "gap" && item.stage === "done" && item.gap !== undefined && isKnownItem(item.gap)) {
99
+ const status = assessment.items[item.gap].status;
100
+ if (status !== "present" && status !== "not-applicable") {
101
+ checks.push(fail("gates", `${item.id} says the ${item.gap} gap is done, but a fresh assessment finds it ${status}.`, `Finish the work, ${backToBuild}`));
102
+ }
103
+ }
104
+ return checks;
105
+ }
106
+ function checkGates(items, config, stage, assessment) {
107
+ const claiming = items.filter((item) => CLAIMS_VERIFIED.includes(item.stage));
108
+ const results = claiming.flatMap((item) => gateWorkItem(item, config, stage, assessment, items));
109
+ const failures = results.filter((check) => check.status === "fail");
110
+ const warnings = results.filter((check) => check.status === "warn");
111
+ if (failures.length > 0)
112
+ return [...failures, ...warnings];
113
+ if (claiming.length === 0)
114
+ return [ok("gates", "No work items at ship or done yet")];
115
+ return [
116
+ ok("gates", `${plural(claiming.length, "work item")} at ship or done, each verified and reviewed`),
117
+ ...warnings,
118
+ ];
119
+ }
120
+ /** Every dependency names a work item that exists, and no items wait on each other in a loop (RFC 0005). */
121
+ function checkDependencies(items) {
122
+ const ids = new Set(items.map((item) => item.id));
123
+ const problems = [];
124
+ for (const item of items) {
125
+ for (const id of item.dependsOn ?? []) {
126
+ if (!ids.has(id)) {
127
+ problems.push(fail("plans", `${item.id} depends on ${id}, which isn't a work item.`, `Remove ${id} from its dependsOn.`));
128
+ }
129
+ }
130
+ }
131
+ const byId = new Map(items.map((item) => [item.id, item]));
132
+ const reported = new Set();
133
+ for (const start of items) {
134
+ // Follow the dependencies depth first; meeting an item already on the path is a loop.
135
+ const path = [];
136
+ const visit = (id) => {
137
+ if (path.includes(id))
138
+ return [...path.slice(path.indexOf(id)), id];
139
+ if (reported.has(id))
140
+ return undefined;
141
+ path.push(id);
142
+ for (const next of byId.get(id)?.dependsOn ?? []) {
143
+ const loop = visit(next);
144
+ if (loop !== undefined)
145
+ return loop;
146
+ }
147
+ path.pop();
148
+ return undefined;
149
+ };
150
+ const loop = visit(start.id);
151
+ if (loop !== undefined && !loop.some((id) => reported.has(id))) {
152
+ for (const id of loop)
153
+ reported.add(id);
154
+ problems.push(fail("plans", `Work items wait on each other in a loop: ${loop.join(" → ")}.`, "Remove one of the dependencies."));
155
+ }
156
+ }
157
+ if (problems.length > 0)
158
+ return problems;
159
+ const planned = items.filter((item) => (item.dependsOn ?? []).length > 0).length;
160
+ return planned === 0 ? [] : [ok("plans", `${plural(planned, "work item")} with dependencies, each on a real item`)];
161
+ }
162
+ function checkVerifyCommand(config, stage) {
163
+ if (stage === "prototype" || (config.commands?.verify ?? undefined) !== undefined)
164
+ return [];
165
+ return [
166
+ warn("verify", "No verify command is set, so nothing is verified before work is called done.", `Set commands.verify in ${CONFIG_FILE}, for example "npm test" or "make check".`),
167
+ ];
168
+ }
169
+ function checkMap(root, assessment) {
170
+ const read = readMap(root);
171
+ if (read === undefined)
172
+ return warn("map", "There is no project map yet.", COMMIT_MAP);
173
+ if (!read.ok)
174
+ return fail("map", `${MAP_FILE} ${read.error}`, COMMIT_MAP);
175
+ const changed = mapChanges(read.value, assessment);
176
+ if (changed.length > 0)
177
+ return warn("map", `The project map is out of date: ${changed.join(", ")}.`, COMMIT_MAP);
178
+ return ok("map", "The project map is up to date");
179
+ }
180
+ /** What the stage needs and is missing should each have an open gap work item. */
181
+ function checkGapsTracked(assessment, stage, items) {
182
+ if (stage === "prototype")
183
+ return ok("gaps", "Nothing on the map is required at the prototype stage");
184
+ const needed = gaps(assessment, stage);
185
+ if (needed.length === 0)
186
+ return ok("gaps", `Everything the ${stage} stage needs is in place`);
187
+ const tracked = new Set(items.filter((item) => item.kind === "gap" && OPEN.includes(item.stage)).map((i) => i.gap));
188
+ const untracked = needed.filter((id) => !tracked.has(id));
189
+ if (untracked.length === 0)
190
+ return ok("gaps", `${plural(needed.length, "gap")} for ${stage}, each with a work item`);
191
+ return warn("gaps", `${plural(untracked.length, "gap")} for ${stage} with no work item: ${untracked.join(", ")}.`, "Add a gap work item for each, so they are planned rather than forgotten.");
192
+ }
193
+ export function evaluate(root, config) {
194
+ const stage = config.project.stage ?? "mvp";
195
+ const assessment = assess(root, config, stage);
196
+ const trackFailures = checkTracks(root, config).filter((check) => check.status === "fail");
197
+ const items = readWorkItems(root).flatMap(({ item }) => (item.ok ? [item.value] : []));
198
+ const checks = [
199
+ ok("config", `${CONFIG_FILE} is valid`),
200
+ ...(trackFailures.length > 0 ? trackFailures : [ok("tracks", "Every track's folder exists")]),
201
+ checkMap(root, assessment),
202
+ ...checkWorkItems(root, config),
203
+ ...checkDependencies(items),
204
+ ...checkGates(items, config, stage, assessment),
205
+ ...checkVerifyCommand(config, stage),
206
+ checkGapsTracked(assessment, stage, items),
207
+ ];
208
+ const setup = setupChecks(root);
209
+ return {
210
+ name: config.project.name,
211
+ stage,
212
+ ok: count(checks, "fail") + count(setup, "fail") === 0,
213
+ checks,
214
+ setup,
215
+ };
216
+ }
217
+ export function formatVerdict(verdict) {
218
+ const all = [...verdict.checks, ...verdict.setup];
219
+ const failures = count(all, "fail");
220
+ const warnings = count(all, "warn");
221
+ const andWarnings = warnings === 0 ? "" : `, and ${plural(warnings, "warning")}`;
222
+ return [
223
+ `Peer AI check: ${verdict.name} (stage: ${verdict.stage})`,
224
+ "",
225
+ ...formatChecks(verdict.checks),
226
+ ...(verdict.setup.length === 0 ? [] : ["", "Setup, as peer-ai doctor checks it:", ...formatChecks(verdict.setup)]),
227
+ "",
228
+ failures > 0
229
+ ? `Failed: ${plural(failures, "problem")}${andWarnings}.`
230
+ : `Passed${warnings === 0 ? "" : `, with ${plural(warnings, "warning")}`}.`,
231
+ ];
232
+ }
233
+ /** Exit code 0 when it passes, even with warnings; 1 when it fails; 2 without a valid config. */
234
+ export function runCheck(options, out) {
235
+ const { config, errors } = loadConfig(options.cwd);
236
+ if (config === undefined) {
237
+ if (errors === undefined)
238
+ out.error(`There is no ${CONFIG_FILE}. Run peer-ai init first.`);
239
+ else {
240
+ out.error(`${CONFIG_FILE} is not valid:`);
241
+ for (const error of errors)
242
+ out.error(` ${error}`);
243
+ }
244
+ return 2;
245
+ }
246
+ const verdict = evaluate(options.cwd, config);
247
+ if (options.json)
248
+ out.log(JSON.stringify(verdict, null, 2));
249
+ else
250
+ for (const line of formatVerdict(verdict))
251
+ out.log(line);
252
+ return verdict.ok ? 0 : 1;
253
+ }
@@ -0,0 +1,15 @@
1
+ export type CheckStatus = "ok" | "warn" | "fail" | "skip";
2
+ export interface Check {
3
+ id: string;
4
+ status: CheckStatus;
5
+ message: string;
6
+ fix?: string;
7
+ }
8
+ export declare const ok: (id: string, message: string) => Check;
9
+ export declare const skip: (id: string, message: string) => Check;
10
+ export declare const warn: (id: string, message: string, fix: string) => Check;
11
+ export declare const fail: (id: string, message: string, fix: string) => Check;
12
+ export declare const plural: (count: number, word: string) => string;
13
+ export declare const count: (checks: Check[], status: CheckStatus) => number;
14
+ /** One line per check, with its fix indented beneath it. */
15
+ export declare function formatChecks(checks: Check[]): string[];
package/dist/checks.js ADDED
@@ -0,0 +1,15 @@
1
+ // The results `peer-ai doctor` and `peer-ai check` report, and how they are printed.
2
+ export const ok = (id, message) => ({ id, status: "ok", message });
3
+ export const skip = (id, message) => ({ id, status: "skip", message });
4
+ export const warn = (id, message, fix) => ({ id, status: "warn", message, fix });
5
+ export const fail = (id, message, fix) => ({ id, status: "fail", message, fix });
6
+ export const plural = (count, word) => `${String(count)} ${word}${count === 1 ? "" : "s"}`;
7
+ export const count = (checks, status) => checks.filter((check) => check.status === status).length;
8
+ const SYMBOL = { ok: "✓", warn: "!", fail: "✗", skip: "–" };
9
+ /** One line per check, with its fix indented beneath it. */
10
+ export function formatChecks(checks) {
11
+ return checks.flatMap((check) => [
12
+ ` ${SYMBOL[check.status]} ${check.message}`,
13
+ ...(check.fix === undefined ? [] : [` ${check.fix}`]),
14
+ ]);
15
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { type Output } from "./init.ts";
3
+ import { type Prompter } from "./prompter.ts";
4
+ export interface Io {
5
+ cwd: string;
6
+ out: Output;
7
+ /** Present when questions can be asked, which means a terminal is attached. */
8
+ prompter?: Prompter;
9
+ }
10
+ export declare function main(argv: string[], io: Io): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ // The peer-ai command. Exit codes: 0 success, 1 refused, cancelled or problems found, 2 usage or
3
+ // config error.
4
+ import { realpathSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { parseArgs } from "node:util";
8
+ import { CLI_COMMAND_IDS, TOOL_IDS } from "peer-ai-workflow";
9
+ import { runAssess } from "./assess.js";
10
+ import { runCheck } from "./check.js";
11
+ import { runCheckDocument } from "./document.js";
12
+ import { runDoctor } from "./doctor.js";
13
+ import { runFeedback } from "./feedback.js";
14
+ import { serveStdio } from "./mcp.js";
15
+ import { runInit } from "./init.js";
16
+ import { VERSION } from "./package-info.js";
17
+ import { createTerminalPrompter } from "./prompter.js";
18
+ import { formatReport } from "./report.js";
19
+ import { runRender } from "./render.js";
20
+ import { runCheckReport } from "./work.js";
21
+ const STAGES = ["prototype", "mvp", "production"];
22
+ const HELP = `peer-ai: from a brief to a shipped product, with any AI tool
23
+
24
+ Usage:
25
+ peer-ai init [options] Set up Peer AI in this repository
26
+ peer-ai assess [options] Map what the project has and what its stage still needs
27
+ peer-ai render [options] Set up each AI tool in the config: instructions and the MCP server
28
+ peer-ai doctor [options] Check that Peer AI is set up correctly, and how to fix it
29
+ peer-ai check [options] The CI gate: fail when work claims more than its record shows
30
+ peer-ai check-report <file> Check a review's report, as the record_review tool does
31
+ peer-ai check-document <file> --skill <skill>
32
+ Check a document against its skill's template, as the
33
+ check_document tool does
34
+ peer-ai feedback List the feedback drafts your AI tool wrote about Peer AI
35
+ peer-ai feedback send <draft>
36
+ Send a draft as an issue on Peer AI's repository
37
+ peer-ai feedback drop <draft>
38
+ Delete a draft
39
+ peer-ai mcp Start the MCP server that AI tools connect to, over stdio
40
+
41
+ Options for init:
42
+ -y, --yes Accept what init detects instead of asking
43
+ --dry-run Print the config instead of writing it
44
+ --name <name> The project's name
45
+ --stage <stage> prototype, mvp or production
46
+ --team <team> solo or team
47
+ --tool <tool> An AI tool you use; repeat for several
48
+ (${TOOL_IDS.join(", ")})
49
+
50
+ Options for assess:
51
+ --target <stage> Assess against a stage other than the project's own,
52
+ for example production before a launch
53
+ --json Print the project map as JSON instead of the report
54
+ --dry-run Don't write .peer-ai/map.json
55
+
56
+ Options for render:
57
+ --check Change nothing; fail when a file is out of date (for CI)
58
+ --skills Write only the skills (what each tool's setup step runs)
59
+ --quiet Print nothing unless something fails
60
+
61
+ Options for check-report:
62
+ --skill <skill> The skill the report is for (default: the one it names)
63
+ --work-item <id> The work item it's for, when there is one
64
+ --json Print the result as JSON
65
+
66
+ Options for check-document:
67
+ --skill <skill> The document skill that wrote it, such as requirements-analysis
68
+ --template <name> Which of the skill's templates it follows (default: the main one)
69
+ --json Print the result as JSON
70
+
71
+ Options for doctor and check:
72
+ --json Print the checks as JSON
73
+
74
+ Other:
75
+ -h, --help Show this help
76
+ -v, --version Show the version`;
77
+ function oneOf(value, allowed, flag) {
78
+ if (allowed.includes(value))
79
+ return value;
80
+ throw new Error(`${flag} must be one of: ${allowed.join(", ")}`);
81
+ }
82
+ async function init(args, io) {
83
+ const { values } = parseArgs({
84
+ args,
85
+ strict: true,
86
+ options: {
87
+ yes: { type: "boolean", short: "y" },
88
+ "dry-run": { type: "boolean" },
89
+ name: { type: "string" },
90
+ stage: { type: "string" },
91
+ team: { type: "string" },
92
+ tool: { type: "string", multiple: true },
93
+ },
94
+ });
95
+ const stage = values.stage === undefined ? undefined : oneOf(values.stage, STAGES, "--stage");
96
+ const team = values.team === undefined ? undefined : oneOf(values.team, ["solo", "team"], "--team");
97
+ const tools = values.tool?.map((tool) => oneOf(tool, TOOL_IDS, "--tool"));
98
+ return runInit({
99
+ cwd: io.cwd,
100
+ yes: values.yes === true,
101
+ dryRun: values["dry-run"] === true,
102
+ ...(values.name === undefined ? {} : { name: values.name }),
103
+ ...(stage === undefined ? {} : { stage }),
104
+ ...(team === undefined ? {} : { team }),
105
+ ...(tools === undefined ? {} : { tools }),
106
+ }, io.prompter, io.out);
107
+ }
108
+ function assess(args, io) {
109
+ const { values } = parseArgs({
110
+ args,
111
+ strict: true,
112
+ options: {
113
+ target: { type: "string" },
114
+ json: { type: "boolean" },
115
+ "dry-run": { type: "boolean" },
116
+ },
117
+ });
118
+ const target = values.target === undefined ? undefined : oneOf(values.target, STAGES, "--target");
119
+ return runAssess({
120
+ cwd: io.cwd,
121
+ json: values.json === true,
122
+ dryRun: values["dry-run"] === true,
123
+ ...(target === undefined ? {} : { target }),
124
+ }, io.out, formatReport);
125
+ }
126
+ function doctor(args, io) {
127
+ const { values } = parseArgs({ args, strict: true, options: { json: { type: "boolean" } } });
128
+ return runDoctor({ cwd: io.cwd, json: values.json === true }, io.out);
129
+ }
130
+ function check(args, io) {
131
+ const { values } = parseArgs({ args, strict: true, options: { json: { type: "boolean" } } });
132
+ return runCheck({ cwd: io.cwd, json: values.json === true }, io.out);
133
+ }
134
+ function checkReportCommand(args, io) {
135
+ const { values, positionals } = parseArgs({
136
+ args,
137
+ strict: true,
138
+ allowPositionals: true,
139
+ options: { skill: { type: "string" }, "work-item": { type: "string" }, json: { type: "boolean" } },
140
+ });
141
+ const [report] = positionals;
142
+ if (report === undefined)
143
+ throw new TypeError("Give the report's path, such as peer-ai check-report .peer-ai/reports/project/security-review.json");
144
+ return runCheckReport({ cwd: io.cwd, report, skill: values.skill, workItem: values["work-item"], json: values.json === true }, io.out);
145
+ }
146
+ function checkDocumentCommand(args, io) {
147
+ const { values, positionals } = parseArgs({
148
+ args,
149
+ strict: true,
150
+ allowPositionals: true,
151
+ options: { skill: { type: "string" }, template: { type: "string" }, json: { type: "boolean" } },
152
+ });
153
+ const [path] = positionals;
154
+ if (path === undefined || values.skill === undefined)
155
+ throw new TypeError("Give the document's path and its skill, such as peer-ai check-document docs/requirements.md --skill requirements-analysis");
156
+ return runCheckDocument({ cwd: io.cwd, path, skill: values.skill, template: values.template, json: values.json === true }, io.out);
157
+ }
158
+ function render(args, io) {
159
+ const { values } = parseArgs({
160
+ args,
161
+ strict: true,
162
+ options: { check: { type: "boolean" }, skills: { type: "boolean" }, quiet: { type: "boolean" } },
163
+ });
164
+ return runRender({ cwd: io.cwd, check: values.check === true, skills: values.skills === true, quiet: values.quiet === true }, io.out);
165
+ }
166
+ function feedback(args, io) {
167
+ const { positionals } = parseArgs({ args, strict: true, allowPositionals: true, options: {} });
168
+ const [action, draft] = positionals;
169
+ return runFeedback({ cwd: io.cwd, action, draft }, io.out);
170
+ }
171
+ async function mcp(args, io) {
172
+ parseArgs({ args, strict: true, options: {} });
173
+ await serveStdio(io.cwd);
174
+ return 0;
175
+ }
176
+ const COMMANDS = {
177
+ init,
178
+ assess,
179
+ render,
180
+ doctor,
181
+ check,
182
+ "check-report": checkReportCommand,
183
+ "check-document": checkDocumentCommand,
184
+ feedback,
185
+ mcp,
186
+ };
187
+ const isCommand = (value) => CLI_COMMAND_IDS.includes(value);
188
+ export async function main(argv, io) {
189
+ const [command, ...rest] = argv;
190
+ if (command === undefined || command === "-h" || command === "--help" || command === "help") {
191
+ io.out.log(HELP);
192
+ return 0;
193
+ }
194
+ if (command === "-v" || command === "--version") {
195
+ io.out.log(VERSION);
196
+ return 0;
197
+ }
198
+ const run = isCommand(command) ? COMMANDS[command] : undefined;
199
+ if (run === undefined) {
200
+ io.out.error(`Unknown command "${command}". Run peer-ai --help to see the commands.`);
201
+ return 2;
202
+ }
203
+ try {
204
+ return await run(rest, io);
205
+ }
206
+ catch (error) {
207
+ if (error instanceof TypeError || (error instanceof Error && error.message.startsWith("--"))) {
208
+ io.out.error(`${error.message}. Run peer-ai --help for usage.`);
209
+ return 2;
210
+ }
211
+ throw error;
212
+ }
213
+ }
214
+ // An installed peer-ai runs through a link in node_modules/.bin, so compare the file the link points to.
215
+ const invokedDirectly = process.argv[1] !== undefined &&
216
+ realpathSync(resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url));
217
+ if (invokedDirectly) {
218
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
219
+ process.exitCode = await main(process.argv.slice(2), {
220
+ cwd: process.cwd(),
221
+ out: {
222
+ log: (line) => {
223
+ console.log(line);
224
+ },
225
+ error: (line) => {
226
+ console.error(line);
227
+ },
228
+ },
229
+ ...(interactive ? { prompter: createTerminalPrompter() } : {}),
230
+ });
231
+ }
@@ -0,0 +1,42 @@
1
+ import type { ToolId } from "peer-ai-workflow";
2
+ import { CONFIG_FILE } from "peer-ai-workflow";
3
+ export { CONFIG_FILE };
4
+ export type TrackKind = "web" | "mobile" | "desktop" | "backend" | "infrastructure" | "library" | "cli" | "other";
5
+ export type RepoHost = "github" | "gitlab" | "bitbucket" | "azure-devops" | "other";
6
+ export interface DetectedTrack {
7
+ id: string;
8
+ kind: TrackKind;
9
+ path?: string;
10
+ stack: string[];
11
+ /** Where it deploys, when a platform's config file says so. */
12
+ deploy?: string;
13
+ }
14
+ export interface Detected {
15
+ name: string;
16
+ description?: string;
17
+ origin: "new" | "existing";
18
+ tools: ToolId[];
19
+ repo: {
20
+ host?: RepoHost;
21
+ remote: string | null;
22
+ };
23
+ /** Present when a CI pipeline already exists, so Peer AI extends it instead of adding another. */
24
+ delivery?: {
25
+ ci: "existing";
26
+ pipeline: string;
27
+ };
28
+ tracks: DetectedTrack[];
29
+ hasConfig: boolean;
30
+ }
31
+ export declare function slugify(text: string): string;
32
+ export declare function detectDeploy(dir: string): string | undefined;
33
+ /** The project's name from its own project file, in whichever language it's written. */
34
+ export declare function detectName(root: string): string;
35
+ export declare function detectTracks(root: string, projectName: string): DetectedTrack[];
36
+ export declare function detectDelivery(root: string): Detected["delivery"];
37
+ export declare function detectTools(root: string): ToolId[];
38
+ /** The host name of a git remote, from an https, ssh:// or scp-style (git@host:path) URL. */
39
+ export declare function remoteHostname(url: string): string | undefined;
40
+ export declare function classifyHost(hostname: string | undefined): RepoHost;
41
+ export declare function detectRepo(root: string): Detected["repo"];
42
+ export declare function detect(root: string): Detected;