@minnovate/shadow-publisher 0.12.1 → 0.12.3

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.12.1",
4
+ "version": "0.12.3",
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"]
@@ -119,6 +119,12 @@ var ERROR_CODES = [
119
119
  "ACCESS_CONFLICT",
120
120
  // publications (begin / finalize / status)
121
121
  "PUBLICATION_NOT_FOUND",
122
+ // A publication of files whose files are the base's already (ADR-0030).
123
+ "NOTHING_CHANGED",
124
+ // A commit the files route was asked for that the repository's refs do not reach (ADR-0030).
125
+ "COMMIT_NOT_FOUND",
126
+ // A path the files route was asked for that is not a file of that commit (ADR-0030).
127
+ "PATH_NOT_FOUND",
122
128
  "PUBLICATION_CONFLICT",
123
129
  "PUBLICATION_EXPIRED",
124
130
  "UPLOAD_MISSING",
@@ -161,6 +167,9 @@ var HTTP_STATUS_FOR_CODE = {
161
167
  REPOSITORY_ARCHIVED: 409,
162
168
  ACCESS_CONFLICT: 412,
163
169
  PUBLICATION_NOT_FOUND: 404,
170
+ NOTHING_CHANGED: 409,
171
+ COMMIT_NOT_FOUND: 404,
172
+ PATH_NOT_FOUND: 404,
164
173
  PUBLICATION_CONFLICT: 409,
165
174
  PUBLICATION_EXPIRED: 410,
166
175
  UPLOAD_MISSING: 409,
@@ -2479,6 +2488,18 @@ function object(shape, params) {
2479
2488
  function extend2(schema, shape) {
2480
2489
  return extend(schema, shape);
2481
2490
  }
2491
+ var ZodMiniUnion = /* @__PURE__ */ $constructor("ZodMiniUnion", (inst, def) => {
2492
+ $ZodUnion.init(inst, def);
2493
+ ZodMiniType.init(inst, def);
2494
+ });
2495
+ // @__NO_SIDE_EFFECTS__
2496
+ function union(options, params) {
2497
+ return new ZodMiniUnion({
2498
+ type: "union",
2499
+ options,
2500
+ ...normalizeParams(params)
2501
+ });
2502
+ }
2482
2503
  var ZodMiniDiscriminatedUnion = /* @__PURE__ */ $constructor("ZodMiniDiscriminatedUnion", (inst, def) => {
2483
2504
  $ZodDiscriminatedUnion.init(inst, def);
2484
2505
  ZodMiniType.init(inst, def);
@@ -2666,7 +2687,14 @@ var LimitsSchema = object({
2666
2687
  * Named `maxRepositoriesPerPrincipal` before 0.9.0, when it counted a creator's repositories.
2667
2688
  * Defaulted so an older server reads as the default, not a failure.
2668
2689
  */
2669
- maxRepositoriesPerOrganisation: _default(positiveInt(), 25)
2690
+ maxRepositoriesPerOrganisation: _default(positiveInt(), 25),
2691
+ // ---- a publication of files (ADR-0030) ----
2692
+ /** the UTF-8 bytes of the files one file publication writes; the commit builder is invoked with them, and Lambda takes 6 MB */
2693
+ maxFilePublicationBytes: _default(positiveInt(), 4 * 1024 * 1024),
2694
+ /** the paths one file publication writes and removes, together */
2695
+ maxFilePublicationPaths: _default(positiveInt(), 1e3),
2696
+ /** the largest file `GET …/files` shows as text, one at a time or in a listing; a larger one is `too-large` */
2697
+ maxShownFileBytes: _default(positiveInt(), 512 * 1024)
2670
2698
  });
2671
2699
  var DEFAULT_LIMITS = {
2672
2700
  maxPackBytes: 256 * 1024 * 1024,
@@ -2684,7 +2712,10 @@ var DEFAULT_LIMITS = {
2684
2712
  maxProcessMemoryBytes: 2 * 1024 * 1024 * 1024,
2685
2713
  maxTemporaryBytes: 3 * 512 * 1024 * 1024,
2686
2714
  gitTimeoutMs: 3e5,
2687
- maxRepositoriesPerOrganisation: 25
2715
+ maxRepositoriesPerOrganisation: 25,
2716
+ maxFilePublicationBytes: 4 * 1024 * 1024,
2717
+ maxFilePublicationPaths: 1e3,
2718
+ maxShownFileBytes: 512 * 1024
2688
2719
  };
2689
2720
 
2690
2721
  // ../protocol/src/storage-schema.ts
@@ -2850,6 +2881,11 @@ var PublicationRequestRecord = object({
2850
2881
  baseGeneration: Generation,
2851
2882
  refUpdates: array(RefUpdate).check(_minLength(1)),
2852
2883
  pack: nullable(PackClaim),
2884
+ /**
2885
+ * A publication of FILES (ADR-0030): the staged file set this service makes the commit from, in
2886
+ * the worker, in the delivery that validates it. Its one ref update names no new commit until then.
2887
+ */
2888
+ files: _default(nullable(PackClaim), null),
2853
2889
  client: nullable(ClientInfo),
2854
2890
  principal: PrincipalId,
2855
2891
  receivedAt: IsoDate,
@@ -3026,6 +3062,62 @@ var BeginPublicationResponse = object({
3026
3062
  upload: nullable(UploadInstructions)
3027
3063
  });
3028
3064
  var PublicationStatusResponse = object({ publication: PublicationSummary });
3065
+ var FilePath = string2().check(_minLength(1), _maxLength(1024));
3066
+ var FilePublicationRequest = object({
3067
+ publicationId: PublicationId,
3068
+ /** the ref to move; the repository's head ref when left out */
3069
+ ref: optional(RefName),
3070
+ /** the commit the files go on top of: the ref's current target, or null for a ref that does not exist yet */
3071
+ base: nullable(Oid),
3072
+ /** path → the file's whole content, as UTF-8 text */
3073
+ files: record(FilePath, string2()),
3074
+ replace: _default(boolean2(), false),
3075
+ delete: _default(array(FilePath), []),
3076
+ message: string2().check(_minLength(1), _maxLength(4e3)),
3077
+ client: _default(nullable(ClientInfo), null)
3078
+ });
3079
+ var FilePublicationResponse = object({ publication: PublicationSummary, ref: RefName, commit: nullable(Oid) });
3080
+ var StagedFileSet = object({
3081
+ files: record(FilePath, string2()),
3082
+ replace: boolean2(),
3083
+ delete: array(FilePath),
3084
+ message: string2()
3085
+ });
3086
+ var RepositoryFilesResponse = object({
3087
+ ref: RefName,
3088
+ /** the ref's current target: the base a publication of files goes on */
3089
+ head: nullable(Oid),
3090
+ /** the commit these files are: `head`, or the one asked for */
3091
+ commit: nullable(Oid),
3092
+ files: record(FilePath, string2()),
3093
+ unreadable: array(FilePath),
3094
+ tooLarge: boolean2()
3095
+ });
3096
+ var FileShown = _enum(["text", "binary", "too-large"]);
3097
+ var ListedEntry = object({
3098
+ path: FilePath,
3099
+ /** bytes; null for a submodule */
3100
+ bytes: nullable(int()),
3101
+ kind: _enum(["file", "symlink", "submodule"]),
3102
+ /** null for a symlink or a submodule */
3103
+ shown: nullable(FileShown)
3104
+ });
3105
+ var FileListing = object({ commit: nullable(Oid), entries: array(ListedEntry) });
3106
+ var RepositoryFileListResponse = object({
3107
+ commit: nullable(Oid),
3108
+ entries: array(extend2(ListedEntry, { url: nullable(string2()) })),
3109
+ linksExpireAt: nullable(string2())
3110
+ });
3111
+ var FileReadJob = object({
3112
+ repoId: RepoId,
3113
+ at: nullable(Oid),
3114
+ read: union([object({ kind: literal("files") }), object({ kind: literal("list") })])
3115
+ });
3116
+ var FileReadResult = union([
3117
+ object({ kind: literal("files"), answer: RepositoryFilesResponse }),
3118
+ object({ kind: literal("list"), answer: FileListing }),
3119
+ object({ kind: literal("refused"), code: _enum(ERROR_CODES), message: string2() })
3120
+ ]);
3029
3121
  var DownloadDescriptor = object({
3030
3122
  url: url(),
3031
3123
  sha256: Sha256Hex,
@@ -3867,14 +3959,14 @@ async function resolveRevs(dir, revs, opts = {}) {
3867
3959
  const out = revs.map(
3868
3960
  (r) => r.startsWith("-") ? null : r.includes("\0") ? new PublisherError("INVALID_REQUEST", "NUL in git argument") : null
3869
3961
  );
3870
- const ask3 = revs.flatMap((r, i) => out[i] === null && !r.startsWith("-") ? [i] : []);
3871
- if (ask3.length === 0) {
3962
+ const ask2 = revs.flatMap((r, i) => out[i] === null && !r.startsWith("-") ? [i] : []);
3963
+ if (ask2.length === 0) {
3872
3964
  return out;
3873
3965
  }
3874
- const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input: ask3.map((i) => revs[i]).join("\0") + "\0", ...opts });
3966
+ const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input: ask2.map((i) => revs[i]).join("\0") + "\0", ...opts });
3875
3967
  const records = stdout.toString("utf8").split("\0");
3876
3968
  const misses = [];
3877
- for (const [j, i] of ask3.entries()) {
3969
+ for (const [j, i] of ask2.entries()) {
3878
3970
  const record2 = records[j] ?? "";
3879
3971
  if (record2 && !/ (missing|ambiguous)$/.test(record2)) {
3880
3972
  out[i] = record2;
@@ -5043,26 +5135,27 @@ async function updateEntry(url2, change) {
5043
5135
  await writeCredentials(file);
5044
5136
  }
5045
5137
 
5046
- // ../client/src/portal-refusal.ts
5047
- function portalRefusalCode(body) {
5048
- if (!isRecord(body)) {
5049
- return null;
5050
- }
5051
- const error2 = body["error"];
5052
- if (typeof error2 === "string") {
5053
- return error2 === "" ? null : error2;
5054
- }
5055
- if (!isRecord(error2)) {
5056
- return null;
5057
- }
5058
- const { code, message } = error2;
5059
- return typeof code === "string" && code !== "" && typeof message === "string" ? code : null;
5060
- }
5061
- function isRecord(value) {
5062
- return typeof value === "object" && value !== null && !Array.isArray(value);
5063
- }
5064
-
5065
5138
  // ../client/src/portal-request.ts
5139
+ var PortalSession = class {
5140
+ constructor(credential) {
5141
+ this.credential = credential;
5142
+ }
5143
+ credential;
5144
+ #refreshed = false;
5145
+ async ask(url2, body) {
5146
+ const { token, refresh } = this.credential;
5147
+ for (; ; ) {
5148
+ const answer2 = await askPortal(this.credential.fetch ?? fetch, url2, await token(), body);
5149
+ if (answer2.status === 401 && !this.#refreshed && refresh !== void 0) {
5150
+ this.#refreshed = true;
5151
+ if (await refresh()) {
5152
+ continue;
5153
+ }
5154
+ }
5155
+ return answer2;
5156
+ }
5157
+ }
5158
+ };
5066
5159
  async function askPortal(fetchImpl, url2, token, body) {
5067
5160
  try {
5068
5161
  const res = await fetchImpl(url2, {
@@ -5081,10 +5174,26 @@ async function askPortal(fetchImpl, url2, token, body) {
5081
5174
  return { status: 0, body: null };
5082
5175
  }
5083
5176
  }
5084
- function isRecord2(value) {
5177
+ function isRecord(value) {
5085
5178
  return typeof value === "object" && value !== null && !Array.isArray(value);
5086
5179
  }
5087
5180
 
5181
+ // ../client/src/portal-refusal.ts
5182
+ function portalRefusalCode(body) {
5183
+ if (!isRecord(body)) {
5184
+ return null;
5185
+ }
5186
+ const error2 = body["error"];
5187
+ if (typeof error2 === "string") {
5188
+ return error2 === "" ? null : error2;
5189
+ }
5190
+ if (!isRecord(error2)) {
5191
+ return null;
5192
+ }
5193
+ const { code, message } = error2;
5194
+ return typeof code === "string" && code !== "" && typeof message === "string" ? code : null;
5195
+ }
5196
+
5088
5197
  // ../client/src/application.ts
5089
5198
  var APPLICATION_CREATION = "application-creation";
5090
5199
  var ATTEMPTS = 3;
@@ -5093,19 +5202,12 @@ var REFUSALS = {
5093
5202
  "repository-conflict": "REPOSITORY_CONFLICT"
5094
5203
  };
5095
5204
  async function createApplication(input, opts) {
5096
- const fetchImpl = opts.fetch ?? fetch;
5097
5205
  const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/applications`;
5098
5206
  const body = { creationId: input.creationId ?? newCreationId(), name: input.name, defaultBranch: input.defaultBranch ?? "main" };
5099
- let refreshed = false;
5207
+ const portal = new PortalSession(opts);
5100
5208
  let lastOutage = "";
5101
5209
  for (let attempt = 0; attempt < ATTEMPTS; ) {
5102
- const answer2 = await askPortal(fetchImpl, url2, await opts.token(), body);
5103
- if (answer2.status === 401 && !refreshed && opts.refresh !== void 0) {
5104
- refreshed = true;
5105
- if (await opts.refresh()) {
5106
- continue;
5107
- }
5108
- }
5210
+ const answer2 = await portal.ask(url2, body);
5109
5211
  if (answer2.status === 200 || answer2.status === 201) {
5110
5212
  return read(answer2.body, answer2.status === 201);
5111
5213
  }
@@ -5131,9 +5233,9 @@ async function createApplication(input, opts) {
5131
5233
  throw new PublisherError("UNAVAILABLE", `could not make the application: ${lastOutage}; run init again`, { retryable: true });
5132
5234
  }
5133
5235
  function read(body, created) {
5134
- const application = isRecord2(body) ? body["application"] : void 0;
5135
- const repository = RepositoryInfo.safeParse(isRecord2(body) ? body["repository"] : void 0);
5136
- if (!isRecord2(application) || typeof application["appId"] !== "string" || !repository.success) {
5236
+ const application = isRecord(body) ? body["application"] : void 0;
5237
+ const repository = RepositoryInfo.safeParse(isRecord(body) ? body["repository"] : void 0);
5238
+ if (!isRecord(application) || typeof application["appId"] !== "string" || !repository.success) {
5137
5239
  throw new PublisherError("UNAVAILABLE", "the portal answered with an application this version cannot read");
5138
5240
  }
5139
5241
  return { appId: application["appId"], created, repository: repository.data };
@@ -5271,7 +5373,7 @@ var RUN_STAGES = ["received", "running", "deploying", "live"];
5271
5373
  var STAGE_WORDS = { received: "received", running: "checked", deploying: "going live", live: "live" };
5272
5374
  var STATES = ["pending", "in-progress", "done", "failed", "skipped"];
5273
5375
  function readStages(run2) {
5274
- if (!isRecord3(run2)) {
5376
+ if (!isRecord(run2)) {
5275
5377
  return { malformed: "a run that is not an object" };
5276
5378
  }
5277
5379
  if (!Object.hasOwn(run2, "stages")) {
@@ -5284,7 +5386,7 @@ function readStages(run2) {
5284
5386
  const out = [];
5285
5387
  for (const [index, entry] of stages.entries()) {
5286
5388
  const stage = RUN_STAGES[index];
5287
- if (!isRecord3(entry) || entry["stage"] !== stage) {
5389
+ if (!isRecord(entry) || entry["stage"] !== stage) {
5288
5390
  return { malformed: `stage ${index + 1} is not ${stage}` };
5289
5391
  }
5290
5392
  const state = entry["state"];
@@ -5317,7 +5419,7 @@ function ending(stages) {
5317
5419
  var POLL_INTERVAL_MS = 5e3;
5318
5420
  var FOLLOW_TIMEOUT_MS = 15 * 6e4;
5319
5421
  async function followRun(input, opts) {
5320
- const fetchImpl = opts.fetch ?? fetch;
5422
+ const portal = new PortalSession(opts);
5321
5423
  const sleep2 = opts.sleep ?? sleep;
5322
5424
  const now = opts.now ?? Date.now;
5323
5425
  const interval2 = opts.intervalMs ?? POLL_INTERVAL_MS;
@@ -5327,16 +5429,9 @@ async function followRun(input, opts) {
5327
5429
  const seen = /* @__PURE__ */ new Map();
5328
5430
  let last = null;
5329
5431
  let lastError = null;
5330
- let refreshed = false;
5331
5432
  let answered = false;
5332
5433
  for (; ; ) {
5333
- const answer2 = await ask(fetchImpl, url2, await opts.token());
5334
- if (answer2.status === 401 && !refreshed && opts.refresh !== void 0) {
5335
- refreshed = true;
5336
- if (await opts.refresh()) {
5337
- continue;
5338
- }
5339
- }
5434
+ const answer2 = await portal.ask(url2, void 0);
5340
5435
  if (answer2.status === 401) {
5341
5436
  return { kind: "unauthorized" };
5342
5437
  }
@@ -5369,12 +5464,12 @@ async function followRun(input, opts) {
5369
5464
  opts.onStage?.(entry);
5370
5465
  moved = true;
5371
5466
  }
5372
- if (isRecord3(run2.run) && run2.run["status"] === "awaiting-approval") {
5467
+ if (isRecord(run2.run) && run2.run["status"] === "awaiting-approval") {
5373
5468
  return { kind: "waiting", stages: read2.stages };
5374
5469
  }
5375
5470
  const end = ending(read2.stages);
5376
5471
  if (end !== null) {
5377
- const runId = isRecord3(run2.run) && typeof run2.run["runId"] === "string" ? run2.run["runId"] : null;
5472
+ const runId = isRecord(run2.run) && typeof run2.run["runId"] === "string" ? run2.run["runId"] : null;
5378
5473
  return { kind: "settled", ending: end, failedAt: read2.stages.find((s) => s.state === "failed")?.word ?? null, stages: read2.stages, runId };
5379
5474
  }
5380
5475
  if (opts.untilChange === true && moved && answered) {
@@ -5393,7 +5488,7 @@ async function followRun(input, opts) {
5393
5488
  }
5394
5489
  }
5395
5490
  function soleRun(body) {
5396
- if (!isRecord3(body) || !Array.isArray(body["runs"])) {
5491
+ if (!isRecord(body) || !Array.isArray(body["runs"])) {
5397
5492
  return { detail: "an answer with no list of runs" };
5398
5493
  }
5399
5494
  const runs = body["runs"];
@@ -5402,23 +5497,6 @@ function soleRun(body) {
5402
5497
  }
5403
5498
  return { run: runs[0] ?? null };
5404
5499
  }
5405
- async function ask(fetchImpl, url2, token) {
5406
- try {
5407
- const res = await fetchImpl(url2, { headers: { authorization: `Bearer ${token}`, accept: "application/json" }, signal: AbortSignal.timeout(3e4) });
5408
- const text = await res.text();
5409
- let body = null;
5410
- try {
5411
- body = text ? JSON.parse(text) : null;
5412
- } catch {
5413
- if (res.status === 200) {
5414
- return { status: 200, body: void 0 };
5415
- }
5416
- }
5417
- return { status: res.status, body };
5418
- } catch {
5419
- return { status: 0, body: null };
5420
- }
5421
- }
5422
5500
  function portalForEndpoint(endpoint) {
5423
5501
  let url2;
5424
5502
  try {
@@ -5450,9 +5528,6 @@ async function confirmPortal(portal, endpoint, fetchImpl = fetch) {
5450
5528
  return null;
5451
5529
  }
5452
5530
  }
5453
- function isRecord3(value) {
5454
- return typeof value === "object" && value !== null && !Array.isArray(value);
5455
- }
5456
5531
 
5457
5532
  // ../client/src/preparation.ts
5458
5533
  var PREPARATION = "preparation";
@@ -5466,38 +5541,28 @@ function answerPreparation(input, opts) {
5466
5541
  return call(input, opts, { id: input.id, revision: input.revision, action: "answers", answers: input.answers });
5467
5542
  }
5468
5543
  async function call(input, opts, body) {
5469
- const fetchImpl = opts.fetch ?? fetch;
5470
5544
  const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/repositories/${encodeURIComponent(input.repoId)}/preparation`;
5471
- let refreshed = false;
5472
- for (; ; ) {
5473
- const answer2 = await askPortal(fetchImpl, url2, await opts.token(), body);
5474
- if (answer2.status === 401 && !refreshed && opts.refresh !== void 0) {
5475
- refreshed = true;
5476
- if (await opts.refresh()) {
5477
- continue;
5478
- }
5479
- }
5480
- if (answer2.status === 401) {
5481
- return { kind: "unauthorized" };
5482
- }
5483
- if (answer2.status === 403) {
5484
- return { kind: "forbidden" };
5485
- }
5486
- if (answer2.status === 409) {
5487
- return { kind: "moved-on" };
5488
- }
5489
- if (answer2.status === 404) {
5490
- return portalRefusalCode(answer2.body) === "no-such-application" ? { kind: "no-application" } : { kind: "unavailable", detail: "the portal does not serve this address" };
5491
- }
5492
- if (answer2.status === 200) {
5493
- const view = readView(answer2.body);
5494
- return "detail" in view ? { kind: "malformed", detail: view.detail } : { kind: "view", view };
5495
- }
5496
- return { kind: "unavailable", detail: answer2.status === 0 ? "the portal could not be reached" : `the portal answered ${answer2.status}` };
5545
+ const answer2 = await new PortalSession(opts).ask(url2, body);
5546
+ if (answer2.status === 401) {
5547
+ return { kind: "unauthorized" };
5497
5548
  }
5549
+ if (answer2.status === 403) {
5550
+ return { kind: "forbidden" };
5551
+ }
5552
+ if (answer2.status === 409) {
5553
+ return { kind: "moved-on" };
5554
+ }
5555
+ if (answer2.status === 404) {
5556
+ return portalRefusalCode(answer2.body) === "no-such-application" ? { kind: "no-application" } : { kind: "unavailable", detail: "the portal does not serve this address" };
5557
+ }
5558
+ if (answer2.status === 200) {
5559
+ const view = readView(answer2.body);
5560
+ return "detail" in view ? { kind: "malformed", detail: view.detail } : { kind: "view", view };
5561
+ }
5562
+ return { kind: "unavailable", detail: answer2.status === 0 ? "the portal could not be reached" : `the portal answered ${answer2.status}` };
5498
5563
  }
5499
5564
  function readView(body) {
5500
- if (!isRecord2(body)) {
5565
+ if (!isRecord(body)) {
5501
5566
  return { detail: "an answer that is not a preparation" };
5502
5567
  }
5503
5568
  const { id, revision, status: status2, message, questions, publication } = body;
@@ -5518,12 +5583,12 @@ function readView(body) {
5518
5583
  }
5519
5584
  const read2 = [];
5520
5585
  for (const q of questions) {
5521
- if (!isRecord2(q) || typeof q["id"] !== "string" || typeof q["text"] !== "string" || !Array.isArray(q["options"]) || q["options"].length === 0) {
5586
+ if (!isRecord(q) || typeof q["id"] !== "string" || typeof q["text"] !== "string" || !Array.isArray(q["options"]) || q["options"].length === 0) {
5522
5587
  return { detail: "a question that cannot be answered" };
5523
5588
  }
5524
5589
  const options = [];
5525
5590
  for (const o of q["options"]) {
5526
- if (!isRecord2(o) || typeof o["value"] !== "string" || typeof o["label"] !== "string") {
5591
+ if (!isRecord(o) || typeof o["value"] !== "string" || typeof o["label"] !== "string") {
5527
5592
  return { detail: "a question with an unreadable option" };
5528
5593
  }
5529
5594
  options.push({ value: o["value"], label: o["label"] });
@@ -5531,7 +5596,7 @@ function readView(body) {
5531
5596
  read2.push({ id: q["id"], text: q["text"], options });
5532
5597
  }
5533
5598
  let pub = null;
5534
- if (isRecord2(publication) && typeof publication["status"] === "string" && typeof publication["message"] === "string") {
5599
+ if (isRecord(publication) && typeof publication["status"] === "string" && typeof publication["message"] === "string") {
5535
5600
  pub = { status: publication["status"], message: publication["message"], ...typeof publication["url"] === "string" ? { url: publication["url"] } : {} };
5536
5601
  }
5537
5602
  return { id, revision, status: status2, message, questions: read2, publication: pub, findings: readFindings(body["findings"]) };
@@ -5546,7 +5611,7 @@ function readFindings(value) {
5546
5611
  if (out.length === MAX_FINDINGS) {
5547
5612
  break;
5548
5613
  }
5549
- if (!isRecord2(f)) {
5614
+ if (!isRecord(f)) {
5550
5615
  continue;
5551
5616
  }
5552
5617
  const kind = text(f["kind"]);
@@ -5572,36 +5637,26 @@ function readFindings(value) {
5572
5637
  // ../client/src/creator-tools.ts
5573
5638
  var CREATOR_TOOLS = "creator-tools";
5574
5639
  async function runCreatorTool(input, opts) {
5575
- const fetchImpl = opts.fetch ?? fetch;
5576
5640
  const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/tools/${encodeURIComponent(input.tool)}`;
5577
- let refreshed = false;
5578
- for (; ; ) {
5579
- const answer2 = await askPortal(fetchImpl, url2, await opts.token(), input.args);
5580
- if (answer2.status === 401 && !refreshed && opts.refresh !== void 0) {
5581
- refreshed = true;
5582
- if (await opts.refresh()) {
5583
- continue;
5584
- }
5585
- }
5586
- if (answer2.status === 401) {
5587
- return { kind: "unauthorized" };
5588
- }
5589
- if (answer2.status === 403) {
5590
- return { kind: "forbidden" };
5591
- }
5592
- if (answer2.status === 404) {
5593
- return { kind: "unavailable", detail: "the portal does not serve this tool" };
5594
- }
5595
- if (answer2.status !== 200) {
5596
- return { kind: "unavailable", detail: answer2.status === 0 ? "the portal could not be reached" : `the portal answered ${answer2.status}` };
5597
- }
5598
- const body = answer2.body;
5599
- if (!isRecord2(body) || typeof body["say"] !== "string" || typeof body["next"] !== "string") {
5600
- return { kind: "malformed", detail: "an answer with no say and next" };
5601
- }
5602
- const { say, next, isError, ...fields } = body;
5603
- return { kind: "answer", answer: { say, next, isError: isError === true, fields } };
5641
+ const answer2 = await new PortalSession(opts).ask(url2, input.args);
5642
+ if (answer2.status === 401) {
5643
+ return { kind: "unauthorized" };
5644
+ }
5645
+ if (answer2.status === 403) {
5646
+ return { kind: "forbidden" };
5604
5647
  }
5648
+ if (answer2.status === 404) {
5649
+ return { kind: "unavailable", detail: "the portal does not serve this tool" };
5650
+ }
5651
+ if (answer2.status !== 200) {
5652
+ return { kind: "unavailable", detail: answer2.status === 0 ? "the portal could not be reached" : `the portal answered ${answer2.status}` };
5653
+ }
5654
+ const body = answer2.body;
5655
+ if (!isRecord(body) || typeof body["say"] !== "string" || typeof body["next"] !== "string") {
5656
+ return { kind: "malformed", detail: "an answer with no say and next" };
5657
+ }
5658
+ const { say, next, isError, ...fields } = body;
5659
+ return { kind: "answer", answer: { say, next, isError: isError === true, fields } };
5605
5660
  }
5606
5661
 
5607
5662
  // ../client/src/onboard.ts
@@ -6874,6 +6929,28 @@ async function discoverEndpoint(portal, fetchImpl = fetch) {
6874
6929
  return endpoint;
6875
6930
  }
6876
6931
 
6932
+ // src/portal.ts
6933
+ var confirmed = /* @__PURE__ */ new Map();
6934
+ async function confirmedPortal(endpoint, opts = {}) {
6935
+ const derived = portalForEndpoint(endpoint);
6936
+ if (derived === null) {
6937
+ return { kind: "none" };
6938
+ }
6939
+ const asked = opts.fetch === void 0 ? confirmed.get(endpoint) ?? confirmPortal(derived, endpoint) : confirmPortal(derived, endpoint, opts.fetch);
6940
+ if (opts.fetch === void 0) {
6941
+ confirmed.set(endpoint, asked);
6942
+ }
6943
+ const config2 = await asked;
6944
+ return config2 === null ? { kind: "unconfirmed", portal: derived } : { kind: "confirmed", portal: derived, terminal: config2.terminal };
6945
+ }
6946
+ async function portalFor(endpoint, route, opts = {}) {
6947
+ const at = await confirmedPortal(endpoint, opts);
6948
+ if (at.kind !== "confirmed") {
6949
+ return { kind: "no-portal" };
6950
+ }
6951
+ return at.terminal.includes(route) ? { kind: "served", portal: at.portal } : { kind: "no-route", portal: at.portal };
6952
+ }
6953
+
6877
6954
  // src/progress.ts
6878
6955
  var SAYS = {
6879
6956
  received: { doing: "Shadow is picking up your changes", done: "Shadow has your changes", still: "still picking up your changes" },
@@ -6974,22 +7051,21 @@ function stageReporter(io) {
6974
7051
 
6975
7052
  // src/follow.ts
6976
7053
  async function followPublication(p, endpoint, o, opts) {
6977
- const derived = portalForEndpoint(endpoint);
6978
- const confirmed2 = derived === null ? null : await confirmPortal(derived, endpoint, opts.fetch);
6979
- const portal = confirmed2 === null ? null : derived;
7054
+ const confirmed2 = await confirmedPortal(endpoint, opts);
7055
+ const portal = confirmed2.kind === "confirmed" ? confirmed2.portal : null;
6980
7056
  const elsewhere = (reason) => ({ kind: "unavailable", reason, portal, hint: inPortal(portal, "Published.", "Follow it") });
6981
- if (portal === null || confirmed2 === null) {
7057
+ if (portal === null || confirmed2.kind !== "confirmed") {
6982
7058
  return elsewhere("no-portal");
6983
7059
  }
6984
7060
  if (!confirmed2.terminal.includes(RUN_PROGRESS)) {
6985
7061
  const noRoute = { kind: "unavailable", reason: "no-route" };
6986
7062
  return { ...noRoute, portal, hint: progressHint(noRoute, portal) };
6987
7063
  }
6988
- const refs = Object.entries(o.refs ?? {});
6989
- if (refs.length !== 1) {
7064
+ const moved = publishedRef(o);
7065
+ if (moved === null) {
6990
7066
  return elsewhere("several-refs");
6991
7067
  }
6992
- const [ref, commit] = refs[0];
7068
+ const { ref, commit } = moved;
6993
7069
  opts.log("Following it to live: received -> checked -> going live -> live. Ctrl-C stops watching; the publication is saved and carries on without you.");
6994
7070
  const outcome2 = await p.followRun(
6995
7071
  { portal, repoId: o.repoId, publicationId: o.publicationId, ref, commit },
@@ -7010,6 +7086,14 @@ async function followPublication(p, endpoint, o, opts) {
7010
7086
  }
7011
7087
  return { ...outcome2, portal, hint: progressHint(outcome2, portal, answerable), ...next === void 0 ? {} : { next } };
7012
7088
  }
7089
+ function publishedRef(o) {
7090
+ const refs = Object.entries(o.refs ?? {});
7091
+ if (refs.length !== 1) {
7092
+ return null;
7093
+ }
7094
+ const [ref, commit] = refs[0];
7095
+ return { ref, commit };
7096
+ }
7013
7097
  async function failedChecks(p, portal, repoId, outcome2, served) {
7014
7098
  if (!served || outcome2.kind !== "settled" || outcome2.ending !== "failed" || outcome2.runId === null) {
7015
7099
  return null;
@@ -7109,7 +7193,7 @@ function resolveChoice(answer2, names) {
7109
7193
  }
7110
7194
  return names.find((n) => n.toLowerCase() === a.toLowerCase()) ?? null;
7111
7195
  }
7112
- async function ask2(question2) {
7196
+ async function ask(question2) {
7113
7197
  const rl = (0, import_promises11.createInterface)({ input: process.stdin, output: process.stderr });
7114
7198
  try {
7115
7199
  return await rl.question(question2);
@@ -7127,7 +7211,7 @@ ${purpose}
7127
7211
  names.forEach((n, i) => process.stderr.write(` ${i + 1}) ${n}
7128
7212
  `));
7129
7213
  for (let attempt = 0; attempt < 3; attempt++) {
7130
- const answer2 = await ask2(`
7214
+ const answer2 = await ask(`
7131
7215
  Which one? [1-${names.length}, or a name] `);
7132
7216
  if (answer2 === null) {
7133
7217
  return null;
@@ -7147,7 +7231,7 @@ Name this connection, so you never have to type ${endpoint} again.
7147
7231
  (for example: prod, dev, staging)
7148
7232
  `);
7149
7233
  for (let attempt = 0; attempt < 3; attempt++) {
7150
- const answer2 = await ask2(suggested ? `
7234
+ const answer2 = await ask(suggested ? `
7151
7235
  Profile name [${suggested}]: ` : `
7152
7236
  Profile name: `);
7153
7237
  if (answer2 === null) {
@@ -7580,12 +7664,12 @@ var skills = {
7580
7664
 
7581
7665
  // src/init.ts
7582
7666
  async function repositoryForInit(p, endpoint, name, opts = {}) {
7583
- const portal = portalForEndpoint(endpoint);
7584
- if (portal === null) {
7667
+ const confirmed2 = await confirmedPortal(endpoint, opts);
7668
+ if (confirmed2.kind === "none") {
7585
7669
  return { repoId: (await p.createRepository({ name, grants: [] })).repoId, appId: null };
7586
7670
  }
7587
- const confirmed2 = await confirmPortal(portal, endpoint, opts.fetch);
7588
- if (confirmed2 === null) {
7671
+ const { portal } = confirmed2;
7672
+ if (confirmed2.kind === "unconfirmed") {
7589
7673
  throw new PublisherError("UNAVAILABLE", `could not confirm that ${portal} publishes to ${endpoint}; run init again`, { retryable: true });
7590
7674
  }
7591
7675
  if (!confirmed2.terminal.includes(APPLICATION_CREATION)) {
@@ -7745,24 +7829,6 @@ function folderFor(name) {
7745
7829
  return slug2 === "" ? "app" : slug2;
7746
7830
  }
7747
7831
 
7748
- // src/portal.ts
7749
- var confirmed = /* @__PURE__ */ new Map();
7750
- async function portalFor(endpoint, route, opts = {}) {
7751
- const derived = portalForEndpoint(endpoint);
7752
- if (derived === null) {
7753
- return { kind: "no-portal" };
7754
- }
7755
- const asked = opts.fetch === void 0 ? confirmed.get(endpoint) ?? confirmPortal(derived, endpoint) : confirmPortal(derived, endpoint, opts.fetch);
7756
- if (opts.fetch === void 0) {
7757
- confirmed.set(endpoint, asked);
7758
- }
7759
- const config2 = await asked;
7760
- if (config2 === null) {
7761
- return { kind: "no-portal" };
7762
- }
7763
- return config2.terminal.includes(route) ? { kind: "served", portal: derived } : { kind: "no-route", portal: derived };
7764
- }
7765
-
7766
7832
  // src/question.ts
7767
7833
  async function questionAt(p, endpoint, repoId, opts = {}) {
7768
7834
  const at = await portalAt(endpoint, opts);
@@ -8043,11 +8109,11 @@ async function answerSuggestion(inv, p, endpoint, repoId, o) {
8043
8109
  if (id === void 0 || repoId === void 0 || o === null || o.state !== "accepted" || o.publicationId === null) {
8044
8110
  return null;
8045
8111
  }
8046
- const ref = "refs/heads/main";
8047
- const commit = o.refs?.[ref];
8048
- if (commit === void 0) {
8112
+ const moved = publishedRef(o);
8113
+ if (moved === null) {
8049
8114
  return null;
8050
8115
  }
8116
+ const { ref, commit } = moved;
8051
8117
  return printed(
8052
8118
  await creatorTool(p, endpoint, "resolve_suggestion", { app: repoId, suggestion: id, status: "done", publication: { publicationId: o.publicationId, ref, commit } })
8053
8119
  );
@@ -8163,6 +8229,9 @@ var EXIT_CODE = {
8163
8229
  REPOSITORY_ARCHIVED: 1,
8164
8230
  ACCESS_CONFLICT: 1,
8165
8231
  PUBLICATION_NOT_FOUND: 1,
8232
+ NOTHING_CHANGED: 1,
8233
+ COMMIT_NOT_FOUND: 1,
8234
+ PATH_NOT_FOUND: 1,
8166
8235
  PUBLICATION_CONFLICT: 1,
8167
8236
  PUBLICATION_EXPIRED: 1,
8168
8237
  UPLOAD_MISSING: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnovate/shadow-publisher",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
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",