@tokenoftrust/cli 2.0.7 → 2.0.9
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/package.json +1 -1
- package/src/commands/clone.mjs +13 -5
- package/src/commands/submit.mjs +59 -7
- package/src/commands/sync.mjs +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"description": "Token of Trust developer CLI — clone a tenant store, run it locally with save→reload, and submit it for preview. Installs the `tot` command.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Token of Trust",
|
package/src/commands/clone.mjs
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
*
|
|
47
47
|
* Dependency-free (global fetch + `git` via child_process).
|
|
48
48
|
*/
|
|
49
|
-
import { execFile } from "node:child_process";
|
|
49
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
50
50
|
import { promisify } from "node:util";
|
|
51
51
|
import { createMcpClient } from "../mcp.mjs";
|
|
52
52
|
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
@@ -54,7 +54,7 @@ import { offerSignIn } from "./login.mjs";
|
|
|
54
54
|
import { CliError, fail, formatError } from "../errors.mjs";
|
|
55
55
|
import { writeNvmrc } from "../sample.mjs";
|
|
56
56
|
import { emitObstacle } from "../obstacle.mjs";
|
|
57
|
-
import {
|
|
57
|
+
import { splitAuthedRemote, basicAuthExtraHeader, installForgeCredentialHelper } from "../git-credential.mjs";
|
|
58
58
|
|
|
59
59
|
const execFileP = promisify(execFile);
|
|
60
60
|
|
|
@@ -366,11 +366,19 @@ async function cloneRepo(gitRemote, dir, redact) {
|
|
|
366
366
|
next: `check the target dir is empty and you can reach the remote, then re-run`,
|
|
367
367
|
});
|
|
368
368
|
}
|
|
369
|
+
// Install the HOST-SCOPED helper, not a bare `credential.helper`. A bare one is appended to
|
|
370
|
+
// whatever is inherited, and on a Mac that inherits `credential.helper = osxkeychain` — which
|
|
371
|
+
// runs FIRST, answers with a stale forge credential, and shadows ours. Gitea reports the
|
|
372
|
+
// resulting unauthorized fetch as "Repository not found", so a freshly cloned checkout looks
|
|
373
|
+
// like a missing repo rather than a credential problem. The host-scoped form writes
|
|
374
|
+
// ["", <helper>]: the empty reset clears the inherited helper for this host only.
|
|
369
375
|
try {
|
|
370
|
-
|
|
376
|
+
const gitSync = (/** @type {string[]} */ cargs) =>
|
|
377
|
+
execFileSync("git", ["-C", dir, ...cargs], { encoding: "utf8" });
|
|
378
|
+
installForgeCredentialHelper(gitSync, { host: new URL(gitRemote).host });
|
|
371
379
|
} catch {
|
|
372
|
-
// Best-effort —
|
|
373
|
-
//
|
|
380
|
+
// Best-effort — never fail a good clone over credential plumbing. The next
|
|
381
|
+
// `tot preview`/`tot sync` installs it, and the fetch that needs it says why.
|
|
374
382
|
}
|
|
375
383
|
const head = (await git(["-C", dir, "log", "-1", "--oneline"])).trim();
|
|
376
384
|
writeNvmrc(dir); // version-manager hooks land on a supported Node on cd
|
package/src/commands/submit.mjs
CHANGED
|
@@ -1154,7 +1154,7 @@ function reportCandidate(result, changeId, { quiet = false } = {}) {
|
|
|
1154
1154
|
* Defaults to null when no numeric PR number was known at result-build time.
|
|
1155
1155
|
* Pure — unit-tested.
|
|
1156
1156
|
* @param {{ ok: boolean, ref?: string|null, commit?: string|null, changeId?: string|null,
|
|
1157
|
-
* candidate?: {prNumber?: number, number?: number, state?: string, url?: string}|null,
|
|
1157
|
+
* candidate?: {prNumber?: number, number?: number, state?: string, url?: string, headSha?: string|null}|null,
|
|
1158
1158
|
* status?: {status?: string, reconcile?: object|null, compliance?: object|null,
|
|
1159
1159
|
* previewUrl?: string|null, shipped?: object|null, dispatched?: boolean|null,
|
|
1160
1160
|
* notDispatched?: boolean, delivery?: object|null}|null,
|
|
@@ -1165,6 +1165,10 @@ export function buildJsonResult({ ok, ref = null, commit = null, changeId = null
|
|
|
1165
1165
|
ok,
|
|
1166
1166
|
ref,
|
|
1167
1167
|
commit,
|
|
1168
|
+
// The sha the `status`/`reconcile`/`compliance` fields above are reported against —
|
|
1169
|
+
// the candidate head when the candidate names one, else the local `commit`. Kept
|
|
1170
|
+
// beside `commit` rather than replacing it so automation can tell the two apart.
|
|
1171
|
+
candidateHeadSha: candidate ? resolvePreviewStatusSha(commit, candidate) : null,
|
|
1168
1172
|
changeId,
|
|
1169
1173
|
candidate: candidate
|
|
1170
1174
|
? { number: candidate.prNumber ?? candidate.number ?? null, state: candidate.state ?? null, url: candidate.url ?? null }
|
|
@@ -1184,6 +1188,43 @@ export function buildJsonResult({ ok, ref = null, commit = null, changeId = null
|
|
|
1184
1188
|
};
|
|
1185
1189
|
}
|
|
1186
1190
|
|
|
1191
|
+
/**
|
|
1192
|
+
* The sha `preview_status` actually resolves against. The candidate the forge opened
|
|
1193
|
+
* may carry a DIFFERENT head than the commit you pushed (it is rebuilt onto the
|
|
1194
|
+
* current store), and `preview_status` only knows the candidate head — so polling the
|
|
1195
|
+
* local commit returns a misleading `pending` / "no reconcile delivery observed"
|
|
1196
|
+
* instead of the real result. Prefer the candidate head whenever the candidate names
|
|
1197
|
+
* one; fall back to the local commit (no candidate, or an older MCP that omits it).
|
|
1198
|
+
* Pure — unit-tested.
|
|
1199
|
+
* @param {string|null} commit local HEAD sha
|
|
1200
|
+
* @param {{headSha?: string|null}|null} candidate the candidate_open projection
|
|
1201
|
+
*/
|
|
1202
|
+
export function resolvePreviewStatusSha(commit, candidate) {
|
|
1203
|
+
const head = typeof candidate?.headSha === "string" ? candidate.headSha.trim() : "";
|
|
1204
|
+
return head || commit;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* The two-sha notice, printed ONLY when they actually differ (when they match there is
|
|
1209
|
+
* nothing to disambiguate and a second line would be noise). Names which sha the
|
|
1210
|
+
* reconcile result is reported against, so a developer polling by hand — or reading a
|
|
1211
|
+
* failure — knows which one to use. Pure — unit-tested.
|
|
1212
|
+
* @param {string|null} commit local HEAD sha
|
|
1213
|
+
* @param {{headSha?: string|null}|null} candidate the candidate_open projection
|
|
1214
|
+
* @returns {string[]} lines to print (empty when there is no distinction to draw)
|
|
1215
|
+
*/
|
|
1216
|
+
export function formatCandidateHeadNotice(commit, candidate) {
|
|
1217
|
+
const head = resolvePreviewStatusSha(commit, candidate);
|
|
1218
|
+
if (!commit || !head || head === commit) return [];
|
|
1219
|
+
return [
|
|
1220
|
+
"",
|
|
1221
|
+
` ▸ local commit: ${commit.slice(0, 9)} (what you committed here)`,
|
|
1222
|
+
` candidate head: ${head.slice(0, 9)} (what preview_status reports on)`,
|
|
1223
|
+
" They differ because your candidate is rebuilt on the current store. Follow the",
|
|
1224
|
+
" candidate head — the local commit reads as \"never dispatched\", not as the result.",
|
|
1225
|
+
];
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1187
1228
|
/** Print the `--json` result as one pretty-printed object on stdout — a no-op
|
|
1188
1229
|
* unless `args.json` was passed, so call sites can invoke it unconditionally. */
|
|
1189
1230
|
function emitJson(args, payload) {
|
|
@@ -1663,9 +1704,18 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1663
1704
|
} catch { /* best-effort local hint — a miss just re-derives the stable id */ }
|
|
1664
1705
|
}
|
|
1665
1706
|
|
|
1707
|
+
// Poll the sha `preview_status` actually resolves against — the candidate head, not
|
|
1708
|
+
// the local commit, whenever the candidate names its own. Printed first (when they
|
|
1709
|
+
// differ) so the developer reads the result against the right sha.
|
|
1710
|
+
const statusSha = resolvePreviewStatusSha(commit, candidate) ?? commit;
|
|
1711
|
+
const statusShort = statusSha.slice(0, 9);
|
|
1712
|
+
if (!args.json) {
|
|
1713
|
+
for (const line of formatCandidateHeadNotice(commit, candidate)) console.log(line);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1666
1716
|
let status;
|
|
1667
1717
|
if (args.noWait) {
|
|
1668
|
-
status = normalizePreviewStatus(await client.callTool("preview_status", { commit }));
|
|
1718
|
+
status = normalizePreviewStatus(await client.callTool("preview_status", { commit: statusSha }));
|
|
1669
1719
|
} else {
|
|
1670
1720
|
// One in-place status line (TTY: a spinner with an elapsed-seconds counter;
|
|
1671
1721
|
// non-TTY: a ~10s heartbeat) instead of a newline per poll — the wait reads
|
|
@@ -1681,11 +1731,11 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1681
1731
|
// stage text ("still reconciling…") IS truthful and is left as-is below.
|
|
1682
1732
|
let phase = "reconcile";
|
|
1683
1733
|
if (!args.json) {
|
|
1684
|
-
progress = startProgress(`waiting for reconcile of ${
|
|
1685
|
-
stages: [{ afterMs: 45_000, text: `still reconciling ${
|
|
1734
|
+
progress = startProgress(`waiting for reconcile of ${statusShort}…`, {
|
|
1735
|
+
stages: [{ afterMs: 45_000, text: `still reconciling ${statusShort}… (larger changes take longer)` }],
|
|
1686
1736
|
});
|
|
1687
1737
|
}
|
|
1688
|
-
status = await pollPreviewStatus(client,
|
|
1738
|
+
status = await pollPreviewStatus(client, statusSha, {
|
|
1689
1739
|
...(args.watch ? WATCH_POLL : DEFAULT_POLL),
|
|
1690
1740
|
onTick: (s) => {
|
|
1691
1741
|
// Reconcile is done but we're still waiting on a ship decision (--watch):
|
|
@@ -1693,7 +1743,7 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1693
1743
|
if (!args.json && s.status === "reconciled" && !s.shipped && phase !== "ship") {
|
|
1694
1744
|
phase = "ship";
|
|
1695
1745
|
progress.stop();
|
|
1696
|
-
progress = startProgress(`reconciled ${
|
|
1746
|
+
progress = startProgress(`reconciled ${statusShort} — waiting for a ship decision…`);
|
|
1697
1747
|
}
|
|
1698
1748
|
},
|
|
1699
1749
|
});
|
|
@@ -1704,7 +1754,9 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1704
1754
|
}
|
|
1705
1755
|
// --json also skips the browser auto-open (open: !args.noOpen && !args.json)
|
|
1706
1756
|
// — automation doesn't want a browser popping up.
|
|
1707
|
-
|
|
1757
|
+
// `commit: statusSha` — the diagnostic blocks name the sha that was actually polled,
|
|
1758
|
+
// so a "never dispatched" hint points at a sha `preview_status` knows about.
|
|
1759
|
+
reportStatus(status, tenant, { open: !args.noOpen && !args.json, quiet: args.json, commit: statusSha, ref, verb, noChanges: patchEntries.length === 0 });
|
|
1708
1760
|
emitJson(args, buildJsonResult({ ok: status?.status !== "failed", ref, commit, changeId, candidate, status, previewPrUrl }));
|
|
1709
1761
|
return status?.status === "failed" ? 1 : 0;
|
|
1710
1762
|
} catch (e) {
|
package/src/commands/sync.mjs
CHANGED
|
@@ -166,8 +166,13 @@ export async function run(argv, ctx) {
|
|
|
166
166
|
// used to 401 even right after signing back in. Best-effort, never blocks sync.
|
|
167
167
|
try {
|
|
168
168
|
ensureTokenlessRemote(git);
|
|
169
|
-
} catch {
|
|
170
|
-
|
|
169
|
+
} catch (e) {
|
|
170
|
+
// Best-effort, but never SILENT: this call installs the host-scoped credential helper, and
|
|
171
|
+
// without it the fetch below fails as "Repository not found" — Gitea's wording for an
|
|
172
|
+
// unauthorized read. Swallowing the reason turns a credential problem into a phantom
|
|
173
|
+
// missing repo, with nothing pointing back here.
|
|
174
|
+
console.error(`~ could not repair this checkout's forge credentials: ${e?.message || e}`);
|
|
175
|
+
console.error(" a fetch failure below is likely this, not a missing repo.");
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
// Refuse a dirty tree up front — a merge on top of uncommitted edits is how
|