peer-ai 1.0.0-next.0 → 1.0.0-next.2
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/README.md +55 -6
- package/dist/cli.js +16 -3
- package/dist/doctor.js +9 -4
- package/dist/gate.d.ts +29 -0
- package/dist/gate.js +141 -0
- package/dist/init.d.ts +3 -2
- package/dist/init.js +3 -3
- package/dist/migrate.d.ts +95 -0
- package/dist/migrate.js +796 -0
- package/dist/pipeline.d.ts +4 -0
- package/dist/pipeline.js +6 -5
- package/dist/render.d.ts +1 -1
- package/dist/render.js +67 -3
- package/dist/v0-fingerprints.d.ts +5 -0
- package/dist/v0-fingerprints.js +393 -0
- package/dist/v0.d.ts +210 -0
- package/dist/v0.js +682 -0
- package/dist/work.d.ts +2 -0
- package/dist/work.js +8 -7
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ npm install --save-dev peer-ai
|
|
|
27
27
|
| Command | What it does |
|
|
28
28
|
|---------|--------------|
|
|
29
29
|
| [`peer-ai init`](#peer-ai-init) | Sets up Peer AI in a repository: one config file, from what it detects |
|
|
30
|
+
| [`peer-ai migrate`](#peer-ai-migrate) | Moves a project from its copy of v0 onto Peer AI 1.0 |
|
|
30
31
|
| [`peer-ai assess`](#peer-ai-assess) | Maps what the project has and what its stage still needs |
|
|
31
32
|
| [`peer-ai render`](#peer-ai-render) | Connects each AI tool: instructions, the MCP server and the skills |
|
|
32
33
|
| [`peer-ai doctor`](#peer-ai-doctor) | Checks the setup and says how to fix what isn't right |
|
|
@@ -76,6 +77,45 @@ It never overwrites an existing `peer-ai.config.json`, and it never assumes anyt
|
|
|
76
77
|
|
|
77
78
|
`0` success, `1` refused (a config already exists) or cancelled, `2` a usage error.
|
|
78
79
|
|
|
80
|
+
## `peer-ai migrate`
|
|
81
|
+
|
|
82
|
+
Moves a project from v0, when Peer AI was a playbook copied into a `peer-ai/` folder, onto the package ([RFC 0008](../../rfcs/0008-moving-a-v0-project-onto-1-0.md)).
|
|
83
|
+
|
|
84
|
+
In the project's folder:
|
|
85
|
+
|
|
86
|
+
1. **Commit or stash any work in progress.** `migrate` won't start with uncommitted changes, so the move is a change of its own.
|
|
87
|
+
2. **Make a branch:** `git switch -c peer-ai-1.0`
|
|
88
|
+
3. **See the plan, changing nothing:** `npx peer-ai migrate --dry-run`
|
|
89
|
+
4. **Migrate:** `npx peer-ai migrate`. It asks the same few questions as `init`, already filled in from what it found.
|
|
90
|
+
5. **Review the change, commit it, and open a pull request.** To undo it instead: `git stash --include-untracked`.
|
|
91
|
+
6. **In your next session,** your AI tool brings up each decision `migrate` left in `docs/peer-ai-migration.md`. Go through them together, then delete the file.
|
|
92
|
+
7. **On GitHub,** make `peer-ai check` a required check, so nothing merges without the gate.
|
|
93
|
+
|
|
94
|
+
It converts what it can read with certainty:
|
|
95
|
+
|
|
96
|
+
| From v0 | Into |
|
|
97
|
+
|---------|------|
|
|
98
|
+
| The Project settings table in the workflow driver | `commands.verify`, `tracker`, `repo.branchNaming`, `repo.mergePolicy` and `design.reference` |
|
|
99
|
+
| `phase-config.json` | `models`, each skill's add-ons and notes, and each activity's notes. A phase that says a part is dormant marks it `dormant`. |
|
|
100
|
+
| Markdown files in `docs/standards/` | `standards.documents`, as standards or an addendum, each scoped to its part |
|
|
101
|
+
| `.peer-ai-state.json` | A work item for each ticket in progress. The tracker keeps the rest. |
|
|
102
|
+
|
|
103
|
+
Then it deletes the `peer-ai/` folder and the state file, takes v0's text out of `CLAUDE.md`, `AGENTS.md` and the other instruction files, maps the project and sets up the AI tools, as `assess` and `render` do.
|
|
104
|
+
|
|
105
|
+
Nothing is dropped silently. Whatever needs judgement, it copies word for word into `docs/peer-ai-migration.md`: the files the project edited in `peer-ai/`, the text it took out of the instruction files, the settings it couldn't place, and anything it left alone that still names v0. A work item, `migrate-v0`, points there, so your AI tool brings each decision up in the next session.
|
|
106
|
+
|
|
107
|
+
It tells the files the project changed from v0's own by their fingerprints: a list of every file in every v0 version, which ships with the package.
|
|
108
|
+
|
|
109
|
+
It never commits. It won't start with uncommitted changes, so the migration is a change of its own: review it, commit it on a branch, and open a pull request. `git stash --include-untracked` undoes it all. It deletes only files git can bring back, leaves files git ignores where they are, and refuses to write outside the project.
|
|
110
|
+
|
|
111
|
+
### Options
|
|
112
|
+
|
|
113
|
+
The same as `init`. With `--dry-run`, it prints what it would convert, rewrite, delete and leave for a decision, and changes nothing.
|
|
114
|
+
|
|
115
|
+
### Exit codes
|
|
116
|
+
|
|
117
|
+
`0` success, `1` refused (a config already exists, there is no v0 copy, the project isn't in git or has uncommitted changes) or cancelled, `2` a usage error.
|
|
118
|
+
|
|
79
119
|
## `peer-ai assess`
|
|
80
120
|
|
|
81
121
|
Maps what a project already has, and what its stage still needs. Run it on any project, at any point: a brief with no code, a prototype, or a product in production with no documents at all.
|
|
@@ -165,7 +205,19 @@ npx peer-ai render
|
|
|
165
205
|
|
|
166
206
|
`AGENTS.md` gets the block whenever a tool other than Claude Code is listed, when it already exists, or when `CLAUDE.md` imports it.
|
|
167
207
|
|
|
168
|
-
|
|
208
|
+
### The CI gate
|
|
209
|
+
|
|
210
|
+
Render sets up `peer-ai check` in the project's CI, so no one has to remember to ([RFC 0009](../../rfcs/0009-the-ci-gate-set-up-by-render.md)):
|
|
211
|
+
|
|
212
|
+
- **On GitHub Actions,** it writes `.github/workflows/peer-ai.yml`: one job, `peer-ai check`, that installs Node 24 and runs the exact version of Peer AI render ran as, so it works with or without a `package.json`. Its actions are pinned to commits. Render keeps the file up to date while it's as render left it, and leaves it alone once someone changes it by hand; `doctor` then checks it still runs the gate. A version bump updates it.
|
|
213
|
+
- **On any other CI,** it prints the step to add.
|
|
214
|
+
- **When a workflow of the project's own already runs `peer-ai check`,** it adds nothing.
|
|
215
|
+
|
|
216
|
+
Make `peer-ai check` a required check in the repository's settings, so nothing merges without it. To run the gate some other way, set `"delivery": { "gate": false }`.
|
|
217
|
+
|
|
218
|
+
### Instructions
|
|
219
|
+
|
|
220
|
+
The instructions are short: how to work through the MCP server, the project's parts, its commands, its compliance packs and its own rules, and the project's settings for models, skills and activities when it has any: the models to use, the add-ons, checklists and notes for each skill, and the files to read and notes for each activity. The server serves the detail when it's needed, rather than every rule on every turn.
|
|
169
221
|
|
|
170
222
|
### Skills
|
|
171
223
|
|
|
@@ -245,6 +297,7 @@ npx peer-ai doctor
|
|
|
245
297
|
| AI tools | A tool set up in the repository, such as a `CLAUDE.md` or `.cursor/`, that the config doesn't list |
|
|
246
298
|
| What render writes | Instructions or MCP registrations that no longer match the config, or skills that are missing or out of date |
|
|
247
299
|
| CI | A config that says there is no CI when the repository has a pipeline, which would lead Peer AI to add a second one |
|
|
300
|
+
| The CI gate | A pipeline that doesn't run `peer-ai check`, or a gate workflow that's out of date or no longer runs it. Through `next_work`, the AI tool hears about it in every session. |
|
|
248
301
|
| The project map | Missing, not valid, or out of date. It runs a fresh assessment and lists every item whose status has changed since `.peer-ai/map.json` was written. |
|
|
249
302
|
| Work items | A file in `.peer-ai/work/` that isn't valid, isn't named after its id, or names a track the config doesn't have |
|
|
250
303
|
| Git | A folder that isn't a git repository, or a `.gitignore` that hides Peer AI's files from the team and CI |
|
|
@@ -302,11 +355,7 @@ A review's result is worked out from its report when it is recorded, so an open
|
|
|
302
355
|
|
|
303
356
|
### In CI
|
|
304
357
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
```yaml
|
|
308
|
-
- run: npx peer-ai check
|
|
309
|
-
```
|
|
358
|
+
`render` sets it up: see [the CI gate](#the-ci-gate). By hand, it's a step after the project's own checks that runs, with Node 24, `npx -y peer-ai@<version> check`.
|
|
310
359
|
|
|
311
360
|
### Options
|
|
312
361
|
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { runDoctor } from "./doctor.js";
|
|
|
13
13
|
import { runFeedback } from "./feedback.js";
|
|
14
14
|
import { serveStdio } from "./mcp.js";
|
|
15
15
|
import { runInit } from "./init.js";
|
|
16
|
+
import { runMigrate } from "./migrate.js";
|
|
16
17
|
import { VERSION } from "./package-info.js";
|
|
17
18
|
import { createTerminalPrompter } from "./prompter.js";
|
|
18
19
|
import { formatReport } from "./report.js";
|
|
@@ -23,6 +24,7 @@ const HELP = `peer-ai: from a brief to a shipped product, with any AI tool
|
|
|
23
24
|
|
|
24
25
|
Usage:
|
|
25
26
|
peer-ai init [options] Set up Peer AI in this repository
|
|
27
|
+
peer-ai migrate [options] Move a project from its copy of v0 onto Peer AI 1.0
|
|
26
28
|
peer-ai assess [options] Map what the project has and what its stage still needs
|
|
27
29
|
peer-ai render [options] Set up each AI tool in the config: instructions and the MCP server
|
|
28
30
|
peer-ai doctor [options] Check that Peer AI is set up correctly, and how to fix it
|
|
@@ -47,6 +49,9 @@ Options for init:
|
|
|
47
49
|
--tool <tool> An AI tool you use; repeat for several
|
|
48
50
|
(${TOOL_IDS.join(", ")})
|
|
49
51
|
|
|
52
|
+
Options for migrate: the same as init. It never commits, and it won't start on
|
|
53
|
+
uncommitted changes. --dry-run prints what it would do, changing nothing.
|
|
54
|
+
|
|
50
55
|
Options for assess:
|
|
51
56
|
--target <stage> Assess against a stage other than the project's own,
|
|
52
57
|
for example production before a launch
|
|
@@ -79,7 +84,8 @@ function oneOf(value, allowed, flag) {
|
|
|
79
84
|
return value;
|
|
80
85
|
throw new Error(`${flag} must be one of: ${allowed.join(", ")}`);
|
|
81
86
|
}
|
|
82
|
-
|
|
87
|
+
/** The options init and migrate share. */
|
|
88
|
+
function initOptions(args, io) {
|
|
83
89
|
const { values } = parseArgs({
|
|
84
90
|
args,
|
|
85
91
|
strict: true,
|
|
@@ -95,7 +101,7 @@ async function init(args, io) {
|
|
|
95
101
|
const stage = values.stage === undefined ? undefined : oneOf(values.stage, STAGES, "--stage");
|
|
96
102
|
const team = values.team === undefined ? undefined : oneOf(values.team, ["solo", "team"], "--team");
|
|
97
103
|
const tools = values.tool?.map((tool) => oneOf(tool, TOOL_IDS, "--tool"));
|
|
98
|
-
return
|
|
104
|
+
return {
|
|
99
105
|
cwd: io.cwd,
|
|
100
106
|
yes: values.yes === true,
|
|
101
107
|
dryRun: values["dry-run"] === true,
|
|
@@ -103,7 +109,13 @@ async function init(args, io) {
|
|
|
103
109
|
...(stage === undefined ? {} : { stage }),
|
|
104
110
|
...(team === undefined ? {} : { team }),
|
|
105
111
|
...(tools === undefined ? {} : { tools }),
|
|
106
|
-
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function init(args, io) {
|
|
115
|
+
return runInit(initOptions(args, io), io.prompter, io.out);
|
|
116
|
+
}
|
|
117
|
+
function migrate(args, io) {
|
|
118
|
+
return runMigrate(initOptions(args, io), io.prompter, io.out);
|
|
107
119
|
}
|
|
108
120
|
function assess(args, io) {
|
|
109
121
|
const { values } = parseArgs({
|
|
@@ -175,6 +187,7 @@ async function mcp(args, io) {
|
|
|
175
187
|
}
|
|
176
188
|
const COMMANDS = {
|
|
177
189
|
init,
|
|
190
|
+
migrate,
|
|
178
191
|
assess,
|
|
179
192
|
render,
|
|
180
193
|
doctor,
|
package/dist/doctor.js
CHANGED
|
@@ -10,6 +10,7 @@ import { DOMAINS } from "peer-ai-workflow";
|
|
|
10
10
|
import { LEGACY_MARKERS, MAP_FILE, assess, loadConfig } from "./assess.js";
|
|
11
11
|
import { count, fail, formatChecks, ok, plural, skip, warn } from "./checks.js";
|
|
12
12
|
import { checkEnforcers, checkProfiles } from "./enforcers.js";
|
|
13
|
+
import { GATE_FILE, checkGate } from "./gate.js";
|
|
13
14
|
import { WORKFLOW_FILE } from "./pipeline.js";
|
|
14
15
|
import { RUFF_FILE } from "./ruff.js";
|
|
15
16
|
import { CONFIG_FILE, detectDelivery, detectName, detectTools, detectTracks } from "./detect.js";
|
|
@@ -105,7 +106,8 @@ function checkTools(root, config) {
|
|
|
105
106
|
return ok("tools", `AI tools: ${listed.join(", ")}`);
|
|
106
107
|
}
|
|
107
108
|
/** What render writes for the AI tools still matches the config. */
|
|
108
|
-
|
|
109
|
+
/** Files with a check of their own: the enforcers, and the CI gate. */
|
|
110
|
+
const ENFORCER_FILES = [WORKFLOW_FILE, RUFF_FILE, GATE_FILE];
|
|
109
111
|
function checkRendered(root, config, skills) {
|
|
110
112
|
const plan = planRender(root, config);
|
|
111
113
|
const withSkills = skills || config.skills?.commit === true;
|
|
@@ -240,9 +242,11 @@ function checkGit(root) {
|
|
|
240
242
|
function checkLegacy(root) {
|
|
241
243
|
if (!LEGACY_MARKERS.some((marker) => existsSync(join(root, marker))))
|
|
242
244
|
return [];
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
245
|
+
// migrate builds the config itself, so it only runs before there is one.
|
|
246
|
+
const fix = existsSync(join(root, CONFIG_FILE))
|
|
247
|
+
? `Move any changes your project made to it into ${CONFIG_FILE}, then remove it with: git rm -r peer-ai`
|
|
248
|
+
: "Run npx peer-ai migrate. It moves what your project changed into the config, and removes the folder.";
|
|
249
|
+
return [warn("legacy", "The peer-ai/ folder is a copy of the v0 playbook, which Peer AI 1.0 doesn't read.", fix)];
|
|
246
250
|
}
|
|
247
251
|
export function diagnose(root, nodeVersion = process.versions.node, today = new Date(), options = {}) {
|
|
248
252
|
const { check: configCheck, config } = checkConfig(root);
|
|
@@ -257,6 +261,7 @@ export function diagnose(root, nodeVersion = process.versions.node, today = new
|
|
|
257
261
|
? needsConfig("render", "What render writes")
|
|
258
262
|
: [checkRendered(root, config, options.skills ?? true)]),
|
|
259
263
|
...(config === undefined ? needsConfig("delivery", "CI") : [checkDelivery(root, config)]),
|
|
264
|
+
...(config === undefined ? needsConfig("gate", "The CI gate") : checkGate(root, config)),
|
|
260
265
|
...(config === undefined
|
|
261
266
|
? needsConfig("standards", "Rules set aside")
|
|
262
267
|
: checkStandards(config, today.toISOString().slice(0, 10))),
|
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PeerAiConfig } from "peer-ai-workflow";
|
|
2
|
+
import { type Check } from "./checks.ts";
|
|
3
|
+
export declare const GATE_FILE = ".github/workflows/peer-ai.yml";
|
|
4
|
+
/** The gate's command, with the exact version render runs as, so CI needs no package.json. */
|
|
5
|
+
export declare const gateCommand: () => string;
|
|
6
|
+
/** The workflow render writes for the gate: Node 24 and the exact version, so any project can run it. */
|
|
7
|
+
export declare function gateWorkflow(config: PeerAiConfig): string;
|
|
8
|
+
export interface Gate {
|
|
9
|
+
/** What render does to the gate's workflow, on GitHub Actions. */
|
|
10
|
+
planned?: {
|
|
11
|
+
path: string;
|
|
12
|
+
action: "create" | "update" | "unchanged" | "kept";
|
|
13
|
+
content?: string;
|
|
14
|
+
note?: string;
|
|
15
|
+
};
|
|
16
|
+
/** The step to add by hand, for any other CI. */
|
|
17
|
+
manual?: string;
|
|
18
|
+
/** Where the gate runs already, when it does. */
|
|
19
|
+
runsIn?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Whether the project's CI is GitHub Actions: workflows of its own, or the gate render wrote. */
|
|
22
|
+
export declare function onGitHubActions(root: string): boolean;
|
|
23
|
+
/** Where the gate stands, and what render does about it. */
|
|
24
|
+
export declare function planGate(root: string, config: PeerAiConfig): Gate;
|
|
25
|
+
/**
|
|
26
|
+
* peer-ai doctor's check: CI runs the gate, unless the config turns it off. With no CI yet there is
|
|
27
|
+
* nothing to say; the delivery check reports that already.
|
|
28
|
+
*/
|
|
29
|
+
export declare function checkGate(root: string, config: PeerAiConfig): Check[];
|
package/dist/gate.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// The CI gate (RFC 0009): CI runs peer-ai check on every change, so work is held to its record
|
|
2
|
+
// without anyone having to remember to set that up. On GitHub Actions, render writes a workflow of
|
|
3
|
+
// its own for it, owned the way the security workflow is: rewritten while it's as render left it,
|
|
4
|
+
// and left alone once someone changes it by hand. For any other CI, render prints the step to add.
|
|
5
|
+
// peer-ai doctor warns while nothing runs it, so the AI tool hears about it in every session.
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { ok, skip, warn } from "./checks.js";
|
|
9
|
+
import { detectDelivery } from "./detect.js";
|
|
10
|
+
import { VERSION } from "./package-info.js";
|
|
11
|
+
import { CHECKOUT, SETUP_NODE, WORKFLOW_FILE, sameFile, unchangedSinceRender, withHash } from "./pipeline.js";
|
|
12
|
+
export const GATE_FILE = ".github/workflows/peer-ai.yml";
|
|
13
|
+
const WORKFLOWS = ".github/workflows";
|
|
14
|
+
/** Workflows Peer AI writes for itself, which on their own aren't a project's CI. */
|
|
15
|
+
const OWN = [GATE_FILE, WORKFLOW_FILE, `${WORKFLOWS}/copilot-setup-steps.yml`];
|
|
16
|
+
/** A command that runs the gate: peer-ai check, with or without a version. */
|
|
17
|
+
const GATE_COMMAND = /\bpeer-ai(@\S+)?\s+check\b/;
|
|
18
|
+
/** Whether a CI file runs the gate: a line with the command, not a comment or a job's name. */
|
|
19
|
+
const runsGate = (text) => text.split(/\r?\n/).some((line) => !/^\s*#/.test(line) && !/^\s*-?\s*name:/.test(line) && GATE_COMMAND.test(line));
|
|
20
|
+
/** The gate's command, with the exact version render runs as, so CI needs no package.json. */
|
|
21
|
+
export const gateCommand = () => `npx -y peer-ai@${VERSION} check`;
|
|
22
|
+
const read = (root, path) => {
|
|
23
|
+
const full = join(root, path);
|
|
24
|
+
return existsSync(full) && statSync(full).isFile() ? readFileSync(full, "utf8") : undefined;
|
|
25
|
+
};
|
|
26
|
+
/** The files of a pipeline: a folder's YAML files, or the file itself. */
|
|
27
|
+
function filesOf(root, pipeline) {
|
|
28
|
+
if (!pipeline.endsWith("/"))
|
|
29
|
+
return [pipeline];
|
|
30
|
+
const dir = join(root, pipeline);
|
|
31
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
32
|
+
return [];
|
|
33
|
+
return readdirSync(dir)
|
|
34
|
+
.filter((name) => /\.ya?ml$/.test(name))
|
|
35
|
+
.sort()
|
|
36
|
+
.map((name) => `${pipeline}${name}`);
|
|
37
|
+
}
|
|
38
|
+
/** The workflow render writes for the gate: Node 24 and the exact version, so any project can run it. */
|
|
39
|
+
export function gateWorkflow(config) {
|
|
40
|
+
const branch = config.repo?.defaultBranch;
|
|
41
|
+
const body = [
|
|
42
|
+
"name: Peer AI",
|
|
43
|
+
"on:",
|
|
44
|
+
" pull_request:",
|
|
45
|
+
// With no default branch in the config, changes are checked in their pull requests.
|
|
46
|
+
...(branch === undefined ? [] : [" push:", ` branches: [${branch}]`]),
|
|
47
|
+
"permissions:",
|
|
48
|
+
" contents: read",
|
|
49
|
+
"jobs:",
|
|
50
|
+
" peer-ai-check:",
|
|
51
|
+
" name: peer-ai check",
|
|
52
|
+
" runs-on: ubuntu-latest",
|
|
53
|
+
" timeout-minutes: 10",
|
|
54
|
+
" steps:",
|
|
55
|
+
` - uses: ${CHECKOUT}`,
|
|
56
|
+
" with:",
|
|
57
|
+
" persist-credentials: false",
|
|
58
|
+
` - uses: ${SETUP_NODE}`,
|
|
59
|
+
" with:",
|
|
60
|
+
" node-version: 24",
|
|
61
|
+
` - run: ${gateCommand()}`,
|
|
62
|
+
"",
|
|
63
|
+
].join("\n");
|
|
64
|
+
return withHash([
|
|
65
|
+
"# Generated by peer-ai render from peer-ai.config.json: the gate, peer-ai check (RFC 0009).",
|
|
66
|
+
"# Change the config, not this file. Render writes it again while it's as render left it, and leaves",
|
|
67
|
+
"# it alone once someone changes it by hand. Make the job a required check: its name never changes.",
|
|
68
|
+
], body);
|
|
69
|
+
}
|
|
70
|
+
/** Whether the project's CI is GitHub Actions: workflows of its own, or the gate render wrote. */
|
|
71
|
+
export function onGitHubActions(root) {
|
|
72
|
+
return read(root, GATE_FILE) !== undefined || filesOf(root, `${WORKFLOWS}/`).some((path) => !OWN.includes(path));
|
|
73
|
+
}
|
|
74
|
+
/** Where the gate stands, and what render does about it. */
|
|
75
|
+
export function planGate(root, config) {
|
|
76
|
+
if (config.delivery?.gate === false)
|
|
77
|
+
return {};
|
|
78
|
+
const existing = read(root, GATE_FILE);
|
|
79
|
+
const others = filesOf(root, `${WORKFLOWS}/`).filter((path) => path !== GATE_FILE);
|
|
80
|
+
const onActions = onGitHubActions(root) || (config.delivery?.pipeline ?? "").startsWith(WORKFLOWS);
|
|
81
|
+
if (onActions) {
|
|
82
|
+
if (existing === undefined) {
|
|
83
|
+
// A gate someone added to a workflow of their own is enough: there is never a second one.
|
|
84
|
+
const elsewhere = others.find((path) => runsGate(read(root, path) ?? ""));
|
|
85
|
+
if (elsewhere !== undefined)
|
|
86
|
+
return { runsIn: elsewhere };
|
|
87
|
+
return { planned: { path: GATE_FILE, action: "create", content: gateWorkflow(config) } };
|
|
88
|
+
}
|
|
89
|
+
if (unchangedSinceRender(existing)) {
|
|
90
|
+
const content = gateWorkflow(config);
|
|
91
|
+
const same = sameFile(existing, content);
|
|
92
|
+
return {
|
|
93
|
+
planned: { path: GATE_FILE, action: same ? "unchanged" : "update", ...(same ? {} : { content }) },
|
|
94
|
+
runsIn: GATE_FILE,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
planned: {
|
|
99
|
+
path: GATE_FILE,
|
|
100
|
+
action: "kept",
|
|
101
|
+
note: "It was changed by hand, so keeping it up to date is yours now; peer-ai doctor checks it still runs peer-ai check. Delete it to have render write Peer AI's again.",
|
|
102
|
+
},
|
|
103
|
+
...(runsGate(existing) ? { runsIn: GATE_FILE } : {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const pipeline = detectDelivery(root)?.pipeline ?? (config.delivery?.ci === "existing" ? config.delivery.pipeline : undefined);
|
|
107
|
+
// A workflows folder that holds only Peer AI's own files isn't the project's CI.
|
|
108
|
+
if (pipeline === undefined || pipeline.startsWith(WORKFLOWS))
|
|
109
|
+
return {};
|
|
110
|
+
const runs = filesOf(root, pipeline).find((path) => runsGate(read(root, path) ?? ""));
|
|
111
|
+
if (runs !== undefined)
|
|
112
|
+
return { runsIn: runs };
|
|
113
|
+
return {
|
|
114
|
+
manual: `Your CI, ${pipeline}, doesn't run Peer AI's gate. Add a step that runs, with Node 24, after the project's own checks: ${gateCommand()}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const NOT_GATED = "No CI runs peer-ai check, so nothing holds work to its record in CI.";
|
|
118
|
+
/**
|
|
119
|
+
* peer-ai doctor's check: CI runs the gate, unless the config turns it off. With no CI yet there is
|
|
120
|
+
* nothing to say; the delivery check reports that already.
|
|
121
|
+
*/
|
|
122
|
+
export function checkGate(root, config) {
|
|
123
|
+
if (config.delivery?.gate === false)
|
|
124
|
+
return [skip("gate", "The CI gate is off: delivery.gate is false.")];
|
|
125
|
+
const gate = planGate(root, config);
|
|
126
|
+
const action = gate.planned?.action;
|
|
127
|
+
if (action === "create")
|
|
128
|
+
return [warn("gate", NOT_GATED, `Run npx peer-ai render. It writes ${GATE_FILE}.`)];
|
|
129
|
+
if (action === "update")
|
|
130
|
+
return [warn("gate", `${GATE_FILE} is out of date.`, "Run npx peer-ai render.")];
|
|
131
|
+
if (action === "kept" && gate.runsIn === undefined) {
|
|
132
|
+
return [
|
|
133
|
+
warn("gate", `${GATE_FILE} was changed by hand, and no longer runs peer-ai check.`, `Put the step back: - run: ${gateCommand()}`),
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
if (gate.runsIn !== undefined)
|
|
137
|
+
return [ok("gate", `CI runs peer-ai check: ${gate.runsIn}`)];
|
|
138
|
+
if (gate.manual !== undefined)
|
|
139
|
+
return [warn("gate", NOT_GATED, gate.manual)];
|
|
140
|
+
return [];
|
|
141
|
+
}
|
package/dist/init.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface Output {
|
|
|
17
17
|
log: (line: string) => void;
|
|
18
18
|
error: (line: string) => void;
|
|
19
19
|
}
|
|
20
|
-
interface Answers {
|
|
20
|
+
export interface Answers {
|
|
21
21
|
name: string;
|
|
22
22
|
description: string;
|
|
23
23
|
tracks: DetectedTrack[];
|
|
@@ -26,6 +26,7 @@ interface Answers {
|
|
|
26
26
|
tools: ToolId[];
|
|
27
27
|
}
|
|
28
28
|
export declare function describeTrack(track: DetectedTrack): string;
|
|
29
|
+
export declare function ask(detected: Detected, prompter: Prompter, title?: string): Promise<Answers>;
|
|
30
|
+
export declare function defaults(detected: Detected, options: InitOptions): Answers;
|
|
29
31
|
export declare function buildConfig(detected: Detected, answers: Answers): Record<string, unknown>;
|
|
30
32
|
export declare function runInit(options: InitOptions, prompter: Prompter | undefined, out: Output): Promise<number>;
|
|
31
|
-
export {};
|
package/dist/init.js
CHANGED
|
@@ -42,8 +42,8 @@ function parseStack(text) {
|
|
|
42
42
|
.map((part) => part.trim().toLowerCase())
|
|
43
43
|
.filter((part) => part !== "");
|
|
44
44
|
}
|
|
45
|
-
async function ask(detected, prompter) {
|
|
46
|
-
prompter.intro(
|
|
45
|
+
export async function ask(detected, prompter, title = "peer-ai init") {
|
|
46
|
+
prompter.intro(title);
|
|
47
47
|
if (detected.tracks.length > 0) {
|
|
48
48
|
prompter.note(detected.tracks.map(describeTrack).join("\n"), "Found in this repository");
|
|
49
49
|
}
|
|
@@ -75,7 +75,7 @@ async function ask(detected, prompter) {
|
|
|
75
75
|
}
|
|
76
76
|
return { name, description, tracks, team, stage, tools };
|
|
77
77
|
}
|
|
78
|
-
function defaults(detected, options) {
|
|
78
|
+
export function defaults(detected, options) {
|
|
79
79
|
return {
|
|
80
80
|
name: options.name ?? detected.name,
|
|
81
81
|
description: detected.description ?? "",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type ActivityId } from "peer-ai-workflow";
|
|
2
|
+
import { type Detected } from "./detect.ts";
|
|
3
|
+
import { type Answers, type InitOptions, type Output } from "./init.ts";
|
|
4
|
+
import { type Prompter } from "./prompter.ts";
|
|
5
|
+
import { type Converted, type Copy, type Fingerprints } from "./v0.ts";
|
|
6
|
+
export declare const NOTES_FILE = "docs/peer-ai-migration.md";
|
|
7
|
+
export declare const MIGRATION_ITEM = "migrate-v0";
|
|
8
|
+
export interface MigrateOptions extends InitOptions {
|
|
9
|
+
now?: Date;
|
|
10
|
+
/** The v0 fingerprints to compare against. Tests pass their own. */
|
|
11
|
+
fingerprints?: Fingerprints;
|
|
12
|
+
}
|
|
13
|
+
/** What git knows about the project, read once before anything changes. */
|
|
14
|
+
export interface RepoState {
|
|
15
|
+
/** The copy's files git tracks, which it can bring back after they're deleted. */
|
|
16
|
+
tracked: string[];
|
|
17
|
+
/** The copy's files git ignores, which migrate leaves where they are. */
|
|
18
|
+
ignored: string[];
|
|
19
|
+
/** The commit before the migration, so the notes can point at the files as they were. */
|
|
20
|
+
base?: string;
|
|
21
|
+
}
|
|
22
|
+
interface Decision {
|
|
23
|
+
title: string;
|
|
24
|
+
body: string[];
|
|
25
|
+
}
|
|
26
|
+
interface PlannedItem {
|
|
27
|
+
id?: string;
|
|
28
|
+
title: string;
|
|
29
|
+
stage: "prepare" | "build" | "verify";
|
|
30
|
+
track?: string;
|
|
31
|
+
position?: {
|
|
32
|
+
activity: ActivityId;
|
|
33
|
+
step: number;
|
|
34
|
+
};
|
|
35
|
+
next: string;
|
|
36
|
+
}
|
|
37
|
+
export interface Plan {
|
|
38
|
+
config: Record<string, unknown>;
|
|
39
|
+
copy: Copy;
|
|
40
|
+
converted: Converted[];
|
|
41
|
+
/** Files rewritten: instructions without v0's text, package.json without v0's scripts. */
|
|
42
|
+
edits: {
|
|
43
|
+
path: string;
|
|
44
|
+
text: string;
|
|
45
|
+
}[];
|
|
46
|
+
/** Files deleted outside the copy, each quoted in the notes or rebuilt from git. */
|
|
47
|
+
removals: string[];
|
|
48
|
+
/** The copy's tracked files, deleted one by one; its ignored files stay. */
|
|
49
|
+
copyFiles: string[];
|
|
50
|
+
decisions: Decision[];
|
|
51
|
+
/** Whole files kept in the notes as v0 had them, for reference. */
|
|
52
|
+
kept: {
|
|
53
|
+
path: string;
|
|
54
|
+
text: string;
|
|
55
|
+
info: string;
|
|
56
|
+
}[];
|
|
57
|
+
items: PlannedItem[];
|
|
58
|
+
base?: string;
|
|
59
|
+
}
|
|
60
|
+
/** A fenced block that holds any text, with a fence longer than any inside it. */
|
|
61
|
+
export declare function fenced(text: string, info?: string): string[];
|
|
62
|
+
/** Inline code that holds any text, even text with backticks of its own. */
|
|
63
|
+
export declare function code(text: string): string;
|
|
64
|
+
/** A work item id for a v0 ticket: PROJ-14 stays, #12 becomes PREFIX-12 or ITEM-12. */
|
|
65
|
+
export declare function ticketId(ticket: string, prefix: string | undefined): string | undefined;
|
|
66
|
+
export interface Scripts {
|
|
67
|
+
text: string;
|
|
68
|
+
removed: {
|
|
69
|
+
name: string;
|
|
70
|
+
command: string;
|
|
71
|
+
}[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* package.json without the scripts that only run v0's files. A script that does anything else as
|
|
75
|
+
* well stays as it is, and is listed for a person.
|
|
76
|
+
*/
|
|
77
|
+
export declare function packageScripts(text: string): {
|
|
78
|
+
edit?: Scripts;
|
|
79
|
+
mentions: {
|
|
80
|
+
name: string;
|
|
81
|
+
command: string;
|
|
82
|
+
}[];
|
|
83
|
+
};
|
|
84
|
+
/** Works out everything migrate will do, without changing anything. */
|
|
85
|
+
export declare function planMigration(root: string, detected: Detected, answers: Answers, fingerprints: Fingerprints, repo: RepoState): Plan;
|
|
86
|
+
/** docs/peer-ai-migration.md: what migrate did, and every decision it left. */
|
|
87
|
+
export declare function notesDocument(plan: Plan, today: string): string;
|
|
88
|
+
/** A path that leads outside the project, through a symbolic link on the way or at the end. */
|
|
89
|
+
export declare function escapes(root: string, path: string): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Exit code 0 when migrated, or with --dry-run; 1 when it refuses, or when the migration was applied
|
|
92
|
+
* but a step after it didn't finish; 2 on a usage error.
|
|
93
|
+
*/
|
|
94
|
+
export declare function runMigrate(options: MigrateOptions, prompter: Prompter | undefined, out: Output): Promise<number>;
|
|
95
|
+
export {};
|