@rness/cli 0.17.3 → 0.18.1
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 +108 -26
- package/dist/bin/rness.js +12 -7
- package/dist/bin/rness.js.map +1 -1
- package/dist/{chunk-MMPMXYZU.js → chunk-JHU2G7K7.js} +564 -264
- package/dist/chunk-JHU2G7K7.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/scaffold/CONVENTIONS.md +5 -0
- package/scaffold/package.json +1 -1
- package/dist/chunk-MMPMXYZU.js.map +0 -1
|
@@ -508,7 +508,7 @@ function parseRepoSpec(spec, org, host) {
|
|
|
508
508
|
}
|
|
509
509
|
|
|
510
510
|
// src/version.ts
|
|
511
|
-
var VERSION = "0.
|
|
511
|
+
var VERSION = "0.18.1";
|
|
512
512
|
|
|
513
513
|
// src/core/pm.ts
|
|
514
514
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -741,6 +741,25 @@ async function assembleContext({
|
|
|
741
741
|
|
|
742
742
|
// src/core/contract.ts
|
|
743
743
|
import { join as join5 } from "path";
|
|
744
|
+
|
|
745
|
+
// src/core/documents.ts
|
|
746
|
+
import { posix as posix2 } from "path";
|
|
747
|
+
function documentDate(rel) {
|
|
748
|
+
return /^((?:19|20)\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]))-/.exec(
|
|
749
|
+
posix2.basename(rel)
|
|
750
|
+
)?.[1] ?? null;
|
|
751
|
+
}
|
|
752
|
+
function documentNumber(rel) {
|
|
753
|
+
if (documentDate(rel) !== null) return null;
|
|
754
|
+
return /^(\d{4})-/.exec(posix2.basename(rel))?.[1] ?? null;
|
|
755
|
+
}
|
|
756
|
+
function documentTitle(heading, number) {
|
|
757
|
+
if (number === null) return heading;
|
|
758
|
+
const rest = new RegExp(`^${number}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
|
|
759
|
+
return rest ?? heading;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/core/contract.ts
|
|
744
763
|
var STATUSES = {
|
|
745
764
|
adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
|
|
746
765
|
specs: [
|
|
@@ -760,9 +779,31 @@ async function checkContract(rnessDir) {
|
|
|
760
779
|
for (const name of COLLECTIONS) {
|
|
761
780
|
const allowed = STATUSES[name];
|
|
762
781
|
const items = await collectMarkdown(join5(rnessDir, name));
|
|
782
|
+
const byNumber = /* @__PURE__ */ new Map();
|
|
763
783
|
for (const item of items) {
|
|
764
784
|
const where = `${name}/${item.rel}`;
|
|
765
785
|
if (SKIP.has(where)) continue;
|
|
786
|
+
const number = allowed === void 0 ? null : documentNumber(item.rel);
|
|
787
|
+
if (number !== null)
|
|
788
|
+
byNumber.set(number, [...byNumber.get(number) ?? [], where]);
|
|
789
|
+
}
|
|
790
|
+
for (const [number, files] of byNumber) {
|
|
791
|
+
if (files.length < 2) continue;
|
|
792
|
+
for (const where of files)
|
|
793
|
+
problems.push(
|
|
794
|
+
`${where}: the number ${number} is also that of ${files.filter((f) => f !== where).join(", ")}`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const next = String(
|
|
798
|
+
Math.max(0, ...[...byNumber.keys()].map(Number)) + 1
|
|
799
|
+
).padStart(4, "0");
|
|
800
|
+
for (const item of items) {
|
|
801
|
+
const where = `${name}/${item.rel}`;
|
|
802
|
+
if (SKIP.has(where)) continue;
|
|
803
|
+
if (allowed !== void 0 && documentNumber(item.rel) === null)
|
|
804
|
+
problems.push(
|
|
805
|
+
`${where}: not numbered; name it ${next}-<slug>.md, the next number of ${name}`
|
|
806
|
+
);
|
|
766
807
|
if (item.fieldsError !== null) {
|
|
767
808
|
problems.push(`${where}: invalid front matter (${item.fieldsError})`);
|
|
768
809
|
continue;
|
|
@@ -808,6 +849,9 @@ var PULSE_SCOPE = "project";
|
|
|
808
849
|
function clientId() {
|
|
809
850
|
return process.env["RNESS_GITHUB_CLIENT_ID"] ?? GITHUB_CLIENT_ID;
|
|
810
851
|
}
|
|
852
|
+
function approvalUrl() {
|
|
853
|
+
return `https://github.com/settings/connections/applications/${clientId()}`;
|
|
854
|
+
}
|
|
811
855
|
function webBase() {
|
|
812
856
|
return (process.env["RNESS_GITHUB_WEB"] ?? GITHUB_WEB).replace(/\/+$/, "");
|
|
813
857
|
}
|
|
@@ -1063,19 +1107,6 @@ async function resolveToken(deps = {}) {
|
|
|
1063
1107
|
// src/core/status.ts
|
|
1064
1108
|
import { readdir } from "fs/promises";
|
|
1065
1109
|
import { join as join7, posix as posix3 } from "path";
|
|
1066
|
-
|
|
1067
|
-
// src/core/documents.ts
|
|
1068
|
-
import { posix as posix2 } from "path";
|
|
1069
|
-
function documentNumber(rel) {
|
|
1070
|
-
return /^(\d{4})-/.exec(posix2.basename(rel))?.[1] ?? null;
|
|
1071
|
-
}
|
|
1072
|
-
function documentTitle(heading, number) {
|
|
1073
|
-
if (number === null) return heading;
|
|
1074
|
-
const rest = new RegExp(`^${number}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
|
|
1075
|
-
return rest ?? heading;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
// src/core/status.ts
|
|
1079
1110
|
var FIXED = [
|
|
1080
1111
|
["adr", "ADR"],
|
|
1081
1112
|
["specs", "Specs"],
|
|
@@ -1109,8 +1140,8 @@ function sessionsOf(item) {
|
|
|
1109
1140
|
}
|
|
1110
1141
|
function rowOf(dir, item) {
|
|
1111
1142
|
const base = posix3.basename(item.rel, ".md");
|
|
1112
|
-
const date =
|
|
1113
|
-
const number =
|
|
1143
|
+
const date = documentDate(item.rel);
|
|
1144
|
+
const number = documentNumber(item.rel);
|
|
1114
1145
|
const sessions = sessionsOf(item);
|
|
1115
1146
|
return {
|
|
1116
1147
|
id: date ?? number ?? base,
|
|
@@ -2052,16 +2083,26 @@ async function getUser(options) {
|
|
|
2052
2083
|
throw new Error(`GitHub API answered ${status2} for /user`);
|
|
2053
2084
|
return { login };
|
|
2054
2085
|
}
|
|
2055
|
-
async function
|
|
2086
|
+
async function listUserMemberships(options) {
|
|
2056
2087
|
const { status: status2, body } = await getJson(
|
|
2057
|
-
`/user/orgs?per_page=${PER_PAGE}`,
|
|
2088
|
+
`/user/memberships/orgs?state=active&per_page=${PER_PAGE}`,
|
|
2058
2089
|
options
|
|
2059
2090
|
);
|
|
2060
2091
|
if (status2 !== 200 || !Array.isArray(body))
|
|
2061
|
-
throw new Error(`GitHub API answered ${status2} for /user/orgs`);
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2092
|
+
throw new Error(`GitHub API answered ${status2} for /user/memberships/orgs`);
|
|
2093
|
+
const memberships = [];
|
|
2094
|
+
for (const m of body) {
|
|
2095
|
+
if (m === null || typeof m !== "object") continue;
|
|
2096
|
+
const entry = m;
|
|
2097
|
+
const org = entry["organization"];
|
|
2098
|
+
const login = org !== null && typeof org === "object" ? org["login"] : void 0;
|
|
2099
|
+
if (typeof login !== "string" || entry["state"] !== "active") continue;
|
|
2100
|
+
memberships.push({
|
|
2101
|
+
login,
|
|
2102
|
+
role: typeof entry["role"] === "string" ? entry["role"] : ""
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
return memberships;
|
|
2065
2106
|
}
|
|
2066
2107
|
async function getOrgMembership(org, options) {
|
|
2067
2108
|
const { status: status2, body } = await getJson(
|
|
@@ -3663,8 +3704,15 @@ var GitHubOAuthProvider = class {
|
|
|
3663
3704
|
}
|
|
3664
3705
|
async listOrganizations() {
|
|
3665
3706
|
if (this.#token === null) return [];
|
|
3666
|
-
const
|
|
3667
|
-
return
|
|
3707
|
+
const memberships = await listUserMemberships(this.#api(this.#token.token));
|
|
3708
|
+
return memberships.map((m) => ({
|
|
3709
|
+
login: m.login,
|
|
3710
|
+
approved: true,
|
|
3711
|
+
canGrant: m.role === "admin"
|
|
3712
|
+
}));
|
|
3713
|
+
}
|
|
3714
|
+
approvalUrl() {
|
|
3715
|
+
return this.#token?.source === "login" ? approvalUrl() : null;
|
|
3668
3716
|
}
|
|
3669
3717
|
async organizationAccess(org) {
|
|
3670
3718
|
if (this.#token === null) return "unknown";
|
|
@@ -3913,6 +3961,9 @@ function inverse(text, stream = process.stdout) {
|
|
|
3913
3961
|
const painted = styleText("inverse", text, { stream, validateStream: true });
|
|
3914
3962
|
return painted === text ? plain : painted;
|
|
3915
3963
|
}
|
|
3964
|
+
function count(n, [one, many]) {
|
|
3965
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
3966
|
+
}
|
|
3916
3967
|
function grey(text, stream = process.stdout) {
|
|
3917
3968
|
if (noColor()) return text;
|
|
3918
3969
|
return styleText("gray", text, { stream, validateStream: true });
|
|
@@ -4059,6 +4110,12 @@ function agentHintLines(platform = process.platform) {
|
|
|
4059
4110
|
` ~/.ssh/config: AddKeysToAgent yes${mac ? ", UseKeychain yes" : ""}`
|
|
4060
4111
|
];
|
|
4061
4112
|
}
|
|
4113
|
+
function passphraseHint(platform = process.platform) {
|
|
4114
|
+
return [
|
|
4115
|
+
"ssh asked for your passphrase, and asks again for every repository unless an agent holds the key:",
|
|
4116
|
+
...agentHintLines(platform).slice(1)
|
|
4117
|
+
].join("\n");
|
|
4118
|
+
}
|
|
4062
4119
|
function sshWorkspaceLines(reason3) {
|
|
4063
4120
|
return [
|
|
4064
4121
|
`this workspace clones over SSH (rness.json) but ssh to github.com fails: ${reason3}`,
|
|
@@ -4308,7 +4365,7 @@ async function ensureClaudeMd(dir) {
|
|
|
4308
4365
|
import { join as join15 } from "path";
|
|
4309
4366
|
|
|
4310
4367
|
// src/core/agent-targets.ts
|
|
4311
|
-
import { mkdir as mkdir2 } from "fs/promises";
|
|
4368
|
+
import { mkdir as mkdir2, rm as rm4, rmdir } from "fs/promises";
|
|
4312
4369
|
import { dirname as dirname4, join as join14 } from "path";
|
|
4313
4370
|
|
|
4314
4371
|
// src/core/claude-skills.ts
|
|
@@ -4317,7 +4374,7 @@ var PLUGIN_JSON = `${JSON.stringify(
|
|
|
4317
4374
|
{
|
|
4318
4375
|
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
4319
4376
|
name: "rness",
|
|
4320
|
-
description: "The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan
|
|
4377
|
+
description: "The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan.",
|
|
4321
4378
|
author: { name: "rness-dev", url: "https://rness.dev" },
|
|
4322
4379
|
homepage: "https://rness.dev",
|
|
4323
4380
|
repository: "https://github.com/rness-dev/rness",
|
|
@@ -4360,53 +4417,68 @@ terminal, from the workspace's \`.rness/\`: \`${localRunner(pm)} rness status\`.
|
|
|
4360
4417
|
}
|
|
4361
4418
|
var STATUS_RULE = `Move a status yourself when the work calls for it, and whenever the
|
|
4362
4419
|
developer says so: a plan \`In progress\` as you start its tasks,
|
|
4363
|
-
\`Completed\` once
|
|
4364
|
-
plan; a document \`Superseded\` by the one that
|
|
4365
|
-
|
|
4366
|
-
|
|
4420
|
+
\`Completed\` once its proofs pass under \`check\`; a specification
|
|
4421
|
+
\`Implemented\` with its last plan; a document \`Superseded\` by the one that
|
|
4422
|
+
replaces it. Accepting an ADR is the developer's; a specification is
|
|
4423
|
+
\`Approved\` by \`/rness:plan from NNNN\`, which is their word for it.`;
|
|
4367
4424
|
var COMMIT_RULE = `Commit each file you changed, in its own repository, and only the files
|
|
4368
4425
|
you alone changed: add them by name, never \`git add -A\` nor \`commit -a\`.
|
|
4369
4426
|
A file that had uncommitted changes before your first edit (\`git status\`
|
|
4370
4427
|
tells you), or that someone else changed since, stays uncommitted: say
|
|
4371
4428
|
which. Never push.`;
|
|
4429
|
+
var SESSION_LINE = `\`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
|
|
4430
|
+
\`sessions:\` in the front matter, unless that id is already there;
|
|
4431
|
+
\`<model>\` is the model you run as, as Claude Code names it, after
|
|
4432
|
+
\`Claude\`: for example \`Claude Opus 5.5\``;
|
|
4433
|
+
var allocateStep = (allocate, collection) => `Allocate the file: run \`${allocate}\` from the project directory, where
|
|
4434
|
+
Claude Code was opened (pre-approved above as typed; \`CLAUDE_PROJECT_DIR\`
|
|
4435
|
+
is not set in your shell, so the path is relative). It writes
|
|
4436
|
+
\`${collection}/NNNN-untitled.md\` with the next number, the front matter
|
|
4437
|
+
and the opening sections, and prints the path. Rename it to
|
|
4438
|
+
\`NNNN-<slug>.md\`, the slug a few lowercase words of the title joined by
|
|
4439
|
+
hyphens, and write the title as \`# NNNN \u2014 <title>\`. Never choose the
|
|
4440
|
+
number yourself.`;
|
|
4441
|
+
var HOOK_LINE = (rness) => `The rness hook checks every edit under \`${rness}/\`: fix what it reports.`;
|
|
4372
4442
|
var LIFECYCLE2 = [
|
|
4373
4443
|
{
|
|
4374
4444
|
name: "adr",
|
|
4375
|
-
description: "Record a decision of the rness workspace as an ADR in .rness/adr/ \u2014 a choice expensive to reverse or that creates a lasting constraint. Use when the developer asks for an ADR, or when the conversation reaches such a decision.",
|
|
4376
|
-
hint: "[subject | NNNN]",
|
|
4445
|
+
description: "Record a decision of the rness workspace as an ADR in .rness/adr/ \u2014 a choice expensive to reverse or that creates a lasting constraint \u2014 or reopen one. Use when the developer asks for an ADR, or when the conversation reaches such a decision.",
|
|
4446
|
+
hint: "[create <subject> | open NNNN]",
|
|
4377
4447
|
tab: "adr",
|
|
4378
|
-
body: (rness) => `You
|
|
4379
|
-
above lists the existing
|
|
4448
|
+
body: (rness, allocate) => `You keep the decisions of this workspace: the ADRs in \`${rness}/adr/\`. The table
|
|
4449
|
+
above lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
|
|
4380
4450
|
|
|
4381
|
-
|
|
4451
|
+
The first word is the verb, when a reference (\`0010\`, or a path) follows
|
|
4452
|
+
it; a bare reference is \`open\`; any other words are the subject of a
|
|
4453
|
+
\`create\` (\`open source licensing\` is a subject):
|
|
4454
|
+
|
|
4455
|
+
- \`create [subject]\`, or nothing: a new ADR, numbered by rness.
|
|
4456
|
+
- \`open NNNN\`: reopen ADR NNNN. An \`Accepted\` ADR is never edited: offer a
|
|
4457
|
+
new one that supersedes it. \`Rejected\` and \`Superseded\` are history too.
|
|
4458
|
+
|
|
4459
|
+
Creating:
|
|
4382
4460
|
|
|
4383
4461
|
1. If the developer did not ask for an ADR in so many words, propose one in
|
|
4384
4462
|
a single line \u2014 its title, and why the choice is hard to reverse \u2014 and
|
|
4385
4463
|
wait for a yes.
|
|
4386
4464
|
2. Read \`${rness}/CONVENTIONS.md\` and \`${rness}/adr/0000-template.md\`: they hold the
|
|
4387
4465
|
rules and the sections; this skill is only the procedure.
|
|
4388
|
-
3.
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
- The template's front matter and sections, filled in, its guidance lines
|
|
4398
|
-
removed: \`date\` today, \`status: Proposed\`, \`repo\` the repositories under
|
|
4399
|
-
\`org/\` it affects.
|
|
4400
|
-
- An ADR it replaces is named in Context. That one becomes \`Superseded\`,
|
|
4401
|
-
with \`superseded_by:\`, only once the new ADR is \`Accepted\`.
|
|
4402
|
-
- Add \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
|
|
4403
|
-
\`sessions:\` in the front matter, unless that id is already there.
|
|
4404
|
-
\`<model>\` is the model you run as, as Claude Code names it, after
|
|
4405
|
-
\`Claude\`: for example \`Claude Opus 5.5\`.
|
|
4406
|
-
- The rness hook checks every edit under \`${rness}/\`: fix what it reports.
|
|
4466
|
+
3. The subject is the arguments, else the decision just discussed. Context,
|
|
4467
|
+
alternatives or consequences missing from the conversation: ask for
|
|
4468
|
+
them, one question at a time. Never invent them.
|
|
4469
|
+
4. ${allocateStep(allocate, "adr")}
|
|
4470
|
+
5. Fill it: \`repo\` the repositories under \`org/\` it affects; the template's
|
|
4471
|
+
sections, their guidance lines removed; an ADR it replaces named in
|
|
4472
|
+
Context \u2014 that one becomes \`Superseded\`, with \`superseded_by:\`, only once
|
|
4473
|
+
the new ADR is \`Accepted\`. Add ${SESSION_LINE}.
|
|
4474
|
+
6. ${HOOK_LINE(rness)}
|
|
4407
4475
|
|
|
4408
|
-
|
|
4409
|
-
\`Proposed
|
|
4476
|
+
Opening: read the ADR and amend what the developer asks, in place while it
|
|
4477
|
+
is \`Proposed\`; add the session line above. Nothing of the file's history is
|
|
4478
|
+
rewritten: a decision that changed is a new ADR.
|
|
4479
|
+
|
|
4480
|
+
After writing: give the path and the Decision section. A new ADR stays
|
|
4481
|
+
\`Proposed\` until the developer accepts it.
|
|
4410
4482
|
|
|
4411
4483
|
${STATUS_RULE}
|
|
4412
4484
|
|
|
@@ -4415,43 +4487,47 @@ ${COMMIT_RULE}
|
|
|
4415
4487
|
},
|
|
4416
4488
|
{
|
|
4417
4489
|
name: "spec",
|
|
4418
|
-
description: "Write a specification of the rness workspace in .rness/specs/ \u2014 the outcome wanted and its scope, before any plan. Use when the developer asks for a spec, or when agreed work is too large to start without one.",
|
|
4419
|
-
hint: "[subject | NNNN]",
|
|
4490
|
+
description: "Write a specification of the rness workspace in .rness/specs/ \u2014 the outcome wanted and its scope, before any plan \u2014 or reopen one. Use when the developer asks for a spec, or when agreed work is too large to start without one.",
|
|
4491
|
+
hint: "[create <subject> | open NNNN]",
|
|
4420
4492
|
tab: "specs",
|
|
4421
|
-
body: (rness) => `You
|
|
4422
|
-
lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
|
|
4493
|
+
body: (rness, allocate) => `You keep the specifications of this workspace, in \`${rness}/specs/\`. The table
|
|
4494
|
+
above lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
|
|
4495
|
+
|
|
4496
|
+
The first word is the verb, when a reference (\`0028\`, or a path) follows
|
|
4497
|
+
it; a bare reference is \`open\`; any other words are the subject of a
|
|
4498
|
+
\`create\`:
|
|
4499
|
+
|
|
4500
|
+
- \`create [subject]\`, or nothing: a new specification, numbered by rness.
|
|
4501
|
+
- \`open NNNN\`: reopen specification NNNN. One that is \`Implemented\`,
|
|
4502
|
+
\`Superseded\` or \`Rejected\` is not edited: offer a new one.
|
|
4423
4503
|
|
|
4424
|
-
|
|
4504
|
+
Creating:
|
|
4425
4505
|
|
|
4426
4506
|
1. If the developer did not ask for a specification in so many words,
|
|
4427
4507
|
propose one in a single line and wait for a yes.
|
|
4428
4508
|
2. Read \`${rness}/CONVENTIONS.md\`, then the most recent specification above: match
|
|
4429
4509
|
its front matter, sections and tone.
|
|
4430
|
-
3.
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
\`sessions:\` in the front matter, unless that id is already there.
|
|
4448
|
-
\`<model>\` is the model you run as, as Claude Code names it, after
|
|
4449
|
-
\`Claude\`: for example \`Claude Opus 5.5\`.
|
|
4450
|
-
- The rness hook checks every edit under \`${rness}/\`: fix what it reports.
|
|
4510
|
+
3. The subject is the arguments, else the work just discussed. The problem,
|
|
4511
|
+
what is in and out of scope, the ADRs it rests on: ask what the
|
|
4512
|
+
conversation does not say, one question at a time.
|
|
4513
|
+
4. ${allocateStep(allocate, "specs")}
|
|
4514
|
+
5. Fill it: \`repo\`, and \`adr:\` linking the decisions it rests on \u2014 or the
|
|
4515
|
+
rationale in the body when none does; the sections it opened with
|
|
4516
|
+
(Summary, Scope, Out of scope, Alternatives considered, To verify,
|
|
4517
|
+
Tests), or the most recent specification's when the team's differ. What
|
|
4518
|
+
was checked in this session (a document read, a command run) is written
|
|
4519
|
+
as verified, with the date; everything else as a proposal or an open
|
|
4520
|
+
question. Add ${SESSION_LINE}.
|
|
4521
|
+
6. ${HOOK_LINE(rness)}
|
|
4522
|
+
|
|
4523
|
+
Opening: read the specification and amend what the developer asks \u2014 a
|
|
4524
|
+
section, a scope, an open question answered \u2014 \`updated\` today, the session
|
|
4525
|
+
line added. A status moves only as the developer says, or by
|
|
4526
|
+
\`/rness:plan from\`.
|
|
4451
4527
|
|
|
4452
|
-
After writing: give the path and the summary.
|
|
4453
|
-
\`Proposed\` for review
|
|
4454
|
-
|
|
4528
|
+
After writing: give the path and the summary. A new specification starts
|
|
4529
|
+
\`Draft\`: \`Proposed\` for review; \`/rness:plan from NNNN\` approves it and
|
|
4530
|
+
plans it.
|
|
4455
4531
|
|
|
4456
4532
|
${STATUS_RULE}
|
|
4457
4533
|
|
|
@@ -4460,105 +4536,104 @@ ${COMMIT_RULE}
|
|
|
4460
4536
|
},
|
|
4461
4537
|
{
|
|
4462
4538
|
name: "plan",
|
|
4463
|
-
description: "
|
|
4464
|
-
hint: "<spec NNNN
|
|
4539
|
+
description: "The plans of the rness workspace in .rness/plans/ \u2014 a plan from a specification (from, which approves it), from the conversation (create), reopened (open), or closed on evidence (check). Use when the developer asks for a plan, to approve a specification, or to check the work of a plan.",
|
|
4540
|
+
hint: "[create <subject> | from <spec> | open NNNN | check NNNN]",
|
|
4465
4541
|
tab: "plans",
|
|
4466
|
-
body: (rness) => `You
|
|
4467
|
-
the existing
|
|
4468
|
-
\`$ARGUMENTS\`.
|
|
4469
|
-
|
|
4470
|
-
Before writing:
|
|
4471
|
-
|
|
4472
|
-
1. If the developer did not ask for a plan in so many words, propose one in
|
|
4473
|
-
a single line and wait for a yes.
|
|
4474
|
-
2. No specification named: ask which. Read it, \`${rness}/CONVENTIONS.md\`, and the
|
|
4475
|
-
most recent plan above: match its front matter and sections.
|
|
4476
|
-
3. A specification that is not \`Approved\`: say so, and plan it only if the
|
|
4477
|
-
developer confirms. A plan of it that is neither \`Completed\` nor
|
|
4478
|
-
\`Abandoned\`: reopen it rather than write a second.
|
|
4479
|
-
4. Read the code and the repositories the specification touches: every
|
|
4480
|
-
task names real files and real commands.
|
|
4542
|
+
body: (rness, allocate) => `You keep the plans of this workspace, in \`${rness}/plans/\`. The table above
|
|
4543
|
+
lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
|
|
4481
4544
|
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
not the specification's.
|
|
4486
|
-
- \`date\` and \`updated\` today, \`status: Draft\`, \`repo\`, \`spec:\` linking the
|
|
4487
|
-
specification.
|
|
4488
|
-
- Tasks in order, as checkboxes. Each gives the files, what changes, and
|
|
4489
|
-
the command that proves it (a test, a build, a run). The last tasks:
|
|
4490
|
-
the documentation the work makes inaccurate, then the release when the
|
|
4491
|
-
repository has one.
|
|
4492
|
-
- Where the plan decides something the specification leaves open, a
|
|
4493
|
-
section says so.
|
|
4494
|
-
- Add \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
|
|
4495
|
-
\`sessions:\` in the front matter, unless that id is already there.
|
|
4496
|
-
\`<model>\` is the model you run as, as Claude Code names it, after
|
|
4497
|
-
\`Claude\`: for example \`Claude Opus 5.5\`.
|
|
4498
|
-
- The rness hook checks every edit under \`${rness}/\`: fix what it reports.
|
|
4545
|
+
The first word is the verb, when a reference (\`0037\`, or a path) follows
|
|
4546
|
+
it \u2014 \`check\` alone means the plan \`In progress\`; a bare reference is
|
|
4547
|
+
\`open\`; any other words are the subject of a \`create\`:
|
|
4499
4548
|
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4549
|
+
- \`from NNNN\`: a plan from specification NNNN, which this approves.
|
|
4550
|
+
- \`create [subject]\`, or nothing: a plan from the conversation, for work
|
|
4551
|
+
too small for a specification; \`spec:\` is left empty.
|
|
4552
|
+
- \`open NNNN\`: reopen plan NNNN.
|
|
4553
|
+
- \`check NNNN\`: run the proofs of plan NNNN; close it when they all pass.
|
|
4503
4554
|
|
|
4504
|
-
|
|
4555
|
+
\`from\`:
|
|
4505
4556
|
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4557
|
+
1. Read the specification, \`${rness}/CONVENTIONS.md\`, and the most recent plan
|
|
4558
|
+
above: match its front matter and sections. A specification that is
|
|
4559
|
+
\`Implemented\`, \`Superseded\` or \`Rejected\` is refused: say its status,
|
|
4560
|
+
write nothing. A plan of it that is neither \`Completed\` nor \`Abandoned\`:
|
|
4561
|
+
reopen it rather than write a second.
|
|
4562
|
+
2. The developer typed this command: that is their approval, and the
|
|
4563
|
+
specification moves from \`Draft\` or \`Proposed\` to \`Approved\`, \`updated\`
|
|
4564
|
+
today, in the same commit as the plan \u2014 no confirmation question. If
|
|
4565
|
+
you are proposing it yourself, propose it in a single line and wait for
|
|
4566
|
+
a yes first: the approval is theirs, never yours.
|
|
4567
|
+
3. Read the code and the repositories the specification touches: every
|
|
4568
|
+
task names real files and real commands.
|
|
4569
|
+
4. ${allocateStep(allocate, "plans")}
|
|
4570
|
+
5. Fill it: \`spec:\` linking the specification; tasks in order, as
|
|
4571
|
+
checkboxes, each with the files, what changes, and the command that
|
|
4572
|
+
proves it (a test, a build, a run) \u2014 the last tasks the documentation
|
|
4573
|
+
the work makes inaccurate, then the release when the repository has
|
|
4574
|
+
one; where the plan decides something the specification leaves open, the
|
|
4575
|
+
section it opened with says so. Add ${SESSION_LINE}.
|
|
4576
|
+
6. ${HOOK_LINE(rness)}
|
|
4577
|
+
|
|
4578
|
+
\`create\`: as \`from\`, without a specification. If the developer did not ask
|
|
4579
|
+
for a plan in so many words, propose one in a single line and wait for a
|
|
4580
|
+
yes. The body says what the work is and why it needs no specification.
|
|
4581
|
+
|
|
4582
|
+
\`open\`: read the plan and its specification; amend what the developer
|
|
4583
|
+
asks \u2014 a task added, split or reworded, a blocker recorded \u2014 \`updated\`
|
|
4584
|
+
today, the session line added; move \`Draft\` to \`Ready\` or \`Blocked\` as they
|
|
4585
|
+
say. A \`Completed\` or \`Abandoned\` plan is not reopened: offer a new plan of
|
|
4586
|
+
the same specification.
|
|
4587
|
+
|
|
4588
|
+
\`check\`:
|
|
4517
4589
|
|
|
4518
4590
|
1. The plan: the one named, else the one \`In progress\`. Several or none:
|
|
4519
4591
|
ask.
|
|
4520
|
-
2. The developer asked
|
|
4521
|
-
this session:
|
|
4522
|
-
for a yes.
|
|
4592
|
+
2. The developer asked for the check, or the plan's last task has just
|
|
4593
|
+
passed in this session: run it. Otherwise propose it in a single line
|
|
4594
|
+
and wait for a yes.
|
|
4523
4595
|
3. Read the plan, its specification and \`${rness}/CONVENTIONS.md\`.
|
|
4524
|
-
4. Evidence first. For each task, what
|
|
4525
|
-
|
|
4526
|
-
check
|
|
4527
|
-
5.
|
|
4596
|
+
4. Evidence first. For each task, what proves it: its command run now, or
|
|
4597
|
+
its result read in this session. Never mark a task done on belief. A
|
|
4598
|
+
check has no cache: run twice, it proves twice.
|
|
4599
|
+
5. Every task proven:
|
|
4528
4600
|
- the plan: \`status: Completed\`, \`updated\` today, the checkboxes of the
|
|
4529
4601
|
proven tasks ticked, and a section \`Verification (YYYY-MM-DD)\` \u2014 what
|
|
4530
4602
|
ran, what it showed, what could not run and why;
|
|
4531
4603
|
- the specification: \`Implemented\` once every plan of it is
|
|
4532
4604
|
\`Completed\`; \`updated\` today;
|
|
4533
|
-
- the plan and the specification: add
|
|
4534
|
-
\`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
|
|
4535
|
-
\`sessions:\` in their front matter, unless that id is already there;
|
|
4536
|
-
\`<model>\` is the model you run as, as Claude Code names it, after
|
|
4537
|
-
\`Claude\`: for example \`Claude Opus 5.5\`;
|
|
4605
|
+
- the plan and the specification: add ${SESSION_LINE};
|
|
4538
4606
|
- the smallest set of documents the work makes inaccurate: \`${rness}/docs/\`,
|
|
4539
4607
|
the READMEs of the repositories it changed.
|
|
4540
|
-
6. A task not proven: the plan stays \`In progress
|
|
4541
|
-
|
|
4542
|
-
|
|
4608
|
+
6. A task not proven: nothing moves, the plan stays \`In progress\`. Say,
|
|
4609
|
+
task by task, what passed and what remains. A blocker the developer
|
|
4610
|
+
names is a move of its own: \`Blocked\`, the blocker written in the plan.
|
|
4611
|
+
7. ${HOOK_LINE(rness)}
|
|
4612
|
+
|
|
4613
|
+
After writing: give the path and the list of tasks, or the statuses moved.
|
|
4614
|
+
A new plan starts \`Draft\`, and becomes \`Ready\` once the developer agrees
|
|
4615
|
+
with it or asks you to carry it out.
|
|
4543
4616
|
|
|
4544
|
-
|
|
4617
|
+
${STATUS_RULE}
|
|
4545
4618
|
|
|
4546
4619
|
${COMMIT_RULE}
|
|
4547
4620
|
`
|
|
4548
4621
|
}
|
|
4549
4622
|
];
|
|
4623
|
+
var RETIRED_SKILLS = ["done"];
|
|
4550
4624
|
function lifecycleSkill(rness, s) {
|
|
4551
4625
|
const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status ${s.tab} --cwd "\${CLAUDE_PROJECT_DIR}"`;
|
|
4626
|
+
const allocate = `node "${rness}/${PINNED_BIN}" doc new ${s.tab}`;
|
|
4552
4627
|
return `---
|
|
4553
4628
|
name: ${s.name}
|
|
4554
4629
|
description: ${s.description}
|
|
4555
4630
|
argument-hint: '${s.hint}'
|
|
4556
|
-
allowed-tools: Bash(${command})
|
|
4631
|
+
allowed-tools: Bash(${command}), Bash(${allocate})
|
|
4557
4632
|
---
|
|
4558
4633
|
|
|
4559
4634
|
!\`${command}\`
|
|
4560
4635
|
|
|
4561
|
-
${s.body(rness)}`;
|
|
4636
|
+
${s.body(rness, allocate)}`;
|
|
4562
4637
|
}
|
|
4563
4638
|
function pluginFiles(rness, at, ctx) {
|
|
4564
4639
|
return [
|
|
@@ -4572,6 +4647,11 @@ function pluginFiles(rness, at, ctx) {
|
|
|
4572
4647
|
file: `${PLUGIN}/skills/${s.name}/SKILL.md`,
|
|
4573
4648
|
at,
|
|
4574
4649
|
content: lifecycleSkill(rness, s)
|
|
4650
|
+
})),
|
|
4651
|
+
...RETIRED_SKILLS.map((name) => ({
|
|
4652
|
+
file: `${PLUGIN}/skills/${name}/SKILL.md`,
|
|
4653
|
+
at,
|
|
4654
|
+
retired: true
|
|
4575
4655
|
}))
|
|
4576
4656
|
];
|
|
4577
4657
|
}
|
|
@@ -5097,6 +5177,21 @@ async function agentTargets(root, manifest, opts) {
|
|
|
5097
5177
|
for (const owned of await filesOf(root, manifest, declared(manifest))) {
|
|
5098
5178
|
const { file, label } = owned;
|
|
5099
5179
|
const existing = await readOrNull(file);
|
|
5180
|
+
if ("retired" in owned) {
|
|
5181
|
+
if (existing === null) continue;
|
|
5182
|
+
if (opts.check) {
|
|
5183
|
+
outcomes.push({
|
|
5184
|
+
label,
|
|
5185
|
+
status: "stale",
|
|
5186
|
+
detail: "no longer written by rness; remove it"
|
|
5187
|
+
});
|
|
5188
|
+
continue;
|
|
5189
|
+
}
|
|
5190
|
+
await rm4(file);
|
|
5191
|
+
await rmdir(dirname4(file)).catch(() => void 0);
|
|
5192
|
+
outcomes.push({ label, status: "removed", detail: null });
|
|
5193
|
+
continue;
|
|
5194
|
+
}
|
|
5100
5195
|
if ("content" in owned) {
|
|
5101
5196
|
const diff = wholeDiff(existing, owned.content);
|
|
5102
5197
|
if (diff === null)
|
|
@@ -5147,6 +5242,7 @@ async function leftoverTargets(root, manifest) {
|
|
|
5147
5242
|
);
|
|
5148
5243
|
const leftovers = [];
|
|
5149
5244
|
for (const owned of await filesOf(root, manifest, undeclared)) {
|
|
5245
|
+
if ("retired" in owned) continue;
|
|
5150
5246
|
const existing = await readOrNull(owned.file);
|
|
5151
5247
|
if (existing === null) continue;
|
|
5152
5248
|
if ("content" in owned) {
|
|
@@ -5435,7 +5531,7 @@ async function askWhatToSync(input) {
|
|
|
5435
5531
|
{
|
|
5436
5532
|
value: SELECT_ALL,
|
|
5437
5533
|
label: "Select all",
|
|
5438
|
-
hint: `${input.missing.length
|
|
5534
|
+
hint: `${count(input.missing.length, ["repository", "repositories"])} from rness.json`
|
|
5439
5535
|
},
|
|
5440
5536
|
...input.missing.map((name) => ({ value: name, label: name }))
|
|
5441
5537
|
],
|
|
@@ -5542,6 +5638,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
5542
5638
|
() => ui.line(...CHECKING)
|
|
5543
5639
|
);
|
|
5544
5640
|
if (unattended) ui.gitIsSilent = true;
|
|
5641
|
+
else if (access2.ok) ui.hint(passphraseHint(), "stderr");
|
|
5545
5642
|
if (!access2.ok) {
|
|
5546
5643
|
process.stderr.write(
|
|
5547
5644
|
`${[...sshWorkspaceLines(access2.reason), ...INSTEAD_OF_LINES].join("\n")}
|
|
@@ -5779,6 +5876,7 @@ async function addCommand(spec, opts, deps = {}) {
|
|
|
5779
5876
|
() => ui.line(...CHECKING)
|
|
5780
5877
|
);
|
|
5781
5878
|
if (unattended || !access2.ok) ui.gitIsSilent = true;
|
|
5879
|
+
else ui.hint(passphraseHint(), "stderr");
|
|
5782
5880
|
const overSsh = Object.values(manifest.repos).some(
|
|
5783
5881
|
(r) => isSshUrl(r.url, transport.hosts)
|
|
5784
5882
|
);
|
|
@@ -6000,9 +6098,23 @@ async function copyScaffold(dest, tokens) {
|
|
|
6000
6098
|
}
|
|
6001
6099
|
|
|
6002
6100
|
// src/commands/login.ts
|
|
6003
|
-
import { spawn as spawn3 } from "child_process";
|
|
6004
6101
|
import { realpath } from "fs/promises";
|
|
6005
6102
|
|
|
6103
|
+
// src/core/browser.ts
|
|
6104
|
+
import { spawn as spawn3 } from "child_process";
|
|
6105
|
+
function openInBrowser(url) {
|
|
6106
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
6107
|
+
try {
|
|
6108
|
+
const child = spawn3(command, args, { stdio: "ignore", detached: true });
|
|
6109
|
+
child.on("error", () => void 0);
|
|
6110
|
+
child.unref();
|
|
6111
|
+
} catch {
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
6114
|
+
function canOpenBrowser() {
|
|
6115
|
+
return process.env["SSH_CONNECTION"] === void 0;
|
|
6116
|
+
}
|
|
6117
|
+
|
|
6006
6118
|
// src/core/setup-git.ts
|
|
6007
6119
|
import { execFile as execFile4 } from "child_process";
|
|
6008
6120
|
import { promisify as promisify4 } from "util";
|
|
@@ -6047,15 +6159,6 @@ async function removeGitSetup() {
|
|
|
6047
6159
|
}
|
|
6048
6160
|
|
|
6049
6161
|
// src/commands/login.ts
|
|
6050
|
-
function openInBrowser(url) {
|
|
6051
|
-
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
6052
|
-
try {
|
|
6053
|
-
const child = spawn3(command, args, { stdio: "ignore", detached: true });
|
|
6054
|
-
child.on("error", () => void 0);
|
|
6055
|
-
child.unref();
|
|
6056
|
-
} catch {
|
|
6057
|
-
}
|
|
6058
|
-
}
|
|
6059
6162
|
async function offerSetupGit(opts, terminal, bin, ui) {
|
|
6060
6163
|
if (opts.setupGit === false) return;
|
|
6061
6164
|
if (!isStableInstall(bin)) {
|
|
@@ -6132,7 +6235,7 @@ async function loginCommand(opts, deps = {}) {
|
|
|
6132
6235
|
ui.line("open", code.verificationUri);
|
|
6133
6236
|
ui.line("code", code.userCode);
|
|
6134
6237
|
}
|
|
6135
|
-
if (terminal.isTty() &&
|
|
6238
|
+
if (terminal.isTty() && canOpenBrowser())
|
|
6136
6239
|
(deps.open ?? openInBrowser)(code.verificationUri);
|
|
6137
6240
|
if (!ui.session)
|
|
6138
6241
|
ui.line("waiting", "for you to approve rness on github.com\u2026");
|
|
@@ -6165,35 +6268,134 @@ async function loginCommand(opts, deps = {}) {
|
|
|
6165
6268
|
return reportError(e);
|
|
6166
6269
|
}
|
|
6167
6270
|
}
|
|
6168
|
-
function revokeUrl() {
|
|
6169
|
-
return `https://github.com/settings/connections/applications/${clientId()}`;
|
|
6170
|
-
}
|
|
6171
6271
|
|
|
6172
6272
|
// src/commands/create.ts
|
|
6173
|
-
import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as
|
|
6273
|
+
import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm7 } from "fs/promises";
|
|
6174
6274
|
import { tmpdir as tmpdir2 } from "os";
|
|
6175
6275
|
import { join as join22, relative as relative4, resolve as resolve5 } from "path";
|
|
6176
6276
|
|
|
6277
|
+
// src/core/approval.ts
|
|
6278
|
+
var APPROVAL_INTERVAL_MS = 5e3;
|
|
6279
|
+
var APPROVAL_WAIT_MS = 10 * 6e4;
|
|
6280
|
+
function approvalQuestion(org) {
|
|
6281
|
+
return `Open github.com to approve rness for ${org} now?`;
|
|
6282
|
+
}
|
|
6283
|
+
async function checkAccess(input) {
|
|
6284
|
+
const { org, provider, ui } = input;
|
|
6285
|
+
if (!provider.authenticated) return "unknown";
|
|
6286
|
+
const login = (await provider.identity())?.login;
|
|
6287
|
+
const as = login === void 0 ? "" : ` (as ${login})`;
|
|
6288
|
+
const member = () => ui.line("member", `${org}${as}`, `You are a member of ${org}${as}`);
|
|
6289
|
+
const access2 = await provider.organizationAccess(org);
|
|
6290
|
+
if (access2 === "member") {
|
|
6291
|
+
member();
|
|
6292
|
+
return access2;
|
|
6293
|
+
}
|
|
6294
|
+
if (access2 !== "restricted") return access2;
|
|
6295
|
+
const url = provider.approvalUrl();
|
|
6296
|
+
ui.warn(
|
|
6297
|
+
`${org} has not approved rness, so its private repositories are hidden`
|
|
6298
|
+
);
|
|
6299
|
+
if (!input.interactive || url === null) {
|
|
6300
|
+
ui.hint(
|
|
6301
|
+
url === null ? `an owner of ${org} has to approve the app your token belongs to` : `an owner approves it at ${url}`,
|
|
6302
|
+
"stderr"
|
|
6303
|
+
);
|
|
6304
|
+
return "restricted";
|
|
6305
|
+
}
|
|
6306
|
+
const p = await input.prompts();
|
|
6307
|
+
const ok = await p.confirm({
|
|
6308
|
+
message: approvalQuestion(org),
|
|
6309
|
+
initialValue: true
|
|
6310
|
+
});
|
|
6311
|
+
if (p.isCancel(ok)) return "cancelled";
|
|
6312
|
+
if (ok !== true) {
|
|
6313
|
+
ui.hint(`approve it later: ${url}`, "stderr");
|
|
6314
|
+
return "restricted";
|
|
6315
|
+
}
|
|
6316
|
+
ui.line("open", url, `Open ${url} and approve rness for ${org}`);
|
|
6317
|
+
if (canOpenBrowser()) (input.deps?.open ?? openInBrowser)(url);
|
|
6318
|
+
const sleep = input.deps?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
6319
|
+
const now = input.deps?.now ?? Date.now;
|
|
6320
|
+
const who = input.canGrant === true ? org : `an owner of ${org}`;
|
|
6321
|
+
const waiting = `for ${who} to approve rness on github.com`;
|
|
6322
|
+
if (!ui.session) ui.line("waiting", `${waiting}\u2026`);
|
|
6323
|
+
let interrupted = false;
|
|
6324
|
+
let wake = () => void 0;
|
|
6325
|
+
const interrupt = () => {
|
|
6326
|
+
interrupted = true;
|
|
6327
|
+
wake();
|
|
6328
|
+
};
|
|
6329
|
+
if (!ui.session) process.once("SIGINT", interrupt);
|
|
6330
|
+
let outcome;
|
|
6331
|
+
try {
|
|
6332
|
+
outcome = await ui.step(
|
|
6333
|
+
{
|
|
6334
|
+
doing: `waiting ${waiting}`,
|
|
6335
|
+
sentence: `Waiting ${waiting}`,
|
|
6336
|
+
quiet: true
|
|
6337
|
+
},
|
|
6338
|
+
async () => {
|
|
6339
|
+
const deadline = now() + APPROVAL_WAIT_MS;
|
|
6340
|
+
for (; ; ) {
|
|
6341
|
+
await Promise.race([
|
|
6342
|
+
sleep(APPROVAL_INTERVAL_MS),
|
|
6343
|
+
new Promise((resolve8) => {
|
|
6344
|
+
wake = resolve8;
|
|
6345
|
+
})
|
|
6346
|
+
]);
|
|
6347
|
+
if (interrupted) return "cancelled";
|
|
6348
|
+
const again = await provider.organizationAccess(org);
|
|
6349
|
+
if (again === "member" || again === "not-member") return again;
|
|
6350
|
+
if (now() >= deadline) return "timeout";
|
|
6351
|
+
}
|
|
6352
|
+
},
|
|
6353
|
+
(r) => r === "member" ? ["member", `${org}${as}`] : null
|
|
6354
|
+
);
|
|
6355
|
+
} finally {
|
|
6356
|
+
process.off("SIGINT", interrupt);
|
|
6357
|
+
}
|
|
6358
|
+
if (outcome === "cancelled") return outcome;
|
|
6359
|
+
if (outcome === "member") {
|
|
6360
|
+
if (!ui.session) member();
|
|
6361
|
+
return outcome;
|
|
6362
|
+
}
|
|
6363
|
+
if (outcome === "not-member") return outcome;
|
|
6364
|
+
ui.warn(
|
|
6365
|
+
`${org} still has not approved rness; its private repositories stay hidden this time`
|
|
6366
|
+
);
|
|
6367
|
+
ui.hint(
|
|
6368
|
+
`once an owner approves it, add its private repositories with ${rnessCommand()} add <repo>`,
|
|
6369
|
+
"stderr"
|
|
6370
|
+
);
|
|
6371
|
+
return "restricted";
|
|
6372
|
+
}
|
|
6373
|
+
|
|
6177
6374
|
// src/core/ask-provider.ts
|
|
6178
6375
|
var PROVIDER_QUESTION = "Where does your organization live?";
|
|
6376
|
+
var BLANK = "blank";
|
|
6377
|
+
var BLANK_LABEL = "No organization yet: a blank local workspace";
|
|
6179
6378
|
async function askProvider(p) {
|
|
6180
6379
|
return p.select({
|
|
6181
6380
|
message: PROVIDER_QUESTION,
|
|
6182
|
-
options:
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6381
|
+
options: [
|
|
6382
|
+
...Object.values(PROVIDERS).map((entry) => ({
|
|
6383
|
+
value: entry.name,
|
|
6384
|
+
label: entry.available ? entry.label : `${entry.label} (coming later)`,
|
|
6385
|
+
disabled: !entry.available
|
|
6386
|
+
})),
|
|
6387
|
+
{ value: BLANK, label: BLANK_LABEL, disabled: false }
|
|
6388
|
+
]
|
|
6187
6389
|
});
|
|
6188
6390
|
}
|
|
6189
6391
|
|
|
6190
6392
|
// src/core/new-workspace.ts
|
|
6191
|
-
import { mkdir as mkdir4, readdir as readdir4, rm as
|
|
6393
|
+
import { mkdir as mkdir4, readdir as readdir4, rm as rm6, stat as stat3 } from "fs/promises";
|
|
6192
6394
|
import { dirname as dirname8, join as join20, relative as relative2 } from "path";
|
|
6193
6395
|
|
|
6194
6396
|
// src/core/scaffold-git.ts
|
|
6195
6397
|
import { execFile as execFile5 } from "child_process";
|
|
6196
|
-
import { mkdtemp, realpath as realpath2, rm as
|
|
6398
|
+
import { mkdtemp, realpath as realpath2, rm as rm5, writeFile as writeFile3 } from "fs/promises";
|
|
6197
6399
|
import { tmpdir } from "os";
|
|
6198
6400
|
import { dirname as dirname7, join as join19, resolve as resolve3 } from "path";
|
|
6199
6401
|
import { promisify as promisify5 } from "util";
|
|
@@ -6321,7 +6523,7 @@ async function buildScaffoldCommit(rnessDir, base, files, version) {
|
|
|
6321
6523
|
scaffoldMessage(`chore: rness scaffold ${version}`, version)
|
|
6322
6524
|
]);
|
|
6323
6525
|
} finally {
|
|
6324
|
-
await
|
|
6526
|
+
await rm5(scratch, { recursive: true, force: true });
|
|
6325
6527
|
}
|
|
6326
6528
|
}
|
|
6327
6529
|
async function blobAt(rnessDir, rev, path) {
|
|
@@ -6445,9 +6647,9 @@ async function buildContext(input) {
|
|
|
6445
6647
|
return true;
|
|
6446
6648
|
} catch (e) {
|
|
6447
6649
|
reportError(e);
|
|
6448
|
-
await
|
|
6650
|
+
await rm6(rnessDir, { recursive: true, force: true }).catch(() => void 0);
|
|
6449
6651
|
if (await isEmptyDir(root).catch(() => false))
|
|
6450
|
-
await
|
|
6652
|
+
await rm6(root, { recursive: true, force: true }).catch(() => void 0);
|
|
6451
6653
|
process.stderr.write(
|
|
6452
6654
|
`removed ${shown}/.rness after the failure; fix it and retry
|
|
6453
6655
|
`
|
|
@@ -6630,22 +6832,6 @@ function cancelled(ui) {
|
|
|
6630
6832
|
}
|
|
6631
6833
|
async function pickRepositories(input) {
|
|
6632
6834
|
const { org, prompts, provider, ui } = input;
|
|
6633
|
-
if (provider.authenticated) {
|
|
6634
|
-
const access2 = await provider.organizationAccess(org);
|
|
6635
|
-
const login = (await provider.identity())?.login;
|
|
6636
|
-
if (access2 === "member")
|
|
6637
|
-
ui.line(
|
|
6638
|
-
"member",
|
|
6639
|
-
`${org}${login === void 0 ? "" : ` (as ${login})`}`,
|
|
6640
|
-
`You are a member of ${org}${login === void 0 ? "" : ` (as ${login})`}`
|
|
6641
|
-
);
|
|
6642
|
-
else if (access2 === "restricted") {
|
|
6643
|
-
ui.warn(
|
|
6644
|
-
`${org} restricts OAuth apps, so its private repositories are hidden from rness`
|
|
6645
|
-
);
|
|
6646
|
-
ui.hint(`ask an owner to approve it: ${revokeUrl()}`, "stderr");
|
|
6647
|
-
}
|
|
6648
|
-
}
|
|
6649
6835
|
let listed = [];
|
|
6650
6836
|
let failed = false;
|
|
6651
6837
|
if (!ui.session) ui.line("listing", `${org} repositories\u2026`);
|
|
@@ -6661,7 +6847,7 @@ async function pickRepositories(input) {
|
|
|
6661
6847
|
// list below it must agree (spec 0009).
|
|
6662
6848
|
(l) => [
|
|
6663
6849
|
"listed",
|
|
6664
|
-
`${l.repositories.filter(isMemberRepo).length
|
|
6850
|
+
`${count(l.repositories.filter(isMemberRepo).length, ["repository", "repositories"])} of ${org}`
|
|
6665
6851
|
]
|
|
6666
6852
|
);
|
|
6667
6853
|
listed = listing.repositories;
|
|
@@ -6795,17 +6981,6 @@ function blankConflict(opts) {
|
|
|
6795
6981
|
if (opts.provider !== void 0) return "--provider";
|
|
6796
6982
|
return null;
|
|
6797
6983
|
}
|
|
6798
|
-
async function chooseStart(prompts) {
|
|
6799
|
-
const answer = await prompts.select({
|
|
6800
|
-
message: "How do you want to start?",
|
|
6801
|
-
options: [
|
|
6802
|
-
{ value: "github", label: "From a GitHub organization" },
|
|
6803
|
-
{ value: "blank", label: "Blank local workspace" }
|
|
6804
|
-
]
|
|
6805
|
-
});
|
|
6806
|
-
if (prompts.isCancel(answer)) return null;
|
|
6807
|
-
return answer === "blank" ? "blank" : "github";
|
|
6808
|
-
}
|
|
6809
6984
|
function providerFlagError(value) {
|
|
6810
6985
|
if (!isProviderName(value))
|
|
6811
6986
|
return `unknown provider "${value}" (${Object.keys(PROVIDERS).join(", ")})`;
|
|
@@ -6814,27 +6989,37 @@ function providerFlagError(value) {
|
|
|
6814
6989
|
return `provider "${value}" is not available yet (available: ${available.join(", ")})`;
|
|
6815
6990
|
}
|
|
6816
6991
|
var OTHER = /* @__PURE__ */ Symbol("another organization");
|
|
6992
|
+
var OTHER_LABEL = "an organization not listed here\u2026";
|
|
6993
|
+
var OTHER_HINT = "not a member, or it has not approved rness yet";
|
|
6994
|
+
var NOT_APPROVED = "rness not approved";
|
|
6817
6995
|
async function chooseOrganization(provider, prompts) {
|
|
6818
6996
|
if (!provider.authenticated) return OTHER;
|
|
6819
|
-
let
|
|
6997
|
+
let organizations;
|
|
6998
|
+
let self;
|
|
6820
6999
|
try {
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
if (self !== void 0) names.push(self);
|
|
7000
|
+
self = (await provider.identity())?.login;
|
|
7001
|
+
organizations = await provider.listOrganizations();
|
|
6824
7002
|
} catch {
|
|
6825
7003
|
return OTHER;
|
|
6826
7004
|
}
|
|
6827
|
-
if (
|
|
7005
|
+
if (organizations.length === 0 && self === void 0) return OTHER;
|
|
7006
|
+
const padlock = unicode();
|
|
7007
|
+
const options = organizations.map((o) => ({
|
|
7008
|
+
value: o.login,
|
|
7009
|
+
label: !o.approved && padlock ? `${o.login} ${PRIVATE_MARK}` : o.login,
|
|
7010
|
+
...o.approved ? {} : { hint: NOT_APPROVED }
|
|
7011
|
+
}));
|
|
7012
|
+
if (self !== void 0)
|
|
7013
|
+
options.push({ value: self, label: self, hint: "your account" });
|
|
7014
|
+
options.push({ value: "", label: OTHER_LABEL, hint: OTHER_HINT });
|
|
6828
7015
|
const answer = await prompts.select({
|
|
6829
7016
|
message: "Which GitHub organization?",
|
|
6830
|
-
options
|
|
6831
|
-
...names.map((name) => ({ value: name, label: name })),
|
|
6832
|
-
{ value: "", label: "another one\u2026" }
|
|
6833
|
-
]
|
|
7017
|
+
options
|
|
6834
7018
|
});
|
|
6835
7019
|
if (prompts.isCancel(answer)) return null;
|
|
6836
7020
|
if (typeof answer !== "string" || answer === "") return OTHER;
|
|
6837
|
-
|
|
7021
|
+
const picked = organizations.find((o) => o.login === answer);
|
|
7022
|
+
return { org: answer, canGrant: picked?.canGrant ?? null };
|
|
6838
7023
|
}
|
|
6839
7024
|
async function publishContext(input) {
|
|
6840
7025
|
const { ui, prompts, provider, org } = input;
|
|
@@ -6873,6 +7058,11 @@ async function publishContext(input) {
|
|
|
6873
7058
|
async function createCommand(opts, deps = {}) {
|
|
6874
7059
|
const terminal = deps.terminal ?? defaultTerminal;
|
|
6875
7060
|
const transport = deps.transport ?? defaultTransport;
|
|
7061
|
+
const clock = {
|
|
7062
|
+
...deps.open === void 0 ? {} : { open: deps.open },
|
|
7063
|
+
...deps.sleep === void 0 ? {} : { sleep: deps.sleep },
|
|
7064
|
+
...deps.now === void 0 ? {} : { now: deps.now }
|
|
7065
|
+
};
|
|
6876
7066
|
let provider = deps.provider;
|
|
6877
7067
|
const getProvider = async () => provider ??= await githubProvider(opts.githubApi);
|
|
6878
7068
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -6938,6 +7128,7 @@ async function createCommand(opts, deps = {}) {
|
|
|
6938
7128
|
() => ui.line(...CHECKING)
|
|
6939
7129
|
);
|
|
6940
7130
|
if (sshTest.unattended) ui.gitIsSilent = true;
|
|
7131
|
+
else if (sshTest.access.ok) ui.hint(passphraseHint(), "stderr");
|
|
6941
7132
|
}
|
|
6942
7133
|
return sshTest.access;
|
|
6943
7134
|
};
|
|
@@ -6967,10 +7158,14 @@ async function createCommand(opts, deps = {}) {
|
|
|
6967
7158
|
return 1;
|
|
6968
7159
|
}
|
|
6969
7160
|
let blank = opts.blank === true;
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
7161
|
+
let chosenProvider = "github";
|
|
7162
|
+
if (opts.provider !== void 0 && isProviderName(opts.provider))
|
|
7163
|
+
chosenProvider = opts.provider;
|
|
7164
|
+
else if (!blank && interactive && opts.org === void 0 && opts.repos === void 0) {
|
|
7165
|
+
const answer = await askProvider(await prompts());
|
|
7166
|
+
if (typeof answer !== "string") return cancelled(ui);
|
|
7167
|
+
if (answer === BLANK) blank = true;
|
|
7168
|
+
else chosenProvider = answer;
|
|
6974
7169
|
}
|
|
6975
7170
|
if (blank)
|
|
6976
7171
|
return await createBlank({
|
|
@@ -6984,14 +7179,6 @@ async function createCommand(opts, deps = {}) {
|
|
|
6984
7179
|
ui,
|
|
6985
7180
|
prompts
|
|
6986
7181
|
});
|
|
6987
|
-
let chosenProvider = "github";
|
|
6988
|
-
if (opts.provider !== void 0 && isProviderName(opts.provider))
|
|
6989
|
-
chosenProvider = opts.provider;
|
|
6990
|
-
else if (interactive) {
|
|
6991
|
-
const answer = await askProvider(await prompts());
|
|
6992
|
-
if (typeof answer !== "string") return cancelled(ui);
|
|
6993
|
-
chosenProvider = answer;
|
|
6994
|
-
}
|
|
6995
7182
|
let prompted = false;
|
|
6996
7183
|
let org = opts.org;
|
|
6997
7184
|
if (org === void 0 && !interactive) {
|
|
@@ -7001,19 +7188,20 @@ async function createCommand(opts, deps = {}) {
|
|
|
7001
7188
|
if (interactive && opts.repos === void 0 && !(await getProvider()).authenticated) {
|
|
7002
7189
|
const p = await prompts();
|
|
7003
7190
|
const login = await p.confirm({
|
|
7004
|
-
message: "Log in to GitHub to list private repositories?",
|
|
7191
|
+
message: "Log in to GitHub to list your organizations and private repositories?",
|
|
7005
7192
|
initialValue: true
|
|
7006
7193
|
});
|
|
7007
7194
|
if (p.isCancel(login)) return cancelled(ui);
|
|
7008
7195
|
if (login === true) {
|
|
7009
7196
|
const code2 = await loginCommand(
|
|
7010
7197
|
opts.githubApi === void 0 ? {} : { githubApi: opts.githubApi },
|
|
7011
|
-
{ terminal, ui }
|
|
7198
|
+
{ terminal, ui, ...clock }
|
|
7012
7199
|
);
|
|
7013
7200
|
if (code2 !== 0) return code2;
|
|
7014
7201
|
provider = await githubProvider(opts.githubApi);
|
|
7015
7202
|
}
|
|
7016
7203
|
}
|
|
7204
|
+
let canGrant = null;
|
|
7017
7205
|
if (org === void 0) {
|
|
7018
7206
|
const chosen = await chooseOrganization(
|
|
7019
7207
|
await getProvider(),
|
|
@@ -7021,7 +7209,8 @@ async function createCommand(opts, deps = {}) {
|
|
|
7021
7209
|
);
|
|
7022
7210
|
if (chosen === null) return cancelled(ui);
|
|
7023
7211
|
if (chosen !== OTHER) {
|
|
7024
|
-
org = chosen;
|
|
7212
|
+
org = chosen.org;
|
|
7213
|
+
canGrant = chosen.canGrant;
|
|
7025
7214
|
prompted = true;
|
|
7026
7215
|
}
|
|
7027
7216
|
}
|
|
@@ -7053,6 +7242,16 @@ async function createCommand(opts, deps = {}) {
|
|
|
7053
7242
|
);
|
|
7054
7243
|
return 2;
|
|
7055
7244
|
}
|
|
7245
|
+
const outcome = await checkAccess({
|
|
7246
|
+
org,
|
|
7247
|
+
provider: await getProvider(),
|
|
7248
|
+
ui,
|
|
7249
|
+
interactive,
|
|
7250
|
+
prompts,
|
|
7251
|
+
canGrant,
|
|
7252
|
+
deps: clock
|
|
7253
|
+
});
|
|
7254
|
+
if (outcome === "cancelled") return cancelled(ui);
|
|
7056
7255
|
const host = await chooseHost();
|
|
7057
7256
|
const contextUrl = repoUrl(host, org, ".rness");
|
|
7058
7257
|
let gitProvider = await getProvider();
|
|
@@ -7292,7 +7491,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
|
|
|
7292
7491
|
return reportError(e);
|
|
7293
7492
|
} finally {
|
|
7294
7493
|
if (staging !== void 0)
|
|
7295
|
-
await
|
|
7494
|
+
await rm7(staging, { recursive: true, force: true }).catch(() => void 0);
|
|
7296
7495
|
}
|
|
7297
7496
|
}
|
|
7298
7497
|
|
|
@@ -7397,7 +7596,7 @@ async function logoutCommand() {
|
|
|
7397
7596
|
`${status("updated", "git: rness no longer answers for https://github.com")}
|
|
7398
7597
|
`
|
|
7399
7598
|
);
|
|
7400
|
-
process.stdout.write(`revoke rness on GitHub: ${
|
|
7599
|
+
process.stdout.write(`revoke rness on GitHub: ${approvalUrl()}
|
|
7401
7600
|
`);
|
|
7402
7601
|
return 0;
|
|
7403
7602
|
} catch (e) {
|
|
@@ -7407,7 +7606,7 @@ async function logoutCommand() {
|
|
|
7407
7606
|
|
|
7408
7607
|
// src/commands/upgrade.ts
|
|
7409
7608
|
import { execFile as execFile6 } from "child_process";
|
|
7410
|
-
import { mkdtemp as mkdtemp3, readFile as readFile10, rm as
|
|
7609
|
+
import { mkdtemp as mkdtemp3, readFile as readFile10, rm as rm8 } from "fs/promises";
|
|
7411
7610
|
import { tmpdir as tmpdir3 } from "os";
|
|
7412
7611
|
import { basename as basename5, join as join24, relative as relative5 } from "path";
|
|
7413
7612
|
import { promisify as promisify6 } from "util";
|
|
@@ -7433,7 +7632,7 @@ async function scaffoldFor(version, rnessDir) {
|
|
|
7433
7632
|
if (installed?.version === version && inside2 !== null && await exists(join24(inside2, "package.json")))
|
|
7434
7633
|
return { dir: inside2 };
|
|
7435
7634
|
const scratch = await mkdtemp3(join24(tmpdir3(), "rness-pack-"));
|
|
7436
|
-
const cleanup = () =>
|
|
7635
|
+
const cleanup = () => rm8(scratch, { recursive: true, force: true });
|
|
7437
7636
|
try {
|
|
7438
7637
|
const { stdout } = await execFileP6(
|
|
7439
7638
|
"npm",
|
|
@@ -7852,6 +8051,102 @@ async function validateCommand(opts, deps = {}) {
|
|
|
7852
8051
|
// src/cli.ts
|
|
7853
8052
|
import { Command, CommanderError, Option } from "commander";
|
|
7854
8053
|
|
|
8054
|
+
// src/commands/doc.ts
|
|
8055
|
+
import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile4 } from "fs/promises";
|
|
8056
|
+
import { join as join25 } from "path";
|
|
8057
|
+
var NUMBERED = Object.keys(STATUSES);
|
|
8058
|
+
var SECTIONS = {
|
|
8059
|
+
specs: [
|
|
8060
|
+
"Summary",
|
|
8061
|
+
"Scope",
|
|
8062
|
+
"Out of scope",
|
|
8063
|
+
"Alternatives considered",
|
|
8064
|
+
"To verify",
|
|
8065
|
+
"Tests"
|
|
8066
|
+
],
|
|
8067
|
+
plans: ["Decided here, where the specification leaves it open", "Tasks"]
|
|
8068
|
+
};
|
|
8069
|
+
function slugOf(title) {
|
|
8070
|
+
const slug = title.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
|
|
8071
|
+
return slug === "" ? "untitled" : slug;
|
|
8072
|
+
}
|
|
8073
|
+
function today() {
|
|
8074
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
8075
|
+
}
|
|
8076
|
+
function headingsOf(template) {
|
|
8077
|
+
return [...template.matchAll(/^## (.+)$/gm)].map((m) => (m[1] ?? "").trim());
|
|
8078
|
+
}
|
|
8079
|
+
async function adrSections(rnessDir) {
|
|
8080
|
+
const own = join25(rnessDir, "adr", "0000-template.md");
|
|
8081
|
+
const shipped = join25(scaffoldDir(), "adr", "0000-template.md");
|
|
8082
|
+
for (const file of [own, shipped]) {
|
|
8083
|
+
try {
|
|
8084
|
+
const sections = headingsOf(await readFile11(file, "utf8"));
|
|
8085
|
+
if (sections.length > 0) return sections;
|
|
8086
|
+
} catch {
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
return ["Context", "Decision", "Alternatives Considered", "Consequences"];
|
|
8090
|
+
}
|
|
8091
|
+
async function writeDocument(file, text) {
|
|
8092
|
+
try {
|
|
8093
|
+
await writeFile4(file, text, { flag: "wx" });
|
|
8094
|
+
} catch (e) {
|
|
8095
|
+
if (e.code !== "EEXIST") throw e;
|
|
8096
|
+
process.stderr.write(`${file} exists; nothing written
|
|
8097
|
+
`);
|
|
8098
|
+
return 1;
|
|
8099
|
+
}
|
|
8100
|
+
process.stdout.write(`${file}
|
|
8101
|
+
`);
|
|
8102
|
+
return 0;
|
|
8103
|
+
}
|
|
8104
|
+
async function docNewCommand(collection, opts) {
|
|
8105
|
+
if (!NUMBERED.includes(collection)) {
|
|
8106
|
+
process.stderr.write(
|
|
8107
|
+
`${collection} is not a numbered collection of this workspace (${NUMBERED.join(", ")})
|
|
8108
|
+
`
|
|
8109
|
+
);
|
|
8110
|
+
return 2;
|
|
8111
|
+
}
|
|
8112
|
+
const name = collection;
|
|
8113
|
+
try {
|
|
8114
|
+
const ws = await findWorkspace(opts.cwd ?? process.cwd());
|
|
8115
|
+
const dir = join25(ws.rnessDir, name);
|
|
8116
|
+
await mkdir7(dir, { recursive: true });
|
|
8117
|
+
let highest = 0;
|
|
8118
|
+
for (const item of await collectMarkdown(dir)) {
|
|
8119
|
+
const number2 = documentNumber(item.rel);
|
|
8120
|
+
if (number2 !== null) highest = Math.max(highest, Number(number2));
|
|
8121
|
+
}
|
|
8122
|
+
const number = String(highest + 1).padStart(4, "0");
|
|
8123
|
+
const title = opts.title?.trim() || "Untitled";
|
|
8124
|
+
const file = join25(dir, `${number}-${slugOf(title)}.md`);
|
|
8125
|
+
const status2 = STATUSES[name]?.[0] ?? "Draft";
|
|
8126
|
+
const date = today();
|
|
8127
|
+
const front = [
|
|
8128
|
+
"---",
|
|
8129
|
+
`date: ${date}`,
|
|
8130
|
+
`status: ${status2}`,
|
|
8131
|
+
"repo: ''",
|
|
8132
|
+
// An ADR is never updated once accepted: no `updated` (CONVENTIONS.md).
|
|
8133
|
+
...name === "adr" ? [] : [`updated: ${date}`],
|
|
8134
|
+
"---"
|
|
8135
|
+
];
|
|
8136
|
+
const sections = name === "adr" ? await adrSections(ws.rnessDir) : SECTIONS[name] ?? [];
|
|
8137
|
+
const body = [
|
|
8138
|
+
`# ${number} \u2014 ${title}`,
|
|
8139
|
+
// The rule under the title, as the ADR and specification templates draw it.
|
|
8140
|
+
...name === "plans" ? [] : ["", "---"],
|
|
8141
|
+
...sections.flatMap((s) => ["", `## ${s}`])
|
|
8142
|
+
];
|
|
8143
|
+
return await writeDocument(file, `${[...front, "", ...body].join("\n")}
|
|
8144
|
+
`);
|
|
8145
|
+
} catch (e) {
|
|
8146
|
+
return reportError(e);
|
|
8147
|
+
}
|
|
8148
|
+
}
|
|
8149
|
+
|
|
7855
8150
|
// src/commands/git-credential.ts
|
|
7856
8151
|
async function readStdin() {
|
|
7857
8152
|
const chunks = [];
|
|
@@ -7880,8 +8175,8 @@ import { realpath as realpath4 } from "fs/promises";
|
|
|
7880
8175
|
import { isAbsolute as isAbsolute2, posix as posix8, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
|
|
7881
8176
|
|
|
7882
8177
|
// src/mcp/tools.ts
|
|
7883
|
-
import { readFile as
|
|
7884
|
-
import { isAbsolute, join as
|
|
8178
|
+
import { readFile as readFile12, realpath as realpath3, stat as stat4 } from "fs/promises";
|
|
8179
|
+
import { isAbsolute, join as join26, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
|
|
7885
8180
|
var LABELS = {
|
|
7886
8181
|
standards: "Standards",
|
|
7887
8182
|
adr: "Decisions",
|
|
@@ -7910,7 +8205,7 @@ async function documents(rnessDir, only) {
|
|
|
7910
8205
|
const docs = [];
|
|
7911
8206
|
for (const collection of COLLECTIONS) {
|
|
7912
8207
|
if (only !== void 0 && collection !== only) continue;
|
|
7913
|
-
for (const item of await collectMarkdown(
|
|
8208
|
+
for (const item of await collectMarkdown(join26(rnessDir, collection))) {
|
|
7914
8209
|
const path = `${collection}/${item.rel}`;
|
|
7915
8210
|
if (path === TEMPLATE2) continue;
|
|
7916
8211
|
const status2 = item.fields?.["status"];
|
|
@@ -8032,7 +8327,7 @@ async function read(cwd, args) {
|
|
|
8032
8327
|
return refuse(
|
|
8033
8328
|
`${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
|
|
8034
8329
|
);
|
|
8035
|
-
return { text: await
|
|
8330
|
+
return { text: await readFile12(real, "utf8") };
|
|
8036
8331
|
}
|
|
8037
8332
|
async function search(cwd, args) {
|
|
8038
8333
|
const { ws } = await workspaceAt(cwd);
|
|
@@ -8140,8 +8435,8 @@ function mcpTools(cwd) {
|
|
|
8140
8435
|
|
|
8141
8436
|
// src/pulse/detached.ts
|
|
8142
8437
|
import { spawn as nodeSpawn } from "child_process";
|
|
8143
|
-
import { mkdir as
|
|
8144
|
-
import { join as
|
|
8438
|
+
import { mkdir as mkdir8, open as open2, readFile as readFile13, rm as rm9, stat as stat5, writeFile as writeFile5 } from "fs/promises";
|
|
8439
|
+
import { join as join27 } from "path";
|
|
8145
8440
|
import { setTimeout as delay2 } from "timers/promises";
|
|
8146
8441
|
var detached = (args, cwd) => {
|
|
8147
8442
|
const bin = process.argv[1];
|
|
@@ -8155,11 +8450,11 @@ var detached = (args, cwd) => {
|
|
|
8155
8450
|
child.on("error", () => void 0);
|
|
8156
8451
|
child.unref();
|
|
8157
8452
|
};
|
|
8158
|
-
var failureFile = () =>
|
|
8453
|
+
var failureFile = () => join27(configDir(), "pulse.json");
|
|
8159
8454
|
async function recordFailure(reason3) {
|
|
8160
8455
|
try {
|
|
8161
|
-
await
|
|
8162
|
-
await
|
|
8456
|
+
await mkdir8(configDir(), { recursive: true, mode: 448 });
|
|
8457
|
+
await writeFile5(
|
|
8163
8458
|
failureFile(),
|
|
8164
8459
|
`${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: reason3 })}
|
|
8165
8460
|
`
|
|
@@ -8169,8 +8464,8 @@ async function recordFailure(reason3) {
|
|
|
8169
8464
|
}
|
|
8170
8465
|
async function takeFailure() {
|
|
8171
8466
|
try {
|
|
8172
|
-
const data = JSON.parse(await
|
|
8173
|
-
await
|
|
8467
|
+
const data = JSON.parse(await readFile13(failureFile(), "utf8"));
|
|
8468
|
+
await rm9(failureFile(), { force: true });
|
|
8174
8469
|
const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
|
|
8175
8470
|
return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
|
|
8176
8471
|
} catch {
|
|
@@ -8179,8 +8474,8 @@ async function takeFailure() {
|
|
|
8179
8474
|
}
|
|
8180
8475
|
var PULSE_LOCK_STALE_MS = 15 * 6e4;
|
|
8181
8476
|
async function oneSyncAtATime(key, work, options = {}) {
|
|
8182
|
-
await
|
|
8183
|
-
const file =
|
|
8477
|
+
await mkdir8(configDir(), { recursive: true, mode: 448 });
|
|
8478
|
+
const file = join27(configDir(), `pulse-${key}.lock`);
|
|
8184
8479
|
for (; ; ) {
|
|
8185
8480
|
try {
|
|
8186
8481
|
const handle = await open2(file, "wx");
|
|
@@ -8192,7 +8487,7 @@ async function oneSyncAtATime(key, work, options = {}) {
|
|
|
8192
8487
|
(s) => Date.now() - s.mtimeMs,
|
|
8193
8488
|
() => 0
|
|
8194
8489
|
);
|
|
8195
|
-
if (age > PULSE_LOCK_STALE_MS) await
|
|
8490
|
+
if (age > PULSE_LOCK_STALE_MS) await rm9(file, { force: true });
|
|
8196
8491
|
else if (options.wait !== true) return false;
|
|
8197
8492
|
else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
|
|
8198
8493
|
}
|
|
@@ -8201,7 +8496,7 @@ async function oneSyncAtATime(key, work, options = {}) {
|
|
|
8201
8496
|
await work();
|
|
8202
8497
|
return true;
|
|
8203
8498
|
} finally {
|
|
8204
|
-
await
|
|
8499
|
+
await rm9(file, { force: true });
|
|
8205
8500
|
}
|
|
8206
8501
|
}
|
|
8207
8502
|
|
|
@@ -8232,9 +8527,6 @@ function cwdOf(input, env) {
|
|
|
8232
8527
|
return env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
|
|
8233
8528
|
}
|
|
8234
8529
|
var toPosix2 = (p) => p.split(sep5).join(posix8.sep);
|
|
8235
|
-
function count(n, [one, many]) {
|
|
8236
|
-
return `${n} ${n === 1 ? one : many}`;
|
|
8237
|
-
}
|
|
8238
8530
|
async function safetyNet(ws, manifest) {
|
|
8239
8531
|
const notes = [];
|
|
8240
8532
|
const drift = await pinDrift(ws.rnessDir);
|
|
@@ -8576,12 +8868,12 @@ async function mcpCommand(opts, io = {
|
|
|
8576
8868
|
}
|
|
8577
8869
|
|
|
8578
8870
|
// src/commands/pulse.ts
|
|
8579
|
-
import { readFile as
|
|
8580
|
-
import { join as
|
|
8871
|
+
import { readFile as readFile15, readdir as readdir5 } from "fs/promises";
|
|
8872
|
+
import { join as join28 } from "path";
|
|
8581
8873
|
import { setTimeout as delay3 } from "timers/promises";
|
|
8582
8874
|
|
|
8583
8875
|
// src/pulse/opened.ts
|
|
8584
|
-
import { mkdir as
|
|
8876
|
+
import { mkdir as mkdir9, readFile as readFile14, rename as rename3, rm as rm10, writeFile as writeFile6 } from "fs/promises";
|
|
8585
8877
|
import { dirname as dirname9 } from "path";
|
|
8586
8878
|
var NAME2 = "rness/opened";
|
|
8587
8879
|
function openedFile(rnessDir) {
|
|
@@ -8593,7 +8885,7 @@ async function readOpened(file) {
|
|
|
8593
8885
|
if (file === null) return opened;
|
|
8594
8886
|
let data;
|
|
8595
8887
|
try {
|
|
8596
|
-
data = JSON.parse(await
|
|
8888
|
+
data = JSON.parse(await readFile14(file, "utf8"));
|
|
8597
8889
|
} catch {
|
|
8598
8890
|
return opened;
|
|
8599
8891
|
}
|
|
@@ -8608,13 +8900,13 @@ async function writeOpened(file, opened) {
|
|
|
8608
8900
|
if (file === null) return;
|
|
8609
8901
|
try {
|
|
8610
8902
|
if (opened.size === 0) {
|
|
8611
|
-
await
|
|
8903
|
+
await rm10(file, { force: true });
|
|
8612
8904
|
return;
|
|
8613
8905
|
}
|
|
8614
8906
|
const entries = [...opened].sort(([a], [b]) => a - b);
|
|
8615
|
-
await
|
|
8907
|
+
await mkdir9(dirname9(file), { recursive: true });
|
|
8616
8908
|
const temporary = `${file}.${process.pid}`;
|
|
8617
|
-
await
|
|
8909
|
+
await writeFile6(
|
|
8618
8910
|
temporary,
|
|
8619
8911
|
`${JSON.stringify(Object.fromEntries(entries), null, 2)}
|
|
8620
8912
|
`
|
|
@@ -8708,7 +9000,7 @@ async function scopeOf(c, name, tabs, owners) {
|
|
|
8708
9000
|
label: labelOf(name),
|
|
8709
9001
|
rows: []
|
|
8710
9002
|
};
|
|
8711
|
-
const dir =
|
|
9003
|
+
const dir = join28(c.rnessDir, name);
|
|
8712
9004
|
const shape = await readShape(c.rnessDir, name);
|
|
8713
9005
|
const fronts = new Map(
|
|
8714
9006
|
(await collectMarkdown(dir)).map((item) => [
|
|
@@ -8754,7 +9046,7 @@ async function working(ui, doing, count3, work) {
|
|
|
8754
9046
|
async function filesOf2(rnessDir) {
|
|
8755
9047
|
const files = /* @__PURE__ */ new Set();
|
|
8756
9048
|
const walk = async (rel) => {
|
|
8757
|
-
const dir = rel === "" ? rnessDir :
|
|
9049
|
+
const dir = rel === "" ? rnessDir : join28(rnessDir, ...rel.split("/"));
|
|
8758
9050
|
for (const e of await readdir5(dir, { withFileTypes: true })) {
|
|
8759
9051
|
const path = rel === "" ? e.name : `${rel}/${e.name}`;
|
|
8760
9052
|
if (e.isDirectory()) {
|
|
@@ -8866,7 +9158,7 @@ async function syncBoard(c, board, ui, sayLayout, scope) {
|
|
|
8866
9158
|
w.path,
|
|
8867
9159
|
issueBody({
|
|
8868
9160
|
path: w.path,
|
|
8869
|
-
text: await
|
|
9161
|
+
text: await readFile15(join28(c.rnessDir, ...w.path.split("/")), "utf8"),
|
|
8870
9162
|
org: c.org,
|
|
8871
9163
|
numbers,
|
|
8872
9164
|
files
|
|
@@ -9358,6 +9650,14 @@ function buildProgram(state) {
|
|
|
9358
9650
|
).argument("[tab]", "open on this tab: adr, specs, plans, \u2026").addOption(new Option("--cwd <dir>").hideHelp()).action(async (tab, opts) => {
|
|
9359
9651
|
state.code = await statusCommand(tab, opts);
|
|
9360
9652
|
});
|
|
9653
|
+
const doc = program.command("doc").description("Documents of the workspace: doc new <collection>");
|
|
9654
|
+
doc.command("new").description(
|
|
9655
|
+
"Write the next numbered document of a collection (adr, specs, plans) and print its path"
|
|
9656
|
+
).argument("<collection>", "adr, specs or plans").option("--title <text>", "the title; the file name takes its slug").addOption(new Option("--cwd <dir>").hideHelp()).action(
|
|
9657
|
+
async (collection, opts) => {
|
|
9658
|
+
state.code = await docNewCommand(collection, opts);
|
|
9659
|
+
}
|
|
9660
|
+
);
|
|
9361
9661
|
program.command("validate").description(
|
|
9362
9662
|
"Check the .rness/ tree against the contract and every generated block"
|
|
9363
9663
|
).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
|
|
@@ -9618,4 +9918,4 @@ export {
|
|
|
9618
9918
|
OWN_COMMANDS,
|
|
9619
9919
|
catchUp
|
|
9620
9920
|
};
|
|
9621
|
-
//# sourceMappingURL=chunk-
|
|
9921
|
+
//# sourceMappingURL=chunk-JHU2G7K7.js.map
|