@rness/cli 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -26
- package/dist/bin/rness.js +12 -7
- package/dist/bin/rness.js.map +1 -1
- package/dist/{chunk-REDELHNC.js → chunk-VGBS7HDY.js} +551 -280
- package/dist/chunk-VGBS7HDY.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/scaffold/package.json +1 -1
- package/dist/chunk-REDELHNC.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.0";
|
|
512
512
|
|
|
513
513
|
// src/core/pm.ts
|
|
514
514
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -741,6 +741,19 @@ 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 documentNumber(rel) {
|
|
748
|
+
return /^(\d{4})-/.exec(posix2.basename(rel))?.[1] ?? null;
|
|
749
|
+
}
|
|
750
|
+
function documentTitle(heading, number) {
|
|
751
|
+
if (number === null) return heading;
|
|
752
|
+
const rest = new RegExp(`^${number}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
|
|
753
|
+
return rest ?? heading;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/core/contract.ts
|
|
744
757
|
var STATUSES = {
|
|
745
758
|
adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
|
|
746
759
|
specs: [
|
|
@@ -760,6 +773,21 @@ async function checkContract(rnessDir) {
|
|
|
760
773
|
for (const name of COLLECTIONS) {
|
|
761
774
|
const allowed = STATUSES[name];
|
|
762
775
|
const items = await collectMarkdown(join5(rnessDir, name));
|
|
776
|
+
const byNumber = /* @__PURE__ */ new Map();
|
|
777
|
+
for (const item of items) {
|
|
778
|
+
const where = `${name}/${item.rel}`;
|
|
779
|
+
if (SKIP.has(where)) continue;
|
|
780
|
+
const number = allowed === void 0 ? null : documentNumber(item.rel);
|
|
781
|
+
if (number !== null)
|
|
782
|
+
byNumber.set(number, [...byNumber.get(number) ?? [], where]);
|
|
783
|
+
}
|
|
784
|
+
for (const [number, files] of byNumber) {
|
|
785
|
+
if (files.length < 2) continue;
|
|
786
|
+
for (const where of files)
|
|
787
|
+
problems.push(
|
|
788
|
+
`${where}: the number ${number} is also that of ${files.filter((f) => f !== where).join(", ")}`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
763
791
|
for (const item of items) {
|
|
764
792
|
const where = `${name}/${item.rel}`;
|
|
765
793
|
if (SKIP.has(where)) continue;
|
|
@@ -808,6 +836,9 @@ var PULSE_SCOPE = "project";
|
|
|
808
836
|
function clientId() {
|
|
809
837
|
return process.env["RNESS_GITHUB_CLIENT_ID"] ?? GITHUB_CLIENT_ID;
|
|
810
838
|
}
|
|
839
|
+
function approvalUrl() {
|
|
840
|
+
return `https://github.com/settings/connections/applications/${clientId()}`;
|
|
841
|
+
}
|
|
811
842
|
function webBase() {
|
|
812
843
|
return (process.env["RNESS_GITHUB_WEB"] ?? GITHUB_WEB).replace(/\/+$/, "");
|
|
813
844
|
}
|
|
@@ -1063,19 +1094,6 @@ async function resolveToken(deps = {}) {
|
|
|
1063
1094
|
// src/core/status.ts
|
|
1064
1095
|
import { readdir } from "fs/promises";
|
|
1065
1096
|
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
1097
|
var FIXED = [
|
|
1080
1098
|
["adr", "ADR"],
|
|
1081
1099
|
["specs", "Specs"],
|
|
@@ -2052,16 +2070,26 @@ async function getUser(options) {
|
|
|
2052
2070
|
throw new Error(`GitHub API answered ${status2} for /user`);
|
|
2053
2071
|
return { login };
|
|
2054
2072
|
}
|
|
2055
|
-
async function
|
|
2073
|
+
async function listUserMemberships(options) {
|
|
2056
2074
|
const { status: status2, body } = await getJson(
|
|
2057
|
-
`/user/orgs?per_page=${PER_PAGE}`,
|
|
2075
|
+
`/user/memberships/orgs?state=active&per_page=${PER_PAGE}`,
|
|
2058
2076
|
options
|
|
2059
2077
|
);
|
|
2060
2078
|
if (status2 !== 200 || !Array.isArray(body))
|
|
2061
|
-
throw new Error(`GitHub API answered ${status2} for /user/orgs`);
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2079
|
+
throw new Error(`GitHub API answered ${status2} for /user/memberships/orgs`);
|
|
2080
|
+
const memberships = [];
|
|
2081
|
+
for (const m of body) {
|
|
2082
|
+
if (m === null || typeof m !== "object") continue;
|
|
2083
|
+
const entry = m;
|
|
2084
|
+
const org = entry["organization"];
|
|
2085
|
+
const login = org !== null && typeof org === "object" ? org["login"] : void 0;
|
|
2086
|
+
if (typeof login !== "string" || entry["state"] !== "active") continue;
|
|
2087
|
+
memberships.push({
|
|
2088
|
+
login,
|
|
2089
|
+
role: typeof entry["role"] === "string" ? entry["role"] : ""
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
return memberships;
|
|
2065
2093
|
}
|
|
2066
2094
|
async function getOrgMembership(org, options) {
|
|
2067
2095
|
const { status: status2, body } = await getJson(
|
|
@@ -3663,8 +3691,15 @@ var GitHubOAuthProvider = class {
|
|
|
3663
3691
|
}
|
|
3664
3692
|
async listOrganizations() {
|
|
3665
3693
|
if (this.#token === null) return [];
|
|
3666
|
-
const
|
|
3667
|
-
return
|
|
3694
|
+
const memberships = await listUserMemberships(this.#api(this.#token.token));
|
|
3695
|
+
return memberships.map((m) => ({
|
|
3696
|
+
login: m.login,
|
|
3697
|
+
approved: true,
|
|
3698
|
+
canGrant: m.role === "admin"
|
|
3699
|
+
}));
|
|
3700
|
+
}
|
|
3701
|
+
approvalUrl() {
|
|
3702
|
+
return this.#token?.source === "login" ? approvalUrl() : null;
|
|
3668
3703
|
}
|
|
3669
3704
|
async organizationAccess(org) {
|
|
3670
3705
|
if (this.#token === null) return "unknown";
|
|
@@ -3986,12 +4021,12 @@ async function detectGithubSsh(opts) {
|
|
|
3986
4021
|
child.stderr?.setEncoding("utf8");
|
|
3987
4022
|
child.stderr?.on("data", (chunk) => chunks.push(chunk));
|
|
3988
4023
|
let settled = false;
|
|
3989
|
-
const settle = (
|
|
4024
|
+
const settle = (access3) => {
|
|
3990
4025
|
if (settled) return;
|
|
3991
4026
|
settled = true;
|
|
3992
4027
|
clearTimeout(timer);
|
|
3993
4028
|
child.stderr?.destroy();
|
|
3994
|
-
resolve8(
|
|
4029
|
+
resolve8(access3);
|
|
3995
4030
|
};
|
|
3996
4031
|
const timer = setTimeout(() => {
|
|
3997
4032
|
child.kill("SIGKILL");
|
|
@@ -4036,8 +4071,8 @@ async function testGithubSsh(transport, interactive, announce) {
|
|
|
4036
4071
|
};
|
|
4037
4072
|
}
|
|
4038
4073
|
var loggedInAs = (login) => login === void 0 || login === null ? "" : `, logged in as ${login}`;
|
|
4039
|
-
function usingSentence(
|
|
4040
|
-
return
|
|
4074
|
+
function usingSentence(access3, login) {
|
|
4075
|
+
return access3.ok ? `SSH works for github.com (as ${access3.login}) \u2014 repositories are cloned over SSH` : `No SSH access to github.com (${access3.reason}) \u2014 repositories are cloned over HTTPS${loggedInAs(login)}`;
|
|
4041
4076
|
}
|
|
4042
4077
|
function isSshUrl(url, hosts) {
|
|
4043
4078
|
return url.startsWith(hosts.ssh);
|
|
@@ -4048,8 +4083,8 @@ var CHECKING = [
|
|
|
4048
4083
|
"ssh access to github.com\u2026",
|
|
4049
4084
|
"Checking SSH access to github.com \u2014 ssh may ask for your passphrase"
|
|
4050
4085
|
];
|
|
4051
|
-
function usingRest(
|
|
4052
|
-
return
|
|
4086
|
+
function usingRest(access3, login) {
|
|
4087
|
+
return access3.ok ? `ssh (github.com as ${access3.login})` : `https (ssh to github.com unavailable: ${access3.reason}${loggedInAs(login)})`;
|
|
4053
4088
|
}
|
|
4054
4089
|
function agentHintLines(platform = process.platform) {
|
|
4055
4090
|
const mac = platform === "darwin";
|
|
@@ -4308,15 +4343,28 @@ async function ensureClaudeMd(dir) {
|
|
|
4308
4343
|
import { join as join15 } from "path";
|
|
4309
4344
|
|
|
4310
4345
|
// src/core/agent-targets.ts
|
|
4311
|
-
import { mkdir as mkdir2 } from "fs/promises";
|
|
4346
|
+
import { mkdir as mkdir2, rm as rm4, rmdir } from "fs/promises";
|
|
4312
4347
|
import { dirname as dirname4, join as join14 } from "path";
|
|
4313
4348
|
|
|
4314
4349
|
// src/core/claude-skills.ts
|
|
4315
4350
|
var PLUGIN = ".claude/skills/rness";
|
|
4316
4351
|
var PLUGIN_JSON = `${JSON.stringify(
|
|
4317
4352
|
{
|
|
4353
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
4318
4354
|
name: "rness",
|
|
4319
|
-
description: "The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan
|
|
4355
|
+
description: "The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan.",
|
|
4356
|
+
author: { name: "rness-dev", url: "https://rness.dev" },
|
|
4357
|
+
homepage: "https://rness.dev",
|
|
4358
|
+
repository: "https://github.com/rness-dev/rness",
|
|
4359
|
+
license: "MIT",
|
|
4360
|
+
keywords: [
|
|
4361
|
+
"rness",
|
|
4362
|
+
"agents",
|
|
4363
|
+
"claude-code",
|
|
4364
|
+
"adr",
|
|
4365
|
+
"specification",
|
|
4366
|
+
"plan"
|
|
4367
|
+
]
|
|
4320
4368
|
},
|
|
4321
4369
|
null,
|
|
4322
4370
|
2
|
|
@@ -4347,53 +4395,68 @@ terminal, from the workspace's \`.rness/\`: \`${localRunner(pm)} rness status\`.
|
|
|
4347
4395
|
}
|
|
4348
4396
|
var STATUS_RULE = `Move a status yourself when the work calls for it, and whenever the
|
|
4349
4397
|
developer says so: a plan \`In progress\` as you start its tasks,
|
|
4350
|
-
\`Completed\` once
|
|
4351
|
-
plan; a document \`Superseded\` by the one that
|
|
4352
|
-
|
|
4353
|
-
|
|
4398
|
+
\`Completed\` once its proofs pass under \`check\`; a specification
|
|
4399
|
+
\`Implemented\` with its last plan; a document \`Superseded\` by the one that
|
|
4400
|
+
replaces it. Accepting an ADR is the developer's; a specification is
|
|
4401
|
+
\`Approved\` by \`/rness:plan from NNNN\`, which is their word for it.`;
|
|
4354
4402
|
var COMMIT_RULE = `Commit each file you changed, in its own repository, and only the files
|
|
4355
4403
|
you alone changed: add them by name, never \`git add -A\` nor \`commit -a\`.
|
|
4356
4404
|
A file that had uncommitted changes before your first edit (\`git status\`
|
|
4357
4405
|
tells you), or that someone else changed since, stays uncommitted: say
|
|
4358
4406
|
which. Never push.`;
|
|
4407
|
+
var SESSION_LINE = `\`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
|
|
4408
|
+
\`sessions:\` in the front matter, unless that id is already there;
|
|
4409
|
+
\`<model>\` is the model you run as, as Claude Code names it, after
|
|
4410
|
+
\`Claude\`: for example \`Claude Opus 5.5\``;
|
|
4411
|
+
var allocateStep = (allocate, collection) => `Allocate the file: run \`${allocate}\` from the project directory, where
|
|
4412
|
+
Claude Code was opened (pre-approved above as typed; \`CLAUDE_PROJECT_DIR\`
|
|
4413
|
+
is not set in your shell, so the path is relative). It writes
|
|
4414
|
+
\`${collection}/NNNN-untitled.md\` with the next number, the front matter
|
|
4415
|
+
and the opening sections, and prints the path. Rename it to
|
|
4416
|
+
\`NNNN-<slug>.md\`, the slug a few lowercase words of the title joined by
|
|
4417
|
+
hyphens, and write the title as \`# NNNN \u2014 <title>\`. Never choose the
|
|
4418
|
+
number yourself.`;
|
|
4419
|
+
var HOOK_LINE = (rness) => `The rness hook checks every edit under \`${rness}/\`: fix what it reports.`;
|
|
4359
4420
|
var LIFECYCLE2 = [
|
|
4360
4421
|
{
|
|
4361
4422
|
name: "adr",
|
|
4362
|
-
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.",
|
|
4363
|
-
hint: "[subject | NNNN]",
|
|
4423
|
+
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.",
|
|
4424
|
+
hint: "[create <subject> | open NNNN]",
|
|
4364
4425
|
tab: "adr",
|
|
4365
|
-
body: (rness) => `You
|
|
4366
|
-
above lists the existing
|
|
4426
|
+
body: (rness, allocate) => `You keep the decisions of this workspace: the ADRs in \`${rness}/adr/\`. The table
|
|
4427
|
+
above lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
|
|
4428
|
+
|
|
4429
|
+
The first word is the verb, when a reference (\`0010\`, or a path) follows
|
|
4430
|
+
it; a bare reference is \`open\`; any other words are the subject of a
|
|
4431
|
+
\`create\` (\`open source licensing\` is a subject):
|
|
4367
4432
|
|
|
4368
|
-
|
|
4433
|
+
- \`create [subject]\`, or nothing: a new ADR, numbered by rness.
|
|
4434
|
+
- \`open NNNN\`: reopen ADR NNNN. An \`Accepted\` ADR is never edited: offer a
|
|
4435
|
+
new one that supersedes it. \`Rejected\` and \`Superseded\` are history too.
|
|
4436
|
+
|
|
4437
|
+
Creating:
|
|
4369
4438
|
|
|
4370
4439
|
1. If the developer did not ask for an ADR in so many words, propose one in
|
|
4371
4440
|
a single line \u2014 its title, and why the choice is hard to reverse \u2014 and
|
|
4372
4441
|
wait for a yes.
|
|
4373
4442
|
2. Read \`${rness}/CONVENTIONS.md\` and \`${rness}/adr/0000-template.md\`: they hold the
|
|
4374
4443
|
rules and the sections; this skill is only the procedure.
|
|
4375
|
-
3.
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
with \`superseded_by:\`, only once the new ADR is \`Accepted\`.
|
|
4389
|
-
- Add \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
|
|
4390
|
-
\`sessions:\` in the front matter, unless that id is already there.
|
|
4391
|
-
\`<model>\` is the model you run as, as Claude Code names it, after
|
|
4392
|
-
\`Claude\`: for example \`Claude Opus 5.5\`.
|
|
4393
|
-
- The rness hook checks every edit under \`${rness}/\`: fix what it reports.
|
|
4444
|
+
3. The subject is the arguments, else the decision just discussed. Context,
|
|
4445
|
+
alternatives or consequences missing from the conversation: ask for
|
|
4446
|
+
them, one question at a time. Never invent them.
|
|
4447
|
+
4. ${allocateStep(allocate, "adr")}
|
|
4448
|
+
5. Fill it: \`repo\` the repositories under \`org/\` it affects; the template's
|
|
4449
|
+
sections, their guidance lines removed; an ADR it replaces named in
|
|
4450
|
+
Context \u2014 that one becomes \`Superseded\`, with \`superseded_by:\`, only once
|
|
4451
|
+
the new ADR is \`Accepted\`. Add ${SESSION_LINE}.
|
|
4452
|
+
6. ${HOOK_LINE(rness)}
|
|
4453
|
+
|
|
4454
|
+
Opening: read the ADR and amend what the developer asks, in place while it
|
|
4455
|
+
is \`Proposed\`; add the session line above. Nothing of the file's history is
|
|
4456
|
+
rewritten: a decision that changed is a new ADR.
|
|
4394
4457
|
|
|
4395
|
-
After writing: give the path and the Decision section.
|
|
4396
|
-
\`Proposed\` until
|
|
4458
|
+
After writing: give the path and the Decision section. A new ADR stays
|
|
4459
|
+
\`Proposed\` until the developer accepts it.
|
|
4397
4460
|
|
|
4398
4461
|
${STATUS_RULE}
|
|
4399
4462
|
|
|
@@ -4402,43 +4465,47 @@ ${COMMIT_RULE}
|
|
|
4402
4465
|
},
|
|
4403
4466
|
{
|
|
4404
4467
|
name: "spec",
|
|
4405
|
-
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.",
|
|
4406
|
-
hint: "[subject | NNNN]",
|
|
4468
|
+
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.",
|
|
4469
|
+
hint: "[create <subject> | open NNNN]",
|
|
4407
4470
|
tab: "specs",
|
|
4408
|
-
body: (rness) => `You
|
|
4409
|
-
lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
|
|
4471
|
+
body: (rness, allocate) => `You keep the specifications of this workspace, in \`${rness}/specs/\`. The table
|
|
4472
|
+
above lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
|
|
4473
|
+
|
|
4474
|
+
The first word is the verb, when a reference (\`0028\`, or a path) follows
|
|
4475
|
+
it; a bare reference is \`open\`; any other words are the subject of a
|
|
4476
|
+
\`create\`:
|
|
4477
|
+
|
|
4478
|
+
- \`create [subject]\`, or nothing: a new specification, numbered by rness.
|
|
4479
|
+
- \`open NNNN\`: reopen specification NNNN. One that is \`Implemented\`,
|
|
4480
|
+
\`Superseded\` or \`Rejected\` is not edited: offer a new one.
|
|
4410
4481
|
|
|
4411
|
-
|
|
4482
|
+
Creating:
|
|
4412
4483
|
|
|
4413
4484
|
1. If the developer did not ask for a specification in so many words,
|
|
4414
4485
|
propose one in a single line and wait for a yes.
|
|
4415
4486
|
2. Read \`${rness}/CONVENTIONS.md\`, then the most recent specification above: match
|
|
4416
4487
|
its front matter, sections and tone.
|
|
4417
|
-
3.
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
\`sessions:\` in the front matter, unless that id is already there.
|
|
4435
|
-
\`<model>\` is the model you run as, as Claude Code names it, after
|
|
4436
|
-
\`Claude\`: for example \`Claude Opus 5.5\`.
|
|
4437
|
-
- The rness hook checks every edit under \`${rness}/\`: fix what it reports.
|
|
4488
|
+
3. The subject is the arguments, else the work just discussed. The problem,
|
|
4489
|
+
what is in and out of scope, the ADRs it rests on: ask what the
|
|
4490
|
+
conversation does not say, one question at a time.
|
|
4491
|
+
4. ${allocateStep(allocate, "specs")}
|
|
4492
|
+
5. Fill it: \`repo\`, and \`adr:\` linking the decisions it rests on \u2014 or the
|
|
4493
|
+
rationale in the body when none does; the sections it opened with
|
|
4494
|
+
(Summary, Scope, Out of scope, Alternatives considered, To verify,
|
|
4495
|
+
Tests), or the most recent specification's when the team's differ. What
|
|
4496
|
+
was checked in this session (a document read, a command run) is written
|
|
4497
|
+
as verified, with the date; everything else as a proposal or an open
|
|
4498
|
+
question. Add ${SESSION_LINE}.
|
|
4499
|
+
6. ${HOOK_LINE(rness)}
|
|
4500
|
+
|
|
4501
|
+
Opening: read the specification and amend what the developer asks \u2014 a
|
|
4502
|
+
section, a scope, an open question answered \u2014 \`updated\` today, the session
|
|
4503
|
+
line added. A status moves only as the developer says, or by
|
|
4504
|
+
\`/rness:plan from\`.
|
|
4438
4505
|
|
|
4439
|
-
After writing: give the path and the summary.
|
|
4440
|
-
\`Proposed\` for review
|
|
4441
|
-
|
|
4506
|
+
After writing: give the path and the summary. A new specification starts
|
|
4507
|
+
\`Draft\`: \`Proposed\` for review; \`/rness:plan from NNNN\` approves it and
|
|
4508
|
+
plans it.
|
|
4442
4509
|
|
|
4443
4510
|
${STATUS_RULE}
|
|
4444
4511
|
|
|
@@ -4447,105 +4514,104 @@ ${COMMIT_RULE}
|
|
|
4447
4514
|
},
|
|
4448
4515
|
{
|
|
4449
4516
|
name: "plan",
|
|
4450
|
-
description: "
|
|
4451
|
-
hint: "<spec NNNN
|
|
4517
|
+
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.",
|
|
4518
|
+
hint: "[create <subject> | from <spec> | open NNNN | check NNNN]",
|
|
4452
4519
|
tab: "plans",
|
|
4453
|
-
body: (rness) => `You
|
|
4454
|
-
the existing
|
|
4455
|
-
\`$ARGUMENTS\`.
|
|
4456
|
-
|
|
4457
|
-
Before writing:
|
|
4458
|
-
|
|
4459
|
-
1. If the developer did not ask for a plan in so many words, propose one in
|
|
4460
|
-
a single line and wait for a yes.
|
|
4461
|
-
2. No specification named: ask which. Read it, \`${rness}/CONVENTIONS.md\`, and the
|
|
4462
|
-
most recent plan above: match its front matter and sections.
|
|
4463
|
-
3. A specification that is not \`Approved\`: say so, and plan it only if the
|
|
4464
|
-
developer confirms. A plan of it that is neither \`Completed\` nor
|
|
4465
|
-
\`Abandoned\`: reopen it rather than write a second.
|
|
4466
|
-
4. Read the code and the repositories the specification touches: every
|
|
4467
|
-
task names real files and real commands.
|
|
4520
|
+
body: (rness, allocate) => `You keep the plans of this workspace, in \`${rness}/plans/\`. The table above
|
|
4521
|
+
lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
|
|
4468
4522
|
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
not the specification's.
|
|
4473
|
-
- \`date\` and \`updated\` today, \`status: Draft\`, \`repo\`, \`spec:\` linking the
|
|
4474
|
-
specification.
|
|
4475
|
-
- Tasks in order, as checkboxes. Each gives the files, what changes, and
|
|
4476
|
-
the command that proves it (a test, a build, a run). The last tasks:
|
|
4477
|
-
the documentation the work makes inaccurate, then the release when the
|
|
4478
|
-
repository has one.
|
|
4479
|
-
- Where the plan decides something the specification leaves open, a
|
|
4480
|
-
section says so.
|
|
4481
|
-
- Add \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
|
|
4482
|
-
\`sessions:\` in the front matter, unless that id is already there.
|
|
4483
|
-
\`<model>\` is the model you run as, as Claude Code names it, after
|
|
4484
|
-
\`Claude\`: for example \`Claude Opus 5.5\`.
|
|
4485
|
-
- The rness hook checks every edit under \`${rness}/\`: fix what it reports.
|
|
4523
|
+
The first word is the verb, when a reference (\`0037\`, or a path) follows
|
|
4524
|
+
it \u2014 \`check\` alone means the plan \`In progress\`; a bare reference is
|
|
4525
|
+
\`open\`; any other words are the subject of a \`create\`:
|
|
4486
4526
|
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4527
|
+
- \`from NNNN\`: a plan from specification NNNN, which this approves.
|
|
4528
|
+
- \`create [subject]\`, or nothing: a plan from the conversation, for work
|
|
4529
|
+
too small for a specification; \`spec:\` is left empty.
|
|
4530
|
+
- \`open NNNN\`: reopen plan NNNN.
|
|
4531
|
+
- \`check NNNN\`: run the proofs of plan NNNN; close it when they all pass.
|
|
4490
4532
|
|
|
4491
|
-
|
|
4533
|
+
\`from\`:
|
|
4492
4534
|
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4535
|
+
1. Read the specification, \`${rness}/CONVENTIONS.md\`, and the most recent plan
|
|
4536
|
+
above: match its front matter and sections. A specification that is
|
|
4537
|
+
\`Implemented\`, \`Superseded\` or \`Rejected\` is refused: say its status,
|
|
4538
|
+
write nothing. A plan of it that is neither \`Completed\` nor \`Abandoned\`:
|
|
4539
|
+
reopen it rather than write a second.
|
|
4540
|
+
2. The developer typed this command: that is their approval, and the
|
|
4541
|
+
specification moves from \`Draft\` or \`Proposed\` to \`Approved\`, \`updated\`
|
|
4542
|
+
today, in the same commit as the plan \u2014 no confirmation question. If
|
|
4543
|
+
you are proposing it yourself, propose it in a single line and wait for
|
|
4544
|
+
a yes first: the approval is theirs, never yours.
|
|
4545
|
+
3. Read the code and the repositories the specification touches: every
|
|
4546
|
+
task names real files and real commands.
|
|
4547
|
+
4. ${allocateStep(allocate, "plans")}
|
|
4548
|
+
5. Fill it: \`spec:\` linking the specification; tasks in order, as
|
|
4549
|
+
checkboxes, each with the files, what changes, and the command that
|
|
4550
|
+
proves it (a test, a build, a run) \u2014 the last tasks the documentation
|
|
4551
|
+
the work makes inaccurate, then the release when the repository has
|
|
4552
|
+
one; where the plan decides something the specification leaves open, the
|
|
4553
|
+
section it opened with says so. Add ${SESSION_LINE}.
|
|
4554
|
+
6. ${HOOK_LINE(rness)}
|
|
4555
|
+
|
|
4556
|
+
\`create\`: as \`from\`, without a specification. If the developer did not ask
|
|
4557
|
+
for a plan in so many words, propose one in a single line and wait for a
|
|
4558
|
+
yes. The body says what the work is and why it needs no specification.
|
|
4559
|
+
|
|
4560
|
+
\`open\`: read the plan and its specification; amend what the developer
|
|
4561
|
+
asks \u2014 a task added, split or reworded, a blocker recorded \u2014 \`updated\`
|
|
4562
|
+
today, the session line added; move \`Draft\` to \`Ready\` or \`Blocked\` as they
|
|
4563
|
+
say. A \`Completed\` or \`Abandoned\` plan is not reopened: offer a new plan of
|
|
4564
|
+
the same specification.
|
|
4565
|
+
|
|
4566
|
+
\`check\`:
|
|
4504
4567
|
|
|
4505
4568
|
1. The plan: the one named, else the one \`In progress\`. Several or none:
|
|
4506
4569
|
ask.
|
|
4507
|
-
2. The developer asked
|
|
4508
|
-
this session:
|
|
4509
|
-
for a yes.
|
|
4570
|
+
2. The developer asked for the check, or the plan's last task has just
|
|
4571
|
+
passed in this session: run it. Otherwise propose it in a single line
|
|
4572
|
+
and wait for a yes.
|
|
4510
4573
|
3. Read the plan, its specification and \`${rness}/CONVENTIONS.md\`.
|
|
4511
|
-
4. Evidence first. For each task, what
|
|
4512
|
-
|
|
4513
|
-
check
|
|
4514
|
-
5.
|
|
4574
|
+
4. Evidence first. For each task, what proves it: its command run now, or
|
|
4575
|
+
its result read in this session. Never mark a task done on belief. A
|
|
4576
|
+
check has no cache: run twice, it proves twice.
|
|
4577
|
+
5. Every task proven:
|
|
4515
4578
|
- the plan: \`status: Completed\`, \`updated\` today, the checkboxes of the
|
|
4516
4579
|
proven tasks ticked, and a section \`Verification (YYYY-MM-DD)\` \u2014 what
|
|
4517
4580
|
ran, what it showed, what could not run and why;
|
|
4518
4581
|
- the specification: \`Implemented\` once every plan of it is
|
|
4519
4582
|
\`Completed\`; \`updated\` today;
|
|
4520
|
-
- the plan and the specification: add
|
|
4521
|
-
\`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
|
|
4522
|
-
\`sessions:\` in their front matter, unless that id is already there;
|
|
4523
|
-
\`<model>\` is the model you run as, as Claude Code names it, after
|
|
4524
|
-
\`Claude\`: for example \`Claude Opus 5.5\`;
|
|
4583
|
+
- the plan and the specification: add ${SESSION_LINE};
|
|
4525
4584
|
- the smallest set of documents the work makes inaccurate: \`${rness}/docs/\`,
|
|
4526
4585
|
the READMEs of the repositories it changed.
|
|
4527
|
-
6. A task not proven: the plan stays \`In progress
|
|
4528
|
-
|
|
4529
|
-
|
|
4586
|
+
6. A task not proven: nothing moves, the plan stays \`In progress\`. Say,
|
|
4587
|
+
task by task, what passed and what remains. A blocker the developer
|
|
4588
|
+
names is a move of its own: \`Blocked\`, the blocker written in the plan.
|
|
4589
|
+
7. ${HOOK_LINE(rness)}
|
|
4530
4590
|
|
|
4531
|
-
After:
|
|
4591
|
+
After writing: give the path and the list of tasks, or the statuses moved.
|
|
4592
|
+
A new plan starts \`Draft\`, and becomes \`Ready\` once the developer agrees
|
|
4593
|
+
with it or asks you to carry it out.
|
|
4594
|
+
|
|
4595
|
+
${STATUS_RULE}
|
|
4532
4596
|
|
|
4533
4597
|
${COMMIT_RULE}
|
|
4534
4598
|
`
|
|
4535
4599
|
}
|
|
4536
4600
|
];
|
|
4601
|
+
var RETIRED_SKILLS = ["done"];
|
|
4537
4602
|
function lifecycleSkill(rness, s) {
|
|
4538
4603
|
const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status ${s.tab} --cwd "\${CLAUDE_PROJECT_DIR}"`;
|
|
4604
|
+
const allocate = `node "${rness}/${PINNED_BIN}" doc new ${s.tab}`;
|
|
4539
4605
|
return `---
|
|
4540
4606
|
name: ${s.name}
|
|
4541
4607
|
description: ${s.description}
|
|
4542
4608
|
argument-hint: '${s.hint}'
|
|
4543
|
-
allowed-tools: Bash(${command})
|
|
4609
|
+
allowed-tools: Bash(${command}), Bash(${allocate})
|
|
4544
4610
|
---
|
|
4545
4611
|
|
|
4546
4612
|
!\`${command}\`
|
|
4547
4613
|
|
|
4548
|
-
${s.body(rness)}`;
|
|
4614
|
+
${s.body(rness, allocate)}`;
|
|
4549
4615
|
}
|
|
4550
4616
|
function pluginFiles(rness, at, ctx) {
|
|
4551
4617
|
return [
|
|
@@ -4559,6 +4625,11 @@ function pluginFiles(rness, at, ctx) {
|
|
|
4559
4625
|
file: `${PLUGIN}/skills/${s.name}/SKILL.md`,
|
|
4560
4626
|
at,
|
|
4561
4627
|
content: lifecycleSkill(rness, s)
|
|
4628
|
+
})),
|
|
4629
|
+
...RETIRED_SKILLS.map((name) => ({
|
|
4630
|
+
file: `${PLUGIN}/skills/${name}/SKILL.md`,
|
|
4631
|
+
at,
|
|
4632
|
+
retired: true
|
|
4562
4633
|
}))
|
|
4563
4634
|
];
|
|
4564
4635
|
}
|
|
@@ -5084,6 +5155,21 @@ async function agentTargets(root, manifest, opts) {
|
|
|
5084
5155
|
for (const owned of await filesOf(root, manifest, declared(manifest))) {
|
|
5085
5156
|
const { file, label } = owned;
|
|
5086
5157
|
const existing = await readOrNull(file);
|
|
5158
|
+
if ("retired" in owned) {
|
|
5159
|
+
if (existing === null) continue;
|
|
5160
|
+
if (opts.check) {
|
|
5161
|
+
outcomes.push({
|
|
5162
|
+
label,
|
|
5163
|
+
status: "stale",
|
|
5164
|
+
detail: "no longer written by rness; remove it"
|
|
5165
|
+
});
|
|
5166
|
+
continue;
|
|
5167
|
+
}
|
|
5168
|
+
await rm4(file);
|
|
5169
|
+
await rmdir(dirname4(file)).catch(() => void 0);
|
|
5170
|
+
outcomes.push({ label, status: "removed", detail: null });
|
|
5171
|
+
continue;
|
|
5172
|
+
}
|
|
5087
5173
|
if ("content" in owned) {
|
|
5088
5174
|
const diff = wholeDiff(existing, owned.content);
|
|
5089
5175
|
if (diff === null)
|
|
@@ -5134,6 +5220,7 @@ async function leftoverTargets(root, manifest) {
|
|
|
5134
5220
|
);
|
|
5135
5221
|
const leftovers = [];
|
|
5136
5222
|
for (const owned of await filesOf(root, manifest, undeclared)) {
|
|
5223
|
+
if ("retired" in owned) continue;
|
|
5137
5224
|
const existing = await readOrNull(owned.file);
|
|
5138
5225
|
if (existing === null) continue;
|
|
5139
5226
|
if ("content" in owned) {
|
|
@@ -5523,15 +5610,15 @@ async function syncCommand(opts, deps = {}) {
|
|
|
5523
5610
|
if (!overSsh) ui.gitIsSilent = true;
|
|
5524
5611
|
if (overSsh) {
|
|
5525
5612
|
const interactive = opts.yes !== true && terminal.isTty();
|
|
5526
|
-
const { access:
|
|
5613
|
+
const { access: access3, unattended } = await testGithubSsh(
|
|
5527
5614
|
transport,
|
|
5528
5615
|
interactive,
|
|
5529
5616
|
() => ui.line(...CHECKING)
|
|
5530
5617
|
);
|
|
5531
5618
|
if (unattended) ui.gitIsSilent = true;
|
|
5532
|
-
if (!
|
|
5619
|
+
if (!access3.ok) {
|
|
5533
5620
|
process.stderr.write(
|
|
5534
|
-
`${[...sshWorkspaceLines(
|
|
5621
|
+
`${[...sshWorkspaceLines(access3.reason), ...INSTEAD_OF_LINES].join("\n")}
|
|
5535
5622
|
`
|
|
5536
5623
|
);
|
|
5537
5624
|
return 1;
|
|
@@ -5760,20 +5847,20 @@ async function addCommand(spec, opts, deps = {}) {
|
|
|
5760
5847
|
if (host === void 0 && opts.https === true) host = transport.hosts.https;
|
|
5761
5848
|
if (host === void 0 && FULL_URL2.test(spec)) host = transport.hosts.https;
|
|
5762
5849
|
if (host === void 0) {
|
|
5763
|
-
const { access:
|
|
5850
|
+
const { access: access3, unattended } = await testGithubSsh(
|
|
5764
5851
|
transport,
|
|
5765
5852
|
interactive,
|
|
5766
5853
|
() => ui.line(...CHECKING)
|
|
5767
5854
|
);
|
|
5768
|
-
if (unattended || !
|
|
5855
|
+
if (unattended || !access3.ok) ui.gitIsSilent = true;
|
|
5769
5856
|
const overSsh = Object.values(manifest.repos).some(
|
|
5770
5857
|
(r) => isSshUrl(r.url, transport.hosts)
|
|
5771
5858
|
);
|
|
5772
|
-
if (
|
|
5773
|
-
ui.line("using", usingRest(
|
|
5774
|
-
host =
|
|
5859
|
+
if (access3.ok || !overSsh) {
|
|
5860
|
+
ui.line("using", usingRest(access3), usingSentence(access3));
|
|
5861
|
+
host = access3.ok ? transport.hosts.ssh : transport.hosts.https;
|
|
5775
5862
|
} else {
|
|
5776
|
-
const lines = sshWorkspaceLines(
|
|
5863
|
+
const lines = sshWorkspaceLines(access3.reason);
|
|
5777
5864
|
if (!interactive) {
|
|
5778
5865
|
process.stderr.write(
|
|
5779
5866
|
`${[...lines, httpsFlagLine(spec)].join("\n")}
|
|
@@ -5987,9 +6074,23 @@ async function copyScaffold(dest, tokens) {
|
|
|
5987
6074
|
}
|
|
5988
6075
|
|
|
5989
6076
|
// src/commands/login.ts
|
|
5990
|
-
import { spawn as spawn3 } from "child_process";
|
|
5991
6077
|
import { realpath } from "fs/promises";
|
|
5992
6078
|
|
|
6079
|
+
// src/core/browser.ts
|
|
6080
|
+
import { spawn as spawn3 } from "child_process";
|
|
6081
|
+
function openInBrowser(url) {
|
|
6082
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
6083
|
+
try {
|
|
6084
|
+
const child = spawn3(command, args, { stdio: "ignore", detached: true });
|
|
6085
|
+
child.on("error", () => void 0);
|
|
6086
|
+
child.unref();
|
|
6087
|
+
} catch {
|
|
6088
|
+
}
|
|
6089
|
+
}
|
|
6090
|
+
function canOpenBrowser() {
|
|
6091
|
+
return process.env["SSH_CONNECTION"] === void 0;
|
|
6092
|
+
}
|
|
6093
|
+
|
|
5993
6094
|
// src/core/setup-git.ts
|
|
5994
6095
|
import { execFile as execFile4 } from "child_process";
|
|
5995
6096
|
import { promisify as promisify4 } from "util";
|
|
@@ -6034,15 +6135,6 @@ async function removeGitSetup() {
|
|
|
6034
6135
|
}
|
|
6035
6136
|
|
|
6036
6137
|
// src/commands/login.ts
|
|
6037
|
-
function openInBrowser(url) {
|
|
6038
|
-
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
6039
|
-
try {
|
|
6040
|
-
const child = spawn3(command, args, { stdio: "ignore", detached: true });
|
|
6041
|
-
child.on("error", () => void 0);
|
|
6042
|
-
child.unref();
|
|
6043
|
-
} catch {
|
|
6044
|
-
}
|
|
6045
|
-
}
|
|
6046
6138
|
async function offerSetupGit(opts, terminal, bin, ui) {
|
|
6047
6139
|
if (opts.setupGit === false) return;
|
|
6048
6140
|
if (!isStableInstall(bin)) {
|
|
@@ -6119,7 +6211,7 @@ async function loginCommand(opts, deps = {}) {
|
|
|
6119
6211
|
ui.line("open", code.verificationUri);
|
|
6120
6212
|
ui.line("code", code.userCode);
|
|
6121
6213
|
}
|
|
6122
|
-
if (terminal.isTty() &&
|
|
6214
|
+
if (terminal.isTty() && canOpenBrowser())
|
|
6123
6215
|
(deps.open ?? openInBrowser)(code.verificationUri);
|
|
6124
6216
|
if (!ui.session)
|
|
6125
6217
|
ui.line("waiting", "for you to approve rness on github.com\u2026");
|
|
@@ -6152,35 +6244,115 @@ async function loginCommand(opts, deps = {}) {
|
|
|
6152
6244
|
return reportError(e);
|
|
6153
6245
|
}
|
|
6154
6246
|
}
|
|
6155
|
-
function revokeUrl() {
|
|
6156
|
-
return `https://github.com/settings/connections/applications/${clientId()}`;
|
|
6157
|
-
}
|
|
6158
6247
|
|
|
6159
6248
|
// src/commands/create.ts
|
|
6160
|
-
import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as
|
|
6249
|
+
import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm7 } from "fs/promises";
|
|
6161
6250
|
import { tmpdir as tmpdir2 } from "os";
|
|
6162
6251
|
import { join as join22, relative as relative4, resolve as resolve5 } from "path";
|
|
6163
6252
|
|
|
6253
|
+
// src/core/approval.ts
|
|
6254
|
+
var APPROVAL_INTERVAL_MS = 5e3;
|
|
6255
|
+
var APPROVAL_WAIT_MS = 10 * 6e4;
|
|
6256
|
+
function approvalQuestion(org) {
|
|
6257
|
+
return `Open github.com to approve rness for ${org} now?`;
|
|
6258
|
+
}
|
|
6259
|
+
async function checkAccess(input) {
|
|
6260
|
+
const { org, provider, ui } = input;
|
|
6261
|
+
if (!provider.authenticated) return "unknown";
|
|
6262
|
+
const login = (await provider.identity())?.login;
|
|
6263
|
+
const as = login === void 0 ? "" : ` (as ${login})`;
|
|
6264
|
+
const member = () => ui.line("member", `${org}${as}`, `You are a member of ${org}${as}`);
|
|
6265
|
+
const access3 = await provider.organizationAccess(org);
|
|
6266
|
+
if (access3 === "member") {
|
|
6267
|
+
member();
|
|
6268
|
+
return access3;
|
|
6269
|
+
}
|
|
6270
|
+
if (access3 !== "restricted") return access3;
|
|
6271
|
+
const url = provider.approvalUrl();
|
|
6272
|
+
ui.warn(
|
|
6273
|
+
`${org} has not approved rness, so its private repositories are hidden`
|
|
6274
|
+
);
|
|
6275
|
+
if (!input.interactive || url === null) {
|
|
6276
|
+
ui.hint(
|
|
6277
|
+
url === null ? `an owner of ${org} has to approve the app your token belongs to` : `an owner approves it at ${url}`,
|
|
6278
|
+
"stderr"
|
|
6279
|
+
);
|
|
6280
|
+
return "restricted";
|
|
6281
|
+
}
|
|
6282
|
+
const p = await input.prompts();
|
|
6283
|
+
const ok = await p.confirm({
|
|
6284
|
+
message: approvalQuestion(org),
|
|
6285
|
+
initialValue: true
|
|
6286
|
+
});
|
|
6287
|
+
if (p.isCancel(ok)) return "cancelled";
|
|
6288
|
+
if (ok !== true) {
|
|
6289
|
+
ui.hint(`approve it later: ${url}`, "stderr");
|
|
6290
|
+
return "restricted";
|
|
6291
|
+
}
|
|
6292
|
+
ui.line("open", url, `Open ${url} and approve rness for ${org}`);
|
|
6293
|
+
if (canOpenBrowser()) (input.deps?.open ?? openInBrowser)(url);
|
|
6294
|
+
const sleep = input.deps?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
6295
|
+
const now = input.deps?.now ?? Date.now;
|
|
6296
|
+
const who = input.canGrant === true ? org : `an owner of ${org}`;
|
|
6297
|
+
const waiting = `for ${who} to approve rness on github.com`;
|
|
6298
|
+
if (!ui.session) ui.line("waiting", `${waiting}\u2026`);
|
|
6299
|
+
const outcome = await ui.step(
|
|
6300
|
+
{
|
|
6301
|
+
doing: `waiting ${waiting}`,
|
|
6302
|
+
sentence: `Waiting ${waiting}`,
|
|
6303
|
+
quiet: true
|
|
6304
|
+
},
|
|
6305
|
+
async () => {
|
|
6306
|
+
const deadline = now() + APPROVAL_WAIT_MS;
|
|
6307
|
+
for (; ; ) {
|
|
6308
|
+
await sleep(APPROVAL_INTERVAL_MS);
|
|
6309
|
+
const again = await provider.organizationAccess(org);
|
|
6310
|
+
if (again === "member" || again === "not-member") return again;
|
|
6311
|
+
if (now() >= deadline) return "timeout";
|
|
6312
|
+
}
|
|
6313
|
+
},
|
|
6314
|
+
(r) => r === "member" ? ["member", `${org}${as}`] : null
|
|
6315
|
+
);
|
|
6316
|
+
if (outcome === "member") {
|
|
6317
|
+
if (!ui.session) member();
|
|
6318
|
+
return outcome;
|
|
6319
|
+
}
|
|
6320
|
+
if (outcome === "not-member") return outcome;
|
|
6321
|
+
ui.warn(
|
|
6322
|
+
`${org} still has not approved rness; its private repositories stay hidden this time`
|
|
6323
|
+
);
|
|
6324
|
+
ui.hint(
|
|
6325
|
+
`once an owner approves it, add its private repositories with ${rnessCommand()} add <repo>`,
|
|
6326
|
+
"stderr"
|
|
6327
|
+
);
|
|
6328
|
+
return "restricted";
|
|
6329
|
+
}
|
|
6330
|
+
|
|
6164
6331
|
// src/core/ask-provider.ts
|
|
6165
6332
|
var PROVIDER_QUESTION = "Where does your organization live?";
|
|
6333
|
+
var BLANK = "blank";
|
|
6334
|
+
var BLANK_LABEL = "No organization yet: a blank local workspace";
|
|
6166
6335
|
async function askProvider(p) {
|
|
6167
6336
|
return p.select({
|
|
6168
6337
|
message: PROVIDER_QUESTION,
|
|
6169
|
-
options:
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6338
|
+
options: [
|
|
6339
|
+
...Object.values(PROVIDERS).map((entry) => ({
|
|
6340
|
+
value: entry.name,
|
|
6341
|
+
label: entry.available ? entry.label : `${entry.label} (coming later)`,
|
|
6342
|
+
disabled: !entry.available
|
|
6343
|
+
})),
|
|
6344
|
+
{ value: BLANK, label: BLANK_LABEL, disabled: false }
|
|
6345
|
+
]
|
|
6174
6346
|
});
|
|
6175
6347
|
}
|
|
6176
6348
|
|
|
6177
6349
|
// src/core/new-workspace.ts
|
|
6178
|
-
import { mkdir as mkdir4, readdir as readdir4, rm as
|
|
6350
|
+
import { mkdir as mkdir4, readdir as readdir4, rm as rm6, stat as stat3 } from "fs/promises";
|
|
6179
6351
|
import { dirname as dirname8, join as join20, relative as relative2 } from "path";
|
|
6180
6352
|
|
|
6181
6353
|
// src/core/scaffold-git.ts
|
|
6182
6354
|
import { execFile as execFile5 } from "child_process";
|
|
6183
|
-
import { mkdtemp, realpath as realpath2, rm as
|
|
6355
|
+
import { mkdtemp, realpath as realpath2, rm as rm5, writeFile as writeFile3 } from "fs/promises";
|
|
6184
6356
|
import { tmpdir } from "os";
|
|
6185
6357
|
import { dirname as dirname7, join as join19, resolve as resolve3 } from "path";
|
|
6186
6358
|
import { promisify as promisify5 } from "util";
|
|
@@ -6308,7 +6480,7 @@ async function buildScaffoldCommit(rnessDir, base, files, version) {
|
|
|
6308
6480
|
scaffoldMessage(`chore: rness scaffold ${version}`, version)
|
|
6309
6481
|
]);
|
|
6310
6482
|
} finally {
|
|
6311
|
-
await
|
|
6483
|
+
await rm5(scratch, { recursive: true, force: true });
|
|
6312
6484
|
}
|
|
6313
6485
|
}
|
|
6314
6486
|
async function blobAt(rnessDir, rev, path) {
|
|
@@ -6432,9 +6604,9 @@ async function buildContext(input) {
|
|
|
6432
6604
|
return true;
|
|
6433
6605
|
} catch (e) {
|
|
6434
6606
|
reportError(e);
|
|
6435
|
-
await
|
|
6607
|
+
await rm6(rnessDir, { recursive: true, force: true }).catch(() => void 0);
|
|
6436
6608
|
if (await isEmptyDir(root).catch(() => false))
|
|
6437
|
-
await
|
|
6609
|
+
await rm6(root, { recursive: true, force: true }).catch(() => void 0);
|
|
6438
6610
|
process.stderr.write(
|
|
6439
6611
|
`removed ${shown}/.rness after the failure; fix it and retry
|
|
6440
6612
|
`
|
|
@@ -6617,22 +6789,6 @@ function cancelled(ui) {
|
|
|
6617
6789
|
}
|
|
6618
6790
|
async function pickRepositories(input) {
|
|
6619
6791
|
const { org, prompts, provider, ui } = input;
|
|
6620
|
-
if (provider.authenticated) {
|
|
6621
|
-
const access2 = await provider.organizationAccess(org);
|
|
6622
|
-
const login = (await provider.identity())?.login;
|
|
6623
|
-
if (access2 === "member")
|
|
6624
|
-
ui.line(
|
|
6625
|
-
"member",
|
|
6626
|
-
`${org}${login === void 0 ? "" : ` (as ${login})`}`,
|
|
6627
|
-
`You are a member of ${org}${login === void 0 ? "" : ` (as ${login})`}`
|
|
6628
|
-
);
|
|
6629
|
-
else if (access2 === "restricted") {
|
|
6630
|
-
ui.warn(
|
|
6631
|
-
`${org} restricts OAuth apps, so its private repositories are hidden from rness`
|
|
6632
|
-
);
|
|
6633
|
-
ui.hint(`ask an owner to approve it: ${revokeUrl()}`, "stderr");
|
|
6634
|
-
}
|
|
6635
|
-
}
|
|
6636
6792
|
let listed = [];
|
|
6637
6793
|
let failed = false;
|
|
6638
6794
|
if (!ui.session) ui.line("listing", `${org} repositories\u2026`);
|
|
@@ -6782,17 +6938,6 @@ function blankConflict(opts) {
|
|
|
6782
6938
|
if (opts.provider !== void 0) return "--provider";
|
|
6783
6939
|
return null;
|
|
6784
6940
|
}
|
|
6785
|
-
async function chooseStart(prompts) {
|
|
6786
|
-
const answer = await prompts.select({
|
|
6787
|
-
message: "How do you want to start?",
|
|
6788
|
-
options: [
|
|
6789
|
-
{ value: "github", label: "From a GitHub organization" },
|
|
6790
|
-
{ value: "blank", label: "Blank local workspace" }
|
|
6791
|
-
]
|
|
6792
|
-
});
|
|
6793
|
-
if (prompts.isCancel(answer)) return null;
|
|
6794
|
-
return answer === "blank" ? "blank" : "github";
|
|
6795
|
-
}
|
|
6796
6941
|
function providerFlagError(value) {
|
|
6797
6942
|
if (!isProviderName(value))
|
|
6798
6943
|
return `unknown provider "${value}" (${Object.keys(PROVIDERS).join(", ")})`;
|
|
@@ -6801,27 +6946,37 @@ function providerFlagError(value) {
|
|
|
6801
6946
|
return `provider "${value}" is not available yet (available: ${available.join(", ")})`;
|
|
6802
6947
|
}
|
|
6803
6948
|
var OTHER = /* @__PURE__ */ Symbol("another organization");
|
|
6949
|
+
var OTHER_LABEL = "an organization not listed here\u2026";
|
|
6950
|
+
var OTHER_HINT = "not a member, or it has not approved rness yet";
|
|
6951
|
+
var NOT_APPROVED = "rness not approved";
|
|
6804
6952
|
async function chooseOrganization(provider, prompts) {
|
|
6805
6953
|
if (!provider.authenticated) return OTHER;
|
|
6806
|
-
let
|
|
6954
|
+
let organizations;
|
|
6955
|
+
let self;
|
|
6807
6956
|
try {
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
if (self !== void 0) names.push(self);
|
|
6957
|
+
self = (await provider.identity())?.login;
|
|
6958
|
+
organizations = await provider.listOrganizations();
|
|
6811
6959
|
} catch {
|
|
6812
6960
|
return OTHER;
|
|
6813
6961
|
}
|
|
6814
|
-
if (
|
|
6962
|
+
if (organizations.length === 0 && self === void 0) return OTHER;
|
|
6963
|
+
const padlock = unicode();
|
|
6964
|
+
const options = organizations.map((o) => ({
|
|
6965
|
+
value: o.login,
|
|
6966
|
+
label: !o.approved && padlock ? `${o.login} ${PRIVATE_MARK}` : o.login,
|
|
6967
|
+
...o.approved ? {} : { hint: NOT_APPROVED }
|
|
6968
|
+
}));
|
|
6969
|
+
if (self !== void 0)
|
|
6970
|
+
options.push({ value: self, label: self, hint: "your account" });
|
|
6971
|
+
options.push({ value: "", label: OTHER_LABEL, hint: OTHER_HINT });
|
|
6815
6972
|
const answer = await prompts.select({
|
|
6816
6973
|
message: "Which GitHub organization?",
|
|
6817
|
-
options
|
|
6818
|
-
...names.map((name) => ({ value: name, label: name })),
|
|
6819
|
-
{ value: "", label: "another one\u2026" }
|
|
6820
|
-
]
|
|
6974
|
+
options
|
|
6821
6975
|
});
|
|
6822
6976
|
if (prompts.isCancel(answer)) return null;
|
|
6823
6977
|
if (typeof answer !== "string" || answer === "") return OTHER;
|
|
6824
|
-
|
|
6978
|
+
const picked = organizations.find((o) => o.login === answer);
|
|
6979
|
+
return { org: answer, canGrant: picked?.canGrant ?? null };
|
|
6825
6980
|
}
|
|
6826
6981
|
async function publishContext(input) {
|
|
6827
6982
|
const { ui, prompts, provider, org } = input;
|
|
@@ -6860,6 +7015,11 @@ async function publishContext(input) {
|
|
|
6860
7015
|
async function createCommand(opts, deps = {}) {
|
|
6861
7016
|
const terminal = deps.terminal ?? defaultTerminal;
|
|
6862
7017
|
const transport = deps.transport ?? defaultTransport;
|
|
7018
|
+
const clock = {
|
|
7019
|
+
...deps.open === void 0 ? {} : { open: deps.open },
|
|
7020
|
+
...deps.sleep === void 0 ? {} : { sleep: deps.sleep },
|
|
7021
|
+
...deps.now === void 0 ? {} : { now: deps.now }
|
|
7022
|
+
};
|
|
6863
7023
|
let provider = deps.provider;
|
|
6864
7024
|
const getProvider = async () => provider ??= await githubProvider(opts.githubApi);
|
|
6865
7025
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -6934,12 +7094,12 @@ async function createCommand(opts, deps = {}) {
|
|
|
6934
7094
|
if (opts.host !== void 0) return opts.host;
|
|
6935
7095
|
if (opts.ssh === true) return transport.hosts.ssh;
|
|
6936
7096
|
if (opts.https === true) return transport.hosts.https;
|
|
6937
|
-
const
|
|
6938
|
-
const github =
|
|
7097
|
+
const access3 = await testSsh();
|
|
7098
|
+
const github = access3.ok ? null : await getProvider();
|
|
6939
7099
|
const login = github?.authenticated ? (await github.identity())?.login : void 0;
|
|
6940
|
-
ui.line("using", usingRest(
|
|
6941
|
-
if (!
|
|
6942
|
-
return
|
|
7100
|
+
ui.line("using", usingRest(access3, login), usingSentence(access3, login));
|
|
7101
|
+
if (!access3.ok) ui.gitIsSilent = true;
|
|
7102
|
+
return access3.ok ? transport.hosts.ssh : transport.hosts.https;
|
|
6943
7103
|
};
|
|
6944
7104
|
let loaded;
|
|
6945
7105
|
const prompts = async () => loaded ??= await terminal.prompts();
|
|
@@ -6954,10 +7114,14 @@ async function createCommand(opts, deps = {}) {
|
|
|
6954
7114
|
return 1;
|
|
6955
7115
|
}
|
|
6956
7116
|
let blank = opts.blank === true;
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
7117
|
+
let chosenProvider = "github";
|
|
7118
|
+
if (opts.provider !== void 0 && isProviderName(opts.provider))
|
|
7119
|
+
chosenProvider = opts.provider;
|
|
7120
|
+
else if (!blank && interactive && opts.org === void 0 && opts.repos === void 0) {
|
|
7121
|
+
const answer = await askProvider(await prompts());
|
|
7122
|
+
if (typeof answer !== "string") return cancelled(ui);
|
|
7123
|
+
if (answer === BLANK) blank = true;
|
|
7124
|
+
else chosenProvider = answer;
|
|
6961
7125
|
}
|
|
6962
7126
|
if (blank)
|
|
6963
7127
|
return await createBlank({
|
|
@@ -6971,14 +7135,6 @@ async function createCommand(opts, deps = {}) {
|
|
|
6971
7135
|
ui,
|
|
6972
7136
|
prompts
|
|
6973
7137
|
});
|
|
6974
|
-
let chosenProvider = "github";
|
|
6975
|
-
if (opts.provider !== void 0 && isProviderName(opts.provider))
|
|
6976
|
-
chosenProvider = opts.provider;
|
|
6977
|
-
else if (interactive) {
|
|
6978
|
-
const answer = await askProvider(await prompts());
|
|
6979
|
-
if (typeof answer !== "string") return cancelled(ui);
|
|
6980
|
-
chosenProvider = answer;
|
|
6981
|
-
}
|
|
6982
7138
|
let prompted = false;
|
|
6983
7139
|
let org = opts.org;
|
|
6984
7140
|
if (org === void 0 && !interactive) {
|
|
@@ -6988,19 +7144,20 @@ async function createCommand(opts, deps = {}) {
|
|
|
6988
7144
|
if (interactive && opts.repos === void 0 && !(await getProvider()).authenticated) {
|
|
6989
7145
|
const p = await prompts();
|
|
6990
7146
|
const login = await p.confirm({
|
|
6991
|
-
message: "Log in to GitHub to list private repositories?",
|
|
7147
|
+
message: "Log in to GitHub to list your organizations and private repositories?",
|
|
6992
7148
|
initialValue: true
|
|
6993
7149
|
});
|
|
6994
7150
|
if (p.isCancel(login)) return cancelled(ui);
|
|
6995
7151
|
if (login === true) {
|
|
6996
7152
|
const code2 = await loginCommand(
|
|
6997
7153
|
opts.githubApi === void 0 ? {} : { githubApi: opts.githubApi },
|
|
6998
|
-
{ terminal, ui }
|
|
7154
|
+
{ terminal, ui, ...clock }
|
|
6999
7155
|
);
|
|
7000
7156
|
if (code2 !== 0) return code2;
|
|
7001
7157
|
provider = await githubProvider(opts.githubApi);
|
|
7002
7158
|
}
|
|
7003
7159
|
}
|
|
7160
|
+
let canGrant = null;
|
|
7004
7161
|
if (org === void 0) {
|
|
7005
7162
|
const chosen = await chooseOrganization(
|
|
7006
7163
|
await getProvider(),
|
|
@@ -7008,7 +7165,8 @@ async function createCommand(opts, deps = {}) {
|
|
|
7008
7165
|
);
|
|
7009
7166
|
if (chosen === null) return cancelled(ui);
|
|
7010
7167
|
if (chosen !== OTHER) {
|
|
7011
|
-
org = chosen;
|
|
7168
|
+
org = chosen.org;
|
|
7169
|
+
canGrant = chosen.canGrant;
|
|
7012
7170
|
prompted = true;
|
|
7013
7171
|
}
|
|
7014
7172
|
}
|
|
@@ -7040,6 +7198,16 @@ async function createCommand(opts, deps = {}) {
|
|
|
7040
7198
|
);
|
|
7041
7199
|
return 2;
|
|
7042
7200
|
}
|
|
7201
|
+
const outcome = await checkAccess({
|
|
7202
|
+
org,
|
|
7203
|
+
provider: await getProvider(),
|
|
7204
|
+
ui,
|
|
7205
|
+
interactive,
|
|
7206
|
+
prompts,
|
|
7207
|
+
canGrant,
|
|
7208
|
+
deps: clock
|
|
7209
|
+
});
|
|
7210
|
+
if (outcome === "cancelled") return cancelled(ui);
|
|
7043
7211
|
const host = await chooseHost();
|
|
7044
7212
|
const contextUrl = repoUrl(host, org, ".rness");
|
|
7045
7213
|
let gitProvider = await getProvider();
|
|
@@ -7105,10 +7273,10 @@ async function createCommand(opts, deps = {}) {
|
|
|
7105
7273
|
(r) => isSshUrl(r.url, transport.hosts)
|
|
7106
7274
|
);
|
|
7107
7275
|
if (overSsh && opts.ssh !== true && opts.host === void 0) {
|
|
7108
|
-
const
|
|
7109
|
-
if (!
|
|
7276
|
+
const access3 = await testSsh();
|
|
7277
|
+
if (!access3.ok) {
|
|
7110
7278
|
process.stderr.write(
|
|
7111
|
-
`${[...sshWorkspaceLines(
|
|
7279
|
+
`${[...sshWorkspaceLines(access3.reason), ...INSTEAD_OF_LINES].join("\n")}
|
|
7112
7280
|
`
|
|
7113
7281
|
);
|
|
7114
7282
|
return 1;
|
|
@@ -7279,7 +7447,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
|
|
|
7279
7447
|
return reportError(e);
|
|
7280
7448
|
} finally {
|
|
7281
7449
|
if (staging !== void 0)
|
|
7282
|
-
await
|
|
7450
|
+
await rm7(staging, { recursive: true, force: true }).catch(() => void 0);
|
|
7283
7451
|
}
|
|
7284
7452
|
}
|
|
7285
7453
|
|
|
@@ -7384,7 +7552,7 @@ async function logoutCommand() {
|
|
|
7384
7552
|
`${status("updated", "git: rness no longer answers for https://github.com")}
|
|
7385
7553
|
`
|
|
7386
7554
|
);
|
|
7387
|
-
process.stdout.write(`revoke rness on GitHub: ${
|
|
7555
|
+
process.stdout.write(`revoke rness on GitHub: ${approvalUrl()}
|
|
7388
7556
|
`);
|
|
7389
7557
|
return 0;
|
|
7390
7558
|
} catch (e) {
|
|
@@ -7394,7 +7562,7 @@ async function logoutCommand() {
|
|
|
7394
7562
|
|
|
7395
7563
|
// src/commands/upgrade.ts
|
|
7396
7564
|
import { execFile as execFile6 } from "child_process";
|
|
7397
|
-
import { mkdtemp as mkdtemp3, readFile as readFile10, rm as
|
|
7565
|
+
import { mkdtemp as mkdtemp3, readFile as readFile10, rm as rm8 } from "fs/promises";
|
|
7398
7566
|
import { tmpdir as tmpdir3 } from "os";
|
|
7399
7567
|
import { basename as basename5, join as join24, relative as relative5 } from "path";
|
|
7400
7568
|
import { promisify as promisify6 } from "util";
|
|
@@ -7420,7 +7588,7 @@ async function scaffoldFor(version, rnessDir) {
|
|
|
7420
7588
|
if (installed?.version === version && inside2 !== null && await exists(join24(inside2, "package.json")))
|
|
7421
7589
|
return { dir: inside2 };
|
|
7422
7590
|
const scratch = await mkdtemp3(join24(tmpdir3(), "rness-pack-"));
|
|
7423
|
-
const cleanup = () =>
|
|
7591
|
+
const cleanup = () => rm8(scratch, { recursive: true, force: true });
|
|
7424
7592
|
try {
|
|
7425
7593
|
const { stdout } = await execFileP6(
|
|
7426
7594
|
"npm",
|
|
@@ -7839,6 +8007,101 @@ async function validateCommand(opts, deps = {}) {
|
|
|
7839
8007
|
// src/cli.ts
|
|
7840
8008
|
import { Command, CommanderError, Option } from "commander";
|
|
7841
8009
|
|
|
8010
|
+
// src/commands/doc.ts
|
|
8011
|
+
import { access as access2, mkdir as mkdir7, readFile as readFile11, writeFile as writeFile4 } from "fs/promises";
|
|
8012
|
+
import { join as join25 } from "path";
|
|
8013
|
+
var NUMBERED = Object.keys(STATUSES);
|
|
8014
|
+
var SECTIONS = {
|
|
8015
|
+
specs: [
|
|
8016
|
+
"Summary",
|
|
8017
|
+
"Scope",
|
|
8018
|
+
"Out of scope",
|
|
8019
|
+
"Alternatives considered",
|
|
8020
|
+
"To verify",
|
|
8021
|
+
"Tests"
|
|
8022
|
+
],
|
|
8023
|
+
plans: ["Decided here, where the specification leaves it open", "Tasks"]
|
|
8024
|
+
};
|
|
8025
|
+
function slugOf(title) {
|
|
8026
|
+
const slug = title.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
|
|
8027
|
+
return slug === "" ? "untitled" : slug;
|
|
8028
|
+
}
|
|
8029
|
+
function today() {
|
|
8030
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
8031
|
+
}
|
|
8032
|
+
function headingsOf(template) {
|
|
8033
|
+
return [...template.matchAll(/^## (.+)$/gm)].map((m) => (m[1] ?? "").trim());
|
|
8034
|
+
}
|
|
8035
|
+
async function adrSections(rnessDir) {
|
|
8036
|
+
const own = join25(rnessDir, "adr", "0000-template.md");
|
|
8037
|
+
const shipped = join25(scaffoldDir(), "adr", "0000-template.md");
|
|
8038
|
+
for (const file of [own, shipped]) {
|
|
8039
|
+
try {
|
|
8040
|
+
const sections = headingsOf(await readFile11(file, "utf8"));
|
|
8041
|
+
if (sections.length > 0) return sections;
|
|
8042
|
+
} catch {
|
|
8043
|
+
}
|
|
8044
|
+
}
|
|
8045
|
+
return ["Context", "Decision", "Alternatives Considered", "Consequences"];
|
|
8046
|
+
}
|
|
8047
|
+
async function docNewCommand(collection, opts) {
|
|
8048
|
+
if (!NUMBERED.includes(collection)) {
|
|
8049
|
+
process.stderr.write(
|
|
8050
|
+
`${collection} is not a numbered collection of this workspace (${NUMBERED.join(", ")})
|
|
8051
|
+
`
|
|
8052
|
+
);
|
|
8053
|
+
return 2;
|
|
8054
|
+
}
|
|
8055
|
+
const name = collection;
|
|
8056
|
+
try {
|
|
8057
|
+
const ws = await findWorkspace(opts.cwd ?? process.cwd());
|
|
8058
|
+
const dir = join25(ws.rnessDir, name);
|
|
8059
|
+
await mkdir7(dir, { recursive: true });
|
|
8060
|
+
let highest = 0;
|
|
8061
|
+
for (const item of await collectMarkdown(dir)) {
|
|
8062
|
+
const number2 = documentNumber(item.rel);
|
|
8063
|
+
if (number2 !== null) highest = Math.max(highest, Number(number2));
|
|
8064
|
+
}
|
|
8065
|
+
const number = String(highest + 1).padStart(4, "0");
|
|
8066
|
+
const title = opts.title?.trim() || "Untitled";
|
|
8067
|
+
const file = join25(dir, `${number}-${slugOf(title)}.md`);
|
|
8068
|
+
const status2 = STATUSES[name]?.[0] ?? "Draft";
|
|
8069
|
+
const date = today();
|
|
8070
|
+
const front = [
|
|
8071
|
+
"---",
|
|
8072
|
+
`date: ${date}`,
|
|
8073
|
+
`status: ${status2}`,
|
|
8074
|
+
"repo: ''",
|
|
8075
|
+
// An ADR is never updated once accepted: no `updated` (CONVENTIONS.md).
|
|
8076
|
+
...name === "adr" ? [] : [`updated: ${date}`],
|
|
8077
|
+
"---"
|
|
8078
|
+
];
|
|
8079
|
+
const sections = name === "adr" ? await adrSections(ws.rnessDir) : SECTIONS[name] ?? [];
|
|
8080
|
+
const body = [
|
|
8081
|
+
`# ${number} \u2014 ${title}`,
|
|
8082
|
+
// The rule under the title, as the ADR and specification templates draw it.
|
|
8083
|
+
...name === "plans" ? [] : ["", "---"],
|
|
8084
|
+
...sections.flatMap((s) => ["", `## ${s}`])
|
|
8085
|
+
];
|
|
8086
|
+
try {
|
|
8087
|
+
await access2(file);
|
|
8088
|
+
process.stderr.write(`${file} exists; nothing written
|
|
8089
|
+
`);
|
|
8090
|
+
return 1;
|
|
8091
|
+
} catch {
|
|
8092
|
+
}
|
|
8093
|
+
await writeFile4(file, `${[...front, "", ...body].join("\n")}
|
|
8094
|
+
`, {
|
|
8095
|
+
flag: "wx"
|
|
8096
|
+
});
|
|
8097
|
+
process.stdout.write(`${file}
|
|
8098
|
+
`);
|
|
8099
|
+
return 0;
|
|
8100
|
+
} catch (e) {
|
|
8101
|
+
return reportError(e);
|
|
8102
|
+
}
|
|
8103
|
+
}
|
|
8104
|
+
|
|
7842
8105
|
// src/commands/git-credential.ts
|
|
7843
8106
|
async function readStdin() {
|
|
7844
8107
|
const chunks = [];
|
|
@@ -7867,8 +8130,8 @@ import { realpath as realpath4 } from "fs/promises";
|
|
|
7867
8130
|
import { isAbsolute as isAbsolute2, posix as posix8, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
|
|
7868
8131
|
|
|
7869
8132
|
// src/mcp/tools.ts
|
|
7870
|
-
import { readFile as
|
|
7871
|
-
import { isAbsolute, join as
|
|
8133
|
+
import { readFile as readFile12, realpath as realpath3, stat as stat4 } from "fs/promises";
|
|
8134
|
+
import { isAbsolute, join as join26, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
|
|
7872
8135
|
var LABELS = {
|
|
7873
8136
|
standards: "Standards",
|
|
7874
8137
|
adr: "Decisions",
|
|
@@ -7897,7 +8160,7 @@ async function documents(rnessDir, only) {
|
|
|
7897
8160
|
const docs = [];
|
|
7898
8161
|
for (const collection of COLLECTIONS) {
|
|
7899
8162
|
if (only !== void 0 && collection !== only) continue;
|
|
7900
|
-
for (const item of await collectMarkdown(
|
|
8163
|
+
for (const item of await collectMarkdown(join26(rnessDir, collection))) {
|
|
7901
8164
|
const path = `${collection}/${item.rel}`;
|
|
7902
8165
|
if (path === TEMPLATE2) continue;
|
|
7903
8166
|
const status2 = item.fields?.["status"];
|
|
@@ -8019,7 +8282,7 @@ async function read(cwd, args) {
|
|
|
8019
8282
|
return refuse(
|
|
8020
8283
|
`${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
|
|
8021
8284
|
);
|
|
8022
|
-
return { text: await
|
|
8285
|
+
return { text: await readFile12(real, "utf8") };
|
|
8023
8286
|
}
|
|
8024
8287
|
async function search(cwd, args) {
|
|
8025
8288
|
const { ws } = await workspaceAt(cwd);
|
|
@@ -8127,8 +8390,8 @@ function mcpTools(cwd) {
|
|
|
8127
8390
|
|
|
8128
8391
|
// src/pulse/detached.ts
|
|
8129
8392
|
import { spawn as nodeSpawn } from "child_process";
|
|
8130
|
-
import { mkdir as
|
|
8131
|
-
import { join as
|
|
8393
|
+
import { mkdir as mkdir8, open as open2, readFile as readFile13, rm as rm9, stat as stat5, writeFile as writeFile5 } from "fs/promises";
|
|
8394
|
+
import { join as join27 } from "path";
|
|
8132
8395
|
import { setTimeout as delay2 } from "timers/promises";
|
|
8133
8396
|
var detached = (args, cwd) => {
|
|
8134
8397
|
const bin = process.argv[1];
|
|
@@ -8142,11 +8405,11 @@ var detached = (args, cwd) => {
|
|
|
8142
8405
|
child.on("error", () => void 0);
|
|
8143
8406
|
child.unref();
|
|
8144
8407
|
};
|
|
8145
|
-
var failureFile = () =>
|
|
8408
|
+
var failureFile = () => join27(configDir(), "pulse.json");
|
|
8146
8409
|
async function recordFailure(reason3) {
|
|
8147
8410
|
try {
|
|
8148
|
-
await
|
|
8149
|
-
await
|
|
8411
|
+
await mkdir8(configDir(), { recursive: true, mode: 448 });
|
|
8412
|
+
await writeFile5(
|
|
8150
8413
|
failureFile(),
|
|
8151
8414
|
`${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: reason3 })}
|
|
8152
8415
|
`
|
|
@@ -8156,8 +8419,8 @@ async function recordFailure(reason3) {
|
|
|
8156
8419
|
}
|
|
8157
8420
|
async function takeFailure() {
|
|
8158
8421
|
try {
|
|
8159
|
-
const data = JSON.parse(await
|
|
8160
|
-
await
|
|
8422
|
+
const data = JSON.parse(await readFile13(failureFile(), "utf8"));
|
|
8423
|
+
await rm9(failureFile(), { force: true });
|
|
8161
8424
|
const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
|
|
8162
8425
|
return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
|
|
8163
8426
|
} catch {
|
|
@@ -8166,8 +8429,8 @@ async function takeFailure() {
|
|
|
8166
8429
|
}
|
|
8167
8430
|
var PULSE_LOCK_STALE_MS = 15 * 6e4;
|
|
8168
8431
|
async function oneSyncAtATime(key, work, options = {}) {
|
|
8169
|
-
await
|
|
8170
|
-
const file =
|
|
8432
|
+
await mkdir8(configDir(), { recursive: true, mode: 448 });
|
|
8433
|
+
const file = join27(configDir(), `pulse-${key}.lock`);
|
|
8171
8434
|
for (; ; ) {
|
|
8172
8435
|
try {
|
|
8173
8436
|
const handle = await open2(file, "wx");
|
|
@@ -8179,7 +8442,7 @@ async function oneSyncAtATime(key, work, options = {}) {
|
|
|
8179
8442
|
(s) => Date.now() - s.mtimeMs,
|
|
8180
8443
|
() => 0
|
|
8181
8444
|
);
|
|
8182
|
-
if (age > PULSE_LOCK_STALE_MS) await
|
|
8445
|
+
if (age > PULSE_LOCK_STALE_MS) await rm9(file, { force: true });
|
|
8183
8446
|
else if (options.wait !== true) return false;
|
|
8184
8447
|
else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
|
|
8185
8448
|
}
|
|
@@ -8188,7 +8451,7 @@ async function oneSyncAtATime(key, work, options = {}) {
|
|
|
8188
8451
|
await work();
|
|
8189
8452
|
return true;
|
|
8190
8453
|
} finally {
|
|
8191
|
-
await
|
|
8454
|
+
await rm9(file, { force: true });
|
|
8192
8455
|
}
|
|
8193
8456
|
}
|
|
8194
8457
|
|
|
@@ -8563,12 +8826,12 @@ async function mcpCommand(opts, io = {
|
|
|
8563
8826
|
}
|
|
8564
8827
|
|
|
8565
8828
|
// src/commands/pulse.ts
|
|
8566
|
-
import { readFile as
|
|
8567
|
-
import { join as
|
|
8829
|
+
import { readFile as readFile15, readdir as readdir5 } from "fs/promises";
|
|
8830
|
+
import { join as join28 } from "path";
|
|
8568
8831
|
import { setTimeout as delay3 } from "timers/promises";
|
|
8569
8832
|
|
|
8570
8833
|
// src/pulse/opened.ts
|
|
8571
|
-
import { mkdir as
|
|
8834
|
+
import { mkdir as mkdir9, readFile as readFile14, rename as rename3, rm as rm10, writeFile as writeFile6 } from "fs/promises";
|
|
8572
8835
|
import { dirname as dirname9 } from "path";
|
|
8573
8836
|
var NAME2 = "rness/opened";
|
|
8574
8837
|
function openedFile(rnessDir) {
|
|
@@ -8580,7 +8843,7 @@ async function readOpened(file) {
|
|
|
8580
8843
|
if (file === null) return opened;
|
|
8581
8844
|
let data;
|
|
8582
8845
|
try {
|
|
8583
|
-
data = JSON.parse(await
|
|
8846
|
+
data = JSON.parse(await readFile14(file, "utf8"));
|
|
8584
8847
|
} catch {
|
|
8585
8848
|
return opened;
|
|
8586
8849
|
}
|
|
@@ -8595,13 +8858,13 @@ async function writeOpened(file, opened) {
|
|
|
8595
8858
|
if (file === null) return;
|
|
8596
8859
|
try {
|
|
8597
8860
|
if (opened.size === 0) {
|
|
8598
|
-
await
|
|
8861
|
+
await rm10(file, { force: true });
|
|
8599
8862
|
return;
|
|
8600
8863
|
}
|
|
8601
8864
|
const entries = [...opened].sort(([a], [b]) => a - b);
|
|
8602
|
-
await
|
|
8865
|
+
await mkdir9(dirname9(file), { recursive: true });
|
|
8603
8866
|
const temporary = `${file}.${process.pid}`;
|
|
8604
|
-
await
|
|
8867
|
+
await writeFile6(
|
|
8605
8868
|
temporary,
|
|
8606
8869
|
`${JSON.stringify(Object.fromEntries(entries), null, 2)}
|
|
8607
8870
|
`
|
|
@@ -8695,7 +8958,7 @@ async function scopeOf(c, name, tabs, owners) {
|
|
|
8695
8958
|
label: labelOf(name),
|
|
8696
8959
|
rows: []
|
|
8697
8960
|
};
|
|
8698
|
-
const dir =
|
|
8961
|
+
const dir = join28(c.rnessDir, name);
|
|
8699
8962
|
const shape = await readShape(c.rnessDir, name);
|
|
8700
8963
|
const fronts = new Map(
|
|
8701
8964
|
(await collectMarkdown(dir)).map((item) => [
|
|
@@ -8741,7 +9004,7 @@ async function working(ui, doing, count3, work) {
|
|
|
8741
9004
|
async function filesOf2(rnessDir) {
|
|
8742
9005
|
const files = /* @__PURE__ */ new Set();
|
|
8743
9006
|
const walk = async (rel) => {
|
|
8744
|
-
const dir = rel === "" ? rnessDir :
|
|
9007
|
+
const dir = rel === "" ? rnessDir : join28(rnessDir, ...rel.split("/"));
|
|
8745
9008
|
for (const e of await readdir5(dir, { withFileTypes: true })) {
|
|
8746
9009
|
const path = rel === "" ? e.name : `${rel}/${e.name}`;
|
|
8747
9010
|
if (e.isDirectory()) {
|
|
@@ -8853,7 +9116,7 @@ async function syncBoard(c, board, ui, sayLayout, scope) {
|
|
|
8853
9116
|
w.path,
|
|
8854
9117
|
issueBody({
|
|
8855
9118
|
path: w.path,
|
|
8856
|
-
text: await
|
|
9119
|
+
text: await readFile15(join28(c.rnessDir, ...w.path.split("/")), "utf8"),
|
|
8857
9120
|
org: c.org,
|
|
8858
9121
|
numbers,
|
|
8859
9122
|
files
|
|
@@ -9345,6 +9608,14 @@ function buildProgram(state) {
|
|
|
9345
9608
|
).argument("[tab]", "open on this tab: adr, specs, plans, \u2026").addOption(new Option("--cwd <dir>").hideHelp()).action(async (tab, opts) => {
|
|
9346
9609
|
state.code = await statusCommand(tab, opts);
|
|
9347
9610
|
});
|
|
9611
|
+
const doc = program.command("doc").description("Documents of the workspace: doc new <collection>");
|
|
9612
|
+
doc.command("new").description(
|
|
9613
|
+
"Write the next numbered document of a collection (adr, specs, plans) and print its path"
|
|
9614
|
+
).argument("<collection>", "adr, specs or plans").option("--title <text>", "the title; the file name takes its slug").addOption(new Option("--cwd <dir>").hideHelp()).action(
|
|
9615
|
+
async (collection, opts) => {
|
|
9616
|
+
state.code = await docNewCommand(collection, opts);
|
|
9617
|
+
}
|
|
9618
|
+
);
|
|
9348
9619
|
program.command("validate").description(
|
|
9349
9620
|
"Check the .rness/ tree against the contract and every generated block"
|
|
9350
9621
|
).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
|
|
@@ -9605,4 +9876,4 @@ export {
|
|
|
9605
9876
|
OWN_COMMANDS,
|
|
9606
9877
|
catchUp
|
|
9607
9878
|
};
|
|
9608
|
-
//# sourceMappingURL=chunk-
|
|
9879
|
+
//# sourceMappingURL=chunk-VGBS7HDY.js.map
|