@asturiu/v0-sdk 0.0.0-stage → 1.0.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/CHANGELOG.md +42 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +82 -2
- package/contract/assertion-vectors.json +90 -0
- package/contract/schemas/branding.schema.json +46 -0
- package/contract/schemas/definitions.schema.json +376 -0
- package/contract/schemas/portal.schema.json +56 -0
- package/contract/tenant-api-routes.json +165 -0
- package/dist/branding.d.ts +131 -0
- package/dist/branding.d.ts.map +1 -0
- package/dist/branding.js +206 -0
- package/dist/branding.js.map +1 -0
- package/dist/changesets.d.ts +58 -0
- package/dist/changesets.d.ts.map +1 -0
- package/dist/changesets.js +18 -0
- package/dist/changesets.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +12 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/changesets.d.ts +14 -0
- package/dist/cli/changesets.d.ts.map +1 -0
- package/dist/cli/changesets.js +252 -0
- package/dist/cli/changesets.js.map +1 -0
- package/dist/cli/cli.d.ts +37 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +415 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/context.d.ts +147 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/context.js +258 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/engine.d.ts +59 -0
- package/dist/cli/engine.d.ts.map +1 -0
- package/dist/cli/engine.js +85 -0
- package/dist/cli/engine.js.map +1 -0
- package/dist/cli/feedback.d.ts +40 -0
- package/dist/cli/feedback.d.ts.map +1 -0
- package/dist/cli/feedback.js +420 -0
- package/dist/cli/feedback.js.map +1 -0
- package/dist/cli/files.d.ts +83 -0
- package/dist/cli/files.d.ts.map +1 -0
- package/dist/cli/files.js +389 -0
- package/dist/cli/files.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/overview.d.ts +6 -0
- package/dist/cli/overview.d.ts.map +1 -0
- package/dist/cli/overview.js +379 -0
- package/dist/cli/overview.js.map +1 -0
- package/dist/cli/parts.d.ts +5 -0
- package/dist/cli/parts.d.ts.map +1 -0
- package/dist/cli/parts.js +414 -0
- package/dist/cli/parts.js.map +1 -0
- package/dist/cli/sources.d.ts +37 -0
- package/dist/cli/sources.d.ts.map +1 -0
- package/dist/cli/sources.js +91 -0
- package/dist/cli/sources.js.map +1 -0
- package/dist/cli/terminal.d.ts +135 -0
- package/dist/cli/terminal.d.ts.map +1 -0
- package/dist/cli/terminal.js +353 -0
- package/dist/cli/terminal.js.map +1 -0
- package/dist/cli/ui-page.d.ts +8 -0
- package/dist/cli/ui-page.d.ts.map +1 -0
- package/dist/cli/ui-page.js +216 -0
- package/dist/cli/ui-page.js.map +1 -0
- package/dist/cli/ui.d.ts +54 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +251 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/client.d.ts +88 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +165 -0
- package/dist/client.js.map +1 -0
- package/dist/contacts.d.ts +43 -0
- package/dist/contacts.d.ts.map +1 -0
- package/dist/contacts.js +46 -0
- package/dist/contacts.js.map +1 -0
- package/dist/definition-set.d.ts +731 -0
- package/dist/definition-set.d.ts.map +1 -0
- package/dist/definition-set.js +659 -0
- package/dist/definition-set.js.map +1 -0
- package/dist/definitions.d.ts +42 -0
- package/dist/definitions.d.ts.map +1 -0
- package/dist/definitions.js +154 -0
- package/dist/definitions.js.map +1 -0
- package/dist/enrolment.d.ts +9 -0
- package/dist/enrolment.d.ts.map +1 -0
- package/dist/enrolment.js +8 -0
- package/dist/enrolment.js.map +1 -0
- package/dist/fill.d.ts +73 -0
- package/dist/fill.d.ts.map +1 -0
- package/dist/fill.js +46 -0
- package/dist/fill.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications.d.ts +33 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +36 -0
- package/dist/notifications.js.map +1 -0
- package/dist/oidc.d.ts +122 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/oidc.js +150 -0
- package/dist/oidc.js.map +1 -0
- package/dist/overlay.d.ts +109 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +58 -0
- package/dist/overlay.js.map +1 -0
- package/dist/passes.d.ts +95 -0
- package/dist/passes.d.ts.map +1 -0
- package/dist/passes.js +140 -0
- package/dist/passes.js.map +1 -0
- package/dist/portal-options.d.ts +65 -0
- package/dist/portal-options.d.ts.map +1 -0
- package/dist/portal-options.js +120 -0
- package/dist/portal-options.js.map +1 -0
- package/dist/reads.d.ts +41 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +44 -0
- package/dist/reads.js.map +1 -0
- package/dist/recovery-words.d.ts +8 -0
- package/dist/recovery-words.d.ts.map +1 -0
- package/dist/recovery-words.js +136 -0
- package/dist/recovery-words.js.map +1 -0
- package/dist/recovery.d.ts +198 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +112 -0
- package/dist/recovery.js.map +1 -0
- package/dist/reveal.d.ts +35 -0
- package/dist/reveal.d.ts.map +1 -0
- package/dist/reveal.js +66 -0
- package/dist/reveal.js.map +1 -0
- package/dist/seal.d.ts +15 -0
- package/dist/seal.d.ts.map +1 -0
- package/dist/seal.js +0 -0
- package/dist/seal.js.map +1 -0
- package/dist/session.d.ts +66 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +61 -0
- package/dist/session.js.map +1 -0
- package/dist/signing-in-mark.d.ts +14 -0
- package/dist/signing-in-mark.d.ts.map +1 -0
- package/dist/signing-in-mark.js +24 -0
- package/dist/signing-in-mark.js.map +1 -0
- package/dist/signing-in.d.ts +22 -0
- package/dist/signing-in.d.ts.map +1 -0
- package/dist/signing-in.js +203 -0
- package/dist/signing-in.js.map +1 -0
- package/dist/tags.d.ts +96 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +88 -0
- package/dist/tags.js.map +1 -0
- package/dist/tenant-subscription.d.ts +47 -0
- package/dist/tenant-subscription.d.ts.map +1 -0
- package/dist/tenant-subscription.js +7 -0
- package/dist/tenant-subscription.js.map +1 -0
- package/dist/testing/index.d.ts +125 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +320 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/theme.d.ts +237 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +504 -0
- package/dist/theme.js.map +1 -0
- package/dist/tickets.d.ts +152 -0
- package/dist/tickets.d.ts.map +1 -0
- package/dist/tickets.js +65 -0
- package/dist/tickets.js.map +1 -0
- package/dist/tiers.d.ts +168 -0
- package/dist/tiers.d.ts.map +1 -0
- package/dist/tiers.js +341 -0
- package/dist/tiers.js.map +1 -0
- package/dist/types.d.ts +885 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/urlid.d.ts +50 -0
- package/dist/urlid.d.ts.map +1 -0
- package/dist/urlid.js +78 -0
- package/dist/urlid.js.map +1 -0
- package/dist/version.d.ts +15 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +15 -0
- package/dist/version.js.map +1 -0
- package/make/asturiu.mk +43 -0
- package/package.json +63 -4
- package/src/branding.ts +221 -0
- package/src/changesets.ts +61 -0
- package/src/cli/bin.ts +12 -0
- package/src/cli/changesets.ts +233 -0
- package/src/cli/cli.ts +425 -0
- package/src/cli/context.ts +299 -0
- package/src/cli/engine.ts +112 -0
- package/src/cli/feedback.ts +406 -0
- package/src/cli/files.ts +371 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/overview.ts +343 -0
- package/src/cli/parts.ts +371 -0
- package/src/cli/sources.ts +111 -0
- package/src/cli/terminal.ts +384 -0
- package/src/cli/ui-page.ts +215 -0
- package/src/cli/ui.ts +266 -0
- package/src/client.ts +202 -0
- package/src/contacts.ts +61 -0
- package/src/definition-set.ts +965 -0
- package/src/definitions.ts +156 -0
- package/src/enrolment.ts +10 -0
- package/src/fill.ts +83 -0
- package/src/index.ts +38 -0
- package/src/notifications.ts +55 -0
- package/src/oidc.ts +199 -0
- package/src/overlay.ts +137 -0
- package/src/passes.ts +168 -0
- package/src/portal-options.ts +125 -0
- package/src/reads.ts +71 -0
- package/src/recovery-words.ts +137 -0
- package/src/recovery.ts +269 -0
- package/src/reveal.ts +72 -0
- package/src/seal.ts +0 -0
- package/src/session.ts +93 -0
- package/src/signing-in-mark.ts +23 -0
- package/src/signing-in.ts +213 -0
- package/src/tags.ts +146 -0
- package/src/tenant-subscription.ts +44 -0
- package/src/testing/index.ts +337 -0
- package/src/theme.ts +628 -0
- package/src/tickets.ts +195 -0
- package/src/tiers.ts +397 -0
- package/src/types.ts +847 -0
- package/src/urlid.ts +77 -0
- package/src/version.ts +15 -0
- package/templates/asturiu/branding.json +4 -0
- package/templates/asturiu/definitions.json +23 -0
- package/templates/asturiu/portal.json +16 -0
package/src/cli/bin.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `asturiu-v0-tenant` command (ADR-074 §5). Run it through the repository's package script
|
|
4
|
+
* (`"asturiu": "asturiu-v0-tenant"`) or `npx --no-install asturiu-v0-tenant`: never plain `npx`, which would fetch
|
|
5
|
+
* whatever the registry holds under that name when the SDK is not installed.
|
|
6
|
+
*/
|
|
7
|
+
import { run } from "./cli.js";
|
|
8
|
+
|
|
9
|
+
// A closed pipe (`… | head`) ends the run quietly.
|
|
10
|
+
for (const stream of [process.stdout, process.stderr]) stream.on("error", (e: NodeJS.ErrnoException) => (e.code === "EPIPE" ? process.exit(0) : undefined));
|
|
11
|
+
|
|
12
|
+
process.exitCode = await run(process.argv.slice(2));
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Engine's changesets with this Tenant's tickets (ADR-054 §7, ADR-059, ADR-074 §9): `changesets`,
|
|
3
|
+
* `changesets pull` and `changeset <n> [<status>]`. The index comes from the Tenant API, the documents from the
|
|
4
|
+
* Engine site; the SDK carries none. A document is data: its prompt is shown to the person, never run.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { type ChangesetStatus } from "../changesets.js";
|
|
10
|
+
import { setChangesetStatus } from "../definition-set.js";
|
|
11
|
+
import { SDK_VERSION } from "../version.js";
|
|
12
|
+
import { type Ctx, EXIT, Fail, agentsSection, call, emit, hostOf, places, pulledDocuments, readFailure, recorded, refuseSecrets, rel, str } from "./context.js";
|
|
13
|
+
import { type Changeset, CLOSED, LOOK, byUrgency, changesetList, isOpen, pill } from "./engine.js";
|
|
14
|
+
import { type Column, ago, pad3, plain, plural, wrap } from "./terminal.js";
|
|
15
|
+
|
|
16
|
+
const REPORTABLE = ["acknowledged", "in_progress", "on_hold", "resolved", "canceled"] as const;
|
|
17
|
+
|
|
18
|
+
/** A changeset's Markdown text as plain words: no code marks or emphasis. */
|
|
19
|
+
const unmark = (text: string) => plain(text).replace(/[`*]/g, "");
|
|
20
|
+
|
|
21
|
+
/** A status's key in LOOK. */
|
|
22
|
+
const statusKey = (c: Changeset, live: boolean) => (!live ? "" : (c.status ?? (c.action_required ? "no ticket" : "nothing asked")));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The prompt for the Tenant's coding agent, as a changeset document writes it: the first fenced block under the
|
|
26
|
+
* heading "Prompt for your coding agent". Text to show the person, never to run.
|
|
27
|
+
*/
|
|
28
|
+
export function promptOf(document: string): string | null {
|
|
29
|
+
const text = plain(document);
|
|
30
|
+
const at = /^##\s+Prompt for your coding agent\s*$/im.exec(text);
|
|
31
|
+
if (!at) return null;
|
|
32
|
+
const rest = text.slice(at.index + at[0].length);
|
|
33
|
+
const fence = /^(`{3,}|~{3,})[^\n]*\n([\s\S]*?)^\1\s*$/m.exec(rest);
|
|
34
|
+
if (fence) return fence[2].replace(/\n$/, "");
|
|
35
|
+
const next = /^##\s/m.exec(rest);
|
|
36
|
+
return (next ? rest.slice(0, next.index) : rest).trim() || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The pulled document of a changeset, with its prompt, when the repository has it. */
|
|
40
|
+
export function pulledDocument(ctx: Ctx, file: string): { path: string; prompt: string | null } | null {
|
|
41
|
+
const doc = path.join(places(ctx).changesets, file);
|
|
42
|
+
if (!existsSync(doc)) return null;
|
|
43
|
+
return { path: rel(ctx, doc), prompt: promptOf(readFileSync(doc, "utf8")) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function cmdChangesets(ctx: Ctx, args: string[]) {
|
|
47
|
+
const { t, flags } = ctx;
|
|
48
|
+
const since = Number(str(flags.since) ?? 0);
|
|
49
|
+
if (!Number.isInteger(since) || since < 0) throw new Fail("--since takes a changeset number, such as --since 20.", { code: EXIT.misunderstood });
|
|
50
|
+
if (args[0] && args[0] !== "pull") throw new Fail(`“changesets ${plain(args[0])}” is not a command.`, { hint: `${ctx.cmd} changesets pull copies the documents; ${ctx.cmd} changeset ${plain(args[0])} shows one`, code: EXIT.misunderstood });
|
|
51
|
+
const cs = await t.busy("Reading the changesets", () => changesetList(ctx, since));
|
|
52
|
+
if (args[0] === "pull") return pullChangesets(ctx, cs);
|
|
53
|
+
|
|
54
|
+
const done = recorded(ctx);
|
|
55
|
+
const have = pulledDocuments(ctx);
|
|
56
|
+
const open = cs.list.filter(isOpen).sort(byUrgency);
|
|
57
|
+
const list = flags.open ? open : cs.list;
|
|
58
|
+
if (!cs.live) ctx.exit = EXIT.failed;
|
|
59
|
+
if (ctx.json) return emit(ctx, { live: cs.live, latest: cs.latest, reason: cs.reason, changesets: list.map((c) => ({ ...c, document: have.has(c.file), recorded: done.has(c.number), open: isOpen(c) })) });
|
|
60
|
+
|
|
61
|
+
t.banner("Changesets", cs.live ? [`${list.length} shown`, `latest ${pad3(cs.latest ?? 0)}`, `live from ${hostOf(ctx.baseUrl)}`] : ["not read"]);
|
|
62
|
+
if (!cs.live) {
|
|
63
|
+
t.callout("fail", "The changesets could not be read", [`${cs.reason}.`, "The SDK carries no changesets: they come from the Tenant API, and their documents from the Engine site."]);
|
|
64
|
+
t.next([[`${ctx.cmd} doctor`, "find out why, and how to fix it"]]);
|
|
65
|
+
t.out();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!list.length) t.item("ok", flags.open ? "No changeset is open." : `No changeset after ${pad3(since)}.`);
|
|
69
|
+
else {
|
|
70
|
+
const yes = t.paint.green(t.sym.ok);
|
|
71
|
+
const no = t.paint.gray(t.sym.dot);
|
|
72
|
+
const cols: Column<Changeset>[] = [
|
|
73
|
+
{ label: "#", value: (c) => pad3(c.number), paint: (s) => t.paint.bold(s) },
|
|
74
|
+
{ label: "Status", value: (c) => pill(t, statusKey(c, true)).text, paint: (s, c) => pill(t, statusKey(c, true)).painted.replace(pill(t, statusKey(c, true)).text, s) },
|
|
75
|
+
{ label: "Doc", value: (c) => (have.has(c.file) ? t.sym.ok : t.sym.dot), paint: (s) => (s === t.sym.ok ? yes : no) },
|
|
76
|
+
{ label: "Done", value: (c) => (done.has(c.number) ? t.sym.ok : t.sym.dot), paint: (s) => (s === t.sym.ok ? yes : no) },
|
|
77
|
+
{ label: "Title", value: (c) => unmark(c.title), flex: true, min: 20, paint: (s, c) => (CLOSED.has(c.status ?? "") ? s : isOpen(c) ? t.paint.bold(s) : s) },
|
|
78
|
+
{ label: "SDK", value: (c) => c.sdk_version ?? "", paint: (s) => t.paint.gray(s) },
|
|
79
|
+
{ label: "Until", value: (c) => c.deadline ?? "", paint: (s) => t.paint.yellow(s) },
|
|
80
|
+
];
|
|
81
|
+
if (!list.some((c) => c.deadline)) cols.pop();
|
|
82
|
+
if (!list.some((c) => c.sdk_version)) cols.splice(cols.findIndex((c) => c.label === "SDK"), 1);
|
|
83
|
+
t.table(list, cols);
|
|
84
|
+
t.out();
|
|
85
|
+
const counts: Record<string, number> = {};
|
|
86
|
+
for (const c of list) counts[statusKey(c, true)] = (counts[statusKey(c, true)] ?? 0) + 1;
|
|
87
|
+
t.out(` ${Object.entries(counts).map(([k, n]) => pill(t, k).painted.replace(LOOK[k]?.[0] ?? k, `${n} ${LOOK[k]?.[0] ?? k}`)).join(" ")}`);
|
|
88
|
+
t.out(` ${t.paint.gray(`Doc: in ${rel(ctx, places(ctx).changesets)} Done: recorded in ${rel(ctx, places(ctx).agents)}`)}`);
|
|
89
|
+
}
|
|
90
|
+
const steps: [string, string][] = [];
|
|
91
|
+
if (open.length) steps.push([`${ctx.cmd} changeset ${open[0].number}`, `see what ${pad3(open[0].number)} asks`]);
|
|
92
|
+
if (cs.list.some((c) => c.action_required && !have.has(c.file))) steps.push([`${ctx.cmd} changesets pull`, "copy the missing documents in"]);
|
|
93
|
+
t.next(steps);
|
|
94
|
+
t.out();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Copies each changeset document not yet in the repository from its address on the Engine site. */
|
|
98
|
+
async function pullChangesets(ctx: Ctx, cs: Awaited<ReturnType<typeof changesetList>>) {
|
|
99
|
+
const { t } = ctx;
|
|
100
|
+
if (!cs.live) throw new Fail("The changesets could not be read, so nothing was pulled.", { lines: [`${cs.reason}.`], hint: `${ctx.cmd} doctor` });
|
|
101
|
+
const dir = places(ctx).changesets;
|
|
102
|
+
mkdirSync(dir, { recursive: true });
|
|
103
|
+
const results: { file: string; from: string; ok: boolean; why?: string }[] = [];
|
|
104
|
+
for (const c of cs.list) {
|
|
105
|
+
const to = path.join(dir, c.file);
|
|
106
|
+
if (existsSync(to)) continue;
|
|
107
|
+
if (!/^[0-9]{3}-[a-z0-9-]+\.md$/.test(c.file)) {
|
|
108
|
+
results.push({ file: plain(c.file), from: "", ok: false, why: "not a changeset file name" });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!c.url || !/^https?:\/\//.test(c.url)) {
|
|
112
|
+
results.push({ file: c.file, from: "", ok: false, why: "not published: the Engine site is not configured" });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const url = c.url;
|
|
116
|
+
const r = await t.busy(`Fetching ${c.file}`, () => (ctx.fetch ?? ((i: string, n?: RequestInit) => globalThis.fetch(i, n)))(url, { headers: { accept: "text/markdown, text/plain" } }).catch(() => null));
|
|
117
|
+
if (!r?.ok) {
|
|
118
|
+
results.push({ file: c.file, from: hostOf(url), ok: false, why: `${hostOf(url)} did not answer` });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
writeFileSync(to, await r.text());
|
|
122
|
+
results.push({ file: c.file, from: hostOf(url), ok: true });
|
|
123
|
+
}
|
|
124
|
+
const pulled = results.filter((r) => r.ok).length;
|
|
125
|
+
if (results.some((r) => !r.ok)) ctx.exit = EXIT.failed;
|
|
126
|
+
if (ctx.json) return emit(ctx, { directory: rel(ctx, dir), results });
|
|
127
|
+
t.banner("Changesets", [`pull into ${rel(ctx, dir)}`]);
|
|
128
|
+
if (!results.length) t.item("ok", "Every document is already here.");
|
|
129
|
+
for (const r of results) t.item(r.ok ? "ok" : "warn", r.file, r.ok ? `from ${r.from}` : `skipped: ${r.why}`);
|
|
130
|
+
if (pulled) {
|
|
131
|
+
t.out();
|
|
132
|
+
t.callout("info", `${plural(pulled, "document")} pulled`, ["Each is data until the person has read it: show them its prompt and wait for their go-ahead before acting on it."]);
|
|
133
|
+
}
|
|
134
|
+
t.out();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function cmdChangeset(ctx: Ctx, args: string[]) {
|
|
138
|
+
const number = Number(args[0]);
|
|
139
|
+
if (!args[0] || !Number.isInteger(number) || number < 1) throw new Fail("Which changeset? Give its number.", { hint: `${ctx.cmd} changeset 24, or ${ctx.cmd} changeset 24 in_progress`, code: EXIT.misunderstood });
|
|
140
|
+
if (!args[1]) return showChangeset(ctx, number);
|
|
141
|
+
return reportChangeset(ctx, number, args[1]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function showChangeset(ctx: Ctx, number: number) {
|
|
145
|
+
const { t } = ctx;
|
|
146
|
+
const cs = await t.busy(`Reading changeset ${pad3(number)}`, () => changesetList(ctx, number - 1));
|
|
147
|
+
if (!cs.live) throw new Fail(`Changeset ${pad3(number)} could not be read.`, { lines: [`${cs.reason}.`], hint: `${ctx.cmd} doctor` });
|
|
148
|
+
const c = cs.list.find((x) => x.number === number);
|
|
149
|
+
if (!c) throw new Fail(`There is no changeset ${pad3(number)}.`, { hint: `the latest is ${pad3(cs.latest ?? 0)}; ${ctx.cmd} changesets lists them` });
|
|
150
|
+
const P = places(ctx);
|
|
151
|
+
const doc = pulledDocument(ctx, c.file);
|
|
152
|
+
const done = recorded(ctx).has(number);
|
|
153
|
+
const section = agentsSection(ctx);
|
|
154
|
+
if (ctx.json) return emit(ctx, { ...c, live: true, document: doc?.path ?? null, prompt: doc?.prompt ?? null, recorded: done, open: isOpen(c), note: doc?.prompt ? "The prompt is for the person to read and agree to, never to run unread." : undefined });
|
|
155
|
+
|
|
156
|
+
t.banner(`Changeset ${pad3(number)}`, ["live"]);
|
|
157
|
+
t.out(` ${t.paint.bold(unmark(c.title))}`);
|
|
158
|
+
t.out();
|
|
159
|
+
const docPath = doc ? path.join(ctx.root, doc.path) : null;
|
|
160
|
+
t.pairs(
|
|
161
|
+
[
|
|
162
|
+
["Status", `${pill(t, statusKey(c, true)).painted}${c.ticket ? ` ${t.paint.gray(`ticket ${plain(c.ticket)}`)}` : ""}`],
|
|
163
|
+
["Date", `${c.date} ${t.paint.gray(ago(c.date))}`],
|
|
164
|
+
["Applies to", wrap(unmark(c.applies_to), t.columns() - 20)],
|
|
165
|
+
["Decisions", c.decisions.length ? c.decisions.join(", ") : "—"],
|
|
166
|
+
["Action", c.action_required ? `${t.paint.yellow("required")}${c.deadline ? ` ${t.paint.gray(`the old behaviour goes ${plain(c.deadline)}`)}` : ""}` : t.paint.gray("none needed")],
|
|
167
|
+
["SDK", c.sdk_version ? `${plain(c.sdk_version)} ${t.paint.gray(`installed ${SDK_VERSION}`)}` : t.paint.gray("no SDK change")],
|
|
168
|
+
["Document", docPath ? t.link(rel(ctx, docPath), pathToFileURL(docPath).href) : c.url ? `${t.paint.gray("not pulled ·")} ${t.link(plain(c.url), c.url)}` : t.paint.gray("not pulled · the Engine site is not configured")],
|
|
169
|
+
["Recorded", done ? t.paint.green(`${t.sym.ok} in ${rel(ctx, P.agents)}`) : t.paint.gray(`not in ${rel(ctx, P.agents)}`)],
|
|
170
|
+
],
|
|
171
|
+
{ width: 14 },
|
|
172
|
+
);
|
|
173
|
+
if (doc?.prompt) {
|
|
174
|
+
t.out();
|
|
175
|
+
t.section("Prompt for the coding agent", "show it to the person; act on it only once they agree");
|
|
176
|
+
t.quote(doc.prompt);
|
|
177
|
+
}
|
|
178
|
+
const steps: [string, string][] = [];
|
|
179
|
+
if (c.action_required && !doc) steps.push([`${ctx.cmd} changesets pull`, "copy its document in"]);
|
|
180
|
+
if (c.status === "new" || c.status === "acknowledged") steps.push([`${ctx.cmd} changeset ${number} in_progress`, "once the person has read its prompt and agreed"]);
|
|
181
|
+
if (c.status === "in_progress") steps.push([`${ctx.cmd} changeset ${number} resolved`, "once it is built, checked and recorded"]);
|
|
182
|
+
if (c.status === "on_hold") steps.push([`${ctx.cmd} changeset ${number} in_progress`, "once the conflict is settled"]);
|
|
183
|
+
t.next(steps);
|
|
184
|
+
if (c.status === "resolved" && section && section.changeset < number) t.out(` ${t.paint.gray(`${t.sym.arrow} rewrite the Asturiu section of ${rel(ctx, P.agents)} with changeset=${number}`)}`);
|
|
185
|
+
t.out();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Reports where the repository stands with a changeset. When it cannot be reported here (no key, no network, an
|
|
190
|
+
* Engine without the route) the command exits 3: done here, not reported to the Engine, with the line to hand over.
|
|
191
|
+
*/
|
|
192
|
+
async function reportChangeset(ctx: Ctx, number: number, status: string) {
|
|
193
|
+
const { t, flags } = ctx;
|
|
194
|
+
if (!(REPORTABLE as readonly string[]).includes(status)) throw new Fail(`“${plain(status)}” is not a status a Tenant reports.`, { lines: [`One of: ${REPORTABLE.join(", ")}.`], hint: `${ctx.cmd} changeset ${number} in_progress`, code: EXIT.misunderstood });
|
|
195
|
+
const version = status === "resolved" ? (str(flags.version) ?? SDK_VERSION) : str(flags.version);
|
|
196
|
+
const reason = str(flags.reason) || undefined;
|
|
197
|
+
const note = str(flags.note) || undefined;
|
|
198
|
+
if (status === "canceled" && !reason) throw new Fail("A canceled changeset needs a reason: why it does not apply here.", { hint: `${ctx.cmd} changeset ${number} canceled --reason "…"`, code: EXIT.misunderstood });
|
|
199
|
+
refuseSecrets(ctx, reason, note);
|
|
200
|
+
const again = `${ctx.cmd} changeset ${number} ${status}${version && status === "resolved" ? ` --version ${version}` : ""}${reason ? ` --reason ${JSON.stringify(reason)}` : ""}${note ? ` --note ${JSON.stringify(note)}` : ""}`;
|
|
201
|
+
const r = await t.busy(`Reporting changeset ${pad3(number)} as ${status.replace("_", " ")}`, () =>
|
|
202
|
+
call(ctx, (fetch, cfg) => setChangesetStatus({ ...cfg, number, status: status as Exclude<ChangesetStatus, "new">, ...(version ? { applied_version: version } : {}), ...(reason ? { reason } : {}), ...(note ? { note } : {}), fetch })),
|
|
203
|
+
);
|
|
204
|
+
if (!r.ok && r.why !== "refused") {
|
|
205
|
+
ctx.exit = EXIT.notReported;
|
|
206
|
+
const why = r.why === "no-route" ? "this Engine takes no changeset status, or the changeset is not this Tenant's" : readFailure(r).replace(/^not read: /, "");
|
|
207
|
+
if (ctx.json) return emit(ctx, { reported: false, number, status, reason: why, command: again });
|
|
208
|
+
t.out();
|
|
209
|
+
t.callout("warn", `Changeset ${pad3(number)} was not reported`, [`The Engine keeps the status it had. Why: ${why}.`, "Run it again where the key and the Tenant API are reachable, or hand this line to the person:"]);
|
|
210
|
+
t.out(` ${t.code(again)}`);
|
|
211
|
+
t.out();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!r.ok) {
|
|
215
|
+
ctx.exit = EXIT.failed;
|
|
216
|
+
if (ctx.json) return emit(ctx, { reported: false, number, status, refused: { status: r.status, type: r.type, message: r.message } });
|
|
217
|
+
t.out();
|
|
218
|
+
t.callout("fail", `The Engine refused: ${r.message}`, [`HTTP ${r.status}. Nothing changed on changeset ${pad3(number)}.`]);
|
|
219
|
+
t.next([[`${ctx.cmd} changeset ${number}`, "its current status"]]);
|
|
220
|
+
t.out();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const v = r.value;
|
|
224
|
+
if (ctx.json) return emit(ctx, { reported: true, ...v });
|
|
225
|
+
t.out();
|
|
226
|
+
t.item("ok", `Changeset ${t.paint.bold(pad3(number))} is now ${pill(t, v.status ?? status).painted}`, [version && status === "resolved" ? `SDK ${version}` : "", v.ticket ? `ticket ${plain(v.ticket)}` : ""].filter(Boolean).join(` ${t.sym.dot} `));
|
|
227
|
+
const section = agentsSection(ctx);
|
|
228
|
+
const steps: [string, string][] = [];
|
|
229
|
+
if (status === "in_progress") steps.push([`${ctx.cmd} changeset ${number} resolved`, "once it is built, checked and recorded"]);
|
|
230
|
+
t.next(steps);
|
|
231
|
+
if (status === "resolved" && (!section || section.changeset < number)) t.out(` ${t.paint.gray(`${t.sym.arrow} rewrite the Asturiu section of ${rel(ctx, places(ctx).agents)} with changeset=${number}`)}`);
|
|
232
|
+
t.out();
|
|
233
|
+
}
|