@tokenoftrust/cli 1.4.0-rc.22 → 1.4.0-rc.24
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/submit.mjs +41 -6
- package/src/git-credential.mjs +91 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "1.4.0-rc.
|
|
3
|
+
"version": "1.4.0-rc.24",
|
|
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/submit.mjs
CHANGED
|
@@ -444,6 +444,41 @@ export function baseCommitsBehind(git, branch) {
|
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Resolve the git ref to diff HEAD against for the candidate's file patch (and its
|
|
449
|
+
* summary). The candidate PR is `preview + your file changes`, and `candidate_open`
|
|
450
|
+
* SERVER-CUTS the candidate branch back to the current preview tip and re-applies the
|
|
451
|
+
* patch — so the patch MUST be the FULL delta of your branch vs its fork point off
|
|
452
|
+
* `preview`, never "what changed since my last candidate push". Diffing against your
|
|
453
|
+
* own candidate tracking ref is what silently produced an empty patch (and the wrong
|
|
454
|
+
* "no file changes" skip, so no PR opened) after a push that landed but failed to open
|
|
455
|
+
* its PR: that ref already equals HEAD, so the since-last-push delta is empty even
|
|
456
|
+
* though the change vs preview is 19 files. Prefer the merge-base (fork point) with
|
|
457
|
+
* `origin/<baseBranch>`; fall back to the candidate tracking ref, then HEAD~1, then ""
|
|
458
|
+
* (single-commit `git show`) when no base ref resolves. Pure git I/O via the injected
|
|
459
|
+
* runner — unit-tested.
|
|
460
|
+
* @param {(cargs:string[])=>string} git
|
|
461
|
+
* @param {string} ref the candidate ref (e.g. "candidate/local-abc")
|
|
462
|
+
* @param {string} [baseBranch] the preview base branch (default FRESHNESS_BASE_BRANCH)
|
|
463
|
+
* @returns {string}
|
|
464
|
+
*/
|
|
465
|
+
export function resolvePatchBase(git, ref, baseBranch = FRESHNESS_BASE_BRANCH) {
|
|
466
|
+
const verify = (r) => {
|
|
467
|
+
try { return git(["rev-parse", "--verify", "--quiet", r]).trim(); } catch { return ""; }
|
|
468
|
+
};
|
|
469
|
+
const previewRef = `refs/remotes/origin/${baseBranch}`;
|
|
470
|
+
if (verify(previewRef)) {
|
|
471
|
+
try {
|
|
472
|
+
const forkPoint = git(["merge-base", "HEAD", previewRef]).trim();
|
|
473
|
+
if (forkPoint) return forkPoint;
|
|
474
|
+
} catch { /* unrelated histories — fall through */ }
|
|
475
|
+
}
|
|
476
|
+
const trackingRef = `refs/remotes/origin/${ref}`;
|
|
477
|
+
if (verify(trackingRef)) return trackingRef;
|
|
478
|
+
if (verify("HEAD~1")) return "HEAD~1";
|
|
479
|
+
return "";
|
|
480
|
+
}
|
|
481
|
+
|
|
447
482
|
// ─── born-rebased at submit (unit c3 — shift-left prevention #2) ─────────────────
|
|
448
483
|
|
|
449
484
|
/** The three strategies the born-rebased rebuild accepts, mirroring `tot accept
|
|
@@ -1355,12 +1390,12 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1355
1390
|
};
|
|
1356
1391
|
function buildSummaryAndPatch(ref) {
|
|
1357
1392
|
const headSubject = gitSafe(["log", "-1", "--format=%s"]).trim();
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1393
|
+
// Diff against the fork point off `preview` — the FULL branch delta — so the patch
|
|
1394
|
+
// is complete for candidate_open's server-cut (which resets the branch to preview
|
|
1395
|
+
// and re-applies this patch). NOT the candidate's own tracking ref: after a push
|
|
1396
|
+
// that landed but failed to open its PR, that ref equals HEAD → empty patch →
|
|
1397
|
+
// wrong "no file changes" → no PR. See resolvePatchBase.
|
|
1398
|
+
const base = resolvePatchBase(gitSafe, ref);
|
|
1364
1399
|
const statusCmd = base ? ["diff", "--name-status", `${base}..HEAD`] : ["show", "--name-status", "--format=", "HEAD"];
|
|
1365
1400
|
const patchEntries = parseNameStatus(gitSafe(statusCmd));
|
|
1366
1401
|
const files = patchEntries.map((e) => e.path);
|
package/src/git-credential.mjs
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
|
-
import { join } from "node:path";
|
|
22
|
+
import { join, dirname } from "node:path";
|
|
23
|
+
import { mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
23
24
|
import { readCredentials, writeCredentials } from "./token-store.mjs";
|
|
24
25
|
|
|
25
26
|
/** The git config value that routes credential requests through `tot` — the
|
|
@@ -141,20 +142,96 @@ export function writeCachedCredential(filePath, { username, password }, { now =
|
|
|
141
142
|
writeCredentials(filePath, { username, password, mintedAt: now });
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
/** Single-quote a string for safe interpolation into the /bin/sh shim. Pure. */
|
|
146
|
+
function shq(s) {
|
|
147
|
+
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Absolute path to the stable, node-version-independent credential-helper shim
|
|
151
|
+
* under `~/.tot/bin` (honors `TOT_HOME`). A checkout's git config points at THIS by
|
|
152
|
+
* absolute path — never a bare `!tot` — so `nvm use` (which swaps the per-node `tot`)
|
|
153
|
+
* can't silently redirect forge auth to an older/missing CLI. Pure. */
|
|
154
|
+
export function forgeShimPath(env = process.env) {
|
|
155
|
+
const home = env.TOT_HOME || homedir();
|
|
156
|
+
return join(home, ".tot", "bin", "tot-git-credential");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The shim's contents: pin the ABSOLUTE node + CLI entry active at write time, with a
|
|
160
|
+
* PATH-search fallback for node, and FAIL LOUD to stderr (never silently) when no node
|
|
161
|
+
* is found — so a broken helper says how to fix itself instead of yielding an opaque
|
|
162
|
+
* "Repository not found". Pure — unit-tested. */
|
|
163
|
+
export function renderForgeShim(nodePath, entryPath) {
|
|
164
|
+
return [
|
|
165
|
+
"#!/bin/sh",
|
|
166
|
+
"# Managed by tot — stable forge credential helper (regenerated each run; do not edit).",
|
|
167
|
+
"# Decouples git auth from which node/tot is active in the shell (nvm-proof).",
|
|
168
|
+
`NODE=${shq(nodePath)}`,
|
|
169
|
+
'[ -x "$NODE" ] || NODE="$(command -v node 2>/dev/null)"',
|
|
170
|
+
'if [ -z "$NODE" ]; then',
|
|
171
|
+
' echo "tot: no node runtime for the git credential helper — reinstall: npm i -g @tokenoftrust/cli@latest" >&2',
|
|
172
|
+
" exit 1",
|
|
173
|
+
"fi",
|
|
174
|
+
`exec "$NODE" ${shq(entryPath)} git-credential "$@"`,
|
|
175
|
+
"",
|
|
176
|
+
].join("\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Write/refresh the shim (0700 dir, 0755 file). Best-effort — returns the shim path,
|
|
180
|
+
* or "" if it couldn't be written (caller then falls back to the bare `!tot` helper). */
|
|
181
|
+
export function writeForgeShim(env = process.env, nodePath = process.execPath, entryPath = process.argv[1]) {
|
|
182
|
+
try {
|
|
183
|
+
if (!nodePath || !entryPath) return "";
|
|
184
|
+
const p = forgeShimPath(env);
|
|
185
|
+
mkdirSync(dirname(p), { recursive: true, mode: 0o700 });
|
|
186
|
+
writeFileSync(p, renderForgeShim(nodePath, entryPath), { mode: 0o755 });
|
|
187
|
+
chmodSync(p, 0o755);
|
|
188
|
+
return p;
|
|
189
|
+
} catch {
|
|
190
|
+
return "";
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Install (or refresh) the HOST-SCOPED forge credential helper on a checkout so that:
|
|
196
|
+
* - it runs the stable `~/.tot` shim by ABSOLUTE path (nvm-proof), not a bare `!tot`;
|
|
197
|
+
* - a leading EMPTY reset value clears any inherited GLOBAL helper for this host — the
|
|
198
|
+
* fix for `credential.helper = osxkeychain` (every Mac dev) running first, returning
|
|
199
|
+
* a stale forge cred, and shadowing our helper so `git pull` 404s even after login.
|
|
200
|
+
* Idempotent: a no-op when the host-scoped list is already `["", <ourHelper>]`. Returns
|
|
201
|
+
* whether it changed anything. `writeShim` is injected for tests. Pure git I/O otherwise.
|
|
202
|
+
* @returns {boolean} changed
|
|
203
|
+
*/
|
|
204
|
+
export function installForgeCredentialHelper(git, {
|
|
205
|
+
host, env = process.env, nodePath = process.execPath, entryPath = process.argv[1],
|
|
206
|
+
writeShim = writeForgeShim,
|
|
207
|
+
} = {}) {
|
|
208
|
+
if (!host) return false;
|
|
209
|
+
const shim = writeShim(env, nodePath, entryPath);
|
|
210
|
+
const helperValue = shim ? `!${shim}` : CREDENTIAL_HELPER;
|
|
211
|
+
const key = `credential.https://${host}.helper`;
|
|
212
|
+
let raw = null;
|
|
213
|
+
try { raw = git(["config", "--local", "--get-all", key]); } catch { raw = null; }
|
|
214
|
+
// git prints one value per line (an empty value = an empty line); our desired list is
|
|
215
|
+
// ["", helperValue] → "\n<helperValue>". Already correct ⇒ nothing to do.
|
|
216
|
+
if (raw !== null && raw.replace(/\n+$/, "") === `\n${helperValue}`) return false;
|
|
217
|
+
try { git(["config", "--local", "--unset-all", key]); } catch { /* none set yet */ }
|
|
218
|
+
git(["config", "--local", "--add", key, ""]); // reset: clear inherited (osxkeychain) for this host
|
|
219
|
+
git(["config", "--local", "--add", key, helperValue]); // our helper is now the sole one
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
144
223
|
/**
|
|
145
|
-
* Self-heal a LEGACY checkout:
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* best-effort (the caller decides how to handle a thrown error — this never
|
|
152
|
-
* blocks the actual command on a migration hiccup). A no-op on an
|
|
153
|
-
* already-migrated, tokenless, or non-http(s) (e.g. ssh) remote.
|
|
224
|
+
* Self-heal a LEGACY checkout, best-effort: strip any embedded token from `origin`
|
|
225
|
+
* (the pre-u10 shape whose token silently expires), then install the stable, host-scoped
|
|
226
|
+
* credential helper (see installForgeCredentialHelper) so future git ops mint fresh creds
|
|
227
|
+
* through the `~/.tot` shim — nvm-proof and un-shadowable by osxkeychain. Called at the top
|
|
228
|
+
* of every command that touches git. A no-op on an already-migrated remote; leaves a
|
|
229
|
+
* non-http(s) (e.g. ssh) remote entirely alone. Never blocks the command on a hiccup.
|
|
154
230
|
* @param {(cargs:string[])=>string} git a `git -C <workspace>` runner
|
|
231
|
+
* @param {{ env?: NodeJS.ProcessEnv, nodePath?: string, entryPath?: string, writeShim?: typeof writeForgeShim }} [opts]
|
|
155
232
|
* @returns {{ migrated: boolean }}
|
|
156
233
|
*/
|
|
157
|
-
export function ensureTokenlessRemote(git) {
|
|
234
|
+
export function ensureTokenlessRemote(git, opts = {}) {
|
|
158
235
|
let remote;
|
|
159
236
|
try {
|
|
160
237
|
remote = git(["remote", "get-url", "origin"]).trim();
|
|
@@ -162,8 +239,10 @@ export function ensureTokenlessRemote(git) {
|
|
|
162
239
|
return { migrated: false }; // no `origin` (or not a git repo) — nothing to migrate
|
|
163
240
|
}
|
|
164
241
|
let migrated = false;
|
|
242
|
+
let host = "";
|
|
165
243
|
try {
|
|
166
244
|
const u = new URL(remote);
|
|
245
|
+
host = u.host;
|
|
167
246
|
if (u.password) {
|
|
168
247
|
git(["remote", "set-url", "origin", `${u.protocol}//${u.host}${u.pathname}`]);
|
|
169
248
|
migrated = true;
|
|
@@ -171,15 +250,6 @@ export function ensureTokenlessRemote(git) {
|
|
|
171
250
|
} catch {
|
|
172
251
|
return { migrated }; // not a parseable URL (e.g. an ssh remote) — leave it alone entirely
|
|
173
252
|
}
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
helper = git(["config", "--local", "--get", "credential.helper"]).trim();
|
|
177
|
-
} catch {
|
|
178
|
-
/* unset — falls through to configuring it below */
|
|
179
|
-
}
|
|
180
|
-
if (helper !== CREDENTIAL_HELPER) {
|
|
181
|
-
git(["config", "--local", "credential.helper", CREDENTIAL_HELPER]);
|
|
182
|
-
migrated = true;
|
|
183
|
-
}
|
|
253
|
+
if (installForgeCredentialHelper(git, { ...opts, host })) migrated = true;
|
|
184
254
|
return { migrated };
|
|
185
255
|
}
|