@minnovate/shadow-publisher 0.6.3 → 0.7.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.6.3",
4
+ "version": "0.7.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"]
@@ -20,6 +20,28 @@ publish it follows the app through received → checked → going live → live
20
20
  result. `merged` says whether anything
21
21
  published elsewhere was brought in (`"fast-forward"` or `"merge"`); mention it when it is not null.
22
22
 
23
+ ## When it waits for an answer
24
+
25
+ Sometimes the app's preparation asks one business question before it can go on (for example
26
+ whether to use the organisation's data). `follow` then stops with `kind: "waiting"` and
27
+ `next.command`. Read the question:
28
+
29
+ ```bash
30
+ publisher question . --json
31
+ ```
32
+
33
+ Tell the person `questions[].text` and the `label` of each option, and ask which they want. Answer
34
+ with the option's `value`, never its label, only once they have chosen — or directly when they
35
+ told you what to answer:
36
+
37
+ ```bash
38
+ publisher answer <question id>=<value> --json
39
+ ```
40
+
41
+ A value that is not an option is refused before anything is sent; `error.details.questions` lists
42
+ the choices. Exit 4 means this sign-in may not answer (an owner or admin can); exit 3 means ask
43
+ again. Then `publisher follow . --publication <id> --json` to see it go live.
44
+
23
45
  ## Just looking
24
46
 
25
47
  To say where the folder stands without changing anything:
@@ -2809,6 +2809,12 @@ var PrincipalRecord = object({
2809
2809
  principal: PrincipalId,
2810
2810
  disabled: boolean2(),
2811
2811
  disabledAt: _default(nullable(IsoDate), null),
2812
+ /**
2813
+ * When the credentials tenant made the change this record reflects (ADR-0017), and what a later
2814
+ * event is ordered against: EventBridge does not order deliveries, and a late enable must not undo
2815
+ * a newer disable. Null on a record written by hand, which any event supersedes.
2816
+ */
2817
+ changedAt: _default(nullable(IsoDate), null),
2812
2818
  note: _default(nullable(string2().check(_maxLength(256))), null)
2813
2819
  });
2814
2820
 
@@ -3585,12 +3591,12 @@ async function resolveRevs(dir, revs, opts = {}) {
3585
3591
  const out = revs.map(
3586
3592
  (r) => r.startsWith("-") ? null : r.includes("\0") ? new PublisherError("INVALID_REQUEST", "NUL in git argument") : null
3587
3593
  );
3588
- const ask3 = revs.flatMap((r, i) => out[i] === null && !r.startsWith("-") ? [i] : []);
3589
- if (ask3.length === 0) return out;
3590
- const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input: ask3.map((i) => revs[i]).join("\0") + "\0", ...opts });
3594
+ const ask4 = revs.flatMap((r, i) => out[i] === null && !r.startsWith("-") ? [i] : []);
3595
+ if (ask4.length === 0) return out;
3596
+ const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input: ask4.map((i) => revs[i]).join("\0") + "\0", ...opts });
3591
3597
  const records = stdout.toString("utf8").split("\0");
3592
3598
  const misses = [];
3593
- for (const [j, i] of ask3.entries()) {
3599
+ for (const [j, i] of ask4.entries()) {
3594
3600
  const record2 = records[j] ?? "";
3595
3601
  if (record2 && !/ (missing|ambiguous)$/.test(record2)) out[i] = record2;
3596
3602
  else if (!revs[i].includes("^{")) misses.push(i);
@@ -3990,7 +3996,7 @@ async function noteAcceptedPublication(directory, outcome, now = /* @__PURE__ */
3990
3996
  if (!oid) return;
3991
3997
  await advanceWorkspaceBase(directory, { oid, generation: outcome.resultGeneration }, now);
3992
3998
  }
3993
- async function adoptWorkspace(client, directory, call = {}, now = /* @__PURE__ */ new Date()) {
3999
+ async function adoptWorkspace(client, directory, call2 = {}, now = /* @__PURE__ */ new Date()) {
3994
4000
  const dir = (0, import_node_path3.resolve)(directory);
3995
4001
  const existing = await getWorkspaceMapping(dir);
3996
4002
  if (existing) return existing;
@@ -3999,13 +4005,13 @@ async function adoptWorkspace(client, directory, call = {}, now = /* @__PURE__ *
3999
4005
  if (!a) throw new PublisherError("LOCAL_STATE", "this folder is not associated with a published repository; run init first");
4000
4006
  const branch = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
4001
4007
  if (!branch) throw new PublisherError("LOCAL_STATE", "HEAD is detached; check out the branch this folder publishes, then run sync again");
4002
- const repo = await client.getRepository(a.repoId, call);
4008
+ const repo = await client.getRepository(a.repoId, call2);
4003
4009
  const remoteOid = repo.refs[`refs/heads/${branch}`] ?? null;
4004
4010
  const tip = await revParse(dir, `refs/heads/${branch}`);
4005
4011
  let baseOid = null;
4006
4012
  let baseGeneration = 0;
4007
4013
  if (remoteOid) {
4008
- if (!await objectExists(dir, remoteOid)) await client.fetch({ repoDir: dir, repoId: a.repoId, remote: a.remote }, call);
4014
+ if (!await objectExists(dir, remoteOid)) await client.fetch({ repoDir: dir, repoId: a.repoId, remote: a.remote }, call2);
4009
4015
  const haveRemote = await objectExists(dir, remoteOid);
4010
4016
  if (!tip) {
4011
4017
  baseOid = haveRemote ? remoteOid : null;
@@ -4064,15 +4070,15 @@ async function removeStalePartials(target, repoId) {
4064
4070
  }
4065
4071
  return removed;
4066
4072
  }
4067
- async function checkoutWorkspace(client, opts, call = {}, now = /* @__PURE__ */ new Date()) {
4073
+ async function checkoutWorkspace(client, opts, call2 = {}, now = /* @__PURE__ */ new Date()) {
4068
4074
  const target = (0, import_node_path3.resolve)(opts.directory);
4069
4075
  const remote = opts.remote ?? "publisher";
4070
4076
  const empty2 = await isEmptyDir(target);
4071
4077
  if (empty2 === false) throw new PublisherError("LOCAL_STATE", `${target} exists and is not empty; checkout never overlays an existing folder`);
4072
4078
  await (0, import_promises4.mkdir)((0, import_node_path3.dirname)(target), { recursive: true });
4073
4079
  const removed = await removeStalePartials(target, opts.repoId);
4074
- if (removed.length) call.onProgress?.({ phase: "checkout", message: `removed ${removed.length} interrupted checkout(s)` });
4075
- const repo = await client.getRepository(opts.repoId, call);
4080
+ if (removed.length) call2.onProgress?.({ phase: "checkout", message: `removed ${removed.length} interrupted checkout(s)` });
4081
+ const repo = await client.getRepository(opts.repoId, call2);
4076
4082
  const branch = opts.branch ?? repo.head.symbolic.replace(/^refs\/heads\//, "");
4077
4083
  const tip = repo.refs[`refs/heads/${branch}`] ?? null;
4078
4084
  if (!tip && Object.keys(repo.refs).length > 0) throw new PublisherError("LOCAL_STATE", `branch ${branch} does not exist on the server`);
@@ -4083,7 +4089,7 @@ async function checkoutWorkspace(client, opts, call = {}, now = /* @__PURE__ */
4083
4089
  await runGit(["init", "-q", `--initial-branch=${branch}`, partial2]);
4084
4090
  await writeAssociation(partial2, { endpoint: client.endpoint, repoId: opts.repoId, remote });
4085
4091
  if (tip) {
4086
- await client.fetch({ repoDir: partial2, repoId: opts.repoId, remote }, call);
4092
+ await client.fetch({ repoDir: partial2, repoId: opts.repoId, remote }, call2);
4087
4093
  const tracking = `refs/remotes/${remote}/${branch}`;
4088
4094
  const have = await revParse(partial2, tracking);
4089
4095
  if (have !== tip) throw new PublisherError("LOCAL_STATE", `fetched ${tracking} is ${have ?? "missing"}, expected ${tip}; retry the checkout`);
@@ -4121,7 +4127,7 @@ async function countCommits(dir, range) {
4121
4127
  const r = await runGit(["rev-list", "--count", range], { cwd: dir, allowFailure: true });
4122
4128
  return r.code === 0 ? Number(r.stdout.toString("utf8").trim()) : 0;
4123
4129
  }
4124
- async function inspectWorkspace(client, directory, call = {}) {
4130
+ async function inspectWorkspace(client, directory, call2 = {}) {
4125
4131
  const dir = (0, import_node_path3.resolve)(directory);
4126
4132
  const mapping = await getWorkspaceMapping(dir);
4127
4133
  const pending = await listPending(dir).catch(() => []);
@@ -4144,7 +4150,7 @@ async function inspectWorkspace(client, directory, call = {}) {
4144
4150
  const hasLocalWork = dirty || untrackedPaths.length > 0;
4145
4151
  const checkedOut = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
4146
4152
  const local = { branch: checkedOut, tip, head, dirty, untracked: untrackedPaths.length, untrackedPaths: untrackedPaths.slice(0, 10), commitsAheadOfBase: 0 };
4147
- const repo = await client.getRepository(mapping.repoId, call);
4153
+ const repo = await client.getRepository(mapping.repoId, call2);
4148
4154
  const remoteOid = repo.refs[branchRef] ?? null;
4149
4155
  const remote = { oid: remoteOid, generation: repo.generation };
4150
4156
  if (mapping.baseOid && tip) local.commitsAheadOfBase = await countCommits(dir, `${mapping.baseOid}..${tip}`);
@@ -4162,7 +4168,7 @@ async function inspectWorkspace(client, directory, call = {}) {
4162
4168
  }
4163
4169
  if (!tip) return done("remote_ahead", hasLocalWork ? `local branch is unborn and there are ${work}; move them aside, then sync` : "local branch is unborn; sync to fast-forward");
4164
4170
  if (!await objectExists(dir, remoteOid)) {
4165
- await client.fetch({ repoDir: dir, repoId: mapping.repoId, remote: mapping.remote }, call);
4171
+ await client.fetch({ repoDir: dir, repoId: mapping.repoId, remote: mapping.remote }, call2);
4166
4172
  if (!await objectExists(dir, remoteOid)) return done("remote_ahead", "server tip could not be fetched; retry");
4167
4173
  }
4168
4174
  if (remoteOid === tip)
@@ -4189,10 +4195,10 @@ async function inspectWorkspace(client, directory, call = {}) {
4189
4195
  merge2.conflicts.map((c) => c.path)
4190
4196
  );
4191
4197
  }
4192
- async function syncWorkspace(client, opts, call = {}, now = /* @__PURE__ */ new Date()) {
4198
+ async function syncWorkspace(client, opts, call2 = {}, now = /* @__PURE__ */ new Date()) {
4193
4199
  const dir = (0, import_node_path3.resolve)(opts.directory);
4194
- await adoptWorkspace(client, dir, call, now);
4195
- const before = await inspectWorkspace(client, dir, call);
4200
+ await adoptWorkspace(client, dir, call2, now);
4201
+ const before = await inspectWorkspace(client, dir, call2);
4196
4202
  if (!before.mapping) throw new PublisherError("LOCAL_STATE", "not a workspace; run checkout");
4197
4203
  const mapping = before.mapping;
4198
4204
  const branchRef = `refs/heads/${mapping.branch}`;
@@ -4216,7 +4222,7 @@ async function syncWorkspace(client, opts, call = {}, now = /* @__PURE__ */ new
4216
4222
  updated = await advanceWorkspaceBase(dir, { oid: before.remote.oid, generation: before.remote.generation }, now);
4217
4223
  }
4218
4224
  }
4219
- const after = applied ? (await inspectWorkspace(client, dir, call)).state : before.state;
4225
+ const after = applied ? (await inspectWorkspace(client, dir, call2)).state : before.state;
4220
4226
  return { before, applied, mapping: updated, after };
4221
4227
  }
4222
4228
 
@@ -4668,6 +4674,7 @@ async function followRun(input, opts) {
4668
4674
  seen.set(entry.stage, entry.state);
4669
4675
  opts.onStage?.(entry);
4670
4676
  }
4677
+ if (isRecord(run.run) && run.run["status"] === "awaiting-approval") return { kind: "waiting", stages: read.stages };
4671
4678
  const end = ending(read.stages);
4672
4679
  if (end !== null) return { kind: "settled", ending: end, failedAt: read.stages.find((s) => s.state === "failed")?.word ?? null, stages: read.stages };
4673
4680
  }
@@ -4729,6 +4736,87 @@ function isRecord(value) {
4729
4736
  return typeof value === "object" && value !== null && !Array.isArray(value);
4730
4737
  }
4731
4738
 
4739
+ // ../client/src/preparation.ts
4740
+ var PREPARATION = "preparation";
4741
+ var STATUSES = ["queued", "running", "needs-choice", "ready", "blocked", "cancelled"];
4742
+ function readPreparation(input, opts) {
4743
+ return call(input, opts, void 0);
4744
+ }
4745
+ function answerPreparation(input, opts) {
4746
+ return call(input, opts, { id: input.id, revision: input.revision, action: "answers", answers: input.answers });
4747
+ }
4748
+ async function call(input, opts, body) {
4749
+ const fetchImpl = opts.fetch ?? fetch;
4750
+ const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/repositories/${encodeURIComponent(input.repoId)}/preparation`;
4751
+ let refreshed = false;
4752
+ for (; ; ) {
4753
+ const answer = await ask2(fetchImpl, url2, await opts.token(), body);
4754
+ if (answer.status === 401 && !refreshed && opts.refresh !== void 0) {
4755
+ refreshed = true;
4756
+ if (await opts.refresh()) continue;
4757
+ }
4758
+ if (answer.status === 401) return { kind: "unauthorized" };
4759
+ if (answer.status === 403) return { kind: "forbidden" };
4760
+ if (answer.status === 409) return { kind: "moved-on" };
4761
+ if (answer.status === 404) {
4762
+ const code = isRecord2(answer.body) ? answer.body["error"] : void 0;
4763
+ return code === "no-such-application" ? { kind: "no-application" } : { kind: "unavailable", detail: "the portal does not serve this address" };
4764
+ }
4765
+ if (answer.status === 200) {
4766
+ const view = readView(answer.body);
4767
+ return "detail" in view ? { kind: "malformed", detail: view.detail } : { kind: "view", view };
4768
+ }
4769
+ return { kind: "unavailable", detail: answer.status === 0 ? "the portal could not be reached" : `the portal answered ${answer.status}` };
4770
+ }
4771
+ }
4772
+ function readView(body) {
4773
+ if (!isRecord2(body)) return { detail: "an answer that is not a preparation" };
4774
+ const { id, revision, status, message, questions, publication } = body;
4775
+ if (typeof id !== "string" || id === "") return { detail: "a preparation with no id" };
4776
+ if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1) return { detail: "a preparation with no revision" };
4777
+ if (!STATUSES.includes(status)) return { detail: "a preparation with an unknown status" };
4778
+ if (typeof message !== "string") return { detail: "a preparation with no message" };
4779
+ if (!Array.isArray(questions)) return { detail: "a preparation with no list of questions" };
4780
+ const read = [];
4781
+ for (const q of questions) {
4782
+ if (!isRecord2(q) || typeof q["id"] !== "string" || typeof q["text"] !== "string" || !Array.isArray(q["options"]) || q["options"].length === 0) {
4783
+ return { detail: "a question that cannot be answered" };
4784
+ }
4785
+ const options = [];
4786
+ for (const o of q["options"]) {
4787
+ if (!isRecord2(o) || typeof o["value"] !== "string" || typeof o["label"] !== "string") return { detail: "a question with an unreadable option" };
4788
+ options.push({ value: o["value"], label: o["label"] });
4789
+ }
4790
+ read.push({ id: q["id"], text: q["text"], options });
4791
+ }
4792
+ let pub = null;
4793
+ if (isRecord2(publication) && typeof publication["status"] === "string" && typeof publication["message"] === "string") {
4794
+ pub = { status: publication["status"], message: publication["message"], ...typeof publication["url"] === "string" ? { url: publication["url"] } : {} };
4795
+ }
4796
+ return { id, revision, status, message, questions: read, publication: pub };
4797
+ }
4798
+ async function ask2(fetchImpl, url2, token, body) {
4799
+ try {
4800
+ const res = await fetchImpl(url2, {
4801
+ method: body === void 0 ? "GET" : "POST",
4802
+ headers: { authorization: `Bearer ${token}`, accept: "application/json", ...body === void 0 ? {} : { "content-type": "application/json" } },
4803
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
4804
+ signal: AbortSignal.timeout(3e4)
4805
+ });
4806
+ const text = await res.text();
4807
+ try {
4808
+ return { status: res.status, body: text ? JSON.parse(text) : null };
4809
+ } catch {
4810
+ return { status: res.status, body: void 0 };
4811
+ }
4812
+ } catch {
4813
+ return { status: 0, body: null };
4814
+ }
4815
+ }
4816
+ function isRecord2(value) {
4817
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4818
+ }
4819
+
4732
4820
  // ../client/src/onboard.ts
4733
4821
  var import_promises6 = require("node:fs/promises");
4734
4822
  var import_node_path5 = require("node:path");
@@ -4968,13 +5056,13 @@ async function uploadPack(fetchImpl, upload, packPath, size, opts) {
4968
5056
  }
4969
5057
 
4970
5058
  // ../client/src/publish.ts
4971
- async function publish(ctx, repoId, remote, opts, call) {
4972
- const progress = call.onProgress ?? (() => void 0);
5059
+ async function publish(ctx, repoId, remote, opts, call2) {
5060
+ const progress = call2.onProgress ?? (() => void 0);
4973
5061
  const { repoDir } = opts;
4974
5062
  const store2 = await openPending(repoDir);
4975
5063
  const pendings = await store2.list();
4976
5064
  progress({ phase: "resolve", message: "reading repository state" });
4977
- const repo = await ctx.getRepository(repoId, call);
5065
+ const repo = await ctx.getRepository(repoId, call2);
4978
5066
  const specs = opts.updates ?? [await defaultUpdate(repoDir)];
4979
5067
  let refUpdates = [];
4980
5068
  let nothingNew = null;
@@ -4990,9 +5078,9 @@ async function publish(ctx, repoId, remote, opts, call) {
4990
5078
  const sameIntent = !nothingNew && pending2.baseGeneration === repo.generation && canonicalJson(refUpdates) === canonicalJson(pending2.refUpdates);
4991
5079
  if (sameIntent && (pending2.pack === null || await packFileUsable(pending2))) {
4992
5080
  progress({ phase: "resolve", message: `resuming publication ${pending2.publicationId}` });
4993
- return runPublication(ctx, repoId, remote, repoDir, store2, pending2, opts, call, true);
5081
+ return runPublication(ctx, repoId, remote, repoDir, store2, pending2, opts, call2, true);
4994
5082
  }
4995
- const outcome = await resolvePending(ctx, repoDir, remote, pending2, call, store2);
5083
+ const outcome = await resolvePending(ctx, repoDir, remote, pending2, call2, store2);
4996
5084
  if (outcome.state === "finalizing" || outcome.state === "unknown") {
4997
5085
  throw new PublisherError("OUTCOME_UNKNOWN", `publication ${pending2.publicationId} is still being processed; run status before publishing again`, {
4998
5086
  retryable: true,
@@ -5000,7 +5088,7 @@ async function publish(ctx, repoId, remote, opts, call) {
5000
5088
  });
5001
5089
  }
5002
5090
  if (outcome.state === "accepted") {
5003
- const fresh = await ctx.getRepository(repoId, call);
5091
+ const fresh = await ctx.getRepository(repoId, call2);
5004
5092
  let remaining = [];
5005
5093
  try {
5006
5094
  remaining = await resolveUpdates(repoDir, fresh, specs);
@@ -5008,7 +5096,7 @@ async function publish(ctx, repoId, remote, opts, call) {
5008
5096
  if (!(e instanceof PublisherError && e.code === "LOCAL_STATE")) throw e;
5009
5097
  }
5010
5098
  if (remaining.length === 0) return toOutcome(outcome, true, pending2.refUpdates);
5011
- return publish(ctx, repoId, remote, opts, call);
5099
+ return publish(ctx, repoId, remote, opts, call2);
5012
5100
  }
5013
5101
  }
5014
5102
  if (nothingNew) throw nothingNew;
@@ -5022,7 +5110,7 @@ async function publish(ctx, repoId, remote, opts, call) {
5022
5110
  const exclude = [];
5023
5111
  for (const info of await batchCheck(repoDir, remoteOids)) if (info.type !== "missing") exclude.push(info.oid);
5024
5112
  const outPath = store2.packPath(publicationId);
5025
- const built = await buildPack(repoDir, { include: wants, exclude, outPath, ...call.signal ? { signal: call.signal } : {} });
5113
+ const built = await buildPack(repoDir, { include: wants, exclude, outPath, ...call2.signal ? { signal: call2.signal } : {} });
5026
5114
  pack = { sha256: built.sha256, size: built.size, path: outPath };
5027
5115
  progress({ phase: "pack", message: `packed ${built.objectCount} objects (${built.size} bytes)`, bytes: built.size, total: built.size });
5028
5116
  }
@@ -5038,7 +5126,7 @@ async function publish(ctx, repoId, remote, opts, call) {
5038
5126
  finalized: false
5039
5127
  };
5040
5128
  await store2.write(pending);
5041
- return runPublication(ctx, repoId, remote, repoDir, store2, pending, opts, call, false);
5129
+ return runPublication(ctx, repoId, remote, repoDir, store2, pending, opts, call2, false);
5042
5130
  }
5043
5131
  async function packFileUsable(p) {
5044
5132
  if (!p.pack) return true;
@@ -5049,9 +5137,9 @@ async function packFileUsable(p) {
5049
5137
  return false;
5050
5138
  }
5051
5139
  }
5052
- async function runPublication(ctx, repoId, remote, repoDir, store2, pending, opts, call, resumed) {
5140
+ async function runPublication(ctx, repoId, remote, repoDir, store2, pending, opts, call2, resumed) {
5053
5141
  try {
5054
- return await sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts, call, resumed);
5142
+ return await sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts, call2, resumed);
5055
5143
  } catch (e) {
5056
5144
  throw unconfirmed(e, pending.publicationId);
5057
5145
  }
@@ -5066,8 +5154,8 @@ function unconfirmed(e, publicationId) {
5066
5154
  details: { publicationId, cause: e.code }
5067
5155
  });
5068
5156
  }
5069
- async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts, call, resumed) {
5070
- const progress = call.onProgress ?? (() => void 0);
5157
+ async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts, call2, resumed) {
5158
+ const progress = call2.onProgress ?? (() => void 0);
5071
5159
  const beginBody = {
5072
5160
  publicationId: pending.publicationId,
5073
5161
  baseGeneration: pending.baseGeneration,
@@ -5078,32 +5166,32 @@ async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts,
5078
5166
  let summary = null;
5079
5167
  if (!pending.finalized) {
5080
5168
  progress({ phase: "begin", message: "starting publication" });
5081
- let begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody, call)).body;
5169
+ let begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody, call2)).body;
5082
5170
  summary = begin.publication;
5083
5171
  if (begin.publication.state === "created") {
5084
5172
  if (begin.upload && pending.pack) {
5085
5173
  progress({ phase: "upload", message: "uploading pack", bytes: 0, total: pending.pack.size });
5086
- let result = await uploadPack(ctx.fetchImpl, begin.upload, pending.pack.path, pending.pack.size, call);
5174
+ let result = await uploadPack(ctx.fetchImpl, begin.upload, pending.pack.path, pending.pack.size, call2);
5087
5175
  if (result === "expired") {
5088
- begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody, call)).body;
5176
+ begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody, call2)).body;
5089
5177
  if (!begin.upload) throw new PublisherError("UPLOAD_MISSING", "server did not return fresh upload instructions");
5090
- result = await uploadPack(ctx.fetchImpl, begin.upload, pending.pack.path, pending.pack.size, call);
5178
+ result = await uploadPack(ctx.fetchImpl, begin.upload, pending.pack.path, pending.pack.size, call2);
5091
5179
  if (result === "expired") throw new PublisherError("NETWORK", "upload authorization expired twice", { retryable: true });
5092
5180
  }
5093
5181
  }
5094
5182
  progress({ phase: "finalize", message: "finalizing" });
5095
- const fin = await ctx.transport.request("POST", `/v1/repos/${repoId}/publications/${pending.publicationId}/finalize`, {}, call);
5183
+ const fin = await ctx.transport.request("POST", `/v1/repos/${repoId}/publications/${pending.publicationId}/finalize`, {}, call2);
5096
5184
  summary = fin.body.publication;
5097
5185
  }
5098
5186
  pending.finalized = true;
5099
5187
  await store2.write(pending);
5100
5188
  }
5101
5189
  if (opts.wait === false) {
5102
- summary = summary ?? await ctx.getStatus(repoId, pending.publicationId, call);
5190
+ summary = summary ?? await ctx.getStatus(repoId, pending.publicationId, call2);
5103
5191
  return toOutcome(summary, resumed, pending.refUpdates);
5104
5192
  }
5105
5193
  progress({ phase: "validate", message: "waiting for validation" });
5106
- summary = await waitForOutcome(ctx, repoId, pending.publicationId, opts.waitTimeoutMs ?? 10 * 6e4, call);
5194
+ summary = await waitForOutcome(ctx, repoId, pending.publicationId, opts.waitTimeoutMs ?? 10 * 6e4, call2);
5107
5195
  if (summary.state === "accepted") {
5108
5196
  await applyTrackingRefs(repoDir, remote, pending.refUpdates);
5109
5197
  await store2.remove(pending.publicationId);
@@ -5114,23 +5202,23 @@ async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts,
5114
5202
  }
5115
5203
  return toOutcome(summary, resumed, pending.refUpdates);
5116
5204
  }
5117
- async function waitForOutcome(ctx, repoId, publicationId, timeoutMs, call) {
5205
+ async function waitForOutcome(ctx, repoId, publicationId, timeoutMs, call2) {
5118
5206
  const deadline = Date.now() + timeoutMs;
5119
5207
  let delay2 = 500;
5120
5208
  for (; ; ) {
5121
- const s = await ctx.getStatus(repoId, publicationId, { ...call, waitSeconds: MAX_WAIT_SECONDS });
5209
+ const s = await ctx.getStatus(repoId, publicationId, { ...call2, waitSeconds: MAX_WAIT_SECONDS });
5122
5210
  if (s.state === "accepted" || s.state === "rejected") return s;
5123
5211
  if (Date.now() > deadline) {
5124
5212
  throw new PublisherError("OUTCOME_UNKNOWN", `publication ${publicationId} has not completed yet; run status later`, { retryable: true, details: { publicationId } });
5125
5213
  }
5126
- await sleep(delay2, call.signal);
5214
+ await sleep(delay2, call2.signal);
5127
5215
  delay2 = Math.min(delay2 * 2, 5e3);
5128
5216
  }
5129
5217
  }
5130
- async function resolvePending(ctx, repoDir, remote, pending, call, store2) {
5218
+ async function resolvePending(ctx, repoDir, remote, pending, call2, store2) {
5131
5219
  let summary;
5132
5220
  try {
5133
- summary = await ctx.getStatus(pending.repoId, pending.publicationId, call);
5221
+ summary = await ctx.getStatus(pending.repoId, pending.publicationId, call2);
5134
5222
  } catch (e) {
5135
5223
  if (e instanceof PublisherError && e.code === "PUBLICATION_NOT_FOUND") {
5136
5224
  await store2.remove(pending.publicationId);
@@ -5240,13 +5328,13 @@ async function writeManifest(gd, installed) {
5240
5328
  await (0, import_promises8.mkdir)((0, import_node_path6.join)(gd, "publisher"), { recursive: true });
5241
5329
  await (0, import_promises8.writeFile)((0, import_node_path6.join)(gd, "publisher", "installed.json"), JSON.stringify(installed));
5242
5330
  }
5243
- async function fetchRepository(ctx, repoId, opts, call) {
5331
+ async function fetchRepository(ctx, repoId, opts, call2) {
5244
5332
  const remote = opts.remote ?? "publisher";
5245
- const progress = call.onProgress ?? (() => void 0);
5333
+ const progress = call2.onProgress ?? (() => void 0);
5246
5334
  progress({ phase: "fetch", message: "describing repository" });
5247
5335
  const [, { body }, gd] = await Promise.all([
5248
5336
  ensureRemote(opts.repoDir, remote, `${ctx.transport.endpoint}/v1/repos/${repoId}`),
5249
- ctx.transport.request("GET", `/v1/repos/${repoId}/bundle`, void 0, call),
5337
+ ctx.transport.request("GET", `/v1/repos/${repoId}/bundle`, void 0, call2),
5250
5338
  gitDir(opts.repoDir)
5251
5339
  ]);
5252
5340
  const artifacts = [...body.base ? [{ kind: "bundle", ...body.base }] : [], ...body.packs.map((p) => ({ kind: "pack", ...p }))];
@@ -5257,14 +5345,14 @@ async function fetchRepository(ctx, repoId, opts, call) {
5257
5345
  ctx,
5258
5346
  gd,
5259
5347
  artifacts.filter((a) => !installed.has(a.sha256)),
5260
- call
5348
+ call2
5261
5349
  );
5262
5350
  if (!await haveAll(opts.repoDir, body.refs)) {
5263
5351
  bytes += await install(
5264
5352
  ctx,
5265
5353
  gd,
5266
5354
  artifacts.filter((a) => installed.has(a.sha256)),
5267
- call
5355
+ call2
5268
5356
  );
5269
5357
  if (!await haveAll(opts.repoDir, body.refs)) throw new PublisherError("NETWORK", "the downloaded artifacts do not contain every advertised ref", { retryable: true });
5270
5358
  }
@@ -5282,16 +5370,16 @@ async function haveAll(repoDir, refs) {
5282
5370
  if (oids.size === 0) return true;
5283
5371
  return (await batchCheck(repoDir, oids)).every((o) => o.type !== "missing");
5284
5372
  }
5285
- async function install(ctx, gd, artifacts, call) {
5373
+ async function install(ctx, gd, artifacts, call2) {
5286
5374
  if (artifacts.length === 0) return 0;
5287
- const progress = call.onProgress ?? (() => void 0);
5375
+ const progress = call2.onProgress ?? (() => void 0);
5288
5376
  const tmp = await (0, import_promises8.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "gitpub-fetch-"));
5289
5377
  let bytes = 0;
5290
5378
  const total = artifacts.reduce((sum, a) => sum + a.size, 0);
5291
5379
  try {
5292
5380
  for (const a of artifacts) {
5293
5381
  const file = (0, import_node_path6.join)(tmp, `${a.sha256}.${a.kind}`);
5294
- bytes += await download(ctx.fetchImpl, a.url, file, a.sha256, a.size, call);
5382
+ bytes += await download(ctx.fetchImpl, a.url, file, a.sha256, a.size, call2);
5295
5383
  progress({ phase: "fetch", message: `installing ${a.kind}`, bytes, total });
5296
5384
  if (a.kind === "bundle") {
5297
5385
  await bundleUnbundleObjects(gd, file);
@@ -5322,10 +5410,10 @@ async function updateRefs(repoDir, remote, refs) {
5322
5410
  await refTransaction(repoDir, updates);
5323
5411
  return tracking;
5324
5412
  }
5325
- async function download(fetchImpl, url2, dest, sha256, size, call) {
5413
+ async function download(fetchImpl, url2, dest, sha256, size, call2) {
5326
5414
  let res;
5327
5415
  try {
5328
- res = await fetchImpl(url2, call.signal ? { signal: call.signal } : {});
5416
+ res = await fetchImpl(url2, call2.signal ? { signal: call2.signal } : {});
5329
5417
  } catch (e) {
5330
5418
  throw new PublisherError("NETWORK", `download failed: ${e.message}`, { retryable: true, cause: e });
5331
5419
  }
@@ -5336,7 +5424,7 @@ async function download(fetchImpl, url2, dest, sha256, size, call) {
5336
5424
  transform(chunk, controller) {
5337
5425
  n += chunk.byteLength;
5338
5426
  hash.update(chunk);
5339
- call.onProgress?.({ phase: "fetch", message: "downloading", bytes: n, total: size });
5427
+ call2.onProgress?.({ phase: "fetch", message: "downloading", bytes: n, total: size });
5340
5428
  controller.enqueue(chunk);
5341
5429
  }
5342
5430
  });
@@ -5392,11 +5480,29 @@ var Publisher = class {
5392
5480
  fetch: this.fetchImpl
5393
5481
  });
5394
5482
  }
5395
- async getCapabilities(call = {}) {
5396
- const r = await this.transport.request("GET", "/v1/capabilities", void 0, call);
5483
+ /**
5484
+ * The question the repository's current preparation waits on, and its answer, at the portal that
5485
+ * serves this endpoint — with this publisher's credential and its one refresh, as `followRun`.
5486
+ * The caller confirms the portal's `/config` lists `PREPARATION` first.
5487
+ */
5488
+ async readPreparation(input) {
5489
+ return readPreparation(input, this.portalCall());
5490
+ }
5491
+ async answerPreparation(input) {
5492
+ return answerPreparation(input, this.portalCall());
5493
+ }
5494
+ portalCall() {
5495
+ return {
5496
+ token: this.tokenFn,
5497
+ refresh: async () => !this.opts.token && refreshStoredCredential(this.endpoint, { fetch: this.fetchImpl }),
5498
+ fetch: this.fetchImpl
5499
+ };
5500
+ }
5501
+ async getCapabilities(call2 = {}) {
5502
+ const r = await this.transport.request("GET", "/v1/capabilities", void 0, call2);
5397
5503
  return r.body;
5398
5504
  }
5399
- async createRepository(options = {}, call = {}) {
5505
+ async createRepository(options = {}, call2 = {}) {
5400
5506
  const body = {
5401
5507
  creationId: options.creationId ?? newCreationId(),
5402
5508
  name: options.name ?? "",
@@ -5404,29 +5510,29 @@ var Publisher = class {
5404
5510
  objectFormat: "sha1",
5405
5511
  grants: options.grants ?? []
5406
5512
  };
5407
- const r = await this.transport.request("POST", "/v1/repos", body, call);
5513
+ const r = await this.transport.request("POST", "/v1/repos", body, call2);
5408
5514
  return r.body.repository;
5409
5515
  }
5410
- async getRepository(repoId, call = {}) {
5411
- const r = await this.transport.request("GET", `/v1/repos/${encodeURIComponent(repoId)}`, void 0, call);
5516
+ async getRepository(repoId, call2 = {}) {
5517
+ const r = await this.transport.request("GET", `/v1/repos/${encodeURIComponent(repoId)}`, void 0, call2);
5412
5518
  return r.body.repository;
5413
5519
  }
5414
5520
  /** One page of the repositories the caller created (ADR-0011). An authenticated route, unlike capabilities. */
5415
- async listRepositories(options = {}, call = {}) {
5521
+ async listRepositories(options = {}, call2 = {}) {
5416
5522
  const q = new URLSearchParams();
5417
5523
  if (options.state) q.set("state", options.state);
5418
5524
  if (options.limit !== void 0) q.set("limit", String(options.limit));
5419
5525
  if (options.cursor) q.set("cursor", options.cursor);
5420
5526
  const qs = q.toString() ? `?${q.toString()}` : "";
5421
- return (await this.transport.request("GET", `/v1/repos${qs}`, void 0, call)).body;
5527
+ return (await this.transport.request("GET", `/v1/repos${qs}`, void 0, call2)).body;
5422
5528
  }
5423
- async getPublicationStatus(repoId, publicationId, call = {}) {
5424
- const q = call.waitSeconds ? `?wait=${Math.min(call.waitSeconds, MAX_WAIT_SECONDS)}` : "";
5529
+ async getPublicationStatus(repoId, publicationId, call2 = {}) {
5530
+ const q = call2.waitSeconds ? `?wait=${Math.min(call2.waitSeconds, MAX_WAIT_SECONDS)}` : "";
5425
5531
  const r = await this.transport.request(
5426
5532
  "GET",
5427
5533
  `/v1/repos/${encodeURIComponent(repoId)}/publications/${encodeURIComponent(publicationId)}${q}`,
5428
5534
  void 0,
5429
- call
5535
+ call2
5430
5536
  );
5431
5537
  return r.body.publication;
5432
5538
  }
@@ -5453,32 +5559,32 @@ var Publisher = class {
5453
5559
  return { repoId: a.repoId, remote: a.remote };
5454
5560
  }
5455
5561
  /** Publish existing local commits. Never modifies the worktree; safe to retry and resume. */
5456
- async publish(options, call = {}) {
5562
+ async publish(options, call2 = {}) {
5457
5563
  const { repoId, remote } = await this.resolveRepo(options.repoDir, options.repoId);
5458
- const outcome = await publish(this.publishContext(), repoId, remote, options, call);
5564
+ const outcome = await publish(this.publishContext(), repoId, remote, options, call2);
5459
5565
  await noteAcceptedPublication(options.repoDir, outcome);
5460
5566
  return outcome;
5461
5567
  }
5462
5568
  // ---- workspaces (docs/workspace-parity.md) ------------------------------------------------
5463
5569
  /** Create a NEW editable workspace (clone + mapping) for a published repository. */
5464
- async checkout(options, call = {}) {
5465
- return checkoutWorkspace(this, options, call);
5570
+ async checkout(options, call2 = {}) {
5571
+ return checkoutWorkspace(this, options, call2);
5466
5572
  }
5467
5573
  /** Make an associated folder a workspace (idempotent); `init` and `sync` both call it. */
5468
- async adoptWorkspace(directory, call = {}) {
5469
- return adoptWorkspace(this, directory, call);
5574
+ async adoptWorkspace(directory, call2 = {}) {
5575
+ return adoptWorkspace(this, directory, call2);
5470
5576
  }
5471
5577
  /** Offline: the persisted workspace mapping, or null. */
5472
5578
  async workspace(directory) {
5473
5579
  return getWorkspaceMapping(directory);
5474
5580
  }
5475
5581
  /** Where the workspace stands relative to the server (decided on real objects). */
5476
- async workspaceStatus(directory, call = {}) {
5477
- return inspectWorkspace(this, directory, call);
5582
+ async workspaceStatus(directory, call2 = {}) {
5583
+ return inspectWorkspace(this, directory, call2);
5478
5584
  }
5479
5585
  /** Fetch and, with `apply`, fast-forward or merge cleanly; advances the base. */
5480
- async sync(options, call = {}) {
5481
- return syncWorkspace(this, options, call);
5586
+ async sync(options, call2 = {}) {
5587
+ return syncWorkspace(this, options, call2);
5482
5588
  }
5483
5589
  /**
5484
5590
  * A creator's whole round trip in one call, so nobody runs git: record their changes, bring in
@@ -5490,7 +5596,7 @@ var Publisher = class {
5490
5596
  * a commit and then an error. `syncWorkspace` itself still never commits or publishes; this is
5491
5597
  * the composition, and the only one.
5492
5598
  */
5493
- async syncChanges(options, call = {}) {
5599
+ async syncChanges(options, call2 = {}) {
5494
5600
  const dir = options.directory;
5495
5601
  await this.resolveRepo(dir);
5496
5602
  const branch = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
@@ -5499,7 +5605,7 @@ var Publisher = class {
5499
5605
  if (mapped && mapped.branch !== branch)
5500
5606
  throw new PublisherError("LOCAL_STATE", `branch ${branch} is checked out, but this folder publishes ${mapped.branch}; check it out, then run sync again`);
5501
5607
  const recorded = await recordChanges({ dir, ...options.message ? { message: options.message } : {} });
5502
- const reconciled = await syncWorkspace(this, { directory: dir, apply: true }, call);
5608
+ const reconciled = await syncWorkspace(this, { directory: dir, apply: true }, call2);
5503
5609
  const base = { recorded: { commit: recorded.commit, committed: recorded.committed, excluded: recorded.excluded }, reconciled };
5504
5610
  if (reconciled.after === "conflict") {
5505
5611
  const remote = reconciled.before.remote?.oid ?? null;
@@ -5508,25 +5614,25 @@ var Publisher = class {
5508
5614
  }
5509
5615
  const tip = await revParse(dir, `refs/heads/${branch}`);
5510
5616
  if (tip === null || tip === (reconciled.before.remote?.oid ?? null)) return { ...base, state: "up_to_date", published: null, marked: false };
5511
- const published = await this.publish({ repoDir: dir }, call);
5617
+ const published = await this.publish({ repoDir: dir }, call2);
5512
5618
  return { ...base, state: published.state === "accepted" ? "published" : published.state === "rejected" ? "rejected" : "unknown", published, marked: false };
5513
5619
  }
5514
5620
  /** Resolve interrupted publications recorded locally without creating duplicates. */
5515
- async resumePublications(repoDir, call = {}) {
5621
+ async resumePublications(repoDir, call2 = {}) {
5516
5622
  const { remote } = await this.resolveRepo(repoDir);
5517
5623
  const out = [];
5518
5624
  const store2 = await openPending(repoDir);
5519
5625
  for (const p of await store2.list()) {
5520
5626
  if (p.endpoint !== this.endpoint) continue;
5521
5627
  if (!p.finalized) {
5522
- out.push(await publish(this.publishContext(), p.repoId, remote, { repoDir, publicationId: p.publicationId }, call).catch((e) => rethrowUnlessResolvable(e, p)));
5628
+ out.push(await publish(this.publishContext(), p.repoId, remote, { repoDir, publicationId: p.publicationId }, call2).catch((e) => rethrowUnlessResolvable(e, p)));
5523
5629
  continue;
5524
5630
  }
5525
- const deadline = Date.now() + (call.waitTimeoutMs ?? 5 * 6e4);
5526
- let s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...call, waitSeconds: MAX_WAIT_SECONDS }, store2);
5631
+ const deadline = Date.now() + (call2.waitTimeoutMs ?? 5 * 6e4);
5632
+ let s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...call2, waitSeconds: MAX_WAIT_SECONDS }, store2);
5527
5633
  while (s.state === "finalizing" && Date.now() < deadline) {
5528
- call.onProgress?.({ phase: "validate", message: `waiting for ${p.publicationId}` });
5529
- s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...call, waitSeconds: MAX_WAIT_SECONDS }, store2);
5634
+ call2.onProgress?.({ phase: "validate", message: `waiting for ${p.publicationId}` });
5635
+ s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...call2, waitSeconds: MAX_WAIT_SECONDS }, store2);
5530
5636
  }
5531
5637
  out.push(toOutcome(s, true, p.refUpdates));
5532
5638
  }
@@ -5541,17 +5647,17 @@ var Publisher = class {
5541
5647
  * branches. Every read is a presigned download of the repository's bundle and packs (ADR-0013);
5542
5648
  * artifacts already installed are not downloaded again.
5543
5649
  */
5544
- async fetch(options, call = {}) {
5650
+ async fetch(options, call2 = {}) {
5545
5651
  const { repoId } = await this.resolveRepo(options.repoDir, options.repoId);
5546
- return fetchRepository({ transport: this.transport, fetchImpl: this.fetchImpl }, repoId, options, call);
5652
+ return fetchRepository({ transport: this.transport, fetchImpl: this.fetchImpl }, repoId, options, call2);
5547
5653
  }
5548
5654
  publishContext() {
5549
5655
  return {
5550
5656
  transport: this.transport,
5551
5657
  fetchImpl: this.fetchImpl,
5552
5658
  clientInfo: this.clientInfo,
5553
- getRepository: (repoId, call) => this.getRepository(repoId, call),
5554
- getStatus: (repoId, publicationId, call) => this.getPublicationStatus(repoId, publicationId, call)
5659
+ getRepository: (repoId, call2) => this.getRepository(repoId, call2),
5660
+ getStatus: (repoId, publicationId, call2) => this.getPublicationStatus(repoId, publicationId, call2)
5555
5661
  };
5556
5662
  }
5557
5663
  };
@@ -5717,7 +5823,7 @@ function resolveChoice(answer, names) {
5717
5823
  }
5718
5824
  return names.find((n) => n.toLowerCase() === a.toLowerCase()) ?? null;
5719
5825
  }
5720
- async function ask2(question) {
5826
+ async function ask3(question) {
5721
5827
  const rl = (0, import_promises10.createInterface)({ input: process.stdin, output: process.stderr });
5722
5828
  try {
5723
5829
  return await rl.question(question);
@@ -5735,7 +5841,7 @@ ${purpose}
5735
5841
  names.forEach((n, i) => process.stderr.write(` ${i + 1}) ${n}
5736
5842
  `));
5737
5843
  for (let attempt = 0; attempt < 3; attempt++) {
5738
- const answer = await ask2(`
5844
+ const answer = await ask3(`
5739
5845
  Which one? [1-${names.length}, or a name] `);
5740
5846
  if (answer === null) return null;
5741
5847
  const chosen = resolveChoice(answer, names);
@@ -5751,7 +5857,7 @@ Name this connection, so you never have to type ${endpoint} again.
5751
5857
  (for example: prod, dev, staging)
5752
5858
  `);
5753
5859
  for (let attempt = 0; attempt < 3; attempt++) {
5754
- const answer = await ask2(suggested ? `
5860
+ const answer = await ask3(suggested ? `
5755
5861
  Profile name [${suggested}]: ` : `
5756
5862
  Profile name: `);
5757
5863
  if (answer === null) return null;
@@ -5874,8 +5980,9 @@ async function followPublication(p, endpoint, o, opts) {
5874
5980
  { portal, repoId: o.repoId, publicationId: o.publicationId, ref, commit },
5875
5981
  { onStage: opts.onStage, ...opts.timeoutMs === void 0 ? {} : { timeoutMs: opts.timeoutMs } }
5876
5982
  );
5877
- const next = outcome.kind === "timed-out" ? { action: "follow", command: `publisher follow --publication ${o.publicationId} --json` } : void 0;
5878
- return { ...outcome, portal, hint: progressHint(outcome, portal), ...next === void 0 ? {} : { next } };
5983
+ const answerable = confirmed.terminal.includes(PREPARATION);
5984
+ const next = outcome.kind === "timed-out" ? { action: "follow", command: `publisher follow --publication ${o.publicationId} --json` } : outcome.kind === "waiting" && answerable ? { action: "question", command: "publisher question --json" } : void 0;
5985
+ return { ...outcome, portal, hint: progressHint(outcome, portal, answerable), ...next === void 0 ? {} : { next } };
5879
5986
  }
5880
5987
  function followTimeoutMs(flag) {
5881
5988
  if (flag === void 0) return FOLLOW_TIMEOUT_MS;
@@ -5888,8 +5995,10 @@ function stageLine(e, json) {
5888
5995
  function inPortal(portal, lead, verb) {
5889
5996
  return `${lead} ${verb} in the portal${portal === null ? "" : `: ${portal}`}.`;
5890
5997
  }
5891
- function progressHint(r, portal) {
5998
+ function progressHint(r, portal, answerable = false) {
5892
5999
  switch (r.kind) {
6000
+ case "waiting":
6001
+ return answerable ? "It is waiting for an answer from you. Run publisher question to see it." : inPortal(portal, "It is waiting for an answer from you.", "Answer it");
5893
6002
  case "settled":
5894
6003
  switch (r.ending) {
5895
6004
  case "live":
@@ -5915,6 +6024,78 @@ function progressHint(r, portal) {
5915
6024
  }
5916
6025
  }
5917
6026
 
6027
+ // src/question.ts
6028
+ async function questionAt(p, endpoint, repoId, opts = {}) {
6029
+ const at = await portalAt(endpoint, opts);
6030
+ if ("reason" in at) return at;
6031
+ return shown(at.portal, await p.readPreparation({ portal: at.portal, repoId }));
6032
+ }
6033
+ async function answerAt(p, endpoint, repoId, specs, opts = {}) {
6034
+ const answers = parseSpecs(specs);
6035
+ const at = await portalAt(endpoint, opts);
6036
+ if ("reason" in at) return at;
6037
+ const read = shown(at.portal, await p.readPreparation({ portal: at.portal, repoId }));
6038
+ if (read.kind !== "view") return read;
6039
+ if (read.questions.length === 0) throw new PublisherError("LOCAL_STATE", `nothing is waiting for an answer (the preparation is ${read.preparation.status})`);
6040
+ for (const [id, value] of Object.entries(answers)) {
6041
+ const question = read.questions.find((q) => q.id === id);
6042
+ if (question === void 0) {
6043
+ throw new PublisherError("INVALID_REQUEST", `no pending question "${id}"; pending: ${read.questions.map((q) => q.id).join(", ")}`, {
6044
+ details: { questions: read.questions }
6045
+ });
6046
+ }
6047
+ const values = question.options.map((o) => o.value);
6048
+ if (!values.includes(value)) {
6049
+ throw new PublisherError("INVALID_REQUEST", `"${value}" is not one of ${values.join(", ")} for "${id}"`, { details: { questions: read.questions } });
6050
+ }
6051
+ }
6052
+ return shown(at.portal, await p.answerPreparation({ portal: at.portal, repoId, id: read.preparation.id, revision: read.preparation.revision, answers }));
6053
+ }
6054
+ function parseSpecs(specs) {
6055
+ if (specs.length === 0) throw new PublisherError("INVALID_REQUEST", "answer takes one or more question=value");
6056
+ const answers = {};
6057
+ for (const spec of specs) {
6058
+ const at = spec.indexOf("=");
6059
+ if (at <= 0 || at === spec.length - 1) throw new PublisherError("INVALID_REQUEST", `"${spec}" is not question=value`);
6060
+ answers[spec.slice(0, at)] = spec.slice(at + 1);
6061
+ }
6062
+ return answers;
6063
+ }
6064
+ async function portalAt(endpoint, opts) {
6065
+ const derived = portalForEndpoint(endpoint);
6066
+ const confirmed = derived === null ? null : await confirmPortal(derived, endpoint, opts.fetch);
6067
+ if (derived === null || confirmed === null) return { kind: "unavailable", reason: "no-portal", portal: null, hint: "Answer it in the portal." };
6068
+ if (!confirmed.terminal.includes(PREPARATION)) {
6069
+ return { kind: "unavailable", reason: "no-route", portal: derived, hint: `This portal cannot show the question in the terminal yet. Answer it in the portal: ${derived}.` };
6070
+ }
6071
+ return { portal: derived };
6072
+ }
6073
+ function shown(portal, r) {
6074
+ switch (r.kind) {
6075
+ case "view": {
6076
+ const { questions, ...preparation } = r.view;
6077
+ return { kind: "view", portal, preparation, questions, hint: hintFor(r.view) };
6078
+ }
6079
+ case "no-application":
6080
+ throw new PublisherError("NOT_FOUND", "this repository has no application in your organisation yet; publish it first");
6081
+ case "moved-on":
6082
+ throw new PublisherError("UNAVAILABLE", "the preparation moved on before the answer arrived; run publisher question again", { retryable: true });
6083
+ case "unauthorized":
6084
+ throw new PublisherError("UNAUTHORIZED", "the portal did not accept this sign-in; run publisher login");
6085
+ case "forbidden":
6086
+ throw new PublisherError("FORBIDDEN", "the portal will not let this sign-in answer for this organisation (an owner or admin can)");
6087
+ case "unavailable":
6088
+ throw new PublisherError("UNAVAILABLE", `the portal could not be asked: ${r.detail}`, { retryable: true });
6089
+ case "malformed":
6090
+ throw new PublisherError("UNAVAILABLE", `the portal's answer could not be read: ${r.detail}`);
6091
+ }
6092
+ }
6093
+ function hintFor(view) {
6094
+ if (view.questions.length === 0) return "Nothing is waiting for an answer.";
6095
+ const how = view.questions.map((q) => `${q.id}=<${q.options.map((o) => o.value).join("|")}>`).join(" ");
6096
+ return `It is waiting for your answer. Run: publisher answer ${how}`;
6097
+ }
6098
+
5918
6099
  // src/main.ts
5919
6100
  var USAGE = `publisher \u2014 publish an app's Git history to the serverless Git backend
5920
6101
 
@@ -5951,6 +6132,10 @@ Usage:
5951
6132
  result's next.command picks it up
5952
6133
  publisher follow [dir] --publication <id> follow an accepted publication's run where a sync left it
5953
6134
  [--timeout <seconds>]
6135
+ publisher question [dir] [--repo <id>] the question the app's preparation waits on, if any,
6136
+ with each option's value (answering needs owner or admin)
6137
+ publisher answer <question>=<value>... [--repo <id>] answer it from this folder; a value that is not one of
6138
+ the question's options is refused before anything is sent
5954
6139
  publisher merge-check [dir] --ours <ref> --theirs <ref> [--apply]
5955
6140
  publisher info [dir] repository state as seen by the server
5956
6141
  publisher doctor environment check (node, git, credentials, association)
@@ -6322,6 +6507,27 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
6322
6507
  out({ publicationId, progress: await followPublication(p, endpoint, publication, { onStage: emitStage, log, timeoutMs }) });
6323
6508
  return 0;
6324
6509
  }
6510
+ /**
6511
+ * The business question the app's current preparation waits on, from this folder's repository.
6512
+ * Exit 0 whether or not one is pending: `questions` says which. An outage is exit 3, never "none".
6513
+ */
6514
+ case "question": {
6515
+ const dir = dirArg(1);
6516
+ const { p, endpoint } = await client(dir);
6517
+ const repoId = str2(flags["repo"]) ?? (await readAssociation(dir))?.repoId;
6518
+ if (!repoId) throw new PublisherError("LOCAL_STATE", "no repository association; pass --repo");
6519
+ out({ repoId, ...await questionAt(p, endpoint, repoId) });
6520
+ return 0;
6521
+ }
6522
+ /** Answer it, as `question=value` pairs, re-reading the preparation first. */
6523
+ case "answer": {
6524
+ const dir = (0, import_node_path8.resolve)(".");
6525
+ const { p, endpoint } = await client(dir);
6526
+ const repoId = str2(flags["repo"]) ?? (await readAssociation(dir))?.repoId;
6527
+ if (!repoId) throw new PublisherError("LOCAL_STATE", "no repository association; pass --repo");
6528
+ out({ repoId, ...await answerAt(p, endpoint, repoId, cmd.slice(1)) });
6529
+ return 0;
6530
+ }
6325
6531
  case "skills": {
6326
6532
  if (cmd[1] !== "install") throw new PublisherError("INVALID_REQUEST", `unknown skills command: ${cmd[1] ?? "(none)"}; expected install`);
6327
6533
  const r = await installSkills({ log });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnovate/shadow-publisher",
3
- "version": "0.6.3",
3
+ "version": "0.7.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",