@minnovate/shadow-publisher 0.10.2 → 0.11.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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-publisher",
|
|
3
3
|
"displayName": "Git Publisher",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"description": "Publish an app folder's Git history to the serverless Git publishing backend and keep it updated (\"publish my app\", \"update my app\", \"fetch the engineered version\").",
|
|
6
6
|
"author": { "name": "git-publisher project" },
|
|
7
7
|
"keywords": ["git", "publish", "deploy", "serverless"]
|
|
@@ -10,6 +10,9 @@ You publish the user's app folder with the `publisher` command. All saving, merg
|
|
|
10
10
|
retry and resume logic lives in it; your job is to run it, read its `--json` output, and tell the
|
|
11
11
|
user what happened in plain language. Never explain S3, packs, commits, sessions or tokens.
|
|
12
12
|
|
|
13
|
+
**When the user asks for Shadow or the publisher, use it at once.** Do not look at other ways to
|
|
14
|
+
host or share the app first.
|
|
15
|
+
|
|
13
16
|
**`publisher sync` is how a change is saved and published. Never run `git add`, `git commit`,
|
|
14
17
|
`git push` or `git stash` in the user's app folder**, and never add a git remote. `sync` records the
|
|
15
18
|
folder's changes itself — and leaves out `.env` files, keys, `node_modules` and anything larger than
|
|
@@ -25,10 +28,25 @@ Every command accepts `--json` and prints exactly one JSON object on stdout. Exi
|
|
|
25
28
|
publisher --version
|
|
26
29
|
```
|
|
27
30
|
|
|
28
|
-
If the command is not found,
|
|
29
|
-
Node.js 22). In a Claude Cowork task
|
|
30
|
-
|
|
31
|
-
publisher. If `npm` itself is missing,
|
|
31
|
+
If the command is not found, or the version is older than 0.11.0, install it:
|
|
32
|
+
`npm install -g @minnovate/shadow-publisher@latest` (it needs Node.js 22). In a Claude Cowork task
|
|
33
|
+
this is normal: the sandbox starts empty every time, so install it without asking. On the person's
|
|
34
|
+
own computer, say in one line that you are installing the publisher. If `npm` itself is missing,
|
|
35
|
+
tell them Node.js 22 must be installed first, and stop.
|
|
36
|
+
|
|
37
|
+
### In a sandbox: keep the sign-in where the next command finds it
|
|
38
|
+
|
|
39
|
+
A sandbox (Claude Cowork, Claude Code on the web, a chat's code tool) can forget every file outside
|
|
40
|
+
its working folder between two commands. A sign-in kept in `/root/.config` is then gone at the
|
|
41
|
+
next command. So in a sandbox, do this from the start:
|
|
42
|
+
|
|
43
|
+
- Put `GIT_PUBLISHER_CREDENTIALS=<working folder>/.shadow-publisher/credentials.json` in front of
|
|
44
|
+
**every** `publisher` command. Use the absolute path of the folder the task works in. Never use
|
|
45
|
+
`/root` or `/tmp`, and never a path inside the app folder.
|
|
46
|
+
- Do nothing about `TMPDIR`. From 0.11.0 the publisher uses `/tmp` when `TMPDIR` names a folder that
|
|
47
|
+
does not exist, and says so in one line.
|
|
48
|
+
|
|
49
|
+
On the person's own computer, use neither: the default place is kept.
|
|
32
50
|
|
|
33
51
|
```bash
|
|
34
52
|
publisher doctor --json
|
|
@@ -49,25 +67,30 @@ publisher doctor --json
|
|
|
49
67
|
|
|
50
68
|
### Signing in
|
|
51
69
|
|
|
52
|
-
`publisher login` waits until the
|
|
53
|
-
|
|
54
|
-
its output going to a file, and read the file:
|
|
70
|
+
`publisher login` waits until the code it prints is approved, and your shell may show nothing until
|
|
71
|
+
a command ends. So sign in with two short commands instead of one that waits:
|
|
55
72
|
|
|
56
73
|
```bash
|
|
57
|
-
publisher login --
|
|
74
|
+
publisher login --no-wait --json
|
|
58
75
|
```
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
It prints one JSON object with `userCode` and `verificationUri`, and `next.command`. Then:
|
|
78
|
+
|
|
79
|
+
1. **If a tool named `approve_publisher_sign_in` is available** (the Shadow connector, which the
|
|
80
|
+
person connected in Claude's settings), call it with `userCode`. Do not give them the link.
|
|
81
|
+
If the tool answers that it did not approve, do step 2 instead.
|
|
82
|
+
2. Otherwise give the person `verificationUri` as a link they can click, and `userCode`, and say
|
|
83
|
+
the page will ask them to sign in with their work account.
|
|
84
|
+
3. Run `next.command` — it is `publisher login --continue --json`. It returns `ok: true` within
|
|
85
|
+
seconds of the approval. If it returns `state: "pending"`, the code is not approved yet: wait
|
|
86
|
+
for the person, then run it again. It refuses after ten minutes; then start again.
|
|
87
|
+
4. **Check that the sign-in was kept before you publish**: run `publisher doctor --json` as a NEW
|
|
88
|
+
command. `next.action` must be `"ready"`. If it is not, or a command says the sign-in or the
|
|
89
|
+
waiting sign-in was not kept (its message names the file it looked for), this environment
|
|
90
|
+
forgot the file: keep it in another folder, as the section above says, and sign in again.
|
|
65
91
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
from the log, then keep reading the log as above — it reaches `ok: true` within a few seconds, with
|
|
69
|
-
nothing for the person to do. If the tool answers that it did not approve, give them the link and
|
|
70
|
-
code as above. The tool never returns a credential, and you never need one.
|
|
92
|
+
In a Claude Cowork task this happens again in every new task, because the sandbox forgets the
|
|
93
|
+
sign-in when the task ends. The tool never returns a credential, and you never need one.
|
|
71
94
|
|
|
72
95
|
**Never ask the user for a token, an endpoint, a URL or a credential — in any words.** They are
|
|
73
96
|
publishing an app; they do not know what those are, and you do not need them: `login` discovers
|
|
@@ -96,7 +119,10 @@ publisher sync . --message "<a few words on what changed>" --until-change --time
|
|
|
96
119
|
```
|
|
97
120
|
|
|
98
121
|
That one command records what changed in the folder, brings in anything published meanwhile (a
|
|
99
|
-
fast-forward or a clean merge — it never rewrites the user's work), and publishes the result.
|
|
122
|
+
fast-forward or a clean merge — it never rewrites the user's work), and publishes the result. Its
|
|
123
|
+
`timings` say how long each stage took (milliseconds): `discovery`, `authentication`, `link`,
|
|
124
|
+
`prepare`, `upload`, and — once the app is followed — `received`, `checks` and `deployment`. Quote
|
|
125
|
+
them when the person asks why it was slow. Write
|
|
100
126
|
the message in the user's words ("Add a contact page"), not git's.
|
|
101
127
|
|
|
102
128
|
A published app runs with no internet access: **it may not load anything from other sites** — no
|
package/dist/publisher.cjs
CHANGED
|
@@ -28,7 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
));
|
|
29
29
|
|
|
30
30
|
// src/main.ts
|
|
31
|
-
var
|
|
31
|
+
var import_node_path9 = require("node:path");
|
|
32
32
|
|
|
33
33
|
// ../protocol/src/versions.ts
|
|
34
34
|
var PROTOCOL_VERSION = 1;
|
|
@@ -2957,6 +2957,7 @@ var MAX_WAIT_SECONDS = 5;
|
|
|
2957
2957
|
var import_node_child_process = require("node:child_process");
|
|
2958
2958
|
var import_node_os = require("node:os");
|
|
2959
2959
|
var import_promises = require("node:fs/promises");
|
|
2960
|
+
var import_node_fs = require("node:fs");
|
|
2960
2961
|
var import_node_path = require("node:path");
|
|
2961
2962
|
var import_node_util = require("node:util");
|
|
2962
2963
|
var import_node_child_process2 = require("node:child_process");
|
|
@@ -2991,6 +2992,23 @@ var GIT_BIN = process.env["GITPUB_GIT_BIN"] ?? "git";
|
|
|
2991
2992
|
function neutralCwd() {
|
|
2992
2993
|
return (0, import_node_os.tmpdir)();
|
|
2993
2994
|
}
|
|
2995
|
+
function startFailure(label, error2, cwd) {
|
|
2996
|
+
const opts = { retryable: true, cause: error2 };
|
|
2997
|
+
if (error2.code !== "ENOENT") return new PublisherError("UNAVAILABLE", `${label} could not be started: ${error2.message}`, opts);
|
|
2998
|
+
if (!isDirectory(cwd)) {
|
|
2999
|
+
const which = cwd === (0, import_node_os.tmpdir)() ? "the temporary directory (TMPDIR)" : "its working directory";
|
|
3000
|
+
return new PublisherError("UNAVAILABLE", `${label} could not be started: ${which} ${cwd} does not exist`, { ...opts, details: { missing: "cwd", cwd } });
|
|
3001
|
+
}
|
|
3002
|
+
const where = GIT_BIN.includes("/") ? `at ${GIT_BIN}` : "on PATH";
|
|
3003
|
+
return new PublisherError("UNAVAILABLE", `${label} could not be started: git was not found ${where}`, { ...opts, details: { missing: "git", git: GIT_BIN } });
|
|
3004
|
+
}
|
|
3005
|
+
function isDirectory(path) {
|
|
3006
|
+
try {
|
|
3007
|
+
return (0, import_node_fs.statSync)(path).isDirectory();
|
|
3008
|
+
} catch {
|
|
3009
|
+
return false;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
2994
3012
|
var gitStats = { invocations: 0, totalMs: 0 };
|
|
2995
3013
|
var liveChildren = /* @__PURE__ */ new Set();
|
|
2996
3014
|
var closedChildren = /* @__PURE__ */ new WeakSet();
|
|
@@ -3158,11 +3176,12 @@ function spawnTracked(args, opts) {
|
|
|
3158
3176
|
if (typeof a !== "string") throw new Error("git args must be strings");
|
|
3159
3177
|
if (a.includes("\0")) throw new PublisherError("INVALID_REQUEST", "NUL in git argument");
|
|
3160
3178
|
}
|
|
3161
|
-
const
|
|
3179
|
+
const cwd = opts.cwd ?? neutralCwd();
|
|
3180
|
+
const child = spawnImpl(GIT_BIN, args, { cwd, env: sanitizedGitEnv(opts.env), stdio: ["pipe", "pipe", "pipe"], windowsHide: true, detached: true });
|
|
3162
3181
|
gitStats.invocations++;
|
|
3163
3182
|
const cleanupBoundMs = opts.cleanupBoundMs ?? CLEANUP_BOUND_MS;
|
|
3164
3183
|
track(child, cleanupBoundMs);
|
|
3165
|
-
return { child, cleanupBoundMs };
|
|
3184
|
+
return { child, cleanupBoundMs, cwd };
|
|
3166
3185
|
}
|
|
3167
3186
|
function feedStdin(child, input, onSourceError) {
|
|
3168
3187
|
let unpipe = () => void 0;
|
|
@@ -3211,7 +3230,7 @@ async function runGit(args, opts = {}) {
|
|
|
3211
3230
|
const started = Date.now();
|
|
3212
3231
|
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
3213
3232
|
const maxOut = opts.maxStdoutBytes ?? 64 * 1024 * 1024;
|
|
3214
|
-
const { child, cleanupBoundMs } = spawnTracked(args, opts);
|
|
3233
|
+
const { child, cleanupBoundMs, cwd } = spawnTracked(args, opts);
|
|
3215
3234
|
child.once("exit", () => armCloseBound(child, cleanupBoundMs));
|
|
3216
3235
|
const stdoutChunks = [];
|
|
3217
3236
|
let outBytes = 0;
|
|
@@ -3261,7 +3280,7 @@ async function runGit(args, opts = {}) {
|
|
|
3261
3280
|
const result = { code, stdout: Buffer.concat(stdoutChunks), stderr, durationMs };
|
|
3262
3281
|
const cleanup = await reapChild(child, { boundMs: cleanupBoundMs, ...opts.groupObserver ? { observer: opts.groupObserver } : {} });
|
|
3263
3282
|
if (!reapSucceeded(cleanup)) throw cleanupFailure(`git ${args[0]}`, cleanup, killedReason ?? void 0);
|
|
3264
|
-
if (ended.error) throw
|
|
3283
|
+
if (ended.error) throw startFailure(`git ${args[0]}`, ended.error, cwd);
|
|
3265
3284
|
if (killedReason) throw killedError(killedReason, killedDetail, args, timeoutMs, maxOut);
|
|
3266
3285
|
if (code !== 0 && !opts.allowFailure) throw new GitCommandError(args, code, stderr, opts.label);
|
|
3267
3286
|
return result;
|
|
@@ -3269,7 +3288,7 @@ async function runGit(args, opts = {}) {
|
|
|
3269
3288
|
function startWatchdog(child, opts, kill, isClosed) {
|
|
3270
3289
|
if (opts.maxMemoryBytes === void 0 && opts.maxDiskBytes === void 0) return { stop: () => void 0, done: Promise.resolve() };
|
|
3271
3290
|
const sampler = opts.sampler ?? defaultResourceSampler;
|
|
3272
|
-
const
|
|
3291
|
+
const interval2 = Math.max(10, opts.sampleIntervalMs ?? 500);
|
|
3273
3292
|
const sampleBoundMs = opts.cleanupBoundMs ?? CLEANUP_BOUND_MS;
|
|
3274
3293
|
const pgid = child.pid;
|
|
3275
3294
|
let stopped = false;
|
|
@@ -3299,7 +3318,7 @@ function startWatchdog(child, opts, kill, isClosed) {
|
|
|
3299
3318
|
}
|
|
3300
3319
|
let tick = 0;
|
|
3301
3320
|
while (!stopped) {
|
|
3302
|
-
await sleep2(
|
|
3321
|
+
await sleep2(interval2);
|
|
3303
3322
|
if (stopped) return;
|
|
3304
3323
|
if (opts.maxMemoryBytes !== void 0) {
|
|
3305
3324
|
const rss = await sample("memory", (signal) => sampler.groupRss(pgid, signal));
|
|
@@ -3334,8 +3353,8 @@ async function scanProcessGroup(pgid, signal) {
|
|
|
3334
3353
|
if (signal?.aborted) throw new Error("process-group scan abandoned by its caller");
|
|
3335
3354
|
if (!/^\d+$/.test(name)) continue;
|
|
3336
3355
|
try {
|
|
3337
|
-
const
|
|
3338
|
-
const rest =
|
|
3356
|
+
const stat4 = await (0, import_promises.readFile)(`/proc/${name}/stat`, "utf8");
|
|
3357
|
+
const rest = stat4.slice(stat4.lastIndexOf(")") + 2).split(" ");
|
|
3339
3358
|
if (Number(rest[2]) !== pgid) continue;
|
|
3340
3359
|
const status = await (0, import_promises.readFile)(`/proc/${name}/status`, "utf8");
|
|
3341
3360
|
members2.push({ pid: Number(name), rss: Number(/^VmRSS:\s+(\d+)/m.exec(status)?.[1] ?? 0) * 1024 });
|
|
@@ -3524,13 +3543,13 @@ function compareVersions(a, b) {
|
|
|
3524
3543
|
|
|
3525
3544
|
// ../git-core/src/hash.ts
|
|
3526
3545
|
var import_node_crypto2 = require("node:crypto");
|
|
3527
|
-
var
|
|
3546
|
+
var import_node_fs2 = require("node:fs");
|
|
3528
3547
|
var import_promises2 = require("node:fs/promises");
|
|
3529
3548
|
async function sha256File(path) {
|
|
3530
3549
|
const h = (0, import_node_crypto2.createHash)("sha256");
|
|
3531
3550
|
let size = 0;
|
|
3532
3551
|
await new Promise((resolve3, reject) => {
|
|
3533
|
-
(0,
|
|
3552
|
+
(0, import_node_fs2.createReadStream)(path).on("data", (c) => {
|
|
3534
3553
|
const b = typeof c === "string" ? Buffer.from(c) : c;
|
|
3535
3554
|
size += b.length;
|
|
3536
3555
|
h.update(b);
|
|
@@ -3542,12 +3561,12 @@ async function sha256File(path) {
|
|
|
3542
3561
|
}
|
|
3543
3562
|
|
|
3544
3563
|
// ../git-core/src/bare-repo.ts
|
|
3545
|
-
var
|
|
3564
|
+
var import_node_fs3 = require("node:fs");
|
|
3546
3565
|
async function indexPackFromFile(dir, packPath, opts = {}) {
|
|
3547
3566
|
const args = ["index-pack", "--stdin", ...opts.keep ?? true ? ["--keep=gitpub"] : []];
|
|
3548
3567
|
if (opts.strict ?? true) args.push("--strict", "--fsck-objects");
|
|
3549
3568
|
if (opts.maxInputBytes) args.push(`--max-input-size=${opts.maxInputBytes}`);
|
|
3550
|
-
const run = { cwd: dir, input: (0,
|
|
3569
|
+
const run = { cwd: dir, input: (0, import_node_fs3.createReadStream)(packPath), label: "index-pack", ...opts };
|
|
3551
3570
|
const res = await runGit(args, { ...run, allowFailure: true });
|
|
3552
3571
|
if (res.code !== 0) {
|
|
3553
3572
|
const msg = res.stderr.trim().split("\n").pop() ?? "index-pack failed";
|
|
@@ -3614,14 +3633,14 @@ function revParse(dir, rev) {
|
|
|
3614
3633
|
}
|
|
3615
3634
|
|
|
3616
3635
|
// ../git-core/src/pack.ts
|
|
3617
|
-
var
|
|
3636
|
+
var import_node_fs4 = require("node:fs");
|
|
3618
3637
|
async function buildPack(repoDir, opts) {
|
|
3619
3638
|
return packObjectsToFile(repoDir, ["--revs"], [...opts.include, ...opts.exclude.map((e) => `^${e}`)], opts);
|
|
3620
3639
|
}
|
|
3621
3640
|
async function packObjectsToFile(repoDir, extraArgs, stdinLines, opts) {
|
|
3622
3641
|
const args = ["pack-objects", ...extraArgs, "--stdout", "-q", "--delta-base-offset"];
|
|
3623
3642
|
const child = spawnGit(args, { cwd: repoDir, ...opts.cleanupBoundMs !== void 0 ? { cleanupBoundMs: opts.cleanupBoundMs } : {} });
|
|
3624
|
-
const out = (0,
|
|
3643
|
+
const out = (0, import_node_fs4.createWriteStream)(opts.outPath);
|
|
3625
3644
|
let stderr = "";
|
|
3626
3645
|
child.stderr.on("data", (c) => stderr += c.toString());
|
|
3627
3646
|
const timeoutMs = opts.timeoutMs ?? 6e5;
|
|
@@ -3652,7 +3671,7 @@ async function packObjectsToFile(repoDir, extraArgs, stdinLines, opts) {
|
|
|
3652
3671
|
}
|
|
3653
3672
|
}
|
|
3654
3673
|
const code = ended.code;
|
|
3655
|
-
if (ended.error) throw
|
|
3674
|
+
if (ended.error) throw startFailure("git pack-objects", ended.error, repoDir);
|
|
3656
3675
|
if (opts.signal?.aborted) throw new PublisherError("CANCELLED", "pack creation cancelled");
|
|
3657
3676
|
if (timedOut) throw new PublisherError("GIT", `pack-objects timed out after ${timeoutMs}ms`, { retryable: true });
|
|
3658
3677
|
if (code !== 0) throw new PublisherError("GIT", `pack-objects failed: ${stderr.trim().split("\n").pop() ?? ""}`);
|
|
@@ -4255,9 +4274,43 @@ async function postForm(base, path, fields, fetchImpl = globalThis.fetch) {
|
|
|
4255
4274
|
// ../client/src/credentials.ts
|
|
4256
4275
|
var REFRESH_EARLY_MS = 6e4;
|
|
4257
4276
|
function credentialsPath() {
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4277
|
+
return credentialsLocation().path;
|
|
4278
|
+
}
|
|
4279
|
+
function credentialsLocation() {
|
|
4280
|
+
const explicit = process.env["GIT_PUBLISHER_CREDENTIALS"];
|
|
4281
|
+
if (explicit) return { path: explicit, from: "GIT_PUBLISHER_CREDENTIALS" };
|
|
4282
|
+
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
4283
|
+
return { path: (0, import_node_path4.join)(xdg ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".config"), "git-publisher", "credentials.json"), from: xdg ? "XDG_CONFIG_HOME" : "HOME" };
|
|
4284
|
+
}
|
|
4285
|
+
async function credentialsFileState() {
|
|
4286
|
+
let raw;
|
|
4287
|
+
try {
|
|
4288
|
+
raw = await (0, import_promises5.readFile)(credentialsPath(), "utf8");
|
|
4289
|
+
} catch (e) {
|
|
4290
|
+
return e.code === "ENOENT" ? "absent" : "unreadable";
|
|
4291
|
+
}
|
|
4292
|
+
try {
|
|
4293
|
+
JSON.parse(raw);
|
|
4294
|
+
return "present";
|
|
4295
|
+
} catch {
|
|
4296
|
+
return "unparseable";
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
async function whereCredentialsWere() {
|
|
4300
|
+
const { path, from } = credentialsLocation();
|
|
4301
|
+
const state = await credentialsFileState();
|
|
4302
|
+
const set = from === "HOME" ? "the default under HOME" : `set by ${from}`;
|
|
4303
|
+
const kept = state === "absent" ? ` ${notKept('"publisher login" answered ok', "sign in again")}` : "";
|
|
4304
|
+
return { sentence: `${WHAT_IS_THERE[state]} ${path} (${set}).${kept}`, details: { path, from, state } };
|
|
4305
|
+
}
|
|
4306
|
+
var WHAT_IS_THERE = {
|
|
4307
|
+
absent: "there is no credentials file at",
|
|
4308
|
+
present: "no connection is saved in",
|
|
4309
|
+
unreadable: "the credentials file cannot be read at",
|
|
4310
|
+
unparseable: "the credentials file cannot be parsed at"
|
|
4311
|
+
};
|
|
4312
|
+
function notKept(earlier, then) {
|
|
4313
|
+
return `If ${earlier} in an earlier command, this environment did not keep that file: set GIT_PUBLISHER_CREDENTIALS to a file in a folder that is kept between commands, outside the app folder, and ${then}.`;
|
|
4261
4314
|
}
|
|
4262
4315
|
function normalize(endpoint) {
|
|
4263
4316
|
return endpoint.replace(/\/+$/, "");
|
|
@@ -4305,11 +4358,13 @@ async function readCredentials() {
|
|
|
4305
4358
|
}
|
|
4306
4359
|
var writes = 0;
|
|
4307
4360
|
async function writeCredentials(file) {
|
|
4308
|
-
|
|
4361
|
+
return writeSecretJson(credentialsPath(), file);
|
|
4362
|
+
}
|
|
4363
|
+
async function writeSecretJson(path, value) {
|
|
4309
4364
|
await (0, import_promises5.mkdir)((0, import_node_path4.dirname)(path), { recursive: true, mode: 448 });
|
|
4310
4365
|
const tmp = `${path}.${process.pid}.${++writes}.tmp`;
|
|
4311
4366
|
try {
|
|
4312
|
-
await (0, import_promises5.writeFile)(tmp, JSON.stringify(
|
|
4367
|
+
await (0, import_promises5.writeFile)(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
4313
4368
|
await (0, import_promises5.chmod)(tmp, 384);
|
|
4314
4369
|
await (0, import_promises5.rename)(tmp, path);
|
|
4315
4370
|
} catch (e) {
|
|
@@ -4458,7 +4513,12 @@ async function resolveToken(endpoint, explicit, opts = {}) {
|
|
|
4458
4513
|
if (given) return given;
|
|
4459
4514
|
const url2 = normalize(endpoint);
|
|
4460
4515
|
const entry = (await readCredentials()).endpoints[url2];
|
|
4461
|
-
if (!entry)
|
|
4516
|
+
if (!entry) {
|
|
4517
|
+
const where = await whereCredentialsWere();
|
|
4518
|
+
throw new PublisherError("UNAUTHORIZED", `no credentials for ${endpoint}: ${where.sentence} Run "publisher login" or set GIT_PUBLISHER_TOKEN.`, {
|
|
4519
|
+
details: { signIn: "missing", credentialsFile: where.details }
|
|
4520
|
+
});
|
|
4521
|
+
}
|
|
4462
4522
|
const now = (opts.now ?? Date.now)();
|
|
4463
4523
|
const lapsed = entry.expiresAt !== void 0 && Date.parse(entry.expiresAt) - REFRESH_EARLY_MS <= now;
|
|
4464
4524
|
if (entry.refreshToken && entry.authEndpoint && lapsed) return refreshCredential(url2, entry, opts);
|
|
@@ -4712,7 +4772,7 @@ async function followRun(input, opts) {
|
|
|
4712
4772
|
const fetchImpl = opts.fetch ?? fetch;
|
|
4713
4773
|
const sleep2 = opts.sleep ?? sleep;
|
|
4714
4774
|
const now = opts.now ?? Date.now;
|
|
4715
|
-
const
|
|
4775
|
+
const interval2 = opts.intervalMs ?? POLL_INTERVAL_MS;
|
|
4716
4776
|
const deadline = now() + (opts.timeoutMs ?? FOLLOW_TIMEOUT_MS);
|
|
4717
4777
|
const query = new URLSearchParams({ publication: input.publicationId, ref: input.ref, commit: input.commit });
|
|
4718
4778
|
const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/repositories/${encodeURIComponent(input.repoId)}/runs?${query}`;
|
|
@@ -4758,8 +4818,8 @@ async function followRun(input, opts) {
|
|
|
4758
4818
|
} else {
|
|
4759
4819
|
lastError = answer.status === 0 ? "the portal could not be reached" : `the portal answered ${answer.status}`;
|
|
4760
4820
|
}
|
|
4761
|
-
if (now() +
|
|
4762
|
-
await sleep2(
|
|
4821
|
+
if (now() + interval2 > deadline) return { kind: "timed-out", stages: last, lastError };
|
|
4822
|
+
await sleep2(interval2);
|
|
4763
4823
|
}
|
|
4764
4824
|
}
|
|
4765
4825
|
function soleRun(body) {
|
|
@@ -5104,7 +5164,7 @@ async function checkIgnore(dir, paths) {
|
|
|
5104
5164
|
}
|
|
5105
5165
|
|
|
5106
5166
|
// ../client/src/fetch.ts
|
|
5107
|
-
var
|
|
5167
|
+
var import_node_fs6 = require("node:fs");
|
|
5108
5168
|
var import_promises8 = require("node:fs/promises");
|
|
5109
5169
|
var import_node_os3 = require("node:os");
|
|
5110
5170
|
var import_node_path6 = require("node:path");
|
|
@@ -5116,12 +5176,12 @@ var import_node_crypto4 = require("node:crypto");
|
|
|
5116
5176
|
var import_promises7 = require("node:fs/promises");
|
|
5117
5177
|
|
|
5118
5178
|
// ../client/src/upload.ts
|
|
5119
|
-
var
|
|
5179
|
+
var import_node_fs5 = require("node:fs");
|
|
5120
5180
|
var import_node_stream = require("node:stream");
|
|
5121
5181
|
async function uploadPack(fetchImpl, upload, packPath, size, opts) {
|
|
5122
5182
|
if (new Date(upload.expiresAt).getTime() < Date.now() + 5e3) return "expired";
|
|
5123
5183
|
let sent = 0;
|
|
5124
|
-
const file = (0,
|
|
5184
|
+
const file = (0, import_node_fs5.createReadStream)(packPath);
|
|
5125
5185
|
const progress = new TransformStream({
|
|
5126
5186
|
transform(chunk, controller) {
|
|
5127
5187
|
sent += chunk.byteLength;
|
|
@@ -5526,7 +5586,7 @@ async function download(fetchImpl, url2, dest, sha256, size, call2) {
|
|
|
5526
5586
|
controller.enqueue(chunk);
|
|
5527
5587
|
}
|
|
5528
5588
|
});
|
|
5529
|
-
await (0, import_promises9.pipeline)(import_node_stream2.Readable.fromWeb(res.body.pipeThrough(counted)), (0,
|
|
5589
|
+
await (0, import_promises9.pipeline)(import_node_stream2.Readable.fromWeb(res.body.pipeThrough(counted)), (0, import_node_fs6.createWriteStream)(dest));
|
|
5530
5590
|
if (n !== size || hash.digest("hex") !== sha256) throw new PublisherError("NETWORK", "downloaded artifact failed checksum verification", { retryable: true });
|
|
5531
5591
|
return n;
|
|
5532
5592
|
}
|
|
@@ -5829,10 +5889,8 @@ async function migrateAssociation(opts) {
|
|
|
5829
5889
|
var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
5830
5890
|
var str = (v) => typeof v === "string" && v.length > 0 ? v : null;
|
|
5831
5891
|
var num = (v, fallback) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : fallback;
|
|
5832
|
-
async function
|
|
5892
|
+
async function startDeviceLogin(opts) {
|
|
5833
5893
|
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
5834
|
-
const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5835
|
-
const now = opts.now ?? Date.now;
|
|
5836
5894
|
const control = normalize(opts.endpoint);
|
|
5837
5895
|
let advertised;
|
|
5838
5896
|
try {
|
|
@@ -5843,8 +5901,7 @@ async function deviceLogin(opts) {
|
|
|
5843
5901
|
if (typeof advertised !== "string" || advertised === "") throw new PublisherError("UNAVAILABLE", "this deployment does not offer device login; obtain a token another way");
|
|
5844
5902
|
const base = normalize(advertised);
|
|
5845
5903
|
assertCredentialEndpoint(base);
|
|
5846
|
-
const
|
|
5847
|
-
const started = await post2("/v1/auth/device", { client_id: opts.clientId ?? "gitpub-cli" });
|
|
5904
|
+
const started = await postForm(base, "/v1/auth/device", { client_id: opts.clientId ?? "gitpub-cli" }, doFetch);
|
|
5848
5905
|
if (started.status !== 200) throw new PublisherError("UNAVAILABLE", `starting the login failed (${started.status})`, { retryable: true });
|
|
5849
5906
|
const s = started.body;
|
|
5850
5907
|
const deviceCode = str(s.device_code);
|
|
@@ -5852,17 +5909,33 @@ async function deviceLogin(opts) {
|
|
|
5852
5909
|
const uri = str(s.verification_uri_complete) ?? str(s.verification_uri);
|
|
5853
5910
|
if (!deviceCode || !userCode || !uri) throw new PublisherError("UNAVAILABLE", "the login response was incomplete");
|
|
5854
5911
|
const expiresInSeconds = num(s.expires_in, 600);
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5912
|
+
return {
|
|
5913
|
+
deviceCode,
|
|
5914
|
+
userCode,
|
|
5915
|
+
verificationUri: uri,
|
|
5916
|
+
expiresInSeconds,
|
|
5917
|
+
intervalSeconds: num(s.interval, 5),
|
|
5918
|
+
authEndpoint: base,
|
|
5919
|
+
expiresAt: (opts.now ?? Date.now)() + expiresInSeconds * 1e3
|
|
5920
|
+
};
|
|
5921
|
+
}
|
|
5922
|
+
async function pollDeviceLogin(started, opts) {
|
|
5923
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
5924
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5925
|
+
const now = opts.now ?? Date.now;
|
|
5926
|
+
let intervalMs = started.intervalSeconds * 1e3;
|
|
5927
|
+
let last = opts.lastPolledAt;
|
|
5858
5928
|
for (; ; ) {
|
|
5859
|
-
|
|
5860
|
-
if (
|
|
5861
|
-
|
|
5929
|
+
const at = Math.max(now(), last === null ? 0 : last + intervalMs);
|
|
5930
|
+
if (opts.until !== void 0 && at > opts.until) return null;
|
|
5931
|
+
if (at > now()) await sleep2(at - now());
|
|
5932
|
+
if (now() >= started.expiresAt) throw new PublisherError("EXPIRED", "the login expired before it was approved");
|
|
5933
|
+
const polled = await postForm(started.authEndpoint, "/v1/auth/token", { grant_type: DEVICE_CODE_GRANT, device_code: started.deviceCode }, doFetch);
|
|
5934
|
+
last = now();
|
|
5862
5935
|
if (polled.status === 200) {
|
|
5863
5936
|
const token = str(polled.body["access_token"]);
|
|
5864
5937
|
if (!token) throw new PublisherError("UNAVAILABLE", "the login returned no credential");
|
|
5865
|
-
return { token, refreshToken: str(polled.body["refresh_token"]), expiresInSeconds: num(polled.body["expires_in"], 900), authEndpoint:
|
|
5938
|
+
return { token, refreshToken: str(polled.body["refresh_token"]), expiresInSeconds: num(polled.body["expires_in"], 900), authEndpoint: started.authEndpoint };
|
|
5866
5939
|
}
|
|
5867
5940
|
const error2 = str(polled.body["error"]);
|
|
5868
5941
|
if (error2 === "authorization_pending") continue;
|
|
@@ -5876,33 +5949,44 @@ async function deviceLogin(opts) {
|
|
|
5876
5949
|
throw new PublisherError("UNAVAILABLE", `the login failed (${polled.status}${error2 ? `: ${error2}` : ""})`);
|
|
5877
5950
|
}
|
|
5878
5951
|
}
|
|
5952
|
+
async function deviceLogin(opts) {
|
|
5953
|
+
const started = await startDeviceLogin(opts);
|
|
5954
|
+
const { deviceCode: _secret, authEndpoint: _auth, expiresAt: _at, ...shown2 } = started;
|
|
5955
|
+
await opts.onPrompt(shown2);
|
|
5956
|
+
const signIn = await pollDeviceLogin(started, { ...opts, lastPolledAt: (opts.now ?? Date.now)() });
|
|
5957
|
+
return signIn;
|
|
5958
|
+
}
|
|
5879
5959
|
|
|
5880
|
-
// src/
|
|
5881
|
-
var
|
|
5882
|
-
|
|
5883
|
-
|
|
5960
|
+
// ../client/src/pending-login.ts
|
|
5961
|
+
var import_promises10 = require("node:fs/promises");
|
|
5962
|
+
var import_node_path7 = require("node:path");
|
|
5963
|
+
function pendingLoginPath() {
|
|
5964
|
+
return (0, import_node_path7.join)((0, import_node_path7.dirname)(credentialsPath()), "login-pending.json");
|
|
5884
5965
|
}
|
|
5885
|
-
async function
|
|
5886
|
-
|
|
5887
|
-
|
|
5966
|
+
async function savePendingLogin(pending) {
|
|
5967
|
+
return writeSecretJson(pendingLoginPath(), { version: 1, ...pending });
|
|
5968
|
+
}
|
|
5969
|
+
async function readPendingLogin() {
|
|
5970
|
+
let raw;
|
|
5888
5971
|
try {
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
}
|
|
5893
|
-
body = await response.json();
|
|
5894
|
-
} catch (e) {
|
|
5895
|
-
if (PublisherError.isPublisherError(e)) throw e;
|
|
5896
|
-
throw new PublisherError("NETWORK", `could not reach ${portal}: ${e instanceof Error ? e.message : String(e)}`, { retryable: true });
|
|
5897
|
-
}
|
|
5898
|
-
const endpoint = body?.publishingEndpoint;
|
|
5899
|
-
if (endpoint === null || endpoint === void 0) {
|
|
5900
|
-
throw new PublisherError("LOCAL_STATE", `${portal} is not set up for publishing; ask whoever runs it, or pass --endpoint`);
|
|
5901
|
-
}
|
|
5902
|
-
if (typeof endpoint !== "string" || endpoint.trim() === "") {
|
|
5903
|
-
throw new PublisherError("UNAVAILABLE", `${portal} answered with something this version does not understand`, { retryable: false });
|
|
5972
|
+
raw = JSON.parse(await (0, import_promises10.readFile)(pendingLoginPath(), "utf8"));
|
|
5973
|
+
} catch {
|
|
5974
|
+
return null;
|
|
5904
5975
|
}
|
|
5905
|
-
return
|
|
5976
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
5977
|
+
const r = raw;
|
|
5978
|
+
const s = r.started;
|
|
5979
|
+
if (r.version !== 1 || typeof r.endpoint !== "string" || !s) return null;
|
|
5980
|
+
if (typeof s.deviceCode !== "string" || typeof s.authEndpoint !== "string" || typeof s.expiresAt !== "number" || typeof s.intervalSeconds !== "number") return null;
|
|
5981
|
+
return {
|
|
5982
|
+
endpoint: r.endpoint,
|
|
5983
|
+
started: s,
|
|
5984
|
+
...typeof r.profile === "string" ? { profile: r.profile } : {},
|
|
5985
|
+
...r.setDefault === true ? { setDefault: true } : {}
|
|
5986
|
+
};
|
|
5987
|
+
}
|
|
5988
|
+
async function removePendingLogin() {
|
|
5989
|
+
await (0, import_promises10.rm)(pendingLoginPath(), { force: true });
|
|
5906
5990
|
}
|
|
5907
5991
|
|
|
5908
5992
|
// src/git-minimum.ts
|
|
@@ -5911,6 +5995,93 @@ function gitSupported(version2) {
|
|
|
5911
5995
|
return compareVersions(version2, CLIENT_GIT_MINIMUM) >= 0;
|
|
5912
5996
|
}
|
|
5913
5997
|
|
|
5998
|
+
// src/progress.ts
|
|
5999
|
+
var SAYS = {
|
|
6000
|
+
received: { doing: "Shadow is picking up your changes", done: "Shadow has your changes", still: "still picking up your changes" },
|
|
6001
|
+
running: { doing: "checking that everything still works", done: "everything still works", still: "still checking" },
|
|
6002
|
+
deploying: { doing: "putting the new version in place; the current one stays up meanwhile", done: "the new version is in place", still: "still putting it in place" },
|
|
6003
|
+
live: { doing: "going live", done: "everyone sees the new version", still: "still going live" }
|
|
6004
|
+
};
|
|
6005
|
+
var HEARTBEAT_MS = 3e4;
|
|
6006
|
+
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
6007
|
+
function took(ms) {
|
|
6008
|
+
const seconds = Math.max(0, Math.floor(ms / 1e3));
|
|
6009
|
+
if (seconds < 60) return `${seconds} s`;
|
|
6010
|
+
const minutes = Math.floor(seconds / 60);
|
|
6011
|
+
return minutes >= 10 ? `${minutes} min` : `${minutes} min ${String(seconds % 60).padStart(2, "0")} s`;
|
|
6012
|
+
}
|
|
6013
|
+
function interval(fn, ms) {
|
|
6014
|
+
const timer = setInterval(fn, ms);
|
|
6015
|
+
timer.unref();
|
|
6016
|
+
return { stop: () => clearInterval(timer) };
|
|
6017
|
+
}
|
|
6018
|
+
function settledLine(e, total) {
|
|
6019
|
+
const word = e.word.padEnd(10);
|
|
6020
|
+
switch (e.state) {
|
|
6021
|
+
case "done":
|
|
6022
|
+
return ` \u2713 ${word} ${SAYS[e.stage].done}${total}
|
|
6023
|
+
`;
|
|
6024
|
+
case "failed":
|
|
6025
|
+
return ` \u2717 ${word} it stopped here
|
|
6026
|
+
`;
|
|
6027
|
+
case "skipped":
|
|
6028
|
+
return ` \u2013 ${word} skipped
|
|
6029
|
+
`;
|
|
6030
|
+
default:
|
|
6031
|
+
return null;
|
|
6032
|
+
}
|
|
6033
|
+
}
|
|
6034
|
+
function stageReporter(io) {
|
|
6035
|
+
const now = io.now ?? Date.now;
|
|
6036
|
+
const every = io.every ?? interval;
|
|
6037
|
+
let ticking = null;
|
|
6038
|
+
let drawn = false;
|
|
6039
|
+
let receivedAt = null;
|
|
6040
|
+
const clear = () => {
|
|
6041
|
+
if (drawn) io.write("\r\x1B[2K");
|
|
6042
|
+
drawn = false;
|
|
6043
|
+
};
|
|
6044
|
+
const stopTicking = () => {
|
|
6045
|
+
ticking?.timer.stop();
|
|
6046
|
+
ticking = null;
|
|
6047
|
+
clear();
|
|
6048
|
+
};
|
|
6049
|
+
const since = (e) => {
|
|
6050
|
+
const at = e.at === null ? NaN : Date.parse(e.at);
|
|
6051
|
+
return Number.isFinite(at) ? Math.min(at, now()) : now();
|
|
6052
|
+
};
|
|
6053
|
+
return {
|
|
6054
|
+
onStage(e) {
|
|
6055
|
+
if (e.state === "in-progress" || ticking?.stage === e.stage) stopTicking();
|
|
6056
|
+
else clear();
|
|
6057
|
+
const says = SAYS[e.stage];
|
|
6058
|
+
if (e.stage === "received" && e.at !== null) receivedAt = since(e);
|
|
6059
|
+
if (e.state !== "in-progress") {
|
|
6060
|
+
const total = e.stage === "live" && e.state === "done" && receivedAt !== null ? ` (${took(since(e) - receivedAt)} after Shadow received it)` : "";
|
|
6061
|
+
const line = settledLine(e, total);
|
|
6062
|
+
if (line !== null) io.write(line);
|
|
6063
|
+
return;
|
|
6064
|
+
}
|
|
6065
|
+
io.write(` \u2026 ${e.word.padEnd(10)} ${says.doing}
|
|
6066
|
+
`);
|
|
6067
|
+
const from = since(e);
|
|
6068
|
+
if (io.isTTY) {
|
|
6069
|
+
let frame = 0;
|
|
6070
|
+
const draw = () => {
|
|
6071
|
+
io.write(`\r\x1B[2K ${SPINNER[frame++ % SPINNER.length]} ${says.still} \xB7 ${took(now() - from)}`);
|
|
6072
|
+
drawn = true;
|
|
6073
|
+
};
|
|
6074
|
+
draw();
|
|
6075
|
+
ticking = { stage: e.stage, timer: every(draw, 1e3) };
|
|
6076
|
+
} else {
|
|
6077
|
+
ticking = { stage: e.stage, timer: every(() => io.write(` ${says.still} (${took(now() - from)} so far)
|
|
6078
|
+
`), HEARTBEAT_MS) };
|
|
6079
|
+
}
|
|
6080
|
+
},
|
|
6081
|
+
stop: stopTicking
|
|
6082
|
+
};
|
|
6083
|
+
}
|
|
6084
|
+
|
|
5914
6085
|
// src/follow.ts
|
|
5915
6086
|
async function followPublication(p, endpoint, o, opts) {
|
|
5916
6087
|
const derived = portalForEndpoint(endpoint);
|
|
@@ -5925,11 +6096,11 @@ async function followPublication(p, endpoint, o, opts) {
|
|
|
5925
6096
|
const refs = Object.entries(o.refs ?? {});
|
|
5926
6097
|
if (refs.length !== 1) return elsewhere("several-refs");
|
|
5927
6098
|
const [ref, commit] = refs[0];
|
|
5928
|
-
opts.log("Following it: received -> checked -> going live -> live. Ctrl-C stops watching; the publication is
|
|
6099
|
+
opts.log("Following it to live: received -> checked -> going live -> live. Ctrl-C stops watching; the publication is saved and carries on without you.");
|
|
5929
6100
|
const outcome = await p.followRun(
|
|
5930
6101
|
{ portal, repoId: o.repoId, publicationId: o.publicationId, ref, commit },
|
|
5931
6102
|
{ onStage: opts.onStage, ...opts.timeoutMs === void 0 ? {} : { timeoutMs: opts.timeoutMs }, ...opts.untilChange === true ? { untilChange: true } : {} }
|
|
5932
|
-
);
|
|
6103
|
+
).finally(() => opts.stopStages?.());
|
|
5933
6104
|
const answerable = confirmed.terminal.includes(PREPARATION);
|
|
5934
6105
|
const dir = shellWord(opts.dir);
|
|
5935
6106
|
const next = outcome.kind === "timed-out" || outcome.kind === "moving" ? { action: "follow", command: `publisher follow ${dir} --publication ${o.publicationId}${opts.untilChange === true ? " --until-change" : ""} --json` } : outcome.kind === "waiting" && answerable ? { action: "question", command: `publisher question ${dir} --json` } : void 0;
|
|
@@ -5965,13 +6136,15 @@ function shellWord(path) {
|
|
|
5965
6136
|
return /^[A-Za-z0-9_./-]+$/.test(path) ? path : `'${path.replace(/'/g, `'\\''`)}'`;
|
|
5966
6137
|
}
|
|
5967
6138
|
function stageEmitter(json, quiet) {
|
|
5968
|
-
|
|
5969
|
-
if (json) process.stdout.write(stageLine(e, true) + "\n");
|
|
5970
|
-
else if (!quiet) process.stderr.write(stageLine(e, false) + "\n");
|
|
6139
|
+
const nothing = () => {
|
|
5971
6140
|
};
|
|
6141
|
+
if (json) return { onStage: (e) => process.stdout.write(stageLine(e) + "\n"), stopStages: nothing };
|
|
6142
|
+
if (quiet) return { onStage: nothing, stopStages: nothing };
|
|
6143
|
+
const { onStage, stop } = stageReporter({ write: (s) => process.stderr.write(s), isTTY: process.stderr.isTTY === true });
|
|
6144
|
+
return { onStage, stopStages: stop };
|
|
5972
6145
|
}
|
|
5973
|
-
function stageLine(e
|
|
5974
|
-
return
|
|
6146
|
+
function stageLine(e) {
|
|
6147
|
+
return JSON.stringify({ event: "stage", stage: e.stage, word: e.word, state: e.state, at: e.at });
|
|
5975
6148
|
}
|
|
5976
6149
|
function inPortal(portal, lead, verb) {
|
|
5977
6150
|
return `${lead} ${verb} in the portal${portal === null ? "" : `: ${portal}`}.`;
|
|
@@ -5993,7 +6166,7 @@ function progressHint(r, portal, answerable = false) {
|
|
|
5993
6166
|
}
|
|
5994
6167
|
break;
|
|
5995
6168
|
case "moving": {
|
|
5996
|
-
const now = r.stages.
|
|
6169
|
+
const now = r.stages.filter((s) => s.state === "in-progress").at(-1) ?? r.stages.filter((s) => s.state !== "pending").at(-1);
|
|
5997
6170
|
return `Now: ${now?.word ?? "received"}. Run next.command to wait for the next step.`;
|
|
5998
6171
|
}
|
|
5999
6172
|
case "timed-out":
|
|
@@ -6043,15 +6216,81 @@ async function repositoryForInit(p, endpoint, name, opts = {}) {
|
|
|
6043
6216
|
return { repoId: made.repository.repoId, appId: made.appId };
|
|
6044
6217
|
}
|
|
6045
6218
|
|
|
6046
|
-
// src/
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6219
|
+
// src/args.ts
|
|
6220
|
+
var import_node_util2 = require("node:util");
|
|
6221
|
+
var OPTIONS = {
|
|
6222
|
+
endpoint: { type: "string" },
|
|
6223
|
+
profile: { type: "string" },
|
|
6224
|
+
token: { type: "string" },
|
|
6225
|
+
portal: { type: "string" },
|
|
6226
|
+
name: { type: "string" },
|
|
6227
|
+
repo: { type: "string" },
|
|
6228
|
+
message: { type: "string" },
|
|
6229
|
+
publication: { type: "string" },
|
|
6230
|
+
timeout: { type: "string" },
|
|
6231
|
+
branch: { type: "string" },
|
|
6232
|
+
ours: { type: "string" },
|
|
6233
|
+
theirs: { type: "string" },
|
|
6234
|
+
to: { type: "string" },
|
|
6235
|
+
cursor: { type: "string" },
|
|
6236
|
+
ref: { type: "string", multiple: true },
|
|
6237
|
+
json: { type: "boolean" },
|
|
6238
|
+
quiet: { type: "boolean" },
|
|
6239
|
+
force: { type: "boolean" },
|
|
6240
|
+
apply: { type: "boolean" },
|
|
6241
|
+
"mark-conflicts": { type: "boolean" },
|
|
6242
|
+
help: { type: "boolean" },
|
|
6243
|
+
version: { type: "boolean" },
|
|
6244
|
+
"set-default": { type: "boolean" },
|
|
6245
|
+
archived: { type: "boolean" },
|
|
6246
|
+
// A flag of its own, not a negation of --wait: there is no --wait.
|
|
6247
|
+
"no-wait": { type: "boolean" },
|
|
6248
|
+
"until-change": { type: "boolean" },
|
|
6249
|
+
// `login --continue`: finish the sign-in a `login --no-wait` started.
|
|
6250
|
+
continue: { type: "boolean" }
|
|
6251
|
+
};
|
|
6252
|
+
function str2(v) {
|
|
6253
|
+
return typeof v === "string" ? v : void 0;
|
|
6254
|
+
}
|
|
6255
|
+
function parseArgs(argv) {
|
|
6256
|
+
try {
|
|
6257
|
+
const { positionals, values } = (0, import_node_util2.parseArgs)({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
6258
|
+
return { cmd: positionals, flags: { ...values } };
|
|
6259
|
+
} catch (e) {
|
|
6260
|
+
throw new PublisherError("INVALID_REQUEST", e instanceof Error ? e.message : String(e));
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
|
|
6264
|
+
// src/discover.ts
|
|
6265
|
+
var DEFAULT_PORTAL_URL = "https://shadow.minnovate.ai";
|
|
6266
|
+
function portalBase(env, flag) {
|
|
6267
|
+
return (flag ?? env["GIT_PUBLISHER_PORTAL"] ?? DEFAULT_PORTAL_URL).replace(/\/+$/, "");
|
|
6268
|
+
}
|
|
6269
|
+
async function discoverEndpoint(portal, fetchImpl = fetch) {
|
|
6270
|
+
const url2 = `${portal}/api/v1/config`;
|
|
6271
|
+
let body;
|
|
6272
|
+
try {
|
|
6273
|
+
const response = await fetchImpl(url2);
|
|
6274
|
+
if (!response.ok) {
|
|
6275
|
+
throw new PublisherError("UNAVAILABLE", `could not ask ${portal} where to publish (it answered ${response.status})`, { retryable: true });
|
|
6276
|
+
}
|
|
6277
|
+
body = await response.json();
|
|
6278
|
+
} catch (e) {
|
|
6279
|
+
if (PublisherError.isPublisherError(e)) throw e;
|
|
6280
|
+
throw new PublisherError("NETWORK", `could not reach ${portal}: ${e instanceof Error ? e.message : String(e)}`, { retryable: true });
|
|
6281
|
+
}
|
|
6282
|
+
const endpoint = body?.publishingEndpoint;
|
|
6283
|
+
if (endpoint === null || endpoint === void 0) {
|
|
6284
|
+
throw new PublisherError("LOCAL_STATE", `${portal} is not set up for publishing; ask whoever runs it, or pass --endpoint`);
|
|
6285
|
+
}
|
|
6286
|
+
if (typeof endpoint !== "string" || endpoint.trim() === "") {
|
|
6287
|
+
throw new PublisherError("UNAVAILABLE", `${portal} answered with something this version does not understand`, { retryable: false });
|
|
6288
|
+
}
|
|
6289
|
+
return endpoint;
|
|
6051
6290
|
}
|
|
6052
6291
|
|
|
6053
6292
|
// src/prompt.ts
|
|
6054
|
-
var
|
|
6293
|
+
var import_promises11 = require("node:readline/promises");
|
|
6055
6294
|
function isInteractive(env) {
|
|
6056
6295
|
return env.isTTY === true && !env.json;
|
|
6057
6296
|
}
|
|
@@ -6068,7 +6307,7 @@ function resolveChoice(answer, names) {
|
|
|
6068
6307
|
return names.find((n) => n.toLowerCase() === a.toLowerCase()) ?? null;
|
|
6069
6308
|
}
|
|
6070
6309
|
async function ask3(question) {
|
|
6071
|
-
const rl = (0,
|
|
6310
|
+
const rl = (0, import_promises11.createInterface)({ input: process.stdin, output: process.stderr });
|
|
6072
6311
|
try {
|
|
6073
6312
|
return await rl.question(question);
|
|
6074
6313
|
} catch {
|
|
@@ -6113,58 +6352,205 @@ Profile name: `);
|
|
|
6113
6352
|
return null;
|
|
6114
6353
|
}
|
|
6115
6354
|
|
|
6116
|
-
// src/
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
}
|
|
6147
|
-
|
|
6355
|
+
// src/timings.ts
|
|
6356
|
+
function stopwatch(now = Date.now) {
|
|
6357
|
+
const started = now();
|
|
6358
|
+
let last = started;
|
|
6359
|
+
const laps = {};
|
|
6360
|
+
return {
|
|
6361
|
+
/** `reached` false records a stage this command did not go through: null, and no time taken. */
|
|
6362
|
+
lap(name, reached = true) {
|
|
6363
|
+
const t = now();
|
|
6364
|
+
if (!reached) {
|
|
6365
|
+
laps[name] ??= null;
|
|
6366
|
+
return t;
|
|
6367
|
+
}
|
|
6368
|
+
laps[name] = (laps[name] ?? 0) + (t - last);
|
|
6369
|
+
last = t;
|
|
6370
|
+
return t;
|
|
6371
|
+
},
|
|
6372
|
+
/** Every lap so far, with `total` since the stopwatch started. */
|
|
6373
|
+
read(extra = {}) {
|
|
6374
|
+
return { ...laps, ...extra, total: now() - started };
|
|
6375
|
+
}
|
|
6376
|
+
};
|
|
6377
|
+
}
|
|
6378
|
+
function splitAtBegin(clock, onProgress) {
|
|
6379
|
+
let sending = false;
|
|
6380
|
+
return {
|
|
6381
|
+
onProgress(e) {
|
|
6382
|
+
if (!sending && e.phase === "begin") {
|
|
6383
|
+
sending = true;
|
|
6384
|
+
clock.lap("prepare");
|
|
6385
|
+
}
|
|
6386
|
+
onProgress(e);
|
|
6387
|
+
},
|
|
6388
|
+
done: () => sending ? clock.lap("upload") : (clock.lap("prepare"), null)
|
|
6389
|
+
};
|
|
6390
|
+
}
|
|
6391
|
+
function between(from, to) {
|
|
6392
|
+
const a = from?.at ? Date.parse(from.at) : NaN;
|
|
6393
|
+
const b = to?.at ? Date.parse(to.at) : NaN;
|
|
6394
|
+
return Number.isFinite(a) && Number.isFinite(b) && to.state !== "skipped" ? Math.max(0, b - a) : null;
|
|
6395
|
+
}
|
|
6396
|
+
function runTimings(stages, acceptedAt) {
|
|
6397
|
+
const s = (name) => stages?.find((e) => e.stage === name);
|
|
6398
|
+
const receivedAt = s("received")?.at ? Date.parse(s("received").at) : NaN;
|
|
6399
|
+
return {
|
|
6400
|
+
handoff: acceptedAt !== null && Number.isFinite(receivedAt) ? Math.max(0, receivedAt - acceptedAt) : null,
|
|
6401
|
+
received: between(s("received"), s("running")),
|
|
6402
|
+
checks: between(s("running"), s("deploying")),
|
|
6403
|
+
deployment: between(s("deploying"), s("live"))
|
|
6404
|
+
};
|
|
6405
|
+
}
|
|
6406
|
+
function duration3(ms) {
|
|
6407
|
+
return ms < 1e4 ? `${(Math.max(0, ms) / 1e3).toFixed(1)} s` : took(ms);
|
|
6408
|
+
}
|
|
6409
|
+
function timingsLine(t) {
|
|
6410
|
+
const parts = Object.entries(t).filter((e) => e[1] !== null).map(([name, ms]) => `${name} ${duration3(ms)}`);
|
|
6411
|
+
return `Took: ${parts.join(" \xB7 ")}`;
|
|
6412
|
+
}
|
|
6413
|
+
|
|
6414
|
+
// src/login.ts
|
|
6415
|
+
async function verifiedCapabilities(publisher) {
|
|
6416
|
+
const caps = await publisher.getCapabilities();
|
|
6417
|
+
await publisher.listRepositories({ limit: 1 });
|
|
6418
|
+
return caps;
|
|
6419
|
+
}
|
|
6420
|
+
var CONTINUE = "publisher login --continue --json";
|
|
6421
|
+
var START_AGAIN = "publisher login --no-wait --json";
|
|
6422
|
+
function prompt({ verificationUri, userCode, expiresInSeconds }) {
|
|
6423
|
+
process.stderr.write(
|
|
6424
|
+
`
|
|
6425
|
+
To sign in, open:
|
|
6426
|
+
${verificationUri}
|
|
6427
|
+
|
|
6428
|
+
and confirm the code:
|
|
6429
|
+
${userCode}
|
|
6430
|
+
|
|
6431
|
+
Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
6432
|
+
`
|
|
6433
|
+
);
|
|
6434
|
+
}
|
|
6435
|
+
async function login(ctx) {
|
|
6436
|
+
if (ctx.flags["continue"] === true) return continueLogin(ctx);
|
|
6437
|
+
const clock = stopwatch();
|
|
6438
|
+
const target = resolveLoginEndpoint(ctx.input);
|
|
6439
|
+
const endpoint = "endpoint" in target ? target.endpoint : await discoverEndpoint(portalBase(process.env, str2(ctx.flags["portal"])));
|
|
6440
|
+
if (!endpoint) throw new PublisherError("INVALID_REQUEST", "login requires --endpoint (or --profile naming a stored one)");
|
|
6441
|
+
clock.lap("discovery");
|
|
6442
|
+
const profile = str2(ctx.flags["profile"]);
|
|
6443
|
+
const setDefault = ctx.flags["set-default"] === true;
|
|
6444
|
+
const given = str2(ctx.flags["token"]) ?? process.env["GIT_PUBLISHER_TOKEN"];
|
|
6445
|
+
if (given) return finish(ctx, { endpoint, token: given, signIn: null, profile, setDefault }, clock);
|
|
6446
|
+
if (ctx.flags["no-wait"] === true) {
|
|
6447
|
+
const started = await startDeviceLogin({ endpoint });
|
|
6448
|
+
clock.lap("start");
|
|
6449
|
+
prompt(started);
|
|
6450
|
+
const pendingFile = await savePendingLogin({ endpoint, started, ...profile ? { profile } : {}, ...setDefault ? { setDefault } : {} });
|
|
6451
|
+
ctx.out(pendingResult(endpoint, started, "Have the code approved, then run next.command to finish signing in.", clock, { pendingFile }));
|
|
6452
|
+
return 0;
|
|
6453
|
+
}
|
|
6454
|
+
const signIn = await deviceLogin({
|
|
6455
|
+
endpoint,
|
|
6456
|
+
onPrompt: (shown2) => {
|
|
6457
|
+
clock.lap("start");
|
|
6458
|
+
prompt(shown2);
|
|
6459
|
+
}
|
|
6460
|
+
});
|
|
6461
|
+
clock.lap("approval");
|
|
6462
|
+
return finish(ctx, { endpoint, token: signIn.token, signIn, profile, setDefault }, clock);
|
|
6463
|
+
}
|
|
6464
|
+
async function continueLogin(ctx) {
|
|
6465
|
+
const clock = stopwatch();
|
|
6466
|
+
const pending = await readPendingLogin();
|
|
6467
|
+
if (pending === null) {
|
|
6468
|
+
const pendingFile = pendingLoginPath();
|
|
6469
|
+
throw new PublisherError("LOCAL_STATE", `no sign-in is waiting at ${pendingFile}. ${notKept(`"${START_AGAIN}" answered`, `run ${START_AGAIN} again`)}`, {
|
|
6470
|
+
details: { pendingFile, credentialsFile: credentialsLocation(), next: { action: "sign-in", ask: null, command: START_AGAIN, profiles: [] } }
|
|
6471
|
+
});
|
|
6472
|
+
}
|
|
6473
|
+
const timeout = str2(ctx.flags["timeout"]);
|
|
6474
|
+
let signIn;
|
|
6148
6475
|
try {
|
|
6149
|
-
|
|
6150
|
-
return { cmd: positionals, flags: { ...values } };
|
|
6476
|
+
signIn = await pollDeviceLogin(pending.started, { lastPolledAt: null, ...timeout === void 0 ? {} : { until: Date.now() + followTimeoutMs(timeout) } });
|
|
6151
6477
|
} catch (e) {
|
|
6152
|
-
|
|
6478
|
+
if (e instanceof PublisherError && (e.code === "EXPIRED" || e.code === "FORBIDDEN")) await removePendingLogin();
|
|
6479
|
+
throw e;
|
|
6480
|
+
}
|
|
6481
|
+
clock.lap("approval");
|
|
6482
|
+
if (signIn === null) {
|
|
6483
|
+
ctx.out(pendingResult(pending.endpoint, pending.started, "The code is not approved yet. Have it approved, then run next.command again.", clock));
|
|
6484
|
+
return 3;
|
|
6153
6485
|
}
|
|
6486
|
+
const code = await finish(
|
|
6487
|
+
ctx,
|
|
6488
|
+
{ endpoint: pending.endpoint, token: signIn.token, signIn, profile: pending.profile ?? str2(ctx.flags["profile"]), setDefault: pending.setDefault === true },
|
|
6489
|
+
clock
|
|
6490
|
+
);
|
|
6491
|
+
await removePendingLogin();
|
|
6492
|
+
return code;
|
|
6493
|
+
}
|
|
6494
|
+
function pendingResult(endpoint, started, hint, clock, extra = {}) {
|
|
6495
|
+
return {
|
|
6496
|
+
ok: false,
|
|
6497
|
+
state: "pending",
|
|
6498
|
+
endpoint,
|
|
6499
|
+
userCode: started.userCode,
|
|
6500
|
+
verificationUri: started.verificationUri,
|
|
6501
|
+
expiresAt: new Date(started.expiresAt).toISOString(),
|
|
6502
|
+
...extra,
|
|
6503
|
+
next: { action: "approve", ask: null, command: CONTINUE },
|
|
6504
|
+
hint,
|
|
6505
|
+
timings: clock.read()
|
|
6506
|
+
};
|
|
6507
|
+
}
|
|
6508
|
+
async function finish(ctx, got, clock) {
|
|
6509
|
+
const { endpoint, token, signIn } = got;
|
|
6510
|
+
const caps = await verifiedCapabilities(createPublisher({ endpoint, token }));
|
|
6511
|
+
clock.lap("verify");
|
|
6512
|
+
let profile = got.profile;
|
|
6513
|
+
if (!profile && isInteractive({ json: ctx.json, isTTY: process.stdin.isTTY })) {
|
|
6514
|
+
const suggested = suggestProfileName(endpoint, new Set(Object.keys(ctx.input.file.profiles)));
|
|
6515
|
+
profile = await askProfileName(endpoint, suggested) ?? void 0;
|
|
6516
|
+
}
|
|
6517
|
+
const path = await saveCredential(endpoint, token, void 0, {
|
|
6518
|
+
...profile ? { profile } : {},
|
|
6519
|
+
setDefault: got.setDefault,
|
|
6520
|
+
// The refresh half, when the service issued one: the sign-in outlives the credential.
|
|
6521
|
+
...signIn ? { signIn } : {}
|
|
6522
|
+
});
|
|
6523
|
+
const file = await readCredentials();
|
|
6524
|
+
clock.lap("save");
|
|
6525
|
+
const named = profileForEndpoint(file, endpoint, profile);
|
|
6526
|
+
const timings = clock.read();
|
|
6527
|
+
ctx.log(timingsLine(timings));
|
|
6528
|
+
ctx.out({
|
|
6529
|
+
ok: true,
|
|
6530
|
+
endpoint,
|
|
6531
|
+
profile: named,
|
|
6532
|
+
isDefault: named !== null && file.defaultProfile === named,
|
|
6533
|
+
server: caps.server,
|
|
6534
|
+
credentialsFile: path,
|
|
6535
|
+
// Which setting chose that path: a sandbox that keeps only its workspace needs it to be there.
|
|
6536
|
+
credentialsFrom: credentialsLocation().from,
|
|
6537
|
+
timings
|
|
6538
|
+
});
|
|
6539
|
+
return 0;
|
|
6154
6540
|
}
|
|
6155
6541
|
|
|
6156
6542
|
// src/skills.ts
|
|
6157
6543
|
var import_node_child_process3 = require("node:child_process");
|
|
6158
|
-
var
|
|
6159
|
-
var
|
|
6544
|
+
var import_node_fs7 = require("node:fs");
|
|
6545
|
+
var import_node_path8 = require("node:path");
|
|
6160
6546
|
var MARKETPLACE = "shadow-publisher";
|
|
6161
6547
|
var PLUGIN = "git-publisher";
|
|
6162
6548
|
function packageRoot() {
|
|
6163
|
-
return (0,
|
|
6549
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)((0, import_node_fs7.realpathSync)(process.argv[1])), "..");
|
|
6164
6550
|
}
|
|
6165
6551
|
function packageVersion() {
|
|
6166
6552
|
try {
|
|
6167
|
-
return JSON.parse((0,
|
|
6553
|
+
return JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path8.join)(packageRoot(), "package.json"), "utf8")).version;
|
|
6168
6554
|
} catch {
|
|
6169
6555
|
return "unknown";
|
|
6170
6556
|
}
|
|
@@ -6181,8 +6567,8 @@ function claude(args) {
|
|
|
6181
6567
|
});
|
|
6182
6568
|
}
|
|
6183
6569
|
async function installSkills({ log }) {
|
|
6184
|
-
const source = (0,
|
|
6185
|
-
if (!(0,
|
|
6570
|
+
const source = (0, import_node_path8.join)(packageRoot(), "dist", "claude");
|
|
6571
|
+
if (!(0, import_node_fs7.existsSync)((0, import_node_path8.join)(source, ".claude-plugin", "marketplace.json"))) {
|
|
6186
6572
|
throw new PublisherError("LOCAL_STATE", `this publisher carries no skills at ${source}; reinstall it with: npm install -g @minnovate/shadow-publisher`);
|
|
6187
6573
|
}
|
|
6188
6574
|
const step = async (what, args) => {
|
|
@@ -6280,6 +6666,46 @@ function hintFor(view) {
|
|
|
6280
6666
|
return `It is waiting for your answer. Run: publisher answer ${how}`;
|
|
6281
6667
|
}
|
|
6282
6668
|
|
|
6669
|
+
// src/tempdir.ts
|
|
6670
|
+
var import_node_fs8 = require("node:fs");
|
|
6671
|
+
var import_promises12 = require("node:fs/promises");
|
|
6672
|
+
var import_node_os4 = require("node:os");
|
|
6673
|
+
var FALLBACK_TEMP_DIRS = ["/tmp", "/var/tmp"];
|
|
6674
|
+
async function unusable(path) {
|
|
6675
|
+
try {
|
|
6676
|
+
if (!(await (0, import_promises12.stat)(path)).isDirectory()) return "is not a directory";
|
|
6677
|
+
} catch {
|
|
6678
|
+
return "does not exist";
|
|
6679
|
+
}
|
|
6680
|
+
try {
|
|
6681
|
+
await (0, import_promises12.access)(path, import_node_fs8.constants.W_OK | import_node_fs8.constants.X_OK);
|
|
6682
|
+
return null;
|
|
6683
|
+
} catch {
|
|
6684
|
+
return "cannot be written";
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
async function ensureTempDir(env = process.env, fallbacks = FALLBACK_TEMP_DIRS) {
|
|
6688
|
+
const named = (0, import_node_os4.tmpdir)();
|
|
6689
|
+
const reason = await unusable(named);
|
|
6690
|
+
if (reason === null) return { path: named, replaced: null };
|
|
6691
|
+
for (const candidate of fallbacks) {
|
|
6692
|
+
if (candidate !== named && await unusable(candidate) === null) {
|
|
6693
|
+
env["TMPDIR"] = candidate;
|
|
6694
|
+
return { path: candidate, replaced: { path: named, reason } };
|
|
6695
|
+
}
|
|
6696
|
+
}
|
|
6697
|
+
throw new PublisherError(
|
|
6698
|
+
"LOCAL_STATE",
|
|
6699
|
+
`the temporary directory ${named} ${reason}, and neither ${fallbacks.join(" nor ")} can be used: set TMPDIR to a directory that can be written`,
|
|
6700
|
+
{
|
|
6701
|
+
details: { tempDir: named, reason, tried: [...fallbacks] }
|
|
6702
|
+
}
|
|
6703
|
+
);
|
|
6704
|
+
}
|
|
6705
|
+
function replacedLine(t) {
|
|
6706
|
+
return t.replaced === null ? null : `note: the temporary directory ${t.replaced.path} ${t.replaced.reason}; using ${t.path} instead`;
|
|
6707
|
+
}
|
|
6708
|
+
|
|
6283
6709
|
// src/main.ts
|
|
6284
6710
|
var USAGE = `publisher \u2014 publish an app's Git history to the serverless Git backend
|
|
6285
6711
|
|
|
@@ -6293,6 +6719,8 @@ Usage:
|
|
|
6293
6719
|
[--endpoint <url>] skip discovery and use this one
|
|
6294
6720
|
[--portal <url>] ask a different platform (or GIT_PUBLISHER_PORTAL)
|
|
6295
6721
|
[--profile <name>] [--set-default] [--token <token>]
|
|
6722
|
+
[--no-wait] print the code and exit; finish with login --continue
|
|
6723
|
+
publisher login --continue [--timeout <seconds>] collect the credential once the code is approved
|
|
6296
6724
|
publisher logout (--endpoint <url> | --profile <name>)
|
|
6297
6725
|
publisher profile list | profile use <name> | profile remove <name>
|
|
6298
6726
|
publisher init [dir] [--name <name>] [--repo <id>] create (or attach to) a repository, associate this folder
|
|
@@ -6349,9 +6777,6 @@ function optStr(flags, key) {
|
|
|
6349
6777
|
const v = str2(flags[key]);
|
|
6350
6778
|
return v ? { [key]: v } : {};
|
|
6351
6779
|
}
|
|
6352
|
-
function str2(v) {
|
|
6353
|
-
return typeof v === "string" ? v : void 0;
|
|
6354
|
-
}
|
|
6355
6780
|
async function main(argv) {
|
|
6356
6781
|
const { cmd, flags } = parseArgs(argv);
|
|
6357
6782
|
const json = flags["json"] === true;
|
|
@@ -6366,8 +6791,8 @@ async function main(argv) {
|
|
|
6366
6791
|
process.stderr.write(`\r[${e.phase}] ${e.message}${pct} `);
|
|
6367
6792
|
if (e.phase === "done") process.stderr.write("\n");
|
|
6368
6793
|
};
|
|
6369
|
-
const dirArg = (i) => (0,
|
|
6370
|
-
const following = {
|
|
6794
|
+
const dirArg = (i) => (0, import_node_path9.resolve)(cmd[i] ?? ".");
|
|
6795
|
+
const following = { ...stageEmitter(json, quiet), log, untilChange: flags["until-change"] === true };
|
|
6371
6796
|
const resolutionInput = async (dir) => {
|
|
6372
6797
|
let association;
|
|
6373
6798
|
if (dir && await isGitRepo(dir)) association = (await readAssociation(dir))?.endpoint;
|
|
@@ -6394,7 +6819,11 @@ async function main(argv) {
|
|
|
6394
6819
|
const client = async (dir) => {
|
|
6395
6820
|
const { endpoint, source } = await endpointFor(dir);
|
|
6396
6821
|
const token = str2(flags["token"]);
|
|
6397
|
-
return {
|
|
6822
|
+
return {
|
|
6823
|
+
endpoint,
|
|
6824
|
+
source,
|
|
6825
|
+
p: createPublisher({ endpoint, endpointSource: source, ...token ? { token } : {}, clientName: "git-publisher-cli", clientVersion: CLIENT_VERSION })
|
|
6826
|
+
};
|
|
6398
6827
|
};
|
|
6399
6828
|
const linkFolder = async (p, endpoint, dir, attach) => {
|
|
6400
6829
|
let onboarding = null;
|
|
@@ -6426,12 +6855,17 @@ async function main(argv) {
|
|
|
6426
6855
|
`);
|
|
6427
6856
|
return 0;
|
|
6428
6857
|
}
|
|
6858
|
+
if (flags["help"] !== true && GIT_COMMANDS.has(cmd[0] ?? "")) {
|
|
6859
|
+
const line = replacedLine(await ensureTempDir());
|
|
6860
|
+
if (line !== null) process.stderr.write(line + "\n");
|
|
6861
|
+
}
|
|
6429
6862
|
switch (flags["help"] === true ? "help" : cmd[0]) {
|
|
6430
6863
|
case void 0:
|
|
6431
6864
|
case "help":
|
|
6432
6865
|
process.stdout.write(USAGE);
|
|
6433
6866
|
return 0;
|
|
6434
6867
|
case "doctor": {
|
|
6868
|
+
const temp = await ensureTempDir().catch((e) => e instanceof PublisherError ? { error: e.message } : null);
|
|
6435
6869
|
const git = await gitVersion().catch(() => null);
|
|
6436
6870
|
const dir = dirArg(1);
|
|
6437
6871
|
const s = credentialStatus(await resolutionInput(dir));
|
|
@@ -6453,6 +6887,7 @@ async function main(argv) {
|
|
|
6453
6887
|
profiles: s.profiles,
|
|
6454
6888
|
defaultProfile: s.defaultProfile,
|
|
6455
6889
|
credentialsFile: credentialsPath(),
|
|
6890
|
+
tempDir: temp,
|
|
6456
6891
|
association: association === null ? null : { endpoint: association.endpoint, repoId: association.repoId },
|
|
6457
6892
|
// Printed as it comes: `moved` carries where to, `unreachable` carries why, and the two
|
|
6458
6893
|
// need different next steps so they are not collapsed into "not ok".
|
|
@@ -6472,48 +6907,8 @@ async function main(argv) {
|
|
|
6472
6907
|
out({ ...result, next: { action: "reauthenticate", ask: null, command } });
|
|
6473
6908
|
return 0;
|
|
6474
6909
|
}
|
|
6475
|
-
case "login":
|
|
6476
|
-
|
|
6477
|
-
const input = await resolutionInput();
|
|
6478
|
-
const target = resolveLoginEndpoint(input);
|
|
6479
|
-
const endpoint = "endpoint" in target ? target.endpoint : await discoverEndpoint(portal);
|
|
6480
|
-
if (!endpoint) throw new PublisherError("INVALID_REQUEST", "login requires --endpoint (or --profile naming a stored one)");
|
|
6481
|
-
const given = str2(flags["token"]) ?? process.env["GIT_PUBLISHER_TOKEN"];
|
|
6482
|
-
const signIn = given ? null : await deviceLogin({
|
|
6483
|
-
endpoint,
|
|
6484
|
-
onPrompt: ({ userCode, verificationUri, expiresInSeconds }) => {
|
|
6485
|
-
process.stderr.write(
|
|
6486
|
-
`
|
|
6487
|
-
To sign in, open:
|
|
6488
|
-
${verificationUri}
|
|
6489
|
-
|
|
6490
|
-
and confirm the code:
|
|
6491
|
-
${userCode}
|
|
6492
|
-
|
|
6493
|
-
Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
6494
|
-
`
|
|
6495
|
-
);
|
|
6496
|
-
}
|
|
6497
|
-
});
|
|
6498
|
-
const token = given ?? signIn.token;
|
|
6499
|
-
const p = createPublisher({ endpoint, token });
|
|
6500
|
-
const caps = await verifiedCapabilities(p);
|
|
6501
|
-
let profileFlag = str2(flags["profile"]);
|
|
6502
|
-
if (!profileFlag && isInteractive({ json, isTTY: process.stdin.isTTY })) {
|
|
6503
|
-
const suggested = suggestProfileName(endpoint, new Set(Object.keys(input.file.profiles)));
|
|
6504
|
-
profileFlag = await askProfileName(endpoint, suggested) ?? void 0;
|
|
6505
|
-
}
|
|
6506
|
-
const path = await saveCredential(endpoint, token, void 0, {
|
|
6507
|
-
...profileFlag ? { profile: profileFlag } : {},
|
|
6508
|
-
setDefault: flags["set-default"] === true,
|
|
6509
|
-
// The refresh half, when the service issued one: the sign-in outlives the credential.
|
|
6510
|
-
...signIn ? { signIn } : {}
|
|
6511
|
-
});
|
|
6512
|
-
const file = await readCredentials();
|
|
6513
|
-
const profile = profileForEndpoint(file, endpoint, profileFlag);
|
|
6514
|
-
out({ ok: true, endpoint, profile, isDefault: profile !== null && file.defaultProfile === profile, server: caps.server, credentialsFile: path });
|
|
6515
|
-
return 0;
|
|
6516
|
-
}
|
|
6910
|
+
case "login":
|
|
6911
|
+
return login({ flags, json, out, log, input: await resolutionInput() });
|
|
6517
6912
|
case "logout": {
|
|
6518
6913
|
const profile = str2(flags["profile"]);
|
|
6519
6914
|
const endpoint = str2(flags["endpoint"]) ?? process.env["GIT_PUBLISHER_ENDPOINT"];
|
|
@@ -6651,11 +7046,20 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6651
7046
|
case "sync": {
|
|
6652
7047
|
const dir = dirArg(1);
|
|
6653
7048
|
const timeoutMs = followTimeoutMs(str2(flags["timeout"]));
|
|
6654
|
-
const
|
|
7049
|
+
const clock = stopwatch();
|
|
7050
|
+
const { p, endpoint, source } = await client(dir);
|
|
7051
|
+
clock.lap("discovery");
|
|
7052
|
+
await resolveToken(endpoint, str2(flags["token"]), { endpointSource: source });
|
|
7053
|
+
clock.lap("authentication");
|
|
6655
7054
|
const linked = await isGitRepo(dir) && await readAssociation(dir) !== null ? null : await linkFolder(p, endpoint, dir, {});
|
|
6656
|
-
|
|
7055
|
+
clock.lap("link", linked !== null);
|
|
7056
|
+
const sending = splitAtBegin(clock, progress);
|
|
7057
|
+
const r = await p.syncChanges({ directory: dir, markConflicts: flags["mark-conflicts"] === true, ...optStr(flags, "message") }, { onProgress: sending.onProgress });
|
|
7058
|
+
const acceptedAt = sending.done();
|
|
6657
7059
|
const o = r.published;
|
|
6658
7060
|
const followed = o !== null && o.state === "accepted" && flags["no-wait"] !== true ? await followPublication(p, endpoint, o, { ...following, timeoutMs, dir }) : null;
|
|
7061
|
+
const syncTimings = clock.read(followed !== null && "stages" in followed ? runTimings(followed.stages, acceptedAt) : {});
|
|
7062
|
+
log(timingsLine(syncTimings));
|
|
6659
7063
|
out({
|
|
6660
7064
|
ok: r.state === "published" || r.state === "up_to_date",
|
|
6661
7065
|
state: r.state,
|
|
@@ -6673,7 +7077,8 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6673
7077
|
...o.next === null ? {} : { next: o.next }
|
|
6674
7078
|
},
|
|
6675
7079
|
hint: syncHint(r.state, r.reconciled.before.conflicts, r.marked),
|
|
6676
|
-
progress: followed
|
|
7080
|
+
progress: followed,
|
|
7081
|
+
timings: syncTimings
|
|
6677
7082
|
});
|
|
6678
7083
|
return r.state === "published" || r.state === "up_to_date" ? 0 : r.state === "unknown" ? 3 : 2;
|
|
6679
7084
|
}
|
|
@@ -6694,7 +7099,11 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6694
7099
|
if (publication.state !== "accepted") {
|
|
6695
7100
|
throw new PublisherError("LOCAL_STATE", `publication ${publicationId} is ${publication.state}, not accepted; run publisher status`);
|
|
6696
7101
|
}
|
|
6697
|
-
|
|
7102
|
+
const clock = stopwatch();
|
|
7103
|
+
const followed = await followPublication(p, endpoint, publication, { ...following, timeoutMs, dir });
|
|
7104
|
+
const timings = clock.read("stages" in followed ? runTimings(followed.stages, null) : {});
|
|
7105
|
+
log(timingsLine(timings));
|
|
7106
|
+
out({ publicationId, progress: followed, timings });
|
|
6698
7107
|
return 0;
|
|
6699
7108
|
}
|
|
6700
7109
|
/**
|
|
@@ -6711,7 +7120,7 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6711
7120
|
}
|
|
6712
7121
|
/** Answer it, as `question=value` pairs, re-reading the preparation first. */
|
|
6713
7122
|
case "answer": {
|
|
6714
|
-
const dir = (0,
|
|
7123
|
+
const dir = (0, import_node_path9.resolve)(".");
|
|
6715
7124
|
const { p, endpoint } = await client(dir);
|
|
6716
7125
|
const repoId = str2(flags["repo"]) ?? (await readAssociation(dir))?.repoId;
|
|
6717
7126
|
if (!repoId) throw new PublisherError("LOCAL_STATE", "no repository association; pass --repo");
|
|
@@ -6754,7 +7163,8 @@ ${USAGE}`);
|
|
|
6754
7163
|
return 1;
|
|
6755
7164
|
}
|
|
6756
7165
|
}
|
|
6757
|
-
|
|
7166
|
+
var GIT_COMMANDS = /* @__PURE__ */ new Set(["init", "publish", "status", "fetch", "checkout", "workspace-status", "sync", "merge-check"]);
|
|
7167
|
+
async function demand(r, input) {
|
|
6758
7168
|
if (r.kind === "resolved") return r;
|
|
6759
7169
|
const next = nextStep(credentialStatus(input));
|
|
6760
7170
|
const details = { profiles: r.kind === "none" ? [] : r.profiles, next };
|
|
@@ -6764,8 +7174,10 @@ function demand(r, input) {
|
|
|
6764
7174
|
throw new PublisherError("LOCAL_STATE", `no such profile: ${r.name}.${run}`, { details });
|
|
6765
7175
|
case "ambiguous":
|
|
6766
7176
|
throw new PublisherError("LOCAL_STATE", `several connections and none is the default: pass --profile <name>, or choose one once.${run}`, { details });
|
|
6767
|
-
case "none":
|
|
6768
|
-
|
|
7177
|
+
case "none": {
|
|
7178
|
+
const where = await whereCredentialsWere();
|
|
7179
|
+
throw new PublisherError("LOCAL_STATE", `not connected to a publishing service yet: ${where.sentence}${run}`, { details: { ...details, credentialsFile: where.details } });
|
|
7180
|
+
}
|
|
6769
7181
|
}
|
|
6770
7182
|
}
|
|
6771
7183
|
function syncHint(state, conflicts, marked) {
|