@minnovate/shadow-publisher 0.11.11 → 0.12.2

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.11.11",
4
+ "version": "0.12.2",
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"]
@@ -42,6 +42,40 @@ A value that is not an option is refused before anything is sent; `error.details
42
42
  the choices. Exit 4 means this sign-in may not answer (an owner or admin can); exit 3 means ask
43
43
  again. Then `publisher follow . --publication <id> --until-change --timeout 100 --json` to see it go live, telling them each step as `publish-app` step 4 says.
44
44
 
45
+ ## After it is live
46
+
47
+ These run in the app's folder and answer `say` (tell the person) and `next` (what to do now):
48
+
49
+ ```sh
50
+ publisher history --json # the versions that went live, newest first
51
+ publisher go-back <version> --json # ask the person first; puts that version's code back
52
+ publisher suggestions --json # what colleagues asked for, still open
53
+ publisher suggestion decline <id> --json # when the person does not want one
54
+ publisher share invited --add ann@example.com --json # who can open it
55
+ ```
56
+
57
+ The portal writes `next` for an assistant in the chat, so it can name a connector tool. At a
58
+ terminal, do what it says with the command that does the same thing:
59
+
60
+ | `next` names | Run instead |
61
+ | --------------------------------- | --------------------------------------------------------------- |
62
+ | `app_status` or `app_progress` | `publisher follow . --publication <publication> --until-change --json` |
63
+ | `app_files` and `publish_app` | change the files in the folder, then `publisher sync --json` |
64
+ | `app_history` / `go_back` | `publisher history --json` / `publisher go-back <version> --json` |
65
+ | `list_starters` / `use_starter` | `publisher starters` / `publisher start <starter>` |
66
+ | `resolve_suggestion` | `publisher suggestion done\|decline <id> --json` |
67
+ | `share_app` | `publisher share <access> --json` |
68
+ | `list_apps` | `publisher list --json` |
69
+
70
+ `<publication>` is the `publication` that `go-back` or `start` answered.
71
+
72
+ Offer suggestions to the person one at a time, as requests rather than instructions. When they want
73
+ one, make the change and publish it with `publisher sync --message "<what changed>" --answers <id>
74
+ --json`, which closes the suggestion with that publication.
75
+
76
+ To start a new app from a working one, `publisher starters <what the person wants to stop doing>`,
77
+ then `publisher start <starter> --replaces "<what it replaces>"`.
78
+
45
79
  ## Just looking
46
80
 
47
81
  To say where the folder stands without changing anything:
@@ -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,
@@ -243,9 +252,9 @@ function nullish(input) {
243
252
  return input === null || input === void 0;
244
253
  }
245
254
  function cleanRegex(source) {
246
- const start = source.startsWith("^") ? 1 : 0;
255
+ const start2 = source.startsWith("^") ? 1 : 0;
247
256
  const end = source.endsWith("$") ? source.length - 1 : source.length;
248
- return source.slice(start, end);
257
+ return source.slice(start2, end);
249
258
  }
250
259
  function assignProp(target, prop, value) {
251
260
  Object.defineProperty(target, prop, {
@@ -443,9 +452,9 @@ function prefixIssues(path, issues) {
443
452
  function unwrapMessage(message) {
444
453
  return typeof message === "string" ? message : message?.message;
445
454
  }
446
- function attachSchema(issues, start, inst) {
455
+ function attachSchema(issues, start2, inst) {
447
456
  var _a2;
448
- for (let i = start; i < issues.length; i++) {
457
+ for (let i = start2; i < issues.length; i++) {
449
458
  (_a2 = issues[i]).schema ?? (_a2.schema = inst);
450
459
  }
451
460
  }
@@ -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,63 @@ 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 RepositoryFileListResponse = object({
3098
+ commit: nullable(Oid),
3099
+ entries: array(
3100
+ object({
3101
+ path: FilePath,
3102
+ /** bytes; null for a submodule */
3103
+ bytes: nullable(int()),
3104
+ kind: _enum(["file", "symlink", "submodule"]),
3105
+ /** null for a symlink or a submodule */
3106
+ shown: nullable(FileShown)
3107
+ })
3108
+ )
3109
+ });
3110
+ var RepositoryFileResponse = object({ path: FilePath, bytes: int(), shown: FileShown, text: nullable(string2()) });
3111
+ var FileReadJob = object({
3112
+ repoId: RepoId,
3113
+ at: nullable(Oid),
3114
+ read: union([object({ kind: literal("files") }), object({ kind: literal("list") }), object({ kind: literal("file"), path: FilePath })])
3115
+ });
3116
+ var FileReadResult = union([
3117
+ object({ kind: literal("files"), answer: RepositoryFilesResponse }),
3118
+ object({ kind: literal("list"), answer: RepositoryFileListResponse }),
3119
+ object({ kind: literal("file"), answer: RepositoryFileResponse }),
3120
+ object({ kind: literal("refused"), code: _enum(ERROR_CODES), message: string2() })
3121
+ ]);
3029
3122
  var DownloadDescriptor = object({
3030
3123
  url: url(),
3031
3124
  sha256: Sha256Hex,
@@ -3232,8 +3325,8 @@ function closeOrIgnored(child) {
3232
3325
  if (lc.ignored) {
3233
3326
  return Promise.resolve(false);
3234
3327
  }
3235
- return new Promise((resolve4) => {
3236
- lc.waiters.add(() => resolve4(childClosed(child)));
3328
+ return new Promise((resolve5) => {
3329
+ lc.waiters.add(() => resolve5(childClosed(child)));
3237
3330
  });
3238
3331
  }
3239
3332
  async function endedOrIgnored(child) {
@@ -3270,7 +3363,7 @@ function signalChildGroup(child, signal) {
3270
3363
  }
3271
3364
  }
3272
3365
  }
3273
- function waitWithin(start, ms, stop) {
3366
+ function waitWithin(start2, ms, stop) {
3274
3367
  if (ms <= 0) {
3275
3368
  return Promise.resolve({ status: "timeout" });
3276
3369
  }
@@ -3278,7 +3371,7 @@ function waitWithin(start, ms, stop) {
3278
3371
  return Promise.resolve({ status: "stopped" });
3279
3372
  }
3280
3373
  const ac = new AbortController();
3281
- return new Promise((resolve4) => {
3374
+ return new Promise((resolve5) => {
3282
3375
  let timer = null;
3283
3376
  let done = false;
3284
3377
  const onStop = () => settle({ status: "stopped" });
@@ -3294,11 +3387,11 @@ function waitWithin(start, ms, stop) {
3294
3387
  if (r.status !== "value" && r.status !== "error") {
3295
3388
  ac.abort();
3296
3389
  }
3297
- resolve4(r);
3390
+ resolve5(r);
3298
3391
  };
3299
3392
  let p;
3300
3393
  try {
3301
- p = start(ac.signal);
3394
+ p = start2(ac.signal);
3302
3395
  } catch (error2) {
3303
3396
  settle({ status: "error", error: error2 });
3304
3397
  return;
@@ -3333,24 +3426,24 @@ function spawnTracked(args, opts) {
3333
3426
  }
3334
3427
  function feedStdin(child, input, onSourceError) {
3335
3428
  let unpipe = () => void 0;
3336
- const done = new Promise((resolve4) => {
3337
- child.stdin.on("error", () => resolve4());
3429
+ const done = new Promise((resolve5) => {
3430
+ child.stdin.on("error", () => resolve5());
3338
3431
  if (input === void 0) {
3339
3432
  child.stdin.end();
3340
- resolve4();
3433
+ resolve5();
3341
3434
  } else if (typeof input === "string" || Buffer.isBuffer(input)) {
3342
- child.stdin.end(input, () => resolve4());
3435
+ child.stdin.end(input, () => resolve5());
3343
3436
  } else {
3344
3437
  input.on("error", () => {
3345
3438
  onSourceError();
3346
- resolve4();
3439
+ resolve5();
3347
3440
  });
3348
3441
  input.pipe(child.stdin);
3349
3442
  unpipe = () => {
3350
3443
  input.unpipe(child.stdin);
3351
- resolve4();
3444
+ resolve5();
3352
3445
  };
3353
- child.stdin.on("finish", () => resolve4());
3446
+ child.stdin.on("finish", () => resolve5());
3354
3447
  child.on("close", unpipe);
3355
3448
  }
3356
3449
  });
@@ -3463,18 +3556,18 @@ function startWatchdog(child, opts, kill, isClosed) {
3463
3556
  const pgid = child.pid;
3464
3557
  let stopped = false;
3465
3558
  const stopper = new AbortController();
3466
- const sleep2 = (ms) => new Promise((resolve4) => {
3559
+ const sleep2 = (ms) => new Promise((resolve5) => {
3467
3560
  const wake = () => {
3468
3561
  clearTimeout(t);
3469
3562
  stopper.signal.removeEventListener("abort", wake);
3470
- resolve4();
3563
+ resolve5();
3471
3564
  };
3472
3565
  const t = setTimeout(wake, ms);
3473
3566
  t.unref();
3474
3567
  stopper.signal.addEventListener("abort", wake, { once: true });
3475
3568
  });
3476
- const sample = async (what, start) => {
3477
- const r = await waitWithin(start, sampleBoundMs, stopper.signal);
3569
+ const sample = async (what, start2) => {
3570
+ const r = await waitWithin(start2, sampleBoundMs, stopper.signal);
3478
3571
  if (isClosed() || stopped) {
3479
3572
  return null;
3480
3573
  }
@@ -3724,7 +3817,7 @@ async function reapChild(child, opts = {}) {
3724
3817
  if (groupCleared || !childClosed(child) || Date.now() >= deadline) {
3725
3818
  break;
3726
3819
  }
3727
- await new Promise((resolve4) => setTimeout(resolve4, Math.min(25, deadline - Date.now())));
3820
+ await new Promise((resolve5) => setTimeout(resolve5, Math.min(25, deadline - Date.now())));
3728
3821
  }
3729
3822
  if (groupCleared === false) {
3730
3823
  timedOut = true;
@@ -3779,12 +3872,12 @@ var import_promises2 = require("node:fs/promises");
3779
3872
  async function sha256File(path) {
3780
3873
  const h = (0, import_node_crypto2.createHash)("sha256");
3781
3874
  let size = 0;
3782
- await new Promise((resolve4, reject) => {
3875
+ await new Promise((resolve5, reject) => {
3783
3876
  (0, import_node_fs2.createReadStream)(path).on("data", (c) => {
3784
3877
  const b = typeof c === "string" ? Buffer.from(c) : c;
3785
3878
  size += b.length;
3786
3879
  h.update(b);
3787
- }).on("error", reject).on("end", () => resolve4());
3880
+ }).on("error", reject).on("end", () => resolve5());
3788
3881
  });
3789
3882
  const st = await (0, import_promises2.stat)(path);
3790
3883
  if (st.size !== size) {
@@ -3803,8 +3896,8 @@ async function indexPackFromFile(dir, packPath, opts = {}) {
3803
3896
  if (opts.maxInputBytes) {
3804
3897
  args.push(`--max-input-size=${opts.maxInputBytes}`);
3805
3898
  }
3806
- const run = { cwd: dir, input: (0, import_node_fs3.createReadStream)(packPath), label: "index-pack", ...opts };
3807
- const res = await runGit(args, { ...run, allowFailure: true });
3899
+ const run2 = { cwd: dir, input: (0, import_node_fs3.createReadStream)(packPath), label: "index-pack", ...opts };
3900
+ const res = await runGit(args, { ...run2, allowFailure: true });
3808
3901
  if (res.code !== 0) {
3809
3902
  const msg = res.stderr.trim().split("\n").pop() ?? "index-pack failed";
3810
3903
  const missing = /did not receive expected object|not a valid|missing/i.test(res.stderr);
@@ -3844,8 +3937,8 @@ async function refTransaction(dir, updates, opts = {}) {
3844
3937
  }
3845
3938
  async function batchCheck(dir, oids, opts = {}) {
3846
3939
  const input = Array.from(oids).join("\n") + "\n";
3847
- const run = { cwd: dir, input, maxStdoutBytes: 512 * 1024 * 1024, ...opts };
3848
- const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"], run);
3940
+ const run2 = { cwd: dir, input, maxStdoutBytes: 512 * 1024 * 1024, ...opts };
3941
+ const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"], run2);
3849
3942
  const out = [];
3850
3943
  for (const line of stdout.toString("utf8").split("\n")) {
3851
3944
  if (!line) {
@@ -3867,14 +3960,14 @@ async function resolveRevs(dir, revs, opts = {}) {
3867
3960
  const out = revs.map(
3868
3961
  (r) => r.startsWith("-") ? null : r.includes("\0") ? new PublisherError("INVALID_REQUEST", "NUL in git argument") : null
3869
3962
  );
3870
- const ask4 = revs.flatMap((r, i) => out[i] === null && !r.startsWith("-") ? [i] : []);
3871
- if (ask4.length === 0) {
3963
+ const ask2 = revs.flatMap((r, i) => out[i] === null && !r.startsWith("-") ? [i] : []);
3964
+ if (ask2.length === 0) {
3872
3965
  return out;
3873
3966
  }
3874
- const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input: ask4.map((i) => revs[i]).join("\0") + "\0", ...opts });
3967
+ const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input: ask2.map((i) => revs[i]).join("\0") + "\0", ...opts });
3875
3968
  const records = stdout.toString("utf8").split("\0");
3876
3969
  const misses = [];
3877
- for (const [j, i] of ask4.entries()) {
3970
+ for (const [j, i] of ask2.entries()) {
3878
3971
  const record2 = records[j] ?? "";
3879
3972
  if (record2 && !/ (missing|ambiguous)$/.test(record2)) {
3880
3973
  out[i] = record2;
@@ -3910,7 +4003,7 @@ async function packObjectsToFile(repoDir, extraArgs, stdinLines, opts) {
3910
4003
  opts.signal?.addEventListener("abort", onAbort, { once: true });
3911
4004
  let failure2 = null;
3912
4005
  try {
3913
- const finished = new Promise((resolve4, reject) => out.on("finish", () => resolve4()).on("error", reject));
4006
+ const finished = new Promise((resolve5, reject) => out.on("finish", () => resolve5()).on("error", reject));
3914
4007
  finished.catch(() => void 0);
3915
4008
  child.stdout.pipe(out);
3916
4009
  child.stdin.on("error", () => void 0);
@@ -3959,8 +4052,8 @@ async function packObjectsToFile(repoDir, extraArgs, stdinLines, opts) {
3959
4052
  }
3960
4053
  function delay(ms) {
3961
4054
  let t;
3962
- const p = new Promise((resolve4) => {
3963
- t = setTimeout(resolve4, ms);
4055
+ const p = new Promise((resolve5) => {
4056
+ t = setTimeout(resolve5, ms);
3964
4057
  t.unref();
3965
4058
  });
3966
4059
  p.cancel = () => clearTimeout(t);
@@ -4109,12 +4202,12 @@ async function commitPaths(dir, paths, opts) {
4109
4202
  if (paths.length === 0) {
4110
4203
  throw new PublisherError("LOCAL_STATE", "no files selected to commit");
4111
4204
  }
4112
- const run = { cwd: dir, env: authorEnv(opts) };
4205
+ const run2 = { cwd: dir, env: authorEnv(opts) };
4113
4206
  if (opts.signal) {
4114
- run.signal = opts.signal;
4207
+ run2.signal = opts.signal;
4115
4208
  }
4116
- await runGit(["add", "--pathspec-from-file=-", "--pathspec-file-nul"], { ...run, input: paths.join("\0") + "\0" });
4117
- await runGit(["commit", "-q", "--no-verify", "-m", opts.message], run);
4209
+ await runGit(["add", "--pathspec-from-file=-", "--pathspec-file-nul"], { ...run2, input: paths.join("\0") + "\0" });
4210
+ await runGit(["commit", "-q", "--no-verify", "-m", opts.message], run2);
4118
4211
  const head = await headCommit(dir);
4119
4212
  if (!head) {
4120
4213
  throw new PublisherError("GIT", "commit did not produce HEAD");
@@ -5043,6 +5136,49 @@ async function updateEntry(url2, change) {
5043
5136
  await writeCredentials(file);
5044
5137
  }
5045
5138
 
5139
+ // ../client/src/portal-request.ts
5140
+ var PortalSession = class {
5141
+ constructor(credential) {
5142
+ this.credential = credential;
5143
+ }
5144
+ credential;
5145
+ #refreshed = false;
5146
+ async ask(url2, body) {
5147
+ const { token, refresh } = this.credential;
5148
+ for (; ; ) {
5149
+ const answer2 = await askPortal(this.credential.fetch ?? fetch, url2, await token(), body);
5150
+ if (answer2.status === 401 && !this.#refreshed && refresh !== void 0) {
5151
+ this.#refreshed = true;
5152
+ if (await refresh()) {
5153
+ continue;
5154
+ }
5155
+ }
5156
+ return answer2;
5157
+ }
5158
+ }
5159
+ };
5160
+ async function askPortal(fetchImpl, url2, token, body) {
5161
+ try {
5162
+ const res = await fetchImpl(url2, {
5163
+ method: body === void 0 ? "GET" : "POST",
5164
+ headers: { authorization: `Bearer ${token}`, accept: "application/json", ...body === void 0 ? {} : { "content-type": "application/json" } },
5165
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
5166
+ signal: AbortSignal.timeout(3e4)
5167
+ });
5168
+ const text = await res.text();
5169
+ try {
5170
+ return { status: res.status, body: text ? JSON.parse(text) : null };
5171
+ } catch {
5172
+ return { status: res.status, body: void 0 };
5173
+ }
5174
+ } catch {
5175
+ return { status: 0, body: null };
5176
+ }
5177
+ }
5178
+ function isRecord(value) {
5179
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5180
+ }
5181
+
5046
5182
  // ../client/src/portal-refusal.ts
5047
5183
  function portalRefusalCode(body) {
5048
5184
  if (!isRecord(body)) {
@@ -5058,9 +5194,6 @@ function portalRefusalCode(body) {
5058
5194
  const { code, message } = error2;
5059
5195
  return typeof code === "string" && code !== "" && typeof message === "string" ? code : null;
5060
5196
  }
5061
- function isRecord(value) {
5062
- return typeof value === "object" && value !== null && !Array.isArray(value);
5063
- }
5064
5197
 
5065
5198
  // ../client/src/application.ts
5066
5199
  var APPLICATION_CREATION = "application-creation";
@@ -5070,19 +5203,12 @@ var REFUSALS = {
5070
5203
  "repository-conflict": "REPOSITORY_CONFLICT"
5071
5204
  };
5072
5205
  async function createApplication(input, opts) {
5073
- const fetchImpl = opts.fetch ?? fetch;
5074
5206
  const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/applications`;
5075
5207
  const body = { creationId: input.creationId ?? newCreationId(), name: input.name, defaultBranch: input.defaultBranch ?? "main" };
5076
- let refreshed = false;
5208
+ const portal = new PortalSession(opts);
5077
5209
  let lastOutage = "";
5078
5210
  for (let attempt = 0; attempt < ATTEMPTS; ) {
5079
- const answer2 = await post(fetchImpl, url2, await opts.token(), body);
5080
- if (answer2.status === 401 && !refreshed && opts.refresh !== void 0) {
5081
- refreshed = true;
5082
- if (await opts.refresh()) {
5083
- continue;
5084
- }
5085
- }
5211
+ const answer2 = await portal.ask(url2, body);
5086
5212
  if (answer2.status === 200 || answer2.status === 201) {
5087
5213
  return read(answer2.body, answer2.status === 201);
5088
5214
  }
@@ -5102,40 +5228,19 @@ async function createApplication(input, opts) {
5102
5228
  lastOutage = answer2.status === 0 ? "the portal could not be reached" : `the portal answered ${answer2.status}`;
5103
5229
  attempt += 1;
5104
5230
  if (attempt < ATTEMPTS) {
5105
- await new Promise((resolve4) => setTimeout(resolve4, (opts.retryDelayMs ?? 1e3) * attempt));
5231
+ await new Promise((resolve5) => setTimeout(resolve5, (opts.retryDelayMs ?? 1e3) * attempt));
5106
5232
  }
5107
5233
  }
5108
5234
  throw new PublisherError("UNAVAILABLE", `could not make the application: ${lastOutage}; run init again`, { retryable: true });
5109
5235
  }
5110
5236
  function read(body, created) {
5111
- const application = isRecord2(body) ? body["application"] : void 0;
5112
- const repository = RepositoryInfo.safeParse(isRecord2(body) ? body["repository"] : void 0);
5113
- if (!isRecord2(application) || typeof application["appId"] !== "string" || !repository.success) {
5237
+ const application = isRecord(body) ? body["application"] : void 0;
5238
+ const repository = RepositoryInfo.safeParse(isRecord(body) ? body["repository"] : void 0);
5239
+ if (!isRecord(application) || typeof application["appId"] !== "string" || !repository.success) {
5114
5240
  throw new PublisherError("UNAVAILABLE", "the portal answered with an application this version cannot read");
5115
5241
  }
5116
5242
  return { appId: application["appId"], created, repository: repository.data };
5117
5243
  }
5118
- async function post(fetchImpl, url2, token, body) {
5119
- try {
5120
- const res = await fetchImpl(url2, {
5121
- method: "POST",
5122
- headers: { authorization: `Bearer ${token}`, accept: "application/json", "content-type": "application/json" },
5123
- body: JSON.stringify(body),
5124
- signal: AbortSignal.timeout(3e4)
5125
- });
5126
- const text = await res.text();
5127
- try {
5128
- return { status: res.status, body: text ? JSON.parse(text) : null };
5129
- } catch {
5130
- return { status: res.status, body: void 0 };
5131
- }
5132
- } catch {
5133
- return { status: 0, body: null };
5134
- }
5135
- }
5136
- function isRecord2(value) {
5137
- return typeof value === "object" && value !== null && !Array.isArray(value);
5138
- }
5139
5244
 
5140
5245
  // ../client/src/transport.ts
5141
5246
  function assertCredentialEndpoint(endpoint) {
@@ -5251,8 +5356,8 @@ function fallbackCode(status2) {
5251
5356
  return status2 >= 500 ? "UNAVAILABLE" : "INVALID_REQUEST";
5252
5357
  }
5253
5358
  function sleep(ms, signal) {
5254
- return new Promise((resolve4, reject) => {
5255
- const t = setTimeout(resolve4, ms);
5359
+ return new Promise((resolve5, reject) => {
5360
+ const t = setTimeout(resolve5, ms);
5256
5361
  signal?.addEventListener(
5257
5362
  "abort",
5258
5363
  () => {
@@ -5268,21 +5373,21 @@ function sleep(ms, signal) {
5268
5373
  var RUN_STAGES = ["received", "running", "deploying", "live"];
5269
5374
  var STAGE_WORDS = { received: "received", running: "checked", deploying: "going live", live: "live" };
5270
5375
  var STATES = ["pending", "in-progress", "done", "failed", "skipped"];
5271
- function readStages(run) {
5272
- if (!isRecord3(run)) {
5376
+ function readStages(run2) {
5377
+ if (!isRecord(run2)) {
5273
5378
  return { malformed: "a run that is not an object" };
5274
5379
  }
5275
- if (!Object.hasOwn(run, "stages")) {
5380
+ if (!Object.hasOwn(run2, "stages")) {
5276
5381
  return null;
5277
5382
  }
5278
- const stages = run["stages"];
5383
+ const stages = run2["stages"];
5279
5384
  if (!Array.isArray(stages) || stages.length !== RUN_STAGES.length) {
5280
5385
  return { malformed: "stages that are not exactly four" };
5281
5386
  }
5282
5387
  const out = [];
5283
5388
  for (const [index, entry] of stages.entries()) {
5284
5389
  const stage = RUN_STAGES[index];
5285
- if (!isRecord3(entry) || entry["stage"] !== stage) {
5390
+ if (!isRecord(entry) || entry["stage"] !== stage) {
5286
5391
  return { malformed: `stage ${index + 1} is not ${stage}` };
5287
5392
  }
5288
5393
  const state = entry["state"];
@@ -5315,7 +5420,7 @@ function ending(stages) {
5315
5420
  var POLL_INTERVAL_MS = 5e3;
5316
5421
  var FOLLOW_TIMEOUT_MS = 15 * 6e4;
5317
5422
  async function followRun(input, opts) {
5318
- const fetchImpl = opts.fetch ?? fetch;
5423
+ const portal = new PortalSession(opts);
5319
5424
  const sleep2 = opts.sleep ?? sleep;
5320
5425
  const now = opts.now ?? Date.now;
5321
5426
  const interval2 = opts.intervalMs ?? POLL_INTERVAL_MS;
@@ -5325,16 +5430,9 @@ async function followRun(input, opts) {
5325
5430
  const seen = /* @__PURE__ */ new Map();
5326
5431
  let last = null;
5327
5432
  let lastError = null;
5328
- let refreshed = false;
5329
5433
  let answered = false;
5330
5434
  for (; ; ) {
5331
- const answer2 = await ask(fetchImpl, url2, await opts.token());
5332
- if (answer2.status === 401 && !refreshed && opts.refresh !== void 0) {
5333
- refreshed = true;
5334
- if (await opts.refresh()) {
5335
- continue;
5336
- }
5337
- }
5435
+ const answer2 = await portal.ask(url2, void 0);
5338
5436
  if (answer2.status === 401) {
5339
5437
  return { kind: "unauthorized" };
5340
5438
  }
@@ -5345,12 +5443,12 @@ async function followRun(input, opts) {
5345
5443
  return { kind: "forbidden" };
5346
5444
  }
5347
5445
  if (answer2.status === 200) {
5348
- const run = soleRun(answer2.body);
5349
- if ("detail" in run) {
5350
- return { kind: "malformed", detail: run.detail };
5446
+ const run2 = soleRun(answer2.body);
5447
+ if ("detail" in run2) {
5448
+ return { kind: "malformed", detail: run2.detail };
5351
5449
  }
5352
- if (run.run !== null) {
5353
- const read2 = readStages(run.run);
5450
+ if (run2.run !== null) {
5451
+ const read2 = readStages(run2.run);
5354
5452
  if (read2 === null) {
5355
5453
  return { kind: "unavailable", reason: "no-stages" };
5356
5454
  }
@@ -5367,12 +5465,12 @@ async function followRun(input, opts) {
5367
5465
  opts.onStage?.(entry);
5368
5466
  moved = true;
5369
5467
  }
5370
- if (isRecord3(run.run) && run.run["status"] === "awaiting-approval") {
5468
+ if (isRecord(run2.run) && run2.run["status"] === "awaiting-approval") {
5371
5469
  return { kind: "waiting", stages: read2.stages };
5372
5470
  }
5373
5471
  const end = ending(read2.stages);
5374
5472
  if (end !== null) {
5375
- const runId = isRecord3(run.run) && typeof run.run["runId"] === "string" ? run.run["runId"] : null;
5473
+ const runId = isRecord(run2.run) && typeof run2.run["runId"] === "string" ? run2.run["runId"] : null;
5376
5474
  return { kind: "settled", ending: end, failedAt: read2.stages.find((s) => s.state === "failed")?.word ?? null, stages: read2.stages, runId };
5377
5475
  }
5378
5476
  if (opts.untilChange === true && moved && answered) {
@@ -5391,7 +5489,7 @@ async function followRun(input, opts) {
5391
5489
  }
5392
5490
  }
5393
5491
  function soleRun(body) {
5394
- if (!isRecord3(body) || !Array.isArray(body["runs"])) {
5492
+ if (!isRecord(body) || !Array.isArray(body["runs"])) {
5395
5493
  return { detail: "an answer with no list of runs" };
5396
5494
  }
5397
5495
  const runs = body["runs"];
@@ -5400,23 +5498,6 @@ function soleRun(body) {
5400
5498
  }
5401
5499
  return { run: runs[0] ?? null };
5402
5500
  }
5403
- async function ask(fetchImpl, url2, token) {
5404
- try {
5405
- const res = await fetchImpl(url2, { headers: { authorization: `Bearer ${token}`, accept: "application/json" }, signal: AbortSignal.timeout(3e4) });
5406
- const text = await res.text();
5407
- let body = null;
5408
- try {
5409
- body = text ? JSON.parse(text) : null;
5410
- } catch {
5411
- if (res.status === 200) {
5412
- return { status: 200, body: void 0 };
5413
- }
5414
- }
5415
- return { status: res.status, body };
5416
- } catch {
5417
- return { status: 0, body: null };
5418
- }
5419
- }
5420
5501
  function portalForEndpoint(endpoint) {
5421
5502
  let url2;
5422
5503
  try {
@@ -5448,9 +5529,6 @@ async function confirmPortal(portal, endpoint, fetchImpl = fetch) {
5448
5529
  return null;
5449
5530
  }
5450
5531
  }
5451
- function isRecord3(value) {
5452
- return typeof value === "object" && value !== null && !Array.isArray(value);
5453
- }
5454
5532
 
5455
5533
  // ../client/src/preparation.ts
5456
5534
  var PREPARATION = "preparation";
@@ -5464,38 +5542,28 @@ function answerPreparation(input, opts) {
5464
5542
  return call(input, opts, { id: input.id, revision: input.revision, action: "answers", answers: input.answers });
5465
5543
  }
5466
5544
  async function call(input, opts, body) {
5467
- const fetchImpl = opts.fetch ?? fetch;
5468
5545
  const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/repositories/${encodeURIComponent(input.repoId)}/preparation`;
5469
- let refreshed = false;
5470
- for (; ; ) {
5471
- const answer2 = await ask2(fetchImpl, url2, await opts.token(), body);
5472
- if (answer2.status === 401 && !refreshed && opts.refresh !== void 0) {
5473
- refreshed = true;
5474
- if (await opts.refresh()) {
5475
- continue;
5476
- }
5477
- }
5478
- if (answer2.status === 401) {
5479
- return { kind: "unauthorized" };
5480
- }
5481
- if (answer2.status === 403) {
5482
- return { kind: "forbidden" };
5483
- }
5484
- if (answer2.status === 409) {
5485
- return { kind: "moved-on" };
5486
- }
5487
- if (answer2.status === 404) {
5488
- return portalRefusalCode(answer2.body) === "no-such-application" ? { kind: "no-application" } : { kind: "unavailable", detail: "the portal does not serve this address" };
5489
- }
5490
- if (answer2.status === 200) {
5491
- const view = readView(answer2.body);
5492
- return "detail" in view ? { kind: "malformed", detail: view.detail } : { kind: "view", view };
5493
- }
5494
- return { kind: "unavailable", detail: answer2.status === 0 ? "the portal could not be reached" : `the portal answered ${answer2.status}` };
5546
+ const answer2 = await new PortalSession(opts).ask(url2, body);
5547
+ if (answer2.status === 401) {
5548
+ return { kind: "unauthorized" };
5495
5549
  }
5550
+ if (answer2.status === 403) {
5551
+ return { kind: "forbidden" };
5552
+ }
5553
+ if (answer2.status === 409) {
5554
+ return { kind: "moved-on" };
5555
+ }
5556
+ if (answer2.status === 404) {
5557
+ return portalRefusalCode(answer2.body) === "no-such-application" ? { kind: "no-application" } : { kind: "unavailable", detail: "the portal does not serve this address" };
5558
+ }
5559
+ if (answer2.status === 200) {
5560
+ const view = readView(answer2.body);
5561
+ return "detail" in view ? { kind: "malformed", detail: view.detail } : { kind: "view", view };
5562
+ }
5563
+ return { kind: "unavailable", detail: answer2.status === 0 ? "the portal could not be reached" : `the portal answered ${answer2.status}` };
5496
5564
  }
5497
5565
  function readView(body) {
5498
- if (!isRecord4(body)) {
5566
+ if (!isRecord(body)) {
5499
5567
  return { detail: "an answer that is not a preparation" };
5500
5568
  }
5501
5569
  const { id, revision, status: status2, message, questions, publication } = body;
@@ -5516,12 +5584,12 @@ function readView(body) {
5516
5584
  }
5517
5585
  const read2 = [];
5518
5586
  for (const q of questions) {
5519
- if (!isRecord4(q) || typeof q["id"] !== "string" || typeof q["text"] !== "string" || !Array.isArray(q["options"]) || q["options"].length === 0) {
5587
+ if (!isRecord(q) || typeof q["id"] !== "string" || typeof q["text"] !== "string" || !Array.isArray(q["options"]) || q["options"].length === 0) {
5520
5588
  return { detail: "a question that cannot be answered" };
5521
5589
  }
5522
5590
  const options = [];
5523
5591
  for (const o of q["options"]) {
5524
- if (!isRecord4(o) || typeof o["value"] !== "string" || typeof o["label"] !== "string") {
5592
+ if (!isRecord(o) || typeof o["value"] !== "string" || typeof o["label"] !== "string") {
5525
5593
  return { detail: "a question with an unreadable option" };
5526
5594
  }
5527
5595
  options.push({ value: o["value"], label: o["label"] });
@@ -5529,7 +5597,7 @@ function readView(body) {
5529
5597
  read2.push({ id: q["id"], text: q["text"], options });
5530
5598
  }
5531
5599
  let pub = null;
5532
- if (isRecord4(publication) && typeof publication["status"] === "string" && typeof publication["message"] === "string") {
5600
+ if (isRecord(publication) && typeof publication["status"] === "string" && typeof publication["message"] === "string") {
5533
5601
  pub = { status: publication["status"], message: publication["message"], ...typeof publication["url"] === "string" ? { url: publication["url"] } : {} };
5534
5602
  }
5535
5603
  return { id, revision, status: status2, message, questions: read2, publication: pub, findings: readFindings(body["findings"]) };
@@ -5544,7 +5612,7 @@ function readFindings(value) {
5544
5612
  if (out.length === MAX_FINDINGS) {
5545
5613
  break;
5546
5614
  }
5547
- if (!isRecord4(f)) {
5615
+ if (!isRecord(f)) {
5548
5616
  continue;
5549
5617
  }
5550
5618
  const kind = text(f["kind"]);
@@ -5566,26 +5634,30 @@ function readFindings(value) {
5566
5634
  }
5567
5635
  return out;
5568
5636
  }
5569
- async function ask2(fetchImpl, url2, token, body) {
5570
- try {
5571
- const res = await fetchImpl(url2, {
5572
- method: body === void 0 ? "GET" : "POST",
5573
- headers: { authorization: `Bearer ${token}`, accept: "application/json", ...body === void 0 ? {} : { "content-type": "application/json" } },
5574
- ...body === void 0 ? {} : { body: JSON.stringify(body) },
5575
- signal: AbortSignal.timeout(3e4)
5576
- });
5577
- const text = await res.text();
5578
- try {
5579
- return { status: res.status, body: text ? JSON.parse(text) : null };
5580
- } catch {
5581
- return { status: res.status, body: void 0 };
5582
- }
5583
- } catch {
5584
- return { status: 0, body: null };
5637
+
5638
+ // ../client/src/creator-tools.ts
5639
+ var CREATOR_TOOLS = "creator-tools";
5640
+ async function runCreatorTool(input, opts) {
5641
+ const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/tools/${encodeURIComponent(input.tool)}`;
5642
+ const answer2 = await new PortalSession(opts).ask(url2, input.args);
5643
+ if (answer2.status === 401) {
5644
+ return { kind: "unauthorized" };
5585
5645
  }
5586
- }
5587
- function isRecord4(value) {
5588
- return typeof value === "object" && value !== null && !Array.isArray(value);
5646
+ if (answer2.status === 403) {
5647
+ return { kind: "forbidden" };
5648
+ }
5649
+ if (answer2.status === 404) {
5650
+ return { kind: "unavailable", detail: "the portal does not serve this tool" };
5651
+ }
5652
+ if (answer2.status !== 200) {
5653
+ return { kind: "unavailable", detail: answer2.status === 0 ? "the portal could not be reached" : `the portal answered ${answer2.status}` };
5654
+ }
5655
+ const body = answer2.body;
5656
+ if (!isRecord(body) || typeof body["say"] !== "string" || typeof body["next"] !== "string") {
5657
+ return { kind: "malformed", detail: "an answer with no say and next" };
5658
+ }
5659
+ const { say, next, isError, ...fields } = body;
5660
+ return { kind: "answer", answer: { say, next, isError: isError === true, fields } };
5589
5661
  }
5590
5662
 
5591
5663
  // ../client/src/onboard.ts
@@ -6374,6 +6446,14 @@ var Publisher = class {
6374
6446
  async answerPreparation(input) {
6375
6447
  return answerPreparation(input, this.portalCall());
6376
6448
  }
6449
+ /**
6450
+ * One of the creator's tools at the portal that serves this endpoint — starters, status, history,
6451
+ * going back, suggestions, sharing — with this publisher's credential and its one refresh. The
6452
+ * caller confirms the portal's `/config` lists `CREATOR_TOOLS` first.
6453
+ */
6454
+ async runCreatorTool(input) {
6455
+ return runCreatorTool(input, this.portalCall());
6456
+ }
6377
6457
  portalCall() {
6378
6458
  return {
6379
6459
  token: this.tokenFn,
@@ -6782,6 +6862,11 @@ var OPTIONS = {
6782
6862
  theirs: { type: "string" },
6783
6863
  to: { type: "string" },
6784
6864
  cursor: { type: "string" },
6865
+ replaces: { type: "string" },
6866
+ run: { type: "string" },
6867
+ answers: { type: "string" },
6868
+ add: { type: "string", multiple: true },
6869
+ remove: { type: "string", multiple: true },
6785
6870
  ref: { type: "string", multiple: true },
6786
6871
  json: { type: "boolean" },
6787
6872
  quiet: { type: "boolean" },
@@ -6845,6 +6930,28 @@ async function discoverEndpoint(portal, fetchImpl = fetch) {
6845
6930
  return endpoint;
6846
6931
  }
6847
6932
 
6933
+ // src/portal.ts
6934
+ var confirmed = /* @__PURE__ */ new Map();
6935
+ async function confirmedPortal(endpoint, opts = {}) {
6936
+ const derived = portalForEndpoint(endpoint);
6937
+ if (derived === null) {
6938
+ return { kind: "none" };
6939
+ }
6940
+ const asked = opts.fetch === void 0 ? confirmed.get(endpoint) ?? confirmPortal(derived, endpoint) : confirmPortal(derived, endpoint, opts.fetch);
6941
+ if (opts.fetch === void 0) {
6942
+ confirmed.set(endpoint, asked);
6943
+ }
6944
+ const config2 = await asked;
6945
+ return config2 === null ? { kind: "unconfirmed", portal: derived } : { kind: "confirmed", portal: derived, terminal: config2.terminal };
6946
+ }
6947
+ async function portalFor(endpoint, route, opts = {}) {
6948
+ const at = await confirmedPortal(endpoint, opts);
6949
+ if (at.kind !== "confirmed") {
6950
+ return { kind: "no-portal" };
6951
+ }
6952
+ return at.terminal.includes(route) ? { kind: "served", portal: at.portal } : { kind: "no-route", portal: at.portal };
6953
+ }
6954
+
6848
6955
  // src/progress.ts
6849
6956
  var SAYS = {
6850
6957
  received: { doing: "Shadow is picking up your changes", done: "Shadow has your changes", still: "still picking up your changes" },
@@ -6945,28 +7052,27 @@ function stageReporter(io) {
6945
7052
 
6946
7053
  // src/follow.ts
6947
7054
  async function followPublication(p, endpoint, o, opts) {
6948
- const derived = portalForEndpoint(endpoint);
6949
- const confirmed = derived === null ? null : await confirmPortal(derived, endpoint, opts.fetch);
6950
- const portal = confirmed === null ? null : derived;
7055
+ const confirmed2 = await confirmedPortal(endpoint, opts);
7056
+ const portal = confirmed2.kind === "confirmed" ? confirmed2.portal : null;
6951
7057
  const elsewhere = (reason) => ({ kind: "unavailable", reason, portal, hint: inPortal(portal, "Published.", "Follow it") });
6952
- if (portal === null || confirmed === null) {
7058
+ if (portal === null || confirmed2.kind !== "confirmed") {
6953
7059
  return elsewhere("no-portal");
6954
7060
  }
6955
- if (!confirmed.terminal.includes(RUN_PROGRESS)) {
7061
+ if (!confirmed2.terminal.includes(RUN_PROGRESS)) {
6956
7062
  const noRoute = { kind: "unavailable", reason: "no-route" };
6957
7063
  return { ...noRoute, portal, hint: progressHint(noRoute, portal) };
6958
7064
  }
6959
- const refs = Object.entries(o.refs ?? {});
6960
- if (refs.length !== 1) {
7065
+ const moved = publishedRef(o);
7066
+ if (moved === null) {
6961
7067
  return elsewhere("several-refs");
6962
7068
  }
6963
- const [ref, commit] = refs[0];
7069
+ const { ref, commit } = moved;
6964
7070
  opts.log("Following it to live: received -> checked -> going live -> live. Ctrl-C stops watching; the publication is saved and carries on without you.");
6965
7071
  const outcome2 = await p.followRun(
6966
7072
  { portal, repoId: o.repoId, publicationId: o.publicationId, ref, commit },
6967
7073
  { onStage: opts.onStage, ...opts.timeoutMs === void 0 ? {} : { timeoutMs: opts.timeoutMs }, ...opts.untilChange === true ? { untilChange: true } : {} }
6968
7074
  ).finally(() => opts.stopStages?.());
6969
- const answerable = confirmed.terminal.includes(PREPARATION);
7075
+ const answerable = confirmed2.terminal.includes(PREPARATION);
6970
7076
  const dir = shellWord(opts.dir);
6971
7077
  const next = outcome2.kind === "timed-out" || outcome2.kind === "moving" ? { action: "follow", command: `publisher follow ${dir} --publication ${o.publicationId}${opts.untilChange === true ? " --until-change" : ""} --json` } : outcome2.kind === "waiting" && answerable ? { action: "question", command: `publisher question ${dir} --json` } : void 0;
6972
7078
  const findings = await failedChecks(p, portal, o.repoId, outcome2, answerable);
@@ -6981,6 +7087,14 @@ async function followPublication(p, endpoint, o, opts) {
6981
7087
  }
6982
7088
  return { ...outcome2, portal, hint: progressHint(outcome2, portal, answerable), ...next === void 0 ? {} : { next } };
6983
7089
  }
7090
+ function publishedRef(o) {
7091
+ const refs = Object.entries(o.refs ?? {});
7092
+ if (refs.length !== 1) {
7093
+ return null;
7094
+ }
7095
+ const [ref, commit] = refs[0];
7096
+ return { ref, commit };
7097
+ }
6984
7098
  async function failedChecks(p, portal, repoId, outcome2, served) {
6985
7099
  if (!served || outcome2.kind !== "settled" || outcome2.ending !== "failed" || outcome2.runId === null) {
6986
7100
  return null;
@@ -7080,7 +7194,7 @@ function resolveChoice(answer2, names) {
7080
7194
  }
7081
7195
  return names.find((n) => n.toLowerCase() === a.toLowerCase()) ?? null;
7082
7196
  }
7083
- async function ask3(question2) {
7197
+ async function ask(question2) {
7084
7198
  const rl = (0, import_promises11.createInterface)({ input: process.stdin, output: process.stderr });
7085
7199
  try {
7086
7200
  return await rl.question(question2);
@@ -7098,7 +7212,7 @@ ${purpose}
7098
7212
  names.forEach((n, i) => process.stderr.write(` ${i + 1}) ${n}
7099
7213
  `));
7100
7214
  for (let attempt = 0; attempt < 3; attempt++) {
7101
- const answer2 = await ask3(`
7215
+ const answer2 = await ask(`
7102
7216
  Which one? [1-${names.length}, or a name] `);
7103
7217
  if (answer2 === null) {
7104
7218
  return null;
@@ -7118,7 +7232,7 @@ Name this connection, so you never have to type ${endpoint} again.
7118
7232
  (for example: prod, dev, staging)
7119
7233
  `);
7120
7234
  for (let attempt = 0; attempt < 3; attempt++) {
7121
- const answer2 = await ask3(suggested ? `
7235
+ const answer2 = await ask(suggested ? `
7122
7236
  Profile name [${suggested}]: ` : `
7123
7237
  Profile name: `);
7124
7238
  if (answer2 === null) {
@@ -7407,14 +7521,14 @@ function packageVersion() {
7407
7521
  }
7408
7522
  }
7409
7523
  function claude(args) {
7410
- return new Promise((resolve4) => {
7524
+ return new Promise((resolve5) => {
7411
7525
  let stdout = "";
7412
7526
  let stderr = "";
7413
7527
  const child = (0, import_node_child_process3.spawn)("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
7414
7528
  child.stdout.on("data", (d) => stdout += d.toString("utf8"));
7415
7529
  child.stderr.on("data", (d) => stderr += d.toString("utf8"));
7416
- child.on("error", (e) => resolve4({ code: null, stdout, stderr: e.message, missing: e.code === "ENOENT" }));
7417
- child.on("close", (code) => resolve4({ code, stdout, stderr, missing: false }));
7530
+ child.on("error", (e) => resolve5({ code: null, stdout, stderr: e.message, missing: e.code === "ENOENT" }));
7531
+ child.on("close", (code) => resolve5({ code, stdout, stderr, missing: false }));
7418
7532
  });
7419
7533
  }
7420
7534
  async function installSkills({ log }) {
@@ -7551,15 +7665,15 @@ var skills = {
7551
7665
 
7552
7666
  // src/init.ts
7553
7667
  async function repositoryForInit(p, endpoint, name, opts = {}) {
7554
- const portal = portalForEndpoint(endpoint);
7555
- if (portal === null) {
7668
+ const confirmed2 = await confirmedPortal(endpoint, opts);
7669
+ if (confirmed2.kind === "none") {
7556
7670
  return { repoId: (await p.createRepository({ name, grants: [] })).repoId, appId: null };
7557
7671
  }
7558
- const confirmed = await confirmPortal(portal, endpoint, opts.fetch);
7559
- if (confirmed === null) {
7672
+ const { portal } = confirmed2;
7673
+ if (confirmed2.kind === "unconfirmed") {
7560
7674
  throw new PublisherError("UNAVAILABLE", `could not confirm that ${portal} publishes to ${endpoint}; run init again`, { retryable: true });
7561
7675
  }
7562
- if (!confirmed.terminal.includes(APPLICATION_CREATION)) {
7676
+ if (!confirmed2.terminal.includes(APPLICATION_CREATION)) {
7563
7677
  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`);
7564
7678
  }
7565
7679
  const made = await p.createApplication({ portal, name });
@@ -7766,15 +7880,20 @@ function parseSpecs(specs) {
7766
7880
  return answers;
7767
7881
  }
7768
7882
  async function portalAt(endpoint, opts) {
7769
- const derived = portalForEndpoint(endpoint);
7770
- const confirmed = derived === null ? null : await confirmPortal(derived, endpoint, opts.fetch);
7771
- if (derived === null || confirmed === null) {
7772
- return { kind: "unavailable", reason: "no-portal", portal: null, hint: "Answer it in the portal." };
7773
- }
7774
- if (!confirmed.terminal.includes(PREPARATION)) {
7775
- 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}.` };
7883
+ const at = await portalFor(endpoint, PREPARATION, opts);
7884
+ switch (at.kind) {
7885
+ case "served":
7886
+ return { portal: at.portal };
7887
+ case "no-portal":
7888
+ return { kind: "unavailable", reason: "no-portal", portal: null, hint: "Answer it in the portal." };
7889
+ case "no-route":
7890
+ return {
7891
+ kind: "unavailable",
7892
+ reason: "no-route",
7893
+ portal: at.portal,
7894
+ hint: `This portal cannot show the question in the terminal yet. Answer it in the portal: ${at.portal}.`
7895
+ };
7776
7896
  }
7777
- return { portal: derived };
7778
7897
  }
7779
7898
  function shown(portal, r) {
7780
7899
  switch (r.kind) {
@@ -7843,6 +7962,116 @@ var answer = {
7843
7962
  }
7844
7963
  };
7845
7964
 
7965
+ // src/commands/creator.ts
7966
+ var import_node_path10 = require("node:path");
7967
+
7968
+ // src/creator.ts
7969
+ async function creatorTool(p, endpoint, tool, args, opts = {}) {
7970
+ const at = await portalFor(endpoint, CREATOR_TOOLS, opts);
7971
+ if (at.kind === "no-portal") {
7972
+ throw new PublisherError("UNAVAILABLE", "no portal answers for this endpoint, so this can only be done in the chat or the portal");
7973
+ }
7974
+ if (at.kind === "no-route") {
7975
+ throw new PublisherError("UNAVAILABLE", `this portal cannot do this from the terminal yet; do it in the chat or at ${at.portal}`);
7976
+ }
7977
+ const result = await p.runCreatorTool({ portal: at.portal, tool, args });
7978
+ switch (result.kind) {
7979
+ case "answer":
7980
+ return result.answer;
7981
+ case "unauthorized":
7982
+ throw new PublisherError("UNAUTHORIZED", "the portal did not accept this sign-in; run publisher login");
7983
+ case "forbidden":
7984
+ throw new PublisherError("FORBIDDEN", "the portal will not let this sign-in do that in this organisation");
7985
+ case "unavailable":
7986
+ throw new PublisherError("UNAVAILABLE", `the portal could not be asked: ${result.detail}`, { retryable: true });
7987
+ case "malformed":
7988
+ throw new PublisherError("UNAVAILABLE", `the portal's answer could not be read: ${result.detail}`);
7989
+ }
7990
+ }
7991
+ function printed(answer2) {
7992
+ return { say: answer2.say, next: answer2.next, refused: answer2.isError, ...answer2.fields };
7993
+ }
7994
+
7995
+ // src/commands/creator.ts
7996
+ async function run(inv, dir, tool, args) {
7997
+ const { p, endpoint } = await inv.client(dir);
7998
+ const answer2 = await creatorTool(p, endpoint, tool, args);
7999
+ inv.out(printed(answer2));
8000
+ return answer2.isError ? 1 : 0;
8001
+ }
8002
+ async function folderApp(inv) {
8003
+ const dir = (0, import_node_path10.resolve)(".");
8004
+ return { dir, app: await inv.repoId(dir) };
8005
+ }
8006
+ function listFlag(inv, name) {
8007
+ const value = inv.flags[name];
8008
+ const parts = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
8009
+ return parts.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
8010
+ }
8011
+ var starters = {
8012
+ git: false,
8013
+ run: (inv) => run(inv, void 0, "list_starters", { need: inv.cmd.slice(1).join(" ") })
8014
+ };
8015
+ var start = {
8016
+ git: false,
8017
+ async run(inv) {
8018
+ const starter = inv.cmd[1];
8019
+ if (starter === void 0) {
8020
+ throw new PublisherError("INVALID_REQUEST", "start takes a starter id; publisher starters lists them");
8021
+ }
8022
+ return run(inv, void 0, "use_starter", { starter, ...inv.optStr("name"), ...inv.optStr("replaces") });
8023
+ }
8024
+ };
8025
+ var history = {
8026
+ git: false,
8027
+ async run(inv) {
8028
+ const { dir, app } = await folderApp(inv);
8029
+ return run(inv, dir, "app_history", { app });
8030
+ }
8031
+ };
8032
+ var goBack = {
8033
+ git: false,
8034
+ async run(inv) {
8035
+ const version2 = Number(inv.cmd[1]);
8036
+ if (!Number.isSafeInteger(version2) || version2 < 1) {
8037
+ throw new PublisherError("INVALID_REQUEST", "go-back takes a version number; publisher history lists them");
8038
+ }
8039
+ const { dir, app } = await folderApp(inv);
8040
+ return run(inv, dir, "go_back", { app, version: version2 });
8041
+ }
8042
+ };
8043
+ var suggestions = {
8044
+ git: false,
8045
+ async run(inv) {
8046
+ const { dir, app } = await folderApp(inv);
8047
+ return run(inv, dir, "app_suggestions", { app });
8048
+ }
8049
+ };
8050
+ var suggestion = {
8051
+ git: false,
8052
+ async run(inv) {
8053
+ const [, verb, id] = inv.cmd;
8054
+ if (verb !== "done" && verb !== "decline" || id === void 0) {
8055
+ throw new PublisherError("INVALID_REQUEST", "suggestion takes done or decline, and a suggestion id from publisher suggestions");
8056
+ }
8057
+ const { dir, app } = await folderApp(inv);
8058
+ const status2 = verb === "done" ? "done" : "declined";
8059
+ const runId = inv.str("run");
8060
+ return run(inv, dir, "resolve_suggestion", { app, suggestion: id, status: status2, ...runId === void 0 ? {} : { runId } });
8061
+ }
8062
+ };
8063
+ var share = {
8064
+ git: false,
8065
+ async run(inv) {
8066
+ const access2 = inv.cmd[1];
8067
+ if (access2 === void 0) {
8068
+ throw new PublisherError("INVALID_REQUEST", "share takes organisation, invited or anyone-with-link");
8069
+ }
8070
+ const { dir, app } = await folderApp(inv);
8071
+ return run(inv, dir, "share_app", { app, access: access2, add: listFlag(inv, "add"), remove: listFlag(inv, "remove") });
8072
+ }
8073
+ };
8074
+
7846
8075
  // src/commands/sync.ts
7847
8076
  var sync = {
7848
8077
  git: true,
@@ -7861,6 +8090,7 @@ var sync = {
7861
8090
  const acceptedAt = sending.done();
7862
8091
  const o = r.published;
7863
8092
  const followed = o !== null && o.state === "accepted" && inv.flags["no-wait"] !== true ? await followPublication(p, endpoint, o, { ...inv.following, timeoutMs, dir }) : null;
8093
+ const answered = await answerSuggestion(inv, p, endpoint, linked?.repoId ?? (await readAssociation(dir))?.repoId, o);
7864
8094
  const syncTimings = clock.read(followed !== null && "stages" in followed ? runTimings(followed.stages, acceptedAt) : {});
7865
8095
  inv.log(timingsLine(syncTimings));
7866
8096
  inv.out({
@@ -7869,11 +8099,26 @@ var sync = {
7869
8099
  linked: linked === null ? null : { repoId: linked.repoId, appId: linked.appId, created: linked.created },
7870
8100
  ...outcome(r),
7871
8101
  progress: followed,
8102
+ ...answered === null ? {} : { answered },
7872
8103
  timings: syncTimings
7873
8104
  });
7874
8105
  return r.state === "published" || r.state === "up_to_date" ? 0 : r.state === "unknown" ? 3 : 2;
7875
8106
  }
7876
8107
  };
8108
+ async function answerSuggestion(inv, p, endpoint, repoId, o) {
8109
+ const id = inv.str("answers");
8110
+ if (id === void 0 || repoId === void 0 || o === null || o.state !== "accepted" || o.publicationId === null) {
8111
+ return null;
8112
+ }
8113
+ const moved = publishedRef(o);
8114
+ if (moved === null) {
8115
+ return null;
8116
+ }
8117
+ const { ref, commit } = moved;
8118
+ return printed(
8119
+ await creatorTool(p, endpoint, "resolve_suggestion", { app: repoId, suggestion: id, status: "done", publication: { publicationId: o.publicationId, ref, commit } })
8120
+ );
8121
+ }
7877
8122
  function outcome(r) {
7878
8123
  const o = r.published;
7879
8124
  return {
@@ -7985,6 +8230,9 @@ var EXIT_CODE = {
7985
8230
  REPOSITORY_ARCHIVED: 1,
7986
8231
  ACCESS_CONFLICT: 1,
7987
8232
  PUBLICATION_NOT_FOUND: 1,
8233
+ NOTHING_CHANGED: 1,
8234
+ COMMIT_NOT_FOUND: 1,
8235
+ PATH_NOT_FOUND: 1,
7988
8236
  PUBLICATION_CONFLICT: 1,
7989
8237
  PUBLICATION_EXPIRED: 1,
7990
8238
  UPLOAD_MISSING: 1,
@@ -8009,7 +8257,7 @@ function exitCodeFor(code) {
8009
8257
  }
8010
8258
 
8011
8259
  // src/invocation.ts
8012
- var import_node_path10 = require("node:path");
8260
+ var import_node_path11 = require("node:path");
8013
8261
  var Invocation = class {
8014
8262
  cmd;
8015
8263
  flags;
@@ -8052,7 +8300,7 @@ var Invocation = class {
8052
8300
  };
8053
8301
  /** The folder named at position `i` of the command path, or the current one. */
8054
8302
  dirArg(i) {
8055
- return (0, import_node_path10.resolve)(this.cmd[i] ?? ".");
8303
+ return (0, import_node_path11.resolve)(this.cmd[i] ?? ".");
8056
8304
  }
8057
8305
  /** A flag's value when it is a string, else undefined. */
8058
8306
  str(key) {
@@ -8109,15 +8357,15 @@ async function demand(r, input) {
8109
8357
  }
8110
8358
  const next = nextStep(credentialStatus(input));
8111
8359
  const details = { profiles: r.kind === "none" ? [] : r.profiles, next };
8112
- const run = next.command === null ? "" : ` Run: ${next.command}`;
8360
+ const run2 = next.command === null ? "" : ` Run: ${next.command}`;
8113
8361
  switch (r.kind) {
8114
8362
  case "unknown-profile":
8115
- throw new PublisherError("LOCAL_STATE", `no such profile: ${r.name}.${run}`, { details });
8363
+ throw new PublisherError("LOCAL_STATE", `no such profile: ${r.name}.${run2}`, { details });
8116
8364
  case "ambiguous":
8117
- throw new PublisherError("LOCAL_STATE", `several connections and none is the default: pass --profile <name>, or choose one once.${run}`, { details });
8365
+ throw new PublisherError("LOCAL_STATE", `several connections and none is the default: pass --profile <name>, or choose one once.${run2}`, { details });
8118
8366
  case "none": {
8119
8367
  const where = await whereCredentialsWere();
8120
- throw new PublisherError("LOCAL_STATE", `not connected to a publishing service yet: ${where.sentence}${run}`, { details: { ...details, credentialsFile: where.details } });
8368
+ throw new PublisherError("LOCAL_STATE", `not connected to a publishing service yet: ${where.sentence}${run2}`, { details: { ...details, credentialsFile: where.details } });
8121
8369
  }
8122
8370
  }
8123
8371
  }
@@ -8164,12 +8412,22 @@ Usage:
8164
8412
  checked -> going live -> live, up to 15 minutes)
8165
8413
  [--timeout <seconds>] follow for at most this long; if it is still going, the
8166
8414
  result's next.command picks it up
8415
+ [--answers <suggestion>] the publication answers that suggestion: close it as done
8167
8416
  publisher follow [dir] --publication <id> follow an accepted publication's run where a sync left it
8168
8417
  [--timeout <seconds>] [--until-change] --until-change (sync too): return at the next step, to tell it
8169
8418
  publisher question [dir] [--repo <id>] the question the app's preparation waits on, if any,
8170
8419
  with each option's value (answering needs owner or admin)
8171
8420
  publisher answer <question>=<value>... [--repo <id>] answer it from this folder; a value that is not one of
8172
8421
  the question's options is refused before anything is sent
8422
+ publisher starters [what you want to stop doing] working apps to start from, the ones that fit first
8423
+ publisher start <starter> [--name <n>] [--replaces <what>] a new app made from a starter, live in about a minute
8424
+ publisher history this folder's app: the versions that went live, newest first
8425
+ publisher go-back <version> put the code of an earlier version back, as the next version
8426
+ (the data and who can open the app stay as they are)
8427
+ publisher suggestions what colleagues suggested for this folder's app, still open
8428
+ publisher suggestion done|decline <id> [--run <id>] close one; sync --answers <id> closes it with that publication
8429
+ publisher share organisation|invited|anyone-with-link [--add <emails>] [--remove <emails>]
8430
+ who can open this folder's app; new invitees get the link by mail
8173
8431
  publisher merge-check [dir] --ours <ref> --theirs <ref> [--apply]
8174
8432
  publisher info [dir] repository state as seen by the server
8175
8433
  publisher doctor environment check (node, git, credentials, association)
@@ -8213,7 +8471,14 @@ var COMMANDS = /* @__PURE__ */ new Map([
8213
8471
  ["answer", answer],
8214
8472
  ["skills", skills],
8215
8473
  ["merge-check", mergeCheck2],
8216
- ["info", info]
8474
+ ["info", info],
8475
+ ["starters", starters],
8476
+ ["start", start],
8477
+ ["history", history],
8478
+ ["go-back", goBack],
8479
+ ["suggestions", suggestions],
8480
+ ["suggestion", suggestion],
8481
+ ["share", share]
8217
8482
  ]);
8218
8483
  async function main(argv) {
8219
8484
  const inv = new Invocation(parseArgs(argv));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnovate/shadow-publisher",
3
- "version": "0.11.11",
3
+ "version": "0.12.2",
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",