@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.10.2",
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, install it: `npm install -g @minnovate/shadow-publisher` (it needs
29
- Node.js 22). In a Claude Cowork task this is normal: the sandbox starts empty every time, so install it
30
- without asking. On the person's own computer, say in one line that you are installing the
31
- publisher. If `npm` itself is missing, tell them Node.js 22 must be installed first, and stop.
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 person has confirmed a code in their browser, and your shell may
53
- show nothing until a command ends. So never run it in the foreground. Start it in the background with
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 --json > /tmp/publisher-login.log 2>&1 &
74
+ publisher login --no-wait --json
58
75
  ```
59
76
 
60
- Within a few seconds the log shows `To sign in, open:` with a link under it, and `and confirm the
61
- code:` with the code under it. Give the person both, as a link they can click, and say the page will ask them to sign in with their work account.
62
- Then read the log every few seconds until it holds a JSON object (`ok: true`), for up to ten minutes.
63
- In a Claude Cowork task this happens again in every new task, because the sandbox forgets the
64
- sign-in when the task ends.
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
- **If a tool named `approve_publisher_sign_in` is available** (the Shadow publisher connector, which
67
- the person connected in Claude's settings), do not give them the link: call that tool with the code
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. Write
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
@@ -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 import_node_path8 = require("node:path");
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 child = spawnImpl(GIT_BIN, args, { cwd: opts.cwd ?? neutralCwd(), env: sanitizedGitEnv(opts.env), stdio: ["pipe", "pipe", "pipe"], windowsHide: true, detached: true });
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 new PublisherError("UNAVAILABLE", `git ${args[0]} could not be started: ${ended.error.message}`, { retryable: true, cause: ended.error });
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 interval = Math.max(10, opts.sampleIntervalMs ?? 500);
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(interval);
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 stat3 = await (0, import_promises.readFile)(`/proc/${name}/stat`, "utf8");
3338
- const rest = stat3.slice(stat3.lastIndexOf(")") + 2).split(" ");
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 import_node_fs = require("node:fs");
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, import_node_fs.createReadStream)(path).on("data", (c) => {
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 import_node_fs2 = require("node:fs");
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, import_node_fs2.createReadStream)(packPath), label: "index-pack", ...opts };
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 import_node_fs3 = require("node:fs");
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, import_node_fs3.createWriteStream)(opts.outPath);
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 new PublisherError("UNAVAILABLE", `git pack-objects could not be started: ${ended.error.message}`, { retryable: true, cause: ended.error });
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
- if (process.env["GIT_PUBLISHER_CREDENTIALS"]) return process.env["GIT_PUBLISHER_CREDENTIALS"];
4259
- const base = process.env["XDG_CONFIG_HOME"] ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".config");
4260
- return (0, import_node_path4.join)(base, "git-publisher", "credentials.json");
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
- const path = credentialsPath();
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(file, null, 2) + "\n", { mode: 384 });
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) throw new PublisherError("UNAUTHORIZED", `no credentials for ${endpoint}; run "publisher login" or set GIT_PUBLISHER_TOKEN`, { details: { signIn: "missing" } });
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 interval = opts.intervalMs ?? POLL_INTERVAL_MS;
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() + interval > deadline) return { kind: "timed-out", stages: last, lastError };
4762
- await sleep2(interval);
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 import_node_fs5 = require("node:fs");
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 import_node_fs4 = require("node:fs");
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, import_node_fs4.createReadStream)(packPath);
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, import_node_fs5.createWriteStream)(dest));
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 deviceLogin(opts) {
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 post2 = (path, fields) => postForm(base, path, fields, doFetch);
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
- let intervalMs = num(s.interval, 5) * 1e3;
5856
- await opts.onPrompt({ userCode, verificationUri: uri, expiresInSeconds, intervalSeconds: intervalMs / 1e3 });
5857
- const deadline = now() + expiresInSeconds * 1e3;
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
- await sleep2(intervalMs);
5860
- if (now() >= deadline) throw new PublisherError("EXPIRED", "the login expired before it was approved");
5861
- const polled = await post2("/v1/auth/token", { grant_type: DEVICE_CODE_GRANT, device_code: deviceCode });
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: base };
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/discover.ts
5881
- var DEFAULT_PORTAL_URL = "https://shadow.minnovate.ai";
5882
- function portalBase(env, flag) {
5883
- return (flag ?? env["GIT_PUBLISHER_PORTAL"] ?? DEFAULT_PORTAL_URL).replace(/\/+$/, "");
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 discoverEndpoint(portal, fetchImpl = fetch) {
5886
- const url2 = `${portal}/api/v1/config`;
5887
- let body;
5966
+ async function savePendingLogin(pending) {
5967
+ return writeSecretJson(pendingLoginPath(), { version: 1, ...pending });
5968
+ }
5969
+ async function readPendingLogin() {
5970
+ let raw;
5888
5971
  try {
5889
- const response = await fetchImpl(url2);
5890
- if (!response.ok) {
5891
- throw new PublisherError("UNAVAILABLE", `could not ask ${portal} where to publish (it answered ${response.status})`, { retryable: true });
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 endpoint;
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 already saved.");
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
- return (e) => {
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, json) {
5974
- return json ? JSON.stringify({ event: "stage", stage: e.stage, word: e.word, state: e.state, at: e.at }) : ` ${e.word}: ${e.state.replace("-", " ")}`;
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.find((s) => s.state === "in-progress") ?? r.stages.filter((s) => s.state !== "pending").at(-1);
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/login.ts
6047
- async function verifiedCapabilities(publisher) {
6048
- const caps = await publisher.getCapabilities();
6049
- await publisher.listRepositories({ limit: 1 });
6050
- return caps;
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 import_promises10 = require("node:readline/promises");
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, import_promises10.createInterface)({ input: process.stdin, output: process.stderr });
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/args.ts
6117
- var import_node_util2 = require("node:util");
6118
- var OPTIONS = {
6119
- endpoint: { type: "string" },
6120
- profile: { type: "string" },
6121
- token: { type: "string" },
6122
- portal: { type: "string" },
6123
- name: { type: "string" },
6124
- repo: { type: "string" },
6125
- message: { type: "string" },
6126
- publication: { type: "string" },
6127
- timeout: { type: "string" },
6128
- branch: { type: "string" },
6129
- ours: { type: "string" },
6130
- theirs: { type: "string" },
6131
- to: { type: "string" },
6132
- cursor: { type: "string" },
6133
- ref: { type: "string", multiple: true },
6134
- json: { type: "boolean" },
6135
- quiet: { type: "boolean" },
6136
- force: { type: "boolean" },
6137
- apply: { type: "boolean" },
6138
- "mark-conflicts": { type: "boolean" },
6139
- help: { type: "boolean" },
6140
- version: { type: "boolean" },
6141
- "set-default": { type: "boolean" },
6142
- archived: { type: "boolean" },
6143
- // A flag of its own, not a negation of --wait: there is no --wait.
6144
- "no-wait": { type: "boolean" },
6145
- "until-change": { type: "boolean" }
6146
- };
6147
- function parseArgs(argv) {
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
- const { positionals, values } = (0, import_node_util2.parseArgs)({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
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
- throw new PublisherError("INVALID_REQUEST", e instanceof Error ? e.message : String(e));
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 import_node_fs6 = require("node:fs");
6159
- var import_node_path7 = require("node:path");
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, import_node_path7.join)((0, import_node_path7.dirname)((0, import_node_fs6.realpathSync)(process.argv[1])), "..");
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, import_node_fs6.readFileSync)((0, import_node_path7.join)(packageRoot(), "package.json"), "utf8")).version;
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, import_node_path7.join)(packageRoot(), "dist", "claude");
6185
- if (!(0, import_node_fs6.existsSync)((0, import_node_path7.join)(source, ".claude-plugin", "marketplace.json"))) {
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, import_node_path8.resolve)(cmd[i] ?? ".");
6370
- const following = { onStage: stageEmitter(json, quiet), log, untilChange: flags["until-change"] === true };
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 { endpoint, p: createPublisher({ endpoint, endpointSource: source, ...token ? { token } : {}, clientName: "git-publisher-cli", clientVersion: CLIENT_VERSION }) };
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
- const portal = portalBase(process.env, str2(flags["portal"]));
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 { p, endpoint } = await client(dir);
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
- const r = await p.syncChanges({ directory: dir, markConflicts: flags["mark-conflicts"] === true, ...optStr(flags, "message") }, { onProgress: progress });
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
- out({ publicationId, progress: await followPublication(p, endpoint, publication, { ...following, timeoutMs, dir }) });
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, import_node_path8.resolve)(".");
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
- function demand(r, input) {
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
- throw new PublisherError("LOCAL_STATE", `not connected to a publishing service yet.${run}`, { details });
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnovate/shadow-publisher",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "publisher: publish an app's Git history to the serverless Git backend",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",