@rness/cli 0.17.3 → 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 +85 -26
- package/dist/bin/rness.js +12 -7
- package/dist/bin/rness.js.map +1 -1
- package/dist/{chunk-MMPMXYZU.js → chunk-VGBS7HDY.js} +538 -280
- package/dist/chunk-VGBS7HDY.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- 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.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,7 +4343,7 @@ 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
|
|
@@ -4317,7 +4352,7 @@ var PLUGIN_JSON = `${JSON.stringify(
|
|
|
4317
4352
|
{
|
|
4318
4353
|
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
4319
4354
|
name: "rness",
|
|
4320
|
-
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.",
|
|
4321
4356
|
author: { name: "rness-dev", url: "https://rness.dev" },
|
|
4322
4357
|
homepage: "https://rness.dev",
|
|
4323
4358
|
repository: "https://github.com/rness-dev/rness",
|
|
@@ -4360,53 +4395,68 @@ terminal, from the workspace's \`.rness/\`: \`${localRunner(pm)} rness status\`.
|
|
|
4360
4395
|
}
|
|
4361
4396
|
var STATUS_RULE = `Move a status yourself when the work calls for it, and whenever the
|
|
4362
4397
|
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
|
-
|
|
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.`;
|
|
4367
4402
|
var COMMIT_RULE = `Commit each file you changed, in its own repository, and only the files
|
|
4368
4403
|
you alone changed: add them by name, never \`git add -A\` nor \`commit -a\`.
|
|
4369
4404
|
A file that had uncommitted changes before your first edit (\`git status\`
|
|
4370
4405
|
tells you), or that someone else changed since, stays uncommitted: say
|
|
4371
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.`;
|
|
4372
4420
|
var LIFECYCLE2 = [
|
|
4373
4421
|
{
|
|
4374
4422
|
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]",
|
|
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]",
|
|
4377
4425
|
tab: "adr",
|
|
4378
|
-
body: (rness) => `You
|
|
4379
|
-
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):
|
|
4432
|
+
|
|
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.
|
|
4380
4436
|
|
|
4381
|
-
|
|
4437
|
+
Creating:
|
|
4382
4438
|
|
|
4383
4439
|
1. If the developer did not ask for an ADR in so many words, propose one in
|
|
4384
4440
|
a single line \u2014 its title, and why the choice is hard to reverse \u2014 and
|
|
4385
4441
|
wait for a yes.
|
|
4386
4442
|
2. Read \`${rness}/CONVENTIONS.md\` and \`${rness}/adr/0000-template.md\`: they hold the
|
|
4387
4443
|
rules and the sections; this skill is only the procedure.
|
|
4388
|
-
3.
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
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.
|
|
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.
|
|
4407
4457
|
|
|
4408
|
-
After writing: give the path and the Decision section.
|
|
4409
|
-
\`Proposed\` until
|
|
4458
|
+
After writing: give the path and the Decision section. A new ADR stays
|
|
4459
|
+
\`Proposed\` until the developer accepts it.
|
|
4410
4460
|
|
|
4411
4461
|
${STATUS_RULE}
|
|
4412
4462
|
|
|
@@ -4415,43 +4465,47 @@ ${COMMIT_RULE}
|
|
|
4415
4465
|
},
|
|
4416
4466
|
{
|
|
4417
4467
|
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]",
|
|
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]",
|
|
4420
4470
|
tab: "specs",
|
|
4421
|
-
body: (rness) => `You
|
|
4422
|
-
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\`.
|
|
4423
4473
|
|
|
4424
|
-
|
|
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.
|
|
4481
|
+
|
|
4482
|
+
Creating:
|
|
4425
4483
|
|
|
4426
4484
|
1. If the developer did not ask for a specification in so many words,
|
|
4427
4485
|
propose one in a single line and wait for a yes.
|
|
4428
4486
|
2. Read \`${rness}/CONVENTIONS.md\`, then the most recent specification above: match
|
|
4429
4487
|
its front matter, sections and tone.
|
|
4430
|
-
3.
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
Alternatives considered, To verify, Tests.
|
|
4443
|
-
- What was checked in this session (a document read, a command run) is
|
|
4444
|
-
written as verified, with the date; everything else as a proposal or an
|
|
4445
|
-
open question.
|
|
4446
|
-
- Add \`{ id: \${CLAUDE_SESSION_ID}, agent: <model> }\` at the end of
|
|
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.
|
|
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)}
|
|
4451
4500
|
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
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\`.
|
|
4505
|
+
|
|
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.
|
|
4455
4509
|
|
|
4456
4510
|
${STATUS_RULE}
|
|
4457
4511
|
|
|
@@ -4460,105 +4514,104 @@ ${COMMIT_RULE}
|
|
|
4460
4514
|
},
|
|
4461
4515
|
{
|
|
4462
4516
|
name: "plan",
|
|
4463
|
-
description: "
|
|
4464
|
-
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]",
|
|
4465
4519
|
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.
|
|
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\`.
|
|
4481
4522
|
|
|
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.
|
|
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\`:
|
|
4499
4526
|
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
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.
|
|
4503
4532
|
|
|
4504
|
-
|
|
4533
|
+
\`from\`:
|
|
4505
4534
|
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
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\`:
|
|
4517
4567
|
|
|
4518
4568
|
1. The plan: the one named, else the one \`In progress\`. Several or none:
|
|
4519
4569
|
ask.
|
|
4520
|
-
2. The developer asked
|
|
4521
|
-
this session:
|
|
4522
|
-
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.
|
|
4523
4573
|
3. Read the plan, its specification and \`${rness}/CONVENTIONS.md\`.
|
|
4524
|
-
4. Evidence first. For each task, what
|
|
4525
|
-
|
|
4526
|
-
check
|
|
4527
|
-
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:
|
|
4528
4578
|
- the plan: \`status: Completed\`, \`updated\` today, the checkboxes of the
|
|
4529
4579
|
proven tasks ticked, and a section \`Verification (YYYY-MM-DD)\` \u2014 what
|
|
4530
4580
|
ran, what it showed, what could not run and why;
|
|
4531
4581
|
- the specification: \`Implemented\` once every plan of it is
|
|
4532
4582
|
\`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\`;
|
|
4583
|
+
- the plan and the specification: add ${SESSION_LINE};
|
|
4538
4584
|
- the smallest set of documents the work makes inaccurate: \`${rness}/docs/\`,
|
|
4539
4585
|
the READMEs of the repositories it changed.
|
|
4540
|
-
6. A task not proven: the plan stays \`In progress
|
|
4541
|
-
|
|
4542
|
-
|
|
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)}
|
|
4590
|
+
|
|
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.
|
|
4543
4594
|
|
|
4544
|
-
|
|
4595
|
+
${STATUS_RULE}
|
|
4545
4596
|
|
|
4546
4597
|
${COMMIT_RULE}
|
|
4547
4598
|
`
|
|
4548
4599
|
}
|
|
4549
4600
|
];
|
|
4601
|
+
var RETIRED_SKILLS = ["done"];
|
|
4550
4602
|
function lifecycleSkill(rness, s) {
|
|
4551
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}`;
|
|
4552
4605
|
return `---
|
|
4553
4606
|
name: ${s.name}
|
|
4554
4607
|
description: ${s.description}
|
|
4555
4608
|
argument-hint: '${s.hint}'
|
|
4556
|
-
allowed-tools: Bash(${command})
|
|
4609
|
+
allowed-tools: Bash(${command}), Bash(${allocate})
|
|
4557
4610
|
---
|
|
4558
4611
|
|
|
4559
4612
|
!\`${command}\`
|
|
4560
4613
|
|
|
4561
|
-
${s.body(rness)}`;
|
|
4614
|
+
${s.body(rness, allocate)}`;
|
|
4562
4615
|
}
|
|
4563
4616
|
function pluginFiles(rness, at, ctx) {
|
|
4564
4617
|
return [
|
|
@@ -4572,6 +4625,11 @@ function pluginFiles(rness, at, ctx) {
|
|
|
4572
4625
|
file: `${PLUGIN}/skills/${s.name}/SKILL.md`,
|
|
4573
4626
|
at,
|
|
4574
4627
|
content: lifecycleSkill(rness, s)
|
|
4628
|
+
})),
|
|
4629
|
+
...RETIRED_SKILLS.map((name) => ({
|
|
4630
|
+
file: `${PLUGIN}/skills/${name}/SKILL.md`,
|
|
4631
|
+
at,
|
|
4632
|
+
retired: true
|
|
4575
4633
|
}))
|
|
4576
4634
|
];
|
|
4577
4635
|
}
|
|
@@ -5097,6 +5155,21 @@ async function agentTargets(root, manifest, opts) {
|
|
|
5097
5155
|
for (const owned of await filesOf(root, manifest, declared(manifest))) {
|
|
5098
5156
|
const { file, label } = owned;
|
|
5099
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
|
+
}
|
|
5100
5173
|
if ("content" in owned) {
|
|
5101
5174
|
const diff = wholeDiff(existing, owned.content);
|
|
5102
5175
|
if (diff === null)
|
|
@@ -5147,6 +5220,7 @@ async function leftoverTargets(root, manifest) {
|
|
|
5147
5220
|
);
|
|
5148
5221
|
const leftovers = [];
|
|
5149
5222
|
for (const owned of await filesOf(root, manifest, undeclared)) {
|
|
5223
|
+
if ("retired" in owned) continue;
|
|
5150
5224
|
const existing = await readOrNull(owned.file);
|
|
5151
5225
|
if (existing === null) continue;
|
|
5152
5226
|
if ("content" in owned) {
|
|
@@ -5536,15 +5610,15 @@ async function syncCommand(opts, deps = {}) {
|
|
|
5536
5610
|
if (!overSsh) ui.gitIsSilent = true;
|
|
5537
5611
|
if (overSsh) {
|
|
5538
5612
|
const interactive = opts.yes !== true && terminal.isTty();
|
|
5539
|
-
const { access:
|
|
5613
|
+
const { access: access3, unattended } = await testGithubSsh(
|
|
5540
5614
|
transport,
|
|
5541
5615
|
interactive,
|
|
5542
5616
|
() => ui.line(...CHECKING)
|
|
5543
5617
|
);
|
|
5544
5618
|
if (unattended) ui.gitIsSilent = true;
|
|
5545
|
-
if (!
|
|
5619
|
+
if (!access3.ok) {
|
|
5546
5620
|
process.stderr.write(
|
|
5547
|
-
`${[...sshWorkspaceLines(
|
|
5621
|
+
`${[...sshWorkspaceLines(access3.reason), ...INSTEAD_OF_LINES].join("\n")}
|
|
5548
5622
|
`
|
|
5549
5623
|
);
|
|
5550
5624
|
return 1;
|
|
@@ -5773,20 +5847,20 @@ async function addCommand(spec, opts, deps = {}) {
|
|
|
5773
5847
|
if (host === void 0 && opts.https === true) host = transport.hosts.https;
|
|
5774
5848
|
if (host === void 0 && FULL_URL2.test(spec)) host = transport.hosts.https;
|
|
5775
5849
|
if (host === void 0) {
|
|
5776
|
-
const { access:
|
|
5850
|
+
const { access: access3, unattended } = await testGithubSsh(
|
|
5777
5851
|
transport,
|
|
5778
5852
|
interactive,
|
|
5779
5853
|
() => ui.line(...CHECKING)
|
|
5780
5854
|
);
|
|
5781
|
-
if (unattended || !
|
|
5855
|
+
if (unattended || !access3.ok) ui.gitIsSilent = true;
|
|
5782
5856
|
const overSsh = Object.values(manifest.repos).some(
|
|
5783
5857
|
(r) => isSshUrl(r.url, transport.hosts)
|
|
5784
5858
|
);
|
|
5785
|
-
if (
|
|
5786
|
-
ui.line("using", usingRest(
|
|
5787
|
-
host =
|
|
5859
|
+
if (access3.ok || !overSsh) {
|
|
5860
|
+
ui.line("using", usingRest(access3), usingSentence(access3));
|
|
5861
|
+
host = access3.ok ? transport.hosts.ssh : transport.hosts.https;
|
|
5788
5862
|
} else {
|
|
5789
|
-
const lines = sshWorkspaceLines(
|
|
5863
|
+
const lines = sshWorkspaceLines(access3.reason);
|
|
5790
5864
|
if (!interactive) {
|
|
5791
5865
|
process.stderr.write(
|
|
5792
5866
|
`${[...lines, httpsFlagLine(spec)].join("\n")}
|
|
@@ -6000,9 +6074,23 @@ async function copyScaffold(dest, tokens) {
|
|
|
6000
6074
|
}
|
|
6001
6075
|
|
|
6002
6076
|
// src/commands/login.ts
|
|
6003
|
-
import { spawn as spawn3 } from "child_process";
|
|
6004
6077
|
import { realpath } from "fs/promises";
|
|
6005
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
|
+
|
|
6006
6094
|
// src/core/setup-git.ts
|
|
6007
6095
|
import { execFile as execFile4 } from "child_process";
|
|
6008
6096
|
import { promisify as promisify4 } from "util";
|
|
@@ -6047,15 +6135,6 @@ async function removeGitSetup() {
|
|
|
6047
6135
|
}
|
|
6048
6136
|
|
|
6049
6137
|
// 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
6138
|
async function offerSetupGit(opts, terminal, bin, ui) {
|
|
6060
6139
|
if (opts.setupGit === false) return;
|
|
6061
6140
|
if (!isStableInstall(bin)) {
|
|
@@ -6132,7 +6211,7 @@ async function loginCommand(opts, deps = {}) {
|
|
|
6132
6211
|
ui.line("open", code.verificationUri);
|
|
6133
6212
|
ui.line("code", code.userCode);
|
|
6134
6213
|
}
|
|
6135
|
-
if (terminal.isTty() &&
|
|
6214
|
+
if (terminal.isTty() && canOpenBrowser())
|
|
6136
6215
|
(deps.open ?? openInBrowser)(code.verificationUri);
|
|
6137
6216
|
if (!ui.session)
|
|
6138
6217
|
ui.line("waiting", "for you to approve rness on github.com\u2026");
|
|
@@ -6165,35 +6244,115 @@ async function loginCommand(opts, deps = {}) {
|
|
|
6165
6244
|
return reportError(e);
|
|
6166
6245
|
}
|
|
6167
6246
|
}
|
|
6168
|
-
function revokeUrl() {
|
|
6169
|
-
return `https://github.com/settings/connections/applications/${clientId()}`;
|
|
6170
|
-
}
|
|
6171
6247
|
|
|
6172
6248
|
// src/commands/create.ts
|
|
6173
|
-
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";
|
|
6174
6250
|
import { tmpdir as tmpdir2 } from "os";
|
|
6175
6251
|
import { join as join22, relative as relative4, resolve as resolve5 } from "path";
|
|
6176
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
|
+
|
|
6177
6331
|
// src/core/ask-provider.ts
|
|
6178
6332
|
var PROVIDER_QUESTION = "Where does your organization live?";
|
|
6333
|
+
var BLANK = "blank";
|
|
6334
|
+
var BLANK_LABEL = "No organization yet: a blank local workspace";
|
|
6179
6335
|
async function askProvider(p) {
|
|
6180
6336
|
return p.select({
|
|
6181
6337
|
message: PROVIDER_QUESTION,
|
|
6182
|
-
options:
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
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
|
+
]
|
|
6187
6346
|
});
|
|
6188
6347
|
}
|
|
6189
6348
|
|
|
6190
6349
|
// src/core/new-workspace.ts
|
|
6191
|
-
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";
|
|
6192
6351
|
import { dirname as dirname8, join as join20, relative as relative2 } from "path";
|
|
6193
6352
|
|
|
6194
6353
|
// src/core/scaffold-git.ts
|
|
6195
6354
|
import { execFile as execFile5 } from "child_process";
|
|
6196
|
-
import { mkdtemp, realpath as realpath2, rm as
|
|
6355
|
+
import { mkdtemp, realpath as realpath2, rm as rm5, writeFile as writeFile3 } from "fs/promises";
|
|
6197
6356
|
import { tmpdir } from "os";
|
|
6198
6357
|
import { dirname as dirname7, join as join19, resolve as resolve3 } from "path";
|
|
6199
6358
|
import { promisify as promisify5 } from "util";
|
|
@@ -6321,7 +6480,7 @@ async function buildScaffoldCommit(rnessDir, base, files, version) {
|
|
|
6321
6480
|
scaffoldMessage(`chore: rness scaffold ${version}`, version)
|
|
6322
6481
|
]);
|
|
6323
6482
|
} finally {
|
|
6324
|
-
await
|
|
6483
|
+
await rm5(scratch, { recursive: true, force: true });
|
|
6325
6484
|
}
|
|
6326
6485
|
}
|
|
6327
6486
|
async function blobAt(rnessDir, rev, path) {
|
|
@@ -6445,9 +6604,9 @@ async function buildContext(input) {
|
|
|
6445
6604
|
return true;
|
|
6446
6605
|
} catch (e) {
|
|
6447
6606
|
reportError(e);
|
|
6448
|
-
await
|
|
6607
|
+
await rm6(rnessDir, { recursive: true, force: true }).catch(() => void 0);
|
|
6449
6608
|
if (await isEmptyDir(root).catch(() => false))
|
|
6450
|
-
await
|
|
6609
|
+
await rm6(root, { recursive: true, force: true }).catch(() => void 0);
|
|
6451
6610
|
process.stderr.write(
|
|
6452
6611
|
`removed ${shown}/.rness after the failure; fix it and retry
|
|
6453
6612
|
`
|
|
@@ -6630,22 +6789,6 @@ function cancelled(ui) {
|
|
|
6630
6789
|
}
|
|
6631
6790
|
async function pickRepositories(input) {
|
|
6632
6791
|
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
6792
|
let listed = [];
|
|
6650
6793
|
let failed = false;
|
|
6651
6794
|
if (!ui.session) ui.line("listing", `${org} repositories\u2026`);
|
|
@@ -6795,17 +6938,6 @@ function blankConflict(opts) {
|
|
|
6795
6938
|
if (opts.provider !== void 0) return "--provider";
|
|
6796
6939
|
return null;
|
|
6797
6940
|
}
|
|
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
6941
|
function providerFlagError(value) {
|
|
6810
6942
|
if (!isProviderName(value))
|
|
6811
6943
|
return `unknown provider "${value}" (${Object.keys(PROVIDERS).join(", ")})`;
|
|
@@ -6814,27 +6946,37 @@ function providerFlagError(value) {
|
|
|
6814
6946
|
return `provider "${value}" is not available yet (available: ${available.join(", ")})`;
|
|
6815
6947
|
}
|
|
6816
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";
|
|
6817
6952
|
async function chooseOrganization(provider, prompts) {
|
|
6818
6953
|
if (!provider.authenticated) return OTHER;
|
|
6819
|
-
let
|
|
6954
|
+
let organizations;
|
|
6955
|
+
let self;
|
|
6820
6956
|
try {
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
if (self !== void 0) names.push(self);
|
|
6957
|
+
self = (await provider.identity())?.login;
|
|
6958
|
+
organizations = await provider.listOrganizations();
|
|
6824
6959
|
} catch {
|
|
6825
6960
|
return OTHER;
|
|
6826
6961
|
}
|
|
6827
|
-
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 });
|
|
6828
6972
|
const answer = await prompts.select({
|
|
6829
6973
|
message: "Which GitHub organization?",
|
|
6830
|
-
options
|
|
6831
|
-
...names.map((name) => ({ value: name, label: name })),
|
|
6832
|
-
{ value: "", label: "another one\u2026" }
|
|
6833
|
-
]
|
|
6974
|
+
options
|
|
6834
6975
|
});
|
|
6835
6976
|
if (prompts.isCancel(answer)) return null;
|
|
6836
6977
|
if (typeof answer !== "string" || answer === "") return OTHER;
|
|
6837
|
-
|
|
6978
|
+
const picked = organizations.find((o) => o.login === answer);
|
|
6979
|
+
return { org: answer, canGrant: picked?.canGrant ?? null };
|
|
6838
6980
|
}
|
|
6839
6981
|
async function publishContext(input) {
|
|
6840
6982
|
const { ui, prompts, provider, org } = input;
|
|
@@ -6873,6 +7015,11 @@ async function publishContext(input) {
|
|
|
6873
7015
|
async function createCommand(opts, deps = {}) {
|
|
6874
7016
|
const terminal = deps.terminal ?? defaultTerminal;
|
|
6875
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
|
+
};
|
|
6876
7023
|
let provider = deps.provider;
|
|
6877
7024
|
const getProvider = async () => provider ??= await githubProvider(opts.githubApi);
|
|
6878
7025
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -6947,12 +7094,12 @@ async function createCommand(opts, deps = {}) {
|
|
|
6947
7094
|
if (opts.host !== void 0) return opts.host;
|
|
6948
7095
|
if (opts.ssh === true) return transport.hosts.ssh;
|
|
6949
7096
|
if (opts.https === true) return transport.hosts.https;
|
|
6950
|
-
const
|
|
6951
|
-
const github =
|
|
7097
|
+
const access3 = await testSsh();
|
|
7098
|
+
const github = access3.ok ? null : await getProvider();
|
|
6952
7099
|
const login = github?.authenticated ? (await github.identity())?.login : void 0;
|
|
6953
|
-
ui.line("using", usingRest(
|
|
6954
|
-
if (!
|
|
6955
|
-
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;
|
|
6956
7103
|
};
|
|
6957
7104
|
let loaded;
|
|
6958
7105
|
const prompts = async () => loaded ??= await terminal.prompts();
|
|
@@ -6967,10 +7114,14 @@ async function createCommand(opts, deps = {}) {
|
|
|
6967
7114
|
return 1;
|
|
6968
7115
|
}
|
|
6969
7116
|
let blank = opts.blank === true;
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
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;
|
|
6974
7125
|
}
|
|
6975
7126
|
if (blank)
|
|
6976
7127
|
return await createBlank({
|
|
@@ -6984,14 +7135,6 @@ async function createCommand(opts, deps = {}) {
|
|
|
6984
7135
|
ui,
|
|
6985
7136
|
prompts
|
|
6986
7137
|
});
|
|
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
7138
|
let prompted = false;
|
|
6996
7139
|
let org = opts.org;
|
|
6997
7140
|
if (org === void 0 && !interactive) {
|
|
@@ -7001,19 +7144,20 @@ async function createCommand(opts, deps = {}) {
|
|
|
7001
7144
|
if (interactive && opts.repos === void 0 && !(await getProvider()).authenticated) {
|
|
7002
7145
|
const p = await prompts();
|
|
7003
7146
|
const login = await p.confirm({
|
|
7004
|
-
message: "Log in to GitHub to list private repositories?",
|
|
7147
|
+
message: "Log in to GitHub to list your organizations and private repositories?",
|
|
7005
7148
|
initialValue: true
|
|
7006
7149
|
});
|
|
7007
7150
|
if (p.isCancel(login)) return cancelled(ui);
|
|
7008
7151
|
if (login === true) {
|
|
7009
7152
|
const code2 = await loginCommand(
|
|
7010
7153
|
opts.githubApi === void 0 ? {} : { githubApi: opts.githubApi },
|
|
7011
|
-
{ terminal, ui }
|
|
7154
|
+
{ terminal, ui, ...clock }
|
|
7012
7155
|
);
|
|
7013
7156
|
if (code2 !== 0) return code2;
|
|
7014
7157
|
provider = await githubProvider(opts.githubApi);
|
|
7015
7158
|
}
|
|
7016
7159
|
}
|
|
7160
|
+
let canGrant = null;
|
|
7017
7161
|
if (org === void 0) {
|
|
7018
7162
|
const chosen = await chooseOrganization(
|
|
7019
7163
|
await getProvider(),
|
|
@@ -7021,7 +7165,8 @@ async function createCommand(opts, deps = {}) {
|
|
|
7021
7165
|
);
|
|
7022
7166
|
if (chosen === null) return cancelled(ui);
|
|
7023
7167
|
if (chosen !== OTHER) {
|
|
7024
|
-
org = chosen;
|
|
7168
|
+
org = chosen.org;
|
|
7169
|
+
canGrant = chosen.canGrant;
|
|
7025
7170
|
prompted = true;
|
|
7026
7171
|
}
|
|
7027
7172
|
}
|
|
@@ -7053,6 +7198,16 @@ async function createCommand(opts, deps = {}) {
|
|
|
7053
7198
|
);
|
|
7054
7199
|
return 2;
|
|
7055
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);
|
|
7056
7211
|
const host = await chooseHost();
|
|
7057
7212
|
const contextUrl = repoUrl(host, org, ".rness");
|
|
7058
7213
|
let gitProvider = await getProvider();
|
|
@@ -7118,10 +7273,10 @@ async function createCommand(opts, deps = {}) {
|
|
|
7118
7273
|
(r) => isSshUrl(r.url, transport.hosts)
|
|
7119
7274
|
);
|
|
7120
7275
|
if (overSsh && opts.ssh !== true && opts.host === void 0) {
|
|
7121
|
-
const
|
|
7122
|
-
if (!
|
|
7276
|
+
const access3 = await testSsh();
|
|
7277
|
+
if (!access3.ok) {
|
|
7123
7278
|
process.stderr.write(
|
|
7124
|
-
`${[...sshWorkspaceLines(
|
|
7279
|
+
`${[...sshWorkspaceLines(access3.reason), ...INSTEAD_OF_LINES].join("\n")}
|
|
7125
7280
|
`
|
|
7126
7281
|
);
|
|
7127
7282
|
return 1;
|
|
@@ -7292,7 +7447,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
|
|
|
7292
7447
|
return reportError(e);
|
|
7293
7448
|
} finally {
|
|
7294
7449
|
if (staging !== void 0)
|
|
7295
|
-
await
|
|
7450
|
+
await rm7(staging, { recursive: true, force: true }).catch(() => void 0);
|
|
7296
7451
|
}
|
|
7297
7452
|
}
|
|
7298
7453
|
|
|
@@ -7397,7 +7552,7 @@ async function logoutCommand() {
|
|
|
7397
7552
|
`${status("updated", "git: rness no longer answers for https://github.com")}
|
|
7398
7553
|
`
|
|
7399
7554
|
);
|
|
7400
|
-
process.stdout.write(`revoke rness on GitHub: ${
|
|
7555
|
+
process.stdout.write(`revoke rness on GitHub: ${approvalUrl()}
|
|
7401
7556
|
`);
|
|
7402
7557
|
return 0;
|
|
7403
7558
|
} catch (e) {
|
|
@@ -7407,7 +7562,7 @@ async function logoutCommand() {
|
|
|
7407
7562
|
|
|
7408
7563
|
// src/commands/upgrade.ts
|
|
7409
7564
|
import { execFile as execFile6 } from "child_process";
|
|
7410
|
-
import { mkdtemp as mkdtemp3, readFile as readFile10, rm as
|
|
7565
|
+
import { mkdtemp as mkdtemp3, readFile as readFile10, rm as rm8 } from "fs/promises";
|
|
7411
7566
|
import { tmpdir as tmpdir3 } from "os";
|
|
7412
7567
|
import { basename as basename5, join as join24, relative as relative5 } from "path";
|
|
7413
7568
|
import { promisify as promisify6 } from "util";
|
|
@@ -7433,7 +7588,7 @@ async function scaffoldFor(version, rnessDir) {
|
|
|
7433
7588
|
if (installed?.version === version && inside2 !== null && await exists(join24(inside2, "package.json")))
|
|
7434
7589
|
return { dir: inside2 };
|
|
7435
7590
|
const scratch = await mkdtemp3(join24(tmpdir3(), "rness-pack-"));
|
|
7436
|
-
const cleanup = () =>
|
|
7591
|
+
const cleanup = () => rm8(scratch, { recursive: true, force: true });
|
|
7437
7592
|
try {
|
|
7438
7593
|
const { stdout } = await execFileP6(
|
|
7439
7594
|
"npm",
|
|
@@ -7852,6 +8007,101 @@ async function validateCommand(opts, deps = {}) {
|
|
|
7852
8007
|
// src/cli.ts
|
|
7853
8008
|
import { Command, CommanderError, Option } from "commander";
|
|
7854
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
|
+
|
|
7855
8105
|
// src/commands/git-credential.ts
|
|
7856
8106
|
async function readStdin() {
|
|
7857
8107
|
const chunks = [];
|
|
@@ -7880,8 +8130,8 @@ import { realpath as realpath4 } from "fs/promises";
|
|
|
7880
8130
|
import { isAbsolute as isAbsolute2, posix as posix8, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
|
|
7881
8131
|
|
|
7882
8132
|
// src/mcp/tools.ts
|
|
7883
|
-
import { readFile as
|
|
7884
|
-
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";
|
|
7885
8135
|
var LABELS = {
|
|
7886
8136
|
standards: "Standards",
|
|
7887
8137
|
adr: "Decisions",
|
|
@@ -7910,7 +8160,7 @@ async function documents(rnessDir, only) {
|
|
|
7910
8160
|
const docs = [];
|
|
7911
8161
|
for (const collection of COLLECTIONS) {
|
|
7912
8162
|
if (only !== void 0 && collection !== only) continue;
|
|
7913
|
-
for (const item of await collectMarkdown(
|
|
8163
|
+
for (const item of await collectMarkdown(join26(rnessDir, collection))) {
|
|
7914
8164
|
const path = `${collection}/${item.rel}`;
|
|
7915
8165
|
if (path === TEMPLATE2) continue;
|
|
7916
8166
|
const status2 = item.fields?.["status"];
|
|
@@ -8032,7 +8282,7 @@ async function read(cwd, args) {
|
|
|
8032
8282
|
return refuse(
|
|
8033
8283
|
`${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
|
|
8034
8284
|
);
|
|
8035
|
-
return { text: await
|
|
8285
|
+
return { text: await readFile12(real, "utf8") };
|
|
8036
8286
|
}
|
|
8037
8287
|
async function search(cwd, args) {
|
|
8038
8288
|
const { ws } = await workspaceAt(cwd);
|
|
@@ -8140,8 +8390,8 @@ function mcpTools(cwd) {
|
|
|
8140
8390
|
|
|
8141
8391
|
// src/pulse/detached.ts
|
|
8142
8392
|
import { spawn as nodeSpawn } from "child_process";
|
|
8143
|
-
import { mkdir as
|
|
8144
|
-
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";
|
|
8145
8395
|
import { setTimeout as delay2 } from "timers/promises";
|
|
8146
8396
|
var detached = (args, cwd) => {
|
|
8147
8397
|
const bin = process.argv[1];
|
|
@@ -8155,11 +8405,11 @@ var detached = (args, cwd) => {
|
|
|
8155
8405
|
child.on("error", () => void 0);
|
|
8156
8406
|
child.unref();
|
|
8157
8407
|
};
|
|
8158
|
-
var failureFile = () =>
|
|
8408
|
+
var failureFile = () => join27(configDir(), "pulse.json");
|
|
8159
8409
|
async function recordFailure(reason3) {
|
|
8160
8410
|
try {
|
|
8161
|
-
await
|
|
8162
|
-
await
|
|
8411
|
+
await mkdir8(configDir(), { recursive: true, mode: 448 });
|
|
8412
|
+
await writeFile5(
|
|
8163
8413
|
failureFile(),
|
|
8164
8414
|
`${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: reason3 })}
|
|
8165
8415
|
`
|
|
@@ -8169,8 +8419,8 @@ async function recordFailure(reason3) {
|
|
|
8169
8419
|
}
|
|
8170
8420
|
async function takeFailure() {
|
|
8171
8421
|
try {
|
|
8172
|
-
const data = JSON.parse(await
|
|
8173
|
-
await
|
|
8422
|
+
const data = JSON.parse(await readFile13(failureFile(), "utf8"));
|
|
8423
|
+
await rm9(failureFile(), { force: true });
|
|
8174
8424
|
const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
|
|
8175
8425
|
return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
|
|
8176
8426
|
} catch {
|
|
@@ -8179,8 +8429,8 @@ async function takeFailure() {
|
|
|
8179
8429
|
}
|
|
8180
8430
|
var PULSE_LOCK_STALE_MS = 15 * 6e4;
|
|
8181
8431
|
async function oneSyncAtATime(key, work, options = {}) {
|
|
8182
|
-
await
|
|
8183
|
-
const file =
|
|
8432
|
+
await mkdir8(configDir(), { recursive: true, mode: 448 });
|
|
8433
|
+
const file = join27(configDir(), `pulse-${key}.lock`);
|
|
8184
8434
|
for (; ; ) {
|
|
8185
8435
|
try {
|
|
8186
8436
|
const handle = await open2(file, "wx");
|
|
@@ -8192,7 +8442,7 @@ async function oneSyncAtATime(key, work, options = {}) {
|
|
|
8192
8442
|
(s) => Date.now() - s.mtimeMs,
|
|
8193
8443
|
() => 0
|
|
8194
8444
|
);
|
|
8195
|
-
if (age > PULSE_LOCK_STALE_MS) await
|
|
8445
|
+
if (age > PULSE_LOCK_STALE_MS) await rm9(file, { force: true });
|
|
8196
8446
|
else if (options.wait !== true) return false;
|
|
8197
8447
|
else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
|
|
8198
8448
|
}
|
|
@@ -8201,7 +8451,7 @@ async function oneSyncAtATime(key, work, options = {}) {
|
|
|
8201
8451
|
await work();
|
|
8202
8452
|
return true;
|
|
8203
8453
|
} finally {
|
|
8204
|
-
await
|
|
8454
|
+
await rm9(file, { force: true });
|
|
8205
8455
|
}
|
|
8206
8456
|
}
|
|
8207
8457
|
|
|
@@ -8576,12 +8826,12 @@ async function mcpCommand(opts, io = {
|
|
|
8576
8826
|
}
|
|
8577
8827
|
|
|
8578
8828
|
// src/commands/pulse.ts
|
|
8579
|
-
import { readFile as
|
|
8580
|
-
import { join as
|
|
8829
|
+
import { readFile as readFile15, readdir as readdir5 } from "fs/promises";
|
|
8830
|
+
import { join as join28 } from "path";
|
|
8581
8831
|
import { setTimeout as delay3 } from "timers/promises";
|
|
8582
8832
|
|
|
8583
8833
|
// src/pulse/opened.ts
|
|
8584
|
-
import { mkdir as
|
|
8834
|
+
import { mkdir as mkdir9, readFile as readFile14, rename as rename3, rm as rm10, writeFile as writeFile6 } from "fs/promises";
|
|
8585
8835
|
import { dirname as dirname9 } from "path";
|
|
8586
8836
|
var NAME2 = "rness/opened";
|
|
8587
8837
|
function openedFile(rnessDir) {
|
|
@@ -8593,7 +8843,7 @@ async function readOpened(file) {
|
|
|
8593
8843
|
if (file === null) return opened;
|
|
8594
8844
|
let data;
|
|
8595
8845
|
try {
|
|
8596
|
-
data = JSON.parse(await
|
|
8846
|
+
data = JSON.parse(await readFile14(file, "utf8"));
|
|
8597
8847
|
} catch {
|
|
8598
8848
|
return opened;
|
|
8599
8849
|
}
|
|
@@ -8608,13 +8858,13 @@ async function writeOpened(file, opened) {
|
|
|
8608
8858
|
if (file === null) return;
|
|
8609
8859
|
try {
|
|
8610
8860
|
if (opened.size === 0) {
|
|
8611
|
-
await
|
|
8861
|
+
await rm10(file, { force: true });
|
|
8612
8862
|
return;
|
|
8613
8863
|
}
|
|
8614
8864
|
const entries = [...opened].sort(([a], [b]) => a - b);
|
|
8615
|
-
await
|
|
8865
|
+
await mkdir9(dirname9(file), { recursive: true });
|
|
8616
8866
|
const temporary = `${file}.${process.pid}`;
|
|
8617
|
-
await
|
|
8867
|
+
await writeFile6(
|
|
8618
8868
|
temporary,
|
|
8619
8869
|
`${JSON.stringify(Object.fromEntries(entries), null, 2)}
|
|
8620
8870
|
`
|
|
@@ -8708,7 +8958,7 @@ async function scopeOf(c, name, tabs, owners) {
|
|
|
8708
8958
|
label: labelOf(name),
|
|
8709
8959
|
rows: []
|
|
8710
8960
|
};
|
|
8711
|
-
const dir =
|
|
8961
|
+
const dir = join28(c.rnessDir, name);
|
|
8712
8962
|
const shape = await readShape(c.rnessDir, name);
|
|
8713
8963
|
const fronts = new Map(
|
|
8714
8964
|
(await collectMarkdown(dir)).map((item) => [
|
|
@@ -8754,7 +9004,7 @@ async function working(ui, doing, count3, work) {
|
|
|
8754
9004
|
async function filesOf2(rnessDir) {
|
|
8755
9005
|
const files = /* @__PURE__ */ new Set();
|
|
8756
9006
|
const walk = async (rel) => {
|
|
8757
|
-
const dir = rel === "" ? rnessDir :
|
|
9007
|
+
const dir = rel === "" ? rnessDir : join28(rnessDir, ...rel.split("/"));
|
|
8758
9008
|
for (const e of await readdir5(dir, { withFileTypes: true })) {
|
|
8759
9009
|
const path = rel === "" ? e.name : `${rel}/${e.name}`;
|
|
8760
9010
|
if (e.isDirectory()) {
|
|
@@ -8866,7 +9116,7 @@ async function syncBoard(c, board, ui, sayLayout, scope) {
|
|
|
8866
9116
|
w.path,
|
|
8867
9117
|
issueBody({
|
|
8868
9118
|
path: w.path,
|
|
8869
|
-
text: await
|
|
9119
|
+
text: await readFile15(join28(c.rnessDir, ...w.path.split("/")), "utf8"),
|
|
8870
9120
|
org: c.org,
|
|
8871
9121
|
numbers,
|
|
8872
9122
|
files
|
|
@@ -9358,6 +9608,14 @@ function buildProgram(state) {
|
|
|
9358
9608
|
).argument("[tab]", "open on this tab: adr, specs, plans, \u2026").addOption(new Option("--cwd <dir>").hideHelp()).action(async (tab, opts) => {
|
|
9359
9609
|
state.code = await statusCommand(tab, opts);
|
|
9360
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
|
+
);
|
|
9361
9619
|
program.command("validate").description(
|
|
9362
9620
|
"Check the .rness/ tree against the contract and every generated block"
|
|
9363
9621
|
).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
|
|
@@ -9618,4 +9876,4 @@ export {
|
|
|
9618
9876
|
OWN_COMMANDS,
|
|
9619
9877
|
catchUp
|
|
9620
9878
|
};
|
|
9621
|
-
//# sourceMappingURL=chunk-
|
|
9879
|
+
//# sourceMappingURL=chunk-VGBS7HDY.js.map
|