@minnovate/shadow-publisher 0.6.3 → 0.8.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.8.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"]
@@ -56,24 +56,16 @@ changing their default.
56
56
 
57
57
  ## 2. First publication of a folder
58
58
 
59
- Run in the app folder (the folder the user is working in):
59
+ There is no separate first step: run step 3 in the app folder, adding `--name "<app name>"` the
60
+ first time. A folder that is not published yet gets its application and repository from that same
61
+ `sync` (the portal makes the application first, in one request), and is then published.
60
62
 
61
- ```bash
62
- publisher info --json # exit 1 with LOCAL_STATE "not associated" means: not published yet
63
- ```
64
-
65
- If not associated:
66
-
67
- ```bash
68
- publisher init . --name "<app name>" --json
69
- ```
70
-
71
- `init` prepares the folder with an explicit file selection: it excludes `.git`, `node_modules`,
72
- build caches, `.env*`, keys/certificates and files larger than 50 MB, and it respects
73
- `.gitignore`. Show the user the `onboarding.excluded` list so they know what was left out (the
74
- exclusion rules are a safety net, not a secret scanner). Then publish it with step 3.
63
+ The first `sync` prepares the folder with an explicit file selection: it excludes `.git`,
64
+ `node_modules`, build caches, `.env*`, keys/certificates and files larger than 50 MB, and it
65
+ respects `.gitignore`. Show the user the `linked` result and the excluded files it reports so they
66
+ know what was left out (the exclusion rules are a safety net, not a secret scanner).
75
67
 
76
- ## 3. Every update — and the first publish after `init`
68
+ ## 3. Every publish, the first one included
77
69
 
78
70
  ```bash
79
71
  publisher sync . --message "<a few words on what changed>" --timeout 90 --json
@@ -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:
@@ -97,6 +97,11 @@ var ERROR_CODES = [
97
97
  // code rather than FORBIDDEN: a caller can act on it (sign in again, choosing an organisation),
98
98
  // and a test that could not tell it from "you may not" would prove nothing about the check.
99
99
  "ORGANISATION_REQUIRED",
100
+ // A repository is created only by the platform's application maker (the portal), for a person,
101
+ // after it has made the application the repository will belong to. Its own code: a terminal that
102
+ // meets it is an older client calling this route directly, and can say "upgrade" rather than
103
+ // "you may not".
104
+ "APPLICATION_REQUIRED",
100
105
  // repositories
101
106
  "REPOSITORY_NOT_FOUND",
102
107
  "REPOSITORY_CONFLICT",
@@ -139,6 +144,7 @@ var HTTP_STATUS_FOR_CODE = {
139
144
  FORBIDDEN: 403,
140
145
  REF_FORBIDDEN: 403,
141
146
  ORGANISATION_REQUIRED: 403,
147
+ APPLICATION_REQUIRED: 403,
142
148
  REPOSITORY_NOT_FOUND: 404,
143
149
  REPOSITORY_CONFLICT: 409,
144
150
  REPOSITORY_ARCHIVED: 409,
@@ -2809,6 +2815,12 @@ var PrincipalRecord = object({
2809
2815
  principal: PrincipalId,
2810
2816
  disabled: boolean2(),
2811
2817
  disabledAt: _default(nullable(IsoDate), null),
2818
+ /**
2819
+ * When the credentials tenant made the change this record reflects (ADR-0017), and what a later
2820
+ * event is ordered against: EventBridge does not order deliveries, and a late enable must not undo
2821
+ * a newer disable. Null on a record written by hand, which any event supersedes.
2822
+ */
2823
+ changedAt: _default(nullable(IsoDate), null),
2812
2824
  note: _default(nullable(string2().check(_maxLength(256))), null)
2813
2825
  });
2814
2826
 
@@ -2848,6 +2860,8 @@ var CreateRepositoryRequest = object({
2848
2860
  objectFormat: _default(ObjectFormat, "sha1"),
2849
2861
  grants: _default(array(Grant).check(_maxLength(200)), [])
2850
2862
  });
2863
+ var RepositoryIdRequest = object({ creationId: CreationId });
2864
+ var RepositoryIdResponse = object({ repoId: RepoId });
2851
2865
  var RepositoryInfo = object({
2852
2866
  repoId: RepoId,
2853
2867
  name: string2(),
@@ -3585,12 +3599,12 @@ async function resolveRevs(dir, revs, opts = {}) {
3585
3599
  const out = revs.map(
3586
3600
  (r) => r.startsWith("-") ? null : r.includes("\0") ? new PublisherError("INVALID_REQUEST", "NUL in git argument") : null
3587
3601
  );
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 });
3602
+ const ask4 = revs.flatMap((r, i) => out[i] === null && !r.startsWith("-") ? [i] : []);
3603
+ if (ask4.length === 0) return out;
3604
+ const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input: ask4.map((i) => revs[i]).join("\0") + "\0", ...opts });
3591
3605
  const records = stdout.toString("utf8").split("\0");
3592
3606
  const misses = [];
3593
- for (const [j, i] of ask3.entries()) {
3607
+ for (const [j, i] of ask4.entries()) {
3594
3608
  const record2 = records[j] ?? "";
3595
3609
  if (record2 && !/ (missing|ambiguous)$/.test(record2)) out[i] = record2;
3596
3610
  else if (!revs[i].includes("^{")) misses.push(i);
@@ -3990,7 +4004,7 @@ async function noteAcceptedPublication(directory, outcome, now = /* @__PURE__ */
3990
4004
  if (!oid) return;
3991
4005
  await advanceWorkspaceBase(directory, { oid, generation: outcome.resultGeneration }, now);
3992
4006
  }
3993
- async function adoptWorkspace(client, directory, call = {}, now = /* @__PURE__ */ new Date()) {
4007
+ async function adoptWorkspace(client, directory, call2 = {}, now = /* @__PURE__ */ new Date()) {
3994
4008
  const dir = (0, import_node_path3.resolve)(directory);
3995
4009
  const existing = await getWorkspaceMapping(dir);
3996
4010
  if (existing) return existing;
@@ -3999,13 +4013,13 @@ async function adoptWorkspace(client, directory, call = {}, now = /* @__PURE__ *
3999
4013
  if (!a) throw new PublisherError("LOCAL_STATE", "this folder is not associated with a published repository; run init first");
4000
4014
  const branch = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
4001
4015
  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);
4016
+ const repo = await client.getRepository(a.repoId, call2);
4003
4017
  const remoteOid = repo.refs[`refs/heads/${branch}`] ?? null;
4004
4018
  const tip = await revParse(dir, `refs/heads/${branch}`);
4005
4019
  let baseOid = null;
4006
4020
  let baseGeneration = 0;
4007
4021
  if (remoteOid) {
4008
- if (!await objectExists(dir, remoteOid)) await client.fetch({ repoDir: dir, repoId: a.repoId, remote: a.remote }, call);
4022
+ if (!await objectExists(dir, remoteOid)) await client.fetch({ repoDir: dir, repoId: a.repoId, remote: a.remote }, call2);
4009
4023
  const haveRemote = await objectExists(dir, remoteOid);
4010
4024
  if (!tip) {
4011
4025
  baseOid = haveRemote ? remoteOid : null;
@@ -4064,15 +4078,15 @@ async function removeStalePartials(target, repoId) {
4064
4078
  }
4065
4079
  return removed;
4066
4080
  }
4067
- async function checkoutWorkspace(client, opts, call = {}, now = /* @__PURE__ */ new Date()) {
4081
+ async function checkoutWorkspace(client, opts, call2 = {}, now = /* @__PURE__ */ new Date()) {
4068
4082
  const target = (0, import_node_path3.resolve)(opts.directory);
4069
4083
  const remote = opts.remote ?? "publisher";
4070
4084
  const empty2 = await isEmptyDir(target);
4071
4085
  if (empty2 === false) throw new PublisherError("LOCAL_STATE", `${target} exists and is not empty; checkout never overlays an existing folder`);
4072
4086
  await (0, import_promises4.mkdir)((0, import_node_path3.dirname)(target), { recursive: true });
4073
4087
  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);
4088
+ if (removed.length) call2.onProgress?.({ phase: "checkout", message: `removed ${removed.length} interrupted checkout(s)` });
4089
+ const repo = await client.getRepository(opts.repoId, call2);
4076
4090
  const branch = opts.branch ?? repo.head.symbolic.replace(/^refs\/heads\//, "");
4077
4091
  const tip = repo.refs[`refs/heads/${branch}`] ?? null;
4078
4092
  if (!tip && Object.keys(repo.refs).length > 0) throw new PublisherError("LOCAL_STATE", `branch ${branch} does not exist on the server`);
@@ -4083,7 +4097,7 @@ async function checkoutWorkspace(client, opts, call = {}, now = /* @__PURE__ */
4083
4097
  await runGit(["init", "-q", `--initial-branch=${branch}`, partial2]);
4084
4098
  await writeAssociation(partial2, { endpoint: client.endpoint, repoId: opts.repoId, remote });
4085
4099
  if (tip) {
4086
- await client.fetch({ repoDir: partial2, repoId: opts.repoId, remote }, call);
4100
+ await client.fetch({ repoDir: partial2, repoId: opts.repoId, remote }, call2);
4087
4101
  const tracking = `refs/remotes/${remote}/${branch}`;
4088
4102
  const have = await revParse(partial2, tracking);
4089
4103
  if (have !== tip) throw new PublisherError("LOCAL_STATE", `fetched ${tracking} is ${have ?? "missing"}, expected ${tip}; retry the checkout`);
@@ -4121,7 +4135,7 @@ async function countCommits(dir, range) {
4121
4135
  const r = await runGit(["rev-list", "--count", range], { cwd: dir, allowFailure: true });
4122
4136
  return r.code === 0 ? Number(r.stdout.toString("utf8").trim()) : 0;
4123
4137
  }
4124
- async function inspectWorkspace(client, directory, call = {}) {
4138
+ async function inspectWorkspace(client, directory, call2 = {}) {
4125
4139
  const dir = (0, import_node_path3.resolve)(directory);
4126
4140
  const mapping = await getWorkspaceMapping(dir);
4127
4141
  const pending = await listPending(dir).catch(() => []);
@@ -4144,7 +4158,7 @@ async function inspectWorkspace(client, directory, call = {}) {
4144
4158
  const hasLocalWork = dirty || untrackedPaths.length > 0;
4145
4159
  const checkedOut = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
4146
4160
  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);
4161
+ const repo = await client.getRepository(mapping.repoId, call2);
4148
4162
  const remoteOid = repo.refs[branchRef] ?? null;
4149
4163
  const remote = { oid: remoteOid, generation: repo.generation };
4150
4164
  if (mapping.baseOid && tip) local.commitsAheadOfBase = await countCommits(dir, `${mapping.baseOid}..${tip}`);
@@ -4162,7 +4176,7 @@ async function inspectWorkspace(client, directory, call = {}) {
4162
4176
  }
4163
4177
  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
4178
  if (!await objectExists(dir, remoteOid)) {
4165
- await client.fetch({ repoDir: dir, repoId: mapping.repoId, remote: mapping.remote }, call);
4179
+ await client.fetch({ repoDir: dir, repoId: mapping.repoId, remote: mapping.remote }, call2);
4166
4180
  if (!await objectExists(dir, remoteOid)) return done("remote_ahead", "server tip could not be fetched; retry");
4167
4181
  }
4168
4182
  if (remoteOid === tip)
@@ -4189,10 +4203,10 @@ async function inspectWorkspace(client, directory, call = {}) {
4189
4203
  merge2.conflicts.map((c) => c.path)
4190
4204
  );
4191
4205
  }
4192
- async function syncWorkspace(client, opts, call = {}, now = /* @__PURE__ */ new Date()) {
4206
+ async function syncWorkspace(client, opts, call2 = {}, now = /* @__PURE__ */ new Date()) {
4193
4207
  const dir = (0, import_node_path3.resolve)(opts.directory);
4194
- await adoptWorkspace(client, dir, call, now);
4195
- const before = await inspectWorkspace(client, dir, call);
4208
+ await adoptWorkspace(client, dir, call2, now);
4209
+ const before = await inspectWorkspace(client, dir, call2);
4196
4210
  if (!before.mapping) throw new PublisherError("LOCAL_STATE", "not a workspace; run checkout");
4197
4211
  const mapping = before.mapping;
4198
4212
  const branchRef = `refs/heads/${mapping.branch}`;
@@ -4216,7 +4230,7 @@ async function syncWorkspace(client, opts, call = {}, now = /* @__PURE__ */ new
4216
4230
  updated = await advanceWorkspaceBase(dir, { oid: before.remote.oid, generation: before.remote.generation }, now);
4217
4231
  }
4218
4232
  }
4219
- const after = applied ? (await inspectWorkspace(client, dir, call)).state : before.state;
4233
+ const after = applied ? (await inspectWorkspace(client, dir, call2)).state : before.state;
4220
4234
  return { before, applied, mapping: updated, after };
4221
4235
  }
4222
4236
 
@@ -4493,6 +4507,69 @@ async function updateEntry(url2, change) {
4493
4507
  await writeCredentials(file);
4494
4508
  }
4495
4509
 
4510
+ // ../client/src/application.ts
4511
+ var APPLICATION_CREATION = "application-creation";
4512
+ var ATTEMPTS = 3;
4513
+ var REFUSALS = {
4514
+ "repository-limit": "LIMIT_EXCEEDED",
4515
+ "repository-conflict": "REPOSITORY_CONFLICT"
4516
+ };
4517
+ async function createApplication(input, opts) {
4518
+ const fetchImpl = opts.fetch ?? fetch;
4519
+ const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/applications`;
4520
+ const body = { creationId: input.creationId ?? newCreationId(), name: input.name, defaultBranch: input.defaultBranch ?? "main" };
4521
+ let refreshed = false;
4522
+ let lastOutage = "";
4523
+ for (let attempt = 0; attempt < ATTEMPTS; ) {
4524
+ const answer = await post(fetchImpl, url2, await opts.token(), body);
4525
+ if (answer.status === 401 && !refreshed && opts.refresh !== void 0) {
4526
+ refreshed = true;
4527
+ if (await opts.refresh()) continue;
4528
+ }
4529
+ if (answer.status === 200 || answer.status === 201) return read(answer.body, answer.status === 201);
4530
+ const code = isRecord(answer.body) && typeof answer.body["error"] === "string" ? answer.body["error"] : "";
4531
+ if (answer.status === 401) throw new PublisherError("UNAUTHORIZED", 'the portal did not accept this credential; run "publisher login" again');
4532
+ if (answer.status === 403) throw new PublisherError("FORBIDDEN", `the portal refused to make an application here (${code || "forbidden"})`);
4533
+ if (answer.status === 409 && REFUSALS[code] !== void 0)
4534
+ throw new PublisherError(REFUSALS[code], `the portal refused the repository (${code})`, { details: { portal: code } });
4535
+ if (answer.status === 400 || answer.status === 409)
4536
+ throw new PublisherError("INVALID_REQUEST", `the portal refused the request (${code || answer.status})`, { details: { portal: code } });
4537
+ lastOutage = answer.status === 0 ? "the portal could not be reached" : `the portal answered ${answer.status}`;
4538
+ attempt += 1;
4539
+ if (attempt < ATTEMPTS) await new Promise((resolve3) => setTimeout(resolve3, (opts.retryDelayMs ?? 1e3) * attempt));
4540
+ }
4541
+ throw new PublisherError("UNAVAILABLE", `could not make the application: ${lastOutage}; run init again`, { retryable: true });
4542
+ }
4543
+ function read(body, created) {
4544
+ const application = isRecord(body) ? body["application"] : void 0;
4545
+ const repository = RepositoryInfo.safeParse(isRecord(body) ? body["repository"] : void 0);
4546
+ if (!isRecord(application) || typeof application["appId"] !== "string" || !repository.success) {
4547
+ throw new PublisherError("UNAVAILABLE", "the portal answered with an application this version cannot read");
4548
+ }
4549
+ return { appId: application["appId"], created, repository: repository.data };
4550
+ }
4551
+ async function post(fetchImpl, url2, token, body) {
4552
+ try {
4553
+ const res = await fetchImpl(url2, {
4554
+ method: "POST",
4555
+ headers: { authorization: `Bearer ${token}`, accept: "application/json", "content-type": "application/json" },
4556
+ body: JSON.stringify(body),
4557
+ signal: AbortSignal.timeout(3e4)
4558
+ });
4559
+ const text = await res.text();
4560
+ try {
4561
+ return { status: res.status, body: text ? JSON.parse(text) : null };
4562
+ } catch {
4563
+ return { status: res.status, body: void 0 };
4564
+ }
4565
+ } catch {
4566
+ return { status: 0, body: null };
4567
+ }
4568
+ }
4569
+ function isRecord(value) {
4570
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4571
+ }
4572
+
4496
4573
  // ../client/src/transport.ts
4497
4574
  function assertCredentialEndpoint(endpoint) {
4498
4575
  let url2;
@@ -4609,14 +4686,14 @@ var RUN_STAGES = ["received", "running", "deploying", "live"];
4609
4686
  var STAGE_WORDS = { received: "received", running: "checked", deploying: "going live", live: "live" };
4610
4687
  var STATES = ["pending", "in-progress", "done", "failed", "skipped"];
4611
4688
  function readStages(run) {
4612
- if (!isRecord(run)) return { malformed: "a run that is not an object" };
4689
+ if (!isRecord2(run)) return { malformed: "a run that is not an object" };
4613
4690
  if (!Object.hasOwn(run, "stages")) return null;
4614
4691
  const stages = run["stages"];
4615
4692
  if (!Array.isArray(stages) || stages.length !== RUN_STAGES.length) return { malformed: "stages that are not exactly four" };
4616
4693
  const out = [];
4617
4694
  for (const [index, entry] of stages.entries()) {
4618
4695
  const stage = RUN_STAGES[index];
4619
- if (!isRecord(entry) || entry["stage"] !== stage) return { malformed: `stage ${index + 1} is not ${stage}` };
4696
+ if (!isRecord2(entry) || entry["stage"] !== stage) return { malformed: `stage ${index + 1} is not ${stage}` };
4620
4697
  const state = entry["state"];
4621
4698
  if (!STATES.includes(state)) return { malformed: `stage ${stage} has an unknown state` };
4622
4699
  const at = entry["at"];
@@ -4659,17 +4736,18 @@ async function followRun(input, opts) {
4659
4736
  const run = soleRun(answer.body);
4660
4737
  if ("detail" in run) return { kind: "malformed", detail: run.detail };
4661
4738
  if (run.run !== null) {
4662
- const read = readStages(run.run);
4663
- if (read === null) return { kind: "unavailable", reason: "no-stages" };
4664
- if ("malformed" in read) return { kind: "malformed", detail: read.malformed };
4665
- last = read.stages;
4666
- for (const entry of read.stages) {
4739
+ const read2 = readStages(run.run);
4740
+ if (read2 === null) return { kind: "unavailable", reason: "no-stages" };
4741
+ if ("malformed" in read2) return { kind: "malformed", detail: read2.malformed };
4742
+ last = read2.stages;
4743
+ for (const entry of read2.stages) {
4667
4744
  if (entry.state === "pending" || seen.get(entry.stage) === entry.state) continue;
4668
4745
  seen.set(entry.stage, entry.state);
4669
4746
  opts.onStage?.(entry);
4670
4747
  }
4671
- const end = ending(read.stages);
4672
- if (end !== null) return { kind: "settled", ending: end, failedAt: read.stages.find((s) => s.state === "failed")?.word ?? null, stages: read.stages };
4748
+ if (isRecord2(run.run) && run.run["status"] === "awaiting-approval") return { kind: "waiting", stages: read2.stages };
4749
+ const end = ending(read2.stages);
4750
+ if (end !== null) return { kind: "settled", ending: end, failedAt: read2.stages.find((s) => s.state === "failed")?.word ?? null, stages: read2.stages };
4673
4751
  }
4674
4752
  lastError = null;
4675
4753
  } else {
@@ -4680,7 +4758,7 @@ async function followRun(input, opts) {
4680
4758
  }
4681
4759
  }
4682
4760
  function soleRun(body) {
4683
- if (!isRecord(body) || !Array.isArray(body["runs"])) return { detail: "an answer with no list of runs" };
4761
+ if (!isRecord2(body) || !Array.isArray(body["runs"])) return { detail: "an answer with no list of runs" };
4684
4762
  const runs = body["runs"];
4685
4763
  if (runs.length > 1) return { detail: "more than the one run this publication started" };
4686
4764
  return { run: runs[0] ?? null };
@@ -4725,7 +4803,88 @@ async function confirmPortal(portal, endpoint, fetchImpl = fetch) {
4725
4803
  return null;
4726
4804
  }
4727
4805
  }
4728
- function isRecord(value) {
4806
+ function isRecord2(value) {
4807
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4808
+ }
4809
+
4810
+ // ../client/src/preparation.ts
4811
+ var PREPARATION = "preparation";
4812
+ var STATUSES = ["queued", "running", "needs-choice", "ready", "blocked", "cancelled"];
4813
+ function readPreparation(input, opts) {
4814
+ return call(input, opts, void 0);
4815
+ }
4816
+ function answerPreparation(input, opts) {
4817
+ return call(input, opts, { id: input.id, revision: input.revision, action: "answers", answers: input.answers });
4818
+ }
4819
+ async function call(input, opts, body) {
4820
+ const fetchImpl = opts.fetch ?? fetch;
4821
+ const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/repositories/${encodeURIComponent(input.repoId)}/preparation`;
4822
+ let refreshed = false;
4823
+ for (; ; ) {
4824
+ const answer = await ask2(fetchImpl, url2, await opts.token(), body);
4825
+ if (answer.status === 401 && !refreshed && opts.refresh !== void 0) {
4826
+ refreshed = true;
4827
+ if (await opts.refresh()) continue;
4828
+ }
4829
+ if (answer.status === 401) return { kind: "unauthorized" };
4830
+ if (answer.status === 403) return { kind: "forbidden" };
4831
+ if (answer.status === 409) return { kind: "moved-on" };
4832
+ if (answer.status === 404) {
4833
+ const code = isRecord3(answer.body) ? answer.body["error"] : void 0;
4834
+ return code === "no-such-application" ? { kind: "no-application" } : { kind: "unavailable", detail: "the portal does not serve this address" };
4835
+ }
4836
+ if (answer.status === 200) {
4837
+ const view = readView(answer.body);
4838
+ return "detail" in view ? { kind: "malformed", detail: view.detail } : { kind: "view", view };
4839
+ }
4840
+ return { kind: "unavailable", detail: answer.status === 0 ? "the portal could not be reached" : `the portal answered ${answer.status}` };
4841
+ }
4842
+ }
4843
+ function readView(body) {
4844
+ if (!isRecord3(body)) return { detail: "an answer that is not a preparation" };
4845
+ const { id, revision, status, message, questions, publication } = body;
4846
+ if (typeof id !== "string" || id === "") return { detail: "a preparation with no id" };
4847
+ if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1) return { detail: "a preparation with no revision" };
4848
+ if (!STATUSES.includes(status)) return { detail: "a preparation with an unknown status" };
4849
+ if (typeof message !== "string") return { detail: "a preparation with no message" };
4850
+ if (!Array.isArray(questions)) return { detail: "a preparation with no list of questions" };
4851
+ const read2 = [];
4852
+ for (const q of questions) {
4853
+ if (!isRecord3(q) || typeof q["id"] !== "string" || typeof q["text"] !== "string" || !Array.isArray(q["options"]) || q["options"].length === 0) {
4854
+ return { detail: "a question that cannot be answered" };
4855
+ }
4856
+ const options = [];
4857
+ for (const o of q["options"]) {
4858
+ if (!isRecord3(o) || typeof o["value"] !== "string" || typeof o["label"] !== "string") return { detail: "a question with an unreadable option" };
4859
+ options.push({ value: o["value"], label: o["label"] });
4860
+ }
4861
+ read2.push({ id: q["id"], text: q["text"], options });
4862
+ }
4863
+ let pub = null;
4864
+ if (isRecord3(publication) && typeof publication["status"] === "string" && typeof publication["message"] === "string") {
4865
+ pub = { status: publication["status"], message: publication["message"], ...typeof publication["url"] === "string" ? { url: publication["url"] } : {} };
4866
+ }
4867
+ return { id, revision, status, message, questions: read2, publication: pub };
4868
+ }
4869
+ async function ask2(fetchImpl, url2, token, body) {
4870
+ try {
4871
+ const res = await fetchImpl(url2, {
4872
+ method: body === void 0 ? "GET" : "POST",
4873
+ headers: { authorization: `Bearer ${token}`, accept: "application/json", ...body === void 0 ? {} : { "content-type": "application/json" } },
4874
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
4875
+ signal: AbortSignal.timeout(3e4)
4876
+ });
4877
+ const text = await res.text();
4878
+ try {
4879
+ return { status: res.status, body: text ? JSON.parse(text) : null };
4880
+ } catch {
4881
+ return { status: res.status, body: void 0 };
4882
+ }
4883
+ } catch {
4884
+ return { status: 0, body: null };
4885
+ }
4886
+ }
4887
+ function isRecord3(value) {
4729
4888
  return typeof value === "object" && value !== null && !Array.isArray(value);
4730
4889
  }
4731
4890
 
@@ -4968,13 +5127,13 @@ async function uploadPack(fetchImpl, upload, packPath, size, opts) {
4968
5127
  }
4969
5128
 
4970
5129
  // ../client/src/publish.ts
4971
- async function publish(ctx, repoId, remote, opts, call) {
4972
- const progress = call.onProgress ?? (() => void 0);
5130
+ async function publish(ctx, repoId, remote, opts, call2) {
5131
+ const progress = call2.onProgress ?? (() => void 0);
4973
5132
  const { repoDir } = opts;
4974
5133
  const store2 = await openPending(repoDir);
4975
5134
  const pendings = await store2.list();
4976
5135
  progress({ phase: "resolve", message: "reading repository state" });
4977
- const repo = await ctx.getRepository(repoId, call);
5136
+ const repo = await ctx.getRepository(repoId, call2);
4978
5137
  const specs = opts.updates ?? [await defaultUpdate(repoDir)];
4979
5138
  let refUpdates = [];
4980
5139
  let nothingNew = null;
@@ -4990,9 +5149,9 @@ async function publish(ctx, repoId, remote, opts, call) {
4990
5149
  const sameIntent = !nothingNew && pending2.baseGeneration === repo.generation && canonicalJson(refUpdates) === canonicalJson(pending2.refUpdates);
4991
5150
  if (sameIntent && (pending2.pack === null || await packFileUsable(pending2))) {
4992
5151
  progress({ phase: "resolve", message: `resuming publication ${pending2.publicationId}` });
4993
- return runPublication(ctx, repoId, remote, repoDir, store2, pending2, opts, call, true);
5152
+ return runPublication(ctx, repoId, remote, repoDir, store2, pending2, opts, call2, true);
4994
5153
  }
4995
- const outcome = await resolvePending(ctx, repoDir, remote, pending2, call, store2);
5154
+ const outcome = await resolvePending(ctx, repoDir, remote, pending2, call2, store2);
4996
5155
  if (outcome.state === "finalizing" || outcome.state === "unknown") {
4997
5156
  throw new PublisherError("OUTCOME_UNKNOWN", `publication ${pending2.publicationId} is still being processed; run status before publishing again`, {
4998
5157
  retryable: true,
@@ -5000,7 +5159,7 @@ async function publish(ctx, repoId, remote, opts, call) {
5000
5159
  });
5001
5160
  }
5002
5161
  if (outcome.state === "accepted") {
5003
- const fresh = await ctx.getRepository(repoId, call);
5162
+ const fresh = await ctx.getRepository(repoId, call2);
5004
5163
  let remaining = [];
5005
5164
  try {
5006
5165
  remaining = await resolveUpdates(repoDir, fresh, specs);
@@ -5008,7 +5167,7 @@ async function publish(ctx, repoId, remote, opts, call) {
5008
5167
  if (!(e instanceof PublisherError && e.code === "LOCAL_STATE")) throw e;
5009
5168
  }
5010
5169
  if (remaining.length === 0) return toOutcome(outcome, true, pending2.refUpdates);
5011
- return publish(ctx, repoId, remote, opts, call);
5170
+ return publish(ctx, repoId, remote, opts, call2);
5012
5171
  }
5013
5172
  }
5014
5173
  if (nothingNew) throw nothingNew;
@@ -5022,7 +5181,7 @@ async function publish(ctx, repoId, remote, opts, call) {
5022
5181
  const exclude = [];
5023
5182
  for (const info of await batchCheck(repoDir, remoteOids)) if (info.type !== "missing") exclude.push(info.oid);
5024
5183
  const outPath = store2.packPath(publicationId);
5025
- const built = await buildPack(repoDir, { include: wants, exclude, outPath, ...call.signal ? { signal: call.signal } : {} });
5184
+ const built = await buildPack(repoDir, { include: wants, exclude, outPath, ...call2.signal ? { signal: call2.signal } : {} });
5026
5185
  pack = { sha256: built.sha256, size: built.size, path: outPath };
5027
5186
  progress({ phase: "pack", message: `packed ${built.objectCount} objects (${built.size} bytes)`, bytes: built.size, total: built.size });
5028
5187
  }
@@ -5038,7 +5197,7 @@ async function publish(ctx, repoId, remote, opts, call) {
5038
5197
  finalized: false
5039
5198
  };
5040
5199
  await store2.write(pending);
5041
- return runPublication(ctx, repoId, remote, repoDir, store2, pending, opts, call, false);
5200
+ return runPublication(ctx, repoId, remote, repoDir, store2, pending, opts, call2, false);
5042
5201
  }
5043
5202
  async function packFileUsable(p) {
5044
5203
  if (!p.pack) return true;
@@ -5049,9 +5208,9 @@ async function packFileUsable(p) {
5049
5208
  return false;
5050
5209
  }
5051
5210
  }
5052
- async function runPublication(ctx, repoId, remote, repoDir, store2, pending, opts, call, resumed) {
5211
+ async function runPublication(ctx, repoId, remote, repoDir, store2, pending, opts, call2, resumed) {
5053
5212
  try {
5054
- return await sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts, call, resumed);
5213
+ return await sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts, call2, resumed);
5055
5214
  } catch (e) {
5056
5215
  throw unconfirmed(e, pending.publicationId);
5057
5216
  }
@@ -5066,8 +5225,8 @@ function unconfirmed(e, publicationId) {
5066
5225
  details: { publicationId, cause: e.code }
5067
5226
  });
5068
5227
  }
5069
- async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts, call, resumed) {
5070
- const progress = call.onProgress ?? (() => void 0);
5228
+ async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts, call2, resumed) {
5229
+ const progress = call2.onProgress ?? (() => void 0);
5071
5230
  const beginBody = {
5072
5231
  publicationId: pending.publicationId,
5073
5232
  baseGeneration: pending.baseGeneration,
@@ -5078,32 +5237,32 @@ async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts,
5078
5237
  let summary = null;
5079
5238
  if (!pending.finalized) {
5080
5239
  progress({ phase: "begin", message: "starting publication" });
5081
- let begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody, call)).body;
5240
+ let begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody, call2)).body;
5082
5241
  summary = begin.publication;
5083
5242
  if (begin.publication.state === "created") {
5084
5243
  if (begin.upload && pending.pack) {
5085
5244
  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);
5245
+ let result = await uploadPack(ctx.fetchImpl, begin.upload, pending.pack.path, pending.pack.size, call2);
5087
5246
  if (result === "expired") {
5088
- begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody, call)).body;
5247
+ begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody, call2)).body;
5089
5248
  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);
5249
+ result = await uploadPack(ctx.fetchImpl, begin.upload, pending.pack.path, pending.pack.size, call2);
5091
5250
  if (result === "expired") throw new PublisherError("NETWORK", "upload authorization expired twice", { retryable: true });
5092
5251
  }
5093
5252
  }
5094
5253
  progress({ phase: "finalize", message: "finalizing" });
5095
- const fin = await ctx.transport.request("POST", `/v1/repos/${repoId}/publications/${pending.publicationId}/finalize`, {}, call);
5254
+ const fin = await ctx.transport.request("POST", `/v1/repos/${repoId}/publications/${pending.publicationId}/finalize`, {}, call2);
5096
5255
  summary = fin.body.publication;
5097
5256
  }
5098
5257
  pending.finalized = true;
5099
5258
  await store2.write(pending);
5100
5259
  }
5101
5260
  if (opts.wait === false) {
5102
- summary = summary ?? await ctx.getStatus(repoId, pending.publicationId, call);
5261
+ summary = summary ?? await ctx.getStatus(repoId, pending.publicationId, call2);
5103
5262
  return toOutcome(summary, resumed, pending.refUpdates);
5104
5263
  }
5105
5264
  progress({ phase: "validate", message: "waiting for validation" });
5106
- summary = await waitForOutcome(ctx, repoId, pending.publicationId, opts.waitTimeoutMs ?? 10 * 6e4, call);
5265
+ summary = await waitForOutcome(ctx, repoId, pending.publicationId, opts.waitTimeoutMs ?? 10 * 6e4, call2);
5107
5266
  if (summary.state === "accepted") {
5108
5267
  await applyTrackingRefs(repoDir, remote, pending.refUpdates);
5109
5268
  await store2.remove(pending.publicationId);
@@ -5114,23 +5273,23 @@ async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts,
5114
5273
  }
5115
5274
  return toOutcome(summary, resumed, pending.refUpdates);
5116
5275
  }
5117
- async function waitForOutcome(ctx, repoId, publicationId, timeoutMs, call) {
5276
+ async function waitForOutcome(ctx, repoId, publicationId, timeoutMs, call2) {
5118
5277
  const deadline = Date.now() + timeoutMs;
5119
5278
  let delay2 = 500;
5120
5279
  for (; ; ) {
5121
- const s = await ctx.getStatus(repoId, publicationId, { ...call, waitSeconds: MAX_WAIT_SECONDS });
5280
+ const s = await ctx.getStatus(repoId, publicationId, { ...call2, waitSeconds: MAX_WAIT_SECONDS });
5122
5281
  if (s.state === "accepted" || s.state === "rejected") return s;
5123
5282
  if (Date.now() > deadline) {
5124
5283
  throw new PublisherError("OUTCOME_UNKNOWN", `publication ${publicationId} has not completed yet; run status later`, { retryable: true, details: { publicationId } });
5125
5284
  }
5126
- await sleep(delay2, call.signal);
5285
+ await sleep(delay2, call2.signal);
5127
5286
  delay2 = Math.min(delay2 * 2, 5e3);
5128
5287
  }
5129
5288
  }
5130
- async function resolvePending(ctx, repoDir, remote, pending, call, store2) {
5289
+ async function resolvePending(ctx, repoDir, remote, pending, call2, store2) {
5131
5290
  let summary;
5132
5291
  try {
5133
- summary = await ctx.getStatus(pending.repoId, pending.publicationId, call);
5292
+ summary = await ctx.getStatus(pending.repoId, pending.publicationId, call2);
5134
5293
  } catch (e) {
5135
5294
  if (e instanceof PublisherError && e.code === "PUBLICATION_NOT_FOUND") {
5136
5295
  await store2.remove(pending.publicationId);
@@ -5240,13 +5399,13 @@ async function writeManifest(gd, installed) {
5240
5399
  await (0, import_promises8.mkdir)((0, import_node_path6.join)(gd, "publisher"), { recursive: true });
5241
5400
  await (0, import_promises8.writeFile)((0, import_node_path6.join)(gd, "publisher", "installed.json"), JSON.stringify(installed));
5242
5401
  }
5243
- async function fetchRepository(ctx, repoId, opts, call) {
5402
+ async function fetchRepository(ctx, repoId, opts, call2) {
5244
5403
  const remote = opts.remote ?? "publisher";
5245
- const progress = call.onProgress ?? (() => void 0);
5404
+ const progress = call2.onProgress ?? (() => void 0);
5246
5405
  progress({ phase: "fetch", message: "describing repository" });
5247
5406
  const [, { body }, gd] = await Promise.all([
5248
5407
  ensureRemote(opts.repoDir, remote, `${ctx.transport.endpoint}/v1/repos/${repoId}`),
5249
- ctx.transport.request("GET", `/v1/repos/${repoId}/bundle`, void 0, call),
5408
+ ctx.transport.request("GET", `/v1/repos/${repoId}/bundle`, void 0, call2),
5250
5409
  gitDir(opts.repoDir)
5251
5410
  ]);
5252
5411
  const artifacts = [...body.base ? [{ kind: "bundle", ...body.base }] : [], ...body.packs.map((p) => ({ kind: "pack", ...p }))];
@@ -5257,14 +5416,14 @@ async function fetchRepository(ctx, repoId, opts, call) {
5257
5416
  ctx,
5258
5417
  gd,
5259
5418
  artifacts.filter((a) => !installed.has(a.sha256)),
5260
- call
5419
+ call2
5261
5420
  );
5262
5421
  if (!await haveAll(opts.repoDir, body.refs)) {
5263
5422
  bytes += await install(
5264
5423
  ctx,
5265
5424
  gd,
5266
5425
  artifacts.filter((a) => installed.has(a.sha256)),
5267
- call
5426
+ call2
5268
5427
  );
5269
5428
  if (!await haveAll(opts.repoDir, body.refs)) throw new PublisherError("NETWORK", "the downloaded artifacts do not contain every advertised ref", { retryable: true });
5270
5429
  }
@@ -5282,16 +5441,16 @@ async function haveAll(repoDir, refs) {
5282
5441
  if (oids.size === 0) return true;
5283
5442
  return (await batchCheck(repoDir, oids)).every((o) => o.type !== "missing");
5284
5443
  }
5285
- async function install(ctx, gd, artifacts, call) {
5444
+ async function install(ctx, gd, artifacts, call2) {
5286
5445
  if (artifacts.length === 0) return 0;
5287
- const progress = call.onProgress ?? (() => void 0);
5446
+ const progress = call2.onProgress ?? (() => void 0);
5288
5447
  const tmp = await (0, import_promises8.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "gitpub-fetch-"));
5289
5448
  let bytes = 0;
5290
5449
  const total = artifacts.reduce((sum, a) => sum + a.size, 0);
5291
5450
  try {
5292
5451
  for (const a of artifacts) {
5293
5452
  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);
5453
+ bytes += await download(ctx.fetchImpl, a.url, file, a.sha256, a.size, call2);
5295
5454
  progress({ phase: "fetch", message: `installing ${a.kind}`, bytes, total });
5296
5455
  if (a.kind === "bundle") {
5297
5456
  await bundleUnbundleObjects(gd, file);
@@ -5322,10 +5481,10 @@ async function updateRefs(repoDir, remote, refs) {
5322
5481
  await refTransaction(repoDir, updates);
5323
5482
  return tracking;
5324
5483
  }
5325
- async function download(fetchImpl, url2, dest, sha256, size, call) {
5484
+ async function download(fetchImpl, url2, dest, sha256, size, call2) {
5326
5485
  let res;
5327
5486
  try {
5328
- res = await fetchImpl(url2, call.signal ? { signal: call.signal } : {});
5487
+ res = await fetchImpl(url2, call2.signal ? { signal: call2.signal } : {});
5329
5488
  } catch (e) {
5330
5489
  throw new PublisherError("NETWORK", `download failed: ${e.message}`, { retryable: true, cause: e });
5331
5490
  }
@@ -5336,7 +5495,7 @@ async function download(fetchImpl, url2, dest, sha256, size, call) {
5336
5495
  transform(chunk, controller) {
5337
5496
  n += chunk.byteLength;
5338
5497
  hash.update(chunk);
5339
- call.onProgress?.({ phase: "fetch", message: "downloading", bytes: n, total: size });
5498
+ call2.onProgress?.({ phase: "fetch", message: "downloading", bytes: n, total: size });
5340
5499
  controller.enqueue(chunk);
5341
5500
  }
5342
5501
  });
@@ -5392,11 +5551,37 @@ var Publisher = class {
5392
5551
  fetch: this.fetchImpl
5393
5552
  });
5394
5553
  }
5395
- async getCapabilities(call = {}) {
5396
- const r = await this.transport.request("GET", "/v1/capabilities", void 0, call);
5554
+ /**
5555
+ * The question the repository's current preparation waits on, and its answer, at the portal that
5556
+ * serves this endpoint — with this publisher's credential and its one refresh, as `followRun`.
5557
+ * The caller confirms the portal's `/config` lists `PREPARATION` first.
5558
+ */
5559
+ /**
5560
+ * A new application and its repository, made by the portal that serves this endpoint — the only
5561
+ * way a repository is created on a deployed stage (ADR-0016 A1). The caller confirms the portal's
5562
+ * `/config` lists `APPLICATION_CREATION` first, as it does for the other terminal routes.
5563
+ */
5564
+ async createApplication(input) {
5565
+ return createApplication(input, this.portalCall());
5566
+ }
5567
+ async readPreparation(input) {
5568
+ return readPreparation(input, this.portalCall());
5569
+ }
5570
+ async answerPreparation(input) {
5571
+ return answerPreparation(input, this.portalCall());
5572
+ }
5573
+ portalCall() {
5574
+ return {
5575
+ token: this.tokenFn,
5576
+ refresh: async () => !this.opts.token && refreshStoredCredential(this.endpoint, { fetch: this.fetchImpl }),
5577
+ fetch: this.fetchImpl
5578
+ };
5579
+ }
5580
+ async getCapabilities(call2 = {}) {
5581
+ const r = await this.transport.request("GET", "/v1/capabilities", void 0, call2);
5397
5582
  return r.body;
5398
5583
  }
5399
- async createRepository(options = {}, call = {}) {
5584
+ async createRepository(options = {}, call2 = {}) {
5400
5585
  const body = {
5401
5586
  creationId: options.creationId ?? newCreationId(),
5402
5587
  name: options.name ?? "",
@@ -5404,29 +5589,29 @@ var Publisher = class {
5404
5589
  objectFormat: "sha1",
5405
5590
  grants: options.grants ?? []
5406
5591
  };
5407
- const r = await this.transport.request("POST", "/v1/repos", body, call);
5592
+ const r = await this.transport.request("POST", "/v1/repos", body, call2);
5408
5593
  return r.body.repository;
5409
5594
  }
5410
- async getRepository(repoId, call = {}) {
5411
- const r = await this.transport.request("GET", `/v1/repos/${encodeURIComponent(repoId)}`, void 0, call);
5595
+ async getRepository(repoId, call2 = {}) {
5596
+ const r = await this.transport.request("GET", `/v1/repos/${encodeURIComponent(repoId)}`, void 0, call2);
5412
5597
  return r.body.repository;
5413
5598
  }
5414
5599
  /** One page of the repositories the caller created (ADR-0011). An authenticated route, unlike capabilities. */
5415
- async listRepositories(options = {}, call = {}) {
5600
+ async listRepositories(options = {}, call2 = {}) {
5416
5601
  const q = new URLSearchParams();
5417
5602
  if (options.state) q.set("state", options.state);
5418
5603
  if (options.limit !== void 0) q.set("limit", String(options.limit));
5419
5604
  if (options.cursor) q.set("cursor", options.cursor);
5420
5605
  const qs = q.toString() ? `?${q.toString()}` : "";
5421
- return (await this.transport.request("GET", `/v1/repos${qs}`, void 0, call)).body;
5606
+ return (await this.transport.request("GET", `/v1/repos${qs}`, void 0, call2)).body;
5422
5607
  }
5423
- async getPublicationStatus(repoId, publicationId, call = {}) {
5424
- const q = call.waitSeconds ? `?wait=${Math.min(call.waitSeconds, MAX_WAIT_SECONDS)}` : "";
5608
+ async getPublicationStatus(repoId, publicationId, call2 = {}) {
5609
+ const q = call2.waitSeconds ? `?wait=${Math.min(call2.waitSeconds, MAX_WAIT_SECONDS)}` : "";
5425
5610
  const r = await this.transport.request(
5426
5611
  "GET",
5427
5612
  `/v1/repos/${encodeURIComponent(repoId)}/publications/${encodeURIComponent(publicationId)}${q}`,
5428
5613
  void 0,
5429
- call
5614
+ call2
5430
5615
  );
5431
5616
  return r.body.publication;
5432
5617
  }
@@ -5453,32 +5638,32 @@ var Publisher = class {
5453
5638
  return { repoId: a.repoId, remote: a.remote };
5454
5639
  }
5455
5640
  /** Publish existing local commits. Never modifies the worktree; safe to retry and resume. */
5456
- async publish(options, call = {}) {
5641
+ async publish(options, call2 = {}) {
5457
5642
  const { repoId, remote } = await this.resolveRepo(options.repoDir, options.repoId);
5458
- const outcome = await publish(this.publishContext(), repoId, remote, options, call);
5643
+ const outcome = await publish(this.publishContext(), repoId, remote, options, call2);
5459
5644
  await noteAcceptedPublication(options.repoDir, outcome);
5460
5645
  return outcome;
5461
5646
  }
5462
5647
  // ---- workspaces (docs/workspace-parity.md) ------------------------------------------------
5463
5648
  /** Create a NEW editable workspace (clone + mapping) for a published repository. */
5464
- async checkout(options, call = {}) {
5465
- return checkoutWorkspace(this, options, call);
5649
+ async checkout(options, call2 = {}) {
5650
+ return checkoutWorkspace(this, options, call2);
5466
5651
  }
5467
5652
  /** Make an associated folder a workspace (idempotent); `init` and `sync` both call it. */
5468
- async adoptWorkspace(directory, call = {}) {
5469
- return adoptWorkspace(this, directory, call);
5653
+ async adoptWorkspace(directory, call2 = {}) {
5654
+ return adoptWorkspace(this, directory, call2);
5470
5655
  }
5471
5656
  /** Offline: the persisted workspace mapping, or null. */
5472
5657
  async workspace(directory) {
5473
5658
  return getWorkspaceMapping(directory);
5474
5659
  }
5475
5660
  /** Where the workspace stands relative to the server (decided on real objects). */
5476
- async workspaceStatus(directory, call = {}) {
5477
- return inspectWorkspace(this, directory, call);
5661
+ async workspaceStatus(directory, call2 = {}) {
5662
+ return inspectWorkspace(this, directory, call2);
5478
5663
  }
5479
5664
  /** Fetch and, with `apply`, fast-forward or merge cleanly; advances the base. */
5480
- async sync(options, call = {}) {
5481
- return syncWorkspace(this, options, call);
5665
+ async sync(options, call2 = {}) {
5666
+ return syncWorkspace(this, options, call2);
5482
5667
  }
5483
5668
  /**
5484
5669
  * A creator's whole round trip in one call, so nobody runs git: record their changes, bring in
@@ -5490,7 +5675,7 @@ var Publisher = class {
5490
5675
  * a commit and then an error. `syncWorkspace` itself still never commits or publishes; this is
5491
5676
  * the composition, and the only one.
5492
5677
  */
5493
- async syncChanges(options, call = {}) {
5678
+ async syncChanges(options, call2 = {}) {
5494
5679
  const dir = options.directory;
5495
5680
  await this.resolveRepo(dir);
5496
5681
  const branch = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
@@ -5499,7 +5684,7 @@ var Publisher = class {
5499
5684
  if (mapped && mapped.branch !== branch)
5500
5685
  throw new PublisherError("LOCAL_STATE", `branch ${branch} is checked out, but this folder publishes ${mapped.branch}; check it out, then run sync again`);
5501
5686
  const recorded = await recordChanges({ dir, ...options.message ? { message: options.message } : {} });
5502
- const reconciled = await syncWorkspace(this, { directory: dir, apply: true }, call);
5687
+ const reconciled = await syncWorkspace(this, { directory: dir, apply: true }, call2);
5503
5688
  const base = { recorded: { commit: recorded.commit, committed: recorded.committed, excluded: recorded.excluded }, reconciled };
5504
5689
  if (reconciled.after === "conflict") {
5505
5690
  const remote = reconciled.before.remote?.oid ?? null;
@@ -5508,25 +5693,25 @@ var Publisher = class {
5508
5693
  }
5509
5694
  const tip = await revParse(dir, `refs/heads/${branch}`);
5510
5695
  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);
5696
+ const published = await this.publish({ repoDir: dir }, call2);
5512
5697
  return { ...base, state: published.state === "accepted" ? "published" : published.state === "rejected" ? "rejected" : "unknown", published, marked: false };
5513
5698
  }
5514
5699
  /** Resolve interrupted publications recorded locally without creating duplicates. */
5515
- async resumePublications(repoDir, call = {}) {
5700
+ async resumePublications(repoDir, call2 = {}) {
5516
5701
  const { remote } = await this.resolveRepo(repoDir);
5517
5702
  const out = [];
5518
5703
  const store2 = await openPending(repoDir);
5519
5704
  for (const p of await store2.list()) {
5520
5705
  if (p.endpoint !== this.endpoint) continue;
5521
5706
  if (!p.finalized) {
5522
- out.push(await publish(this.publishContext(), p.repoId, remote, { repoDir, publicationId: p.publicationId }, call).catch((e) => rethrowUnlessResolvable(e, p)));
5707
+ out.push(await publish(this.publishContext(), p.repoId, remote, { repoDir, publicationId: p.publicationId }, call2).catch((e) => rethrowUnlessResolvable(e, p)));
5523
5708
  continue;
5524
5709
  }
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);
5710
+ const deadline = Date.now() + (call2.waitTimeoutMs ?? 5 * 6e4);
5711
+ let s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...call2, waitSeconds: MAX_WAIT_SECONDS }, store2);
5527
5712
  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);
5713
+ call2.onProgress?.({ phase: "validate", message: `waiting for ${p.publicationId}` });
5714
+ s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...call2, waitSeconds: MAX_WAIT_SECONDS }, store2);
5530
5715
  }
5531
5716
  out.push(toOutcome(s, true, p.refUpdates));
5532
5717
  }
@@ -5541,17 +5726,17 @@ var Publisher = class {
5541
5726
  * branches. Every read is a presigned download of the repository's bundle and packs (ADR-0013);
5542
5727
  * artifacts already installed are not downloaded again.
5543
5728
  */
5544
- async fetch(options, call = {}) {
5729
+ async fetch(options, call2 = {}) {
5545
5730
  const { repoId } = await this.resolveRepo(options.repoDir, options.repoId);
5546
- return fetchRepository({ transport: this.transport, fetchImpl: this.fetchImpl }, repoId, options, call);
5731
+ return fetchRepository({ transport: this.transport, fetchImpl: this.fetchImpl }, repoId, options, call2);
5547
5732
  }
5548
5733
  publishContext() {
5549
5734
  return {
5550
5735
  transport: this.transport,
5551
5736
  fetchImpl: this.fetchImpl,
5552
5737
  clientInfo: this.clientInfo,
5553
- getRepository: (repoId, call) => this.getRepository(repoId, call),
5554
- getStatus: (repoId, publicationId, call) => this.getPublicationStatus(repoId, publicationId, call)
5738
+ getRepository: (repoId, call2) => this.getRepository(repoId, call2),
5739
+ getStatus: (repoId, publicationId, call2) => this.getPublicationStatus(repoId, publicationId, call2)
5555
5740
  };
5556
5741
  }
5557
5742
  };
@@ -5631,8 +5816,8 @@ async function deviceLogin(opts) {
5631
5816
  if (typeof advertised !== "string" || advertised === "") throw new PublisherError("UNAVAILABLE", "this deployment does not offer device login; obtain a token another way");
5632
5817
  const base = normalize(advertised);
5633
5818
  assertCredentialEndpoint(base);
5634
- const post = (path, fields) => postForm(base, path, fields, doFetch);
5635
- const started = await post("/v1/auth/device", { client_id: opts.clientId ?? "gitpub-cli" });
5819
+ const post2 = (path, fields) => postForm(base, path, fields, doFetch);
5820
+ const started = await post2("/v1/auth/device", { client_id: opts.clientId ?? "gitpub-cli" });
5636
5821
  if (started.status !== 200) throw new PublisherError("UNAVAILABLE", `starting the login failed (${started.status})`, { retryable: true });
5637
5822
  const s = started.body;
5638
5823
  const deviceCode = str(s.device_code);
@@ -5646,7 +5831,7 @@ async function deviceLogin(opts) {
5646
5831
  for (; ; ) {
5647
5832
  await sleep2(intervalMs);
5648
5833
  if (now() >= deadline) throw new PublisherError("EXPIRED", "the login expired before it was approved");
5649
- const polled = await post("/v1/auth/token", { grant_type: DEVICE_CODE_GRANT, device_code: deviceCode });
5834
+ const polled = await post2("/v1/auth/token", { grant_type: DEVICE_CODE_GRANT, device_code: deviceCode });
5650
5835
  if (polled.status === 200) {
5651
5836
  const token = str(polled.body["access_token"]);
5652
5837
  if (!token) throw new PublisherError("UNAVAILABLE", "the login returned no credential");
@@ -5693,6 +5878,21 @@ async function discoverEndpoint(portal, fetchImpl = fetch) {
5693
5878
  return endpoint;
5694
5879
  }
5695
5880
 
5881
+ // src/init.ts
5882
+ async function repositoryForInit(p, endpoint, name, opts = {}) {
5883
+ const portal = portalForEndpoint(endpoint);
5884
+ if (portal === null) return { repoId: (await p.createRepository({ name, grants: [] })).repoId, appId: null };
5885
+ const confirmed = await confirmPortal(portal, endpoint, opts.fetch);
5886
+ if (confirmed === null) {
5887
+ throw new PublisherError("UNAVAILABLE", `could not confirm that ${portal} publishes to ${endpoint}; run init again`, { retryable: true });
5888
+ }
5889
+ if (!confirmed.terminal.includes(APPLICATION_CREATION)) {
5890
+ throw new PublisherError("UNAVAILABLE", `${portal} cannot make an application from a terminal yet, and a repository needs one; ask whoever runs it to update the portal`);
5891
+ }
5892
+ const made = await p.createApplication({ portal, name });
5893
+ return { repoId: made.repository.repoId, appId: made.appId };
5894
+ }
5895
+
5696
5896
  // src/login.ts
5697
5897
  async function verifiedCapabilities(publisher) {
5698
5898
  const caps = await publisher.getCapabilities();
@@ -5717,7 +5917,7 @@ function resolveChoice(answer, names) {
5717
5917
  }
5718
5918
  return names.find((n) => n.toLowerCase() === a.toLowerCase()) ?? null;
5719
5919
  }
5720
- async function ask2(question) {
5920
+ async function ask3(question) {
5721
5921
  const rl = (0, import_promises10.createInterface)({ input: process.stdin, output: process.stderr });
5722
5922
  try {
5723
5923
  return await rl.question(question);
@@ -5735,7 +5935,7 @@ ${purpose}
5735
5935
  names.forEach((n, i) => process.stderr.write(` ${i + 1}) ${n}
5736
5936
  `));
5737
5937
  for (let attempt = 0; attempt < 3; attempt++) {
5738
- const answer = await ask2(`
5938
+ const answer = await ask3(`
5739
5939
  Which one? [1-${names.length}, or a name] `);
5740
5940
  if (answer === null) return null;
5741
5941
  const chosen = resolveChoice(answer, names);
@@ -5751,7 +5951,7 @@ Name this connection, so you never have to type ${endpoint} again.
5751
5951
  (for example: prod, dev, staging)
5752
5952
  `);
5753
5953
  for (let attempt = 0; attempt < 3; attempt++) {
5754
- const answer = await ask2(suggested ? `
5954
+ const answer = await ask3(suggested ? `
5755
5955
  Profile name [${suggested}]: ` : `
5756
5956
  Profile name: `);
5757
5957
  if (answer === null) return null;
@@ -5874,8 +6074,9 @@ async function followPublication(p, endpoint, o, opts) {
5874
6074
  { portal, repoId: o.repoId, publicationId: o.publicationId, ref, commit },
5875
6075
  { onStage: opts.onStage, ...opts.timeoutMs === void 0 ? {} : { timeoutMs: opts.timeoutMs } }
5876
6076
  );
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 } };
6077
+ const answerable = confirmed.terminal.includes(PREPARATION);
6078
+ 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;
6079
+ return { ...outcome, portal, hint: progressHint(outcome, portal, answerable), ...next === void 0 ? {} : { next } };
5879
6080
  }
5880
6081
  function followTimeoutMs(flag) {
5881
6082
  if (flag === void 0) return FOLLOW_TIMEOUT_MS;
@@ -5888,8 +6089,10 @@ function stageLine(e, json) {
5888
6089
  function inPortal(portal, lead, verb) {
5889
6090
  return `${lead} ${verb} in the portal${portal === null ? "" : `: ${portal}`}.`;
5890
6091
  }
5891
- function progressHint(r, portal) {
6092
+ function progressHint(r, portal, answerable = false) {
5892
6093
  switch (r.kind) {
6094
+ case "waiting":
6095
+ 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
6096
  case "settled":
5894
6097
  switch (r.ending) {
5895
6098
  case "live":
@@ -5915,6 +6118,78 @@ function progressHint(r, portal) {
5915
6118
  }
5916
6119
  }
5917
6120
 
6121
+ // src/question.ts
6122
+ async function questionAt(p, endpoint, repoId, opts = {}) {
6123
+ const at = await portalAt(endpoint, opts);
6124
+ if ("reason" in at) return at;
6125
+ return shown(at.portal, await p.readPreparation({ portal: at.portal, repoId }));
6126
+ }
6127
+ async function answerAt(p, endpoint, repoId, specs, opts = {}) {
6128
+ const answers = parseSpecs(specs);
6129
+ const at = await portalAt(endpoint, opts);
6130
+ if ("reason" in at) return at;
6131
+ const read2 = shown(at.portal, await p.readPreparation({ portal: at.portal, repoId }));
6132
+ if (read2.kind !== "view") return read2;
6133
+ if (read2.questions.length === 0) throw new PublisherError("LOCAL_STATE", `nothing is waiting for an answer (the preparation is ${read2.preparation.status})`);
6134
+ for (const [id, value] of Object.entries(answers)) {
6135
+ const question = read2.questions.find((q) => q.id === id);
6136
+ if (question === void 0) {
6137
+ throw new PublisherError("INVALID_REQUEST", `no pending question "${id}"; pending: ${read2.questions.map((q) => q.id).join(", ")}`, {
6138
+ details: { questions: read2.questions }
6139
+ });
6140
+ }
6141
+ const values = question.options.map((o) => o.value);
6142
+ if (!values.includes(value)) {
6143
+ throw new PublisherError("INVALID_REQUEST", `"${value}" is not one of ${values.join(", ")} for "${id}"`, { details: { questions: read2.questions } });
6144
+ }
6145
+ }
6146
+ return shown(at.portal, await p.answerPreparation({ portal: at.portal, repoId, id: read2.preparation.id, revision: read2.preparation.revision, answers }));
6147
+ }
6148
+ function parseSpecs(specs) {
6149
+ if (specs.length === 0) throw new PublisherError("INVALID_REQUEST", "answer takes one or more question=value");
6150
+ const answers = {};
6151
+ for (const spec of specs) {
6152
+ const at = spec.indexOf("=");
6153
+ if (at <= 0 || at === spec.length - 1) throw new PublisherError("INVALID_REQUEST", `"${spec}" is not question=value`);
6154
+ answers[spec.slice(0, at)] = spec.slice(at + 1);
6155
+ }
6156
+ return answers;
6157
+ }
6158
+ async function portalAt(endpoint, opts) {
6159
+ const derived = portalForEndpoint(endpoint);
6160
+ const confirmed = derived === null ? null : await confirmPortal(derived, endpoint, opts.fetch);
6161
+ if (derived === null || confirmed === null) return { kind: "unavailable", reason: "no-portal", portal: null, hint: "Answer it in the portal." };
6162
+ if (!confirmed.terminal.includes(PREPARATION)) {
6163
+ 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}.` };
6164
+ }
6165
+ return { portal: derived };
6166
+ }
6167
+ function shown(portal, r) {
6168
+ switch (r.kind) {
6169
+ case "view": {
6170
+ const { questions, ...preparation } = r.view;
6171
+ return { kind: "view", portal, preparation, questions, hint: hintFor(r.view) };
6172
+ }
6173
+ case "no-application":
6174
+ throw new PublisherError("NOT_FOUND", "this repository has no application in your organisation yet; publish it first");
6175
+ case "moved-on":
6176
+ throw new PublisherError("UNAVAILABLE", "the preparation moved on before the answer arrived; run publisher question again", { retryable: true });
6177
+ case "unauthorized":
6178
+ throw new PublisherError("UNAUTHORIZED", "the portal did not accept this sign-in; run publisher login");
6179
+ case "forbidden":
6180
+ throw new PublisherError("FORBIDDEN", "the portal will not let this sign-in answer for this organisation (an owner or admin can)");
6181
+ case "unavailable":
6182
+ throw new PublisherError("UNAVAILABLE", `the portal could not be asked: ${r.detail}`, { retryable: true });
6183
+ case "malformed":
6184
+ throw new PublisherError("UNAVAILABLE", `the portal's answer could not be read: ${r.detail}`);
6185
+ }
6186
+ }
6187
+ function hintFor(view) {
6188
+ if (view.questions.length === 0) return "Nothing is waiting for an answer.";
6189
+ const how = view.questions.map((q) => `${q.id}=<${q.options.map((o) => o.value).join("|")}>`).join(" ");
6190
+ return `It is waiting for your answer. Run: publisher answer ${how}`;
6191
+ }
6192
+
5918
6193
  // src/main.ts
5919
6194
  var USAGE = `publisher \u2014 publish an app's Git history to the serverless Git backend
5920
6195
 
@@ -5938,7 +6213,8 @@ Usage:
5938
6213
  publisher fetch [dir]
5939
6214
  publisher checkout <dir> --repo <id> [--branch <name>] create a NEW editable workspace (never overlays a folder)
5940
6215
  publisher workspace-status [dir] offline mapping + where the workspace stands (real three-way check)
5941
- publisher sync [dir] [--message <text>] THE way to save and publish a change: records the folder's
6216
+ publisher sync [dir] [--message <text>] [--name <app>] THE way to publish: a folder linked to nothing first gets its
6217
+ application and repository (what init does); then it records the
5942
6218
  changes (never .env, keys or node_modules), brings in what
5943
6219
  was published meanwhile (fast-forward or a clean merge,
5944
6220
  never a rewrite) and publishes. Stops on a conflict with
@@ -5951,6 +6227,10 @@ Usage:
5951
6227
  result's next.command picks it up
5952
6228
  publisher follow [dir] --publication <id> follow an accepted publication's run where a sync left it
5953
6229
  [--timeout <seconds>]
6230
+ publisher question [dir] [--repo <id>] the question the app's preparation waits on, if any,
6231
+ with each option's value (answering needs owner or admin)
6232
+ publisher answer <question>=<value>... [--repo <id>] answer it from this folder; a value that is not one of
6233
+ the question's options is refused before anything is sent
5954
6234
  publisher merge-check [dir] --ours <ref> --theirs <ref> [--apply]
5955
6235
  publisher info [dir] repository state as seen by the server
5956
6236
  publisher doctor environment check (node, git, credentials, association)
@@ -6027,6 +6307,31 @@ async function main(argv) {
6027
6307
  const token = str2(flags["token"]);
6028
6308
  return { endpoint, p: createPublisher({ endpoint, endpointSource: source, ...token ? { token } : {}, clientName: "git-publisher-cli", clientVersion: CLIENT_VERSION }) };
6029
6309
  };
6310
+ const linkFolder = async (p, endpoint, dir, attach) => {
6311
+ let onboarding = null;
6312
+ if (!await isGitRepo(dir) || !await headCommit(dir)) {
6313
+ onboarding = await onboardFolder({ dir, ...optStr(flags, "message") });
6314
+ log(`prepared ${onboarding.committed.length} files (${onboarding.excluded.length} excluded)`);
6315
+ }
6316
+ let repoId = attach.repoId ?? null;
6317
+ let appId = null;
6318
+ if (repoId === null) {
6319
+ const made = await repositoryForInit(p, endpoint, str2(flags["name"]) ?? dir.split("/").pop() ?? "");
6320
+ repoId = made.repoId;
6321
+ appId = made.appId;
6322
+ } else {
6323
+ await p.getRepository(repoId);
6324
+ }
6325
+ await p.associate(dir, repoId);
6326
+ const workspace = await p.adoptWorkspace(dir).catch(() => null);
6327
+ return {
6328
+ repoId,
6329
+ appId,
6330
+ created: attach.repoId === void 0,
6331
+ onboarding: onboarding ? { commit: onboarding.commit, committed: onboarding.committed.length, excluded: onboarding.excluded } : null,
6332
+ workspace: workspace === null ? null : { branch: workspace.branch, baseOid: workspace.baseOid, baseGeneration: workspace.baseGeneration }
6333
+ };
6334
+ };
6030
6335
  if (flags["version"] === true) {
6031
6336
  process.stdout.write(`${packageVersion()}
6032
6337
  `);
@@ -6160,33 +6465,9 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
6160
6465
  case "init": {
6161
6466
  const dir = dirArg(1);
6162
6467
  const { p, endpoint } = await client();
6163
- const existing = await isGitRepo(dir);
6164
- let onboarding = null;
6165
- if (!existing || !await headCommit(dir)) {
6166
- onboarding = await onboardFolder({ dir, ...optStr(flags, "message") });
6167
- log(`prepared ${onboarding.committed.length} files (${onboarding.excluded.length} excluded)`);
6168
- }
6169
- let repoId = str2(flags["repo"]);
6170
- let created = false;
6171
- if (!repoId) {
6172
- const grants = [];
6173
- const repo = await p.createRepository({ name: str2(flags["name"]) ?? dir.split("/").pop() ?? "", grants });
6174
- repoId = repo.repoId;
6175
- created = true;
6176
- } else {
6177
- await p.getRepository(repoId);
6178
- }
6179
- await p.associate(dir, repoId);
6180
- const workspace = await p.adoptWorkspace(dir).catch(() => null);
6181
- out({
6182
- ok: true,
6183
- repoId,
6184
- created,
6185
- endpoint,
6186
- dir,
6187
- onboarding: onboarding ? { commit: onboarding.commit, committed: onboarding.committed.length, excluded: onboarding.excluded } : null,
6188
- workspace: workspace === null ? null : { branch: workspace.branch, baseOid: workspace.baseOid, baseGeneration: workspace.baseGeneration }
6189
- });
6468
+ const repo = str2(flags["repo"]);
6469
+ const linked = await linkFolder(p, endpoint, dir, repo ? { repoId: repo } : {});
6470
+ out({ ok: true, ...linked, endpoint, dir });
6190
6471
  return 0;
6191
6472
  }
6192
6473
  case "publish": {
@@ -6279,12 +6560,14 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
6279
6560
  const dir = dirArg(1);
6280
6561
  const timeoutMs = followTimeoutMs(str2(flags["timeout"]));
6281
6562
  const { p, endpoint } = await client(dir);
6563
+ const linked = await isGitRepo(dir) && await readAssociation(dir) !== null ? null : await linkFolder(p, endpoint, dir, {});
6282
6564
  const r = await p.syncChanges({ directory: dir, markConflicts: flags["mark-conflicts"] === true, ...optStr(flags, "message") }, { onProgress: progress });
6283
6565
  const o = r.published;
6284
6566
  const followed = o !== null && o.state === "accepted" && flags["no-wait"] !== true ? await followPublication(p, endpoint, o, { onStage: emitStage, log, timeoutMs }) : null;
6285
6567
  out({
6286
6568
  ok: r.state === "published" || r.state === "up_to_date",
6287
6569
  state: r.state,
6570
+ linked: linked === null ? null : { repoId: linked.repoId, appId: linked.appId, created: linked.created },
6288
6571
  recorded: r.recorded,
6289
6572
  merged: r.reconciled.applied,
6290
6573
  conflicts: r.reconciled.before.conflicts,
@@ -6322,6 +6605,27 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
6322
6605
  out({ publicationId, progress: await followPublication(p, endpoint, publication, { onStage: emitStage, log, timeoutMs }) });
6323
6606
  return 0;
6324
6607
  }
6608
+ /**
6609
+ * The business question the app's current preparation waits on, from this folder's repository.
6610
+ * Exit 0 whether or not one is pending: `questions` says which. An outage is exit 3, never "none".
6611
+ */
6612
+ case "question": {
6613
+ const dir = dirArg(1);
6614
+ const { p, endpoint } = await client(dir);
6615
+ const repoId = str2(flags["repo"]) ?? (await readAssociation(dir))?.repoId;
6616
+ if (!repoId) throw new PublisherError("LOCAL_STATE", "no repository association; pass --repo");
6617
+ out({ repoId, ...await questionAt(p, endpoint, repoId) });
6618
+ return 0;
6619
+ }
6620
+ /** Answer it, as `question=value` pairs, re-reading the preparation first. */
6621
+ case "answer": {
6622
+ const dir = (0, import_node_path8.resolve)(".");
6623
+ const { p, endpoint } = await client(dir);
6624
+ const repoId = str2(flags["repo"]) ?? (await readAssociation(dir))?.repoId;
6625
+ if (!repoId) throw new PublisherError("LOCAL_STATE", "no repository association; pass --repo");
6626
+ out({ repoId, ...await answerAt(p, endpoint, repoId, cmd.slice(1)) });
6627
+ return 0;
6628
+ }
6325
6629
  case "skills": {
6326
6630
  if (cmd[1] !== "install") throw new PublisherError("INVALID_REQUEST", `unknown skills command: ${cmd[1] ?? "(none)"}; expected install`);
6327
6631
  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.8.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",