@minnovate/shadow-publisher 0.8.4 → 0.9.1

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.8.4",
4
+ "version": "0.9.1",
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"]
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: publish-app
3
3
  description: Publish or update the user's app (a folder) with the publisher. Use when the user says "publish my app", "update my app", "save my changes", "push my changes", "save this version online", or asks to resolve an interrupted publication. Handles first-time setup, every later update, interruption recovery and conflicts by running the `publisher` command; never uses git to commit or push.
4
- allowed-tools: Bash(publisher *), Bash(git status *), Bash(git diff *), Bash(git log *)
4
+ allowed-tools: Bash(publisher *), Bash(npm install -g @minnovate/shadow-publisher*), Bash(git status *), Bash(git diff *), Bash(git log *)
5
5
  ---
6
6
 
7
7
  # Publish or update the user's app
@@ -25,14 +25,16 @@ Every command accepts `--json` and prints exactly one JSON object on stdout. Exi
25
25
  publisher --version
26
26
  ```
27
27
 
28
- If the command is not found, tell the user the publisher is not installed and that one command
29
- installs it: `npm install -g @minnovate/shadow-publisher` (it needs Node.js 22). Stop there.
28
+ If the command is not found, install it: `npm install -g @minnovate/shadow-publisher` (it needs
29
+ Node.js 22). In a Claude Cowork task this is normal: the sandbox starts empty every time, so install it
30
+ without asking. On the person's own computer, say in one line that you are installing the
31
+ publisher. If `npm` itself is missing, tell them Node.js 22 must be installed first, and stop.
30
32
 
31
33
  ```bash
32
34
  publisher doctor --json
33
35
  ```
34
36
 
35
- - `gitOk:false` → tell the user Git 2.46 or later must be installed; stop.
37
+ - `gitOk:false` → tell the user Git 2.43 or later must be installed; stop.
36
38
  - Otherwise do exactly what **`next.action`** says. `doctor` works this out for you, and the same
37
39
  `next` object rides on `error.details` of any command that cannot resolve a connection — so you
38
40
  never have to interpret `credentials`, `endpoint` or `profiles` yourself.
@@ -40,11 +42,27 @@ publisher doctor --json
40
42
  | `next.action` | What it means | What to do |
41
43
  |---|---|---|
42
44
  | `"ready"` | Signed in and pointed somewhere. | **Nothing. Go to step 2 and do not mention any of this.** |
43
- | `"sign-in"` | Not connected to anything yet. | **Ask them nothing** — `next.ask` is `null`. Just run `next.command` (`publisher login --json`). It finds the service, opens their browser and prints a code for them to confirm; tell them to expect that, and wait. Use `--portal <url>` only if they say they are on a different platform. |
45
+ | `"sign-in"` | Not connected to anything yet. | **Ask them nothing** — `next.ask` is `null`. Run `next.command` the way "Signing in" below says. Use `--portal <url>` only if they say they are on a different platform. |
44
46
  | `"choose-profile"` | Several places are set up and none is marked usual. | Show `next.profiles` and ask `next.ask`. Then run `next.command` with the name they picked. |
45
- | `"reauthenticate"` | The place is known; the credential is gone or expired. | Just run `next.command`. `next.ask` is `null` — do **not** ask them for anything; the browser sign-in asks them itself. |
47
+ | `"reauthenticate"` | The place is known; the credential is gone or expired. | Run `next.command` the way "Signing in" below says. `next.ask` is `null` — do **not** ask them for anything; the browser sign-in asks them itself. |
46
48
  | `"bind-endpoint"` | A token is set in the environment, but nothing said where it may go. | **Stop** and tell them publishing is blocked by a token set in this shell. `next.command` is `null` on purpose: never set `GIT_PUBLISHER_ENDPOINT` or pass `--endpoint` yourself — that is exactly the choice the check leaves to whoever set the token. |
47
49
 
50
+ ### Signing in
51
+
52
+ `publisher login` waits until the person has confirmed a code in their browser, and your shell may
53
+ show nothing until a command ends. So never run it in the foreground. Start it in the background with
54
+ its output going to a file, and read the file:
55
+
56
+ ```bash
57
+ publisher login --json > /tmp/publisher-login.log 2>&1 &
58
+ ```
59
+
60
+ Within a few seconds the log shows `To sign in, open:` with a link under it, and `and confirm the
61
+ code:` with the code under it. Give the person both, as a link they can click, and say the page will ask them to sign in with their work account.
62
+ Then read the log every few seconds until it holds a JSON object (`ok: true`), for up to ten minutes.
63
+ In a Claude Cowork task this happens again in every new task, because the sandbox forgets the
64
+ sign-in when the task ends.
65
+
48
66
  **Never ask the user for a token, an endpoint, a URL or a credential — in any words.** They are
49
67
  publishing an app; they do not know what those are, and you do not need them: `login` discovers
50
68
  where publishing lives and signs them in through their browser. `next.ask` is `null` whenever
@@ -68,36 +86,36 @@ know what was left out (the exclusion rules are a safety net, not a secret scann
68
86
  ## 3. Every publish, the first one included
69
87
 
70
88
  ```bash
71
- publisher sync . --message "<a few words on what changed>" --timeout 90 --json
89
+ publisher sync . --message "<a few words on what changed>" --until-change --timeout 100 --json
72
90
  ```
73
91
 
74
92
  That one command records what changed in the folder, brings in anything published meanwhile (a
75
93
  fast-forward or a clean merge — it never rewrites the user's work), and publishes the result. Write
76
94
  the message in the user's words ("Add a contact page"), not git's.
77
95
 
78
- ## 4. Reading the result
96
+ ## 4. Telling them how it goes, step by step
79
97
 
80
- After a publish, the command follows the app going live, asking every 5 seconds, for at most
81
- `--timeout` seconds. Keep that at 90: your shell tool shows output only when a command ends, and
82
- cuts one off after two minutes. With `--json` it prints one line per step, then the result object
83
- as the LAST line.
98
+ After a publish, the app goes through four steps: **received**, **checked**, **going live**,
99
+ **live**. It takes a few minutes, and the person sees nothing unless you tell them. So the command
100
+ returns as soon as the next step starts (`--until-change`), and you tell them, then wait for the
101
+ next one:
84
102
 
85
- If the run is still going when the 90 seconds are up, `progress.kind` is `"timed-out"` and
86
- `progress.next.command` picks it up. Run it (it is `publisher follow --publication <id> --json`;
87
- add `--timeout 90`) and repeat until `progress.kind` is no longer `"timed-out"`, up to about 15
88
- minutes in all. Each call prints the steps as they now stand, from the first: relay only the ones
89
- you have not relayed yet.
103
+ 1. Read the result from the LAST line of the output (the lines before it are the steps, as JSON).
104
+ 2. If `progress.kind` is `"moving"` or `"timed-out"`, the app is still on its way. Tell the person
105
+ where it is in one short sentence, in their language — `progress.hint` says it ("Now: going
106
+ live.") — then run `progress.next.command` exactly as given, adding `--timeout 100`.
107
+ 3. Repeat until `progress.kind` is anything else, for up to about 15 minutes in all. Then tell them
108
+ how it ended (below).
90
109
 
91
- ```json
92
- {"event":"stage","stage":"running","word":"checked","state":"in-progress","at":"2026-09-23T10:00:10Z"}
93
- ```
110
+ Name a step only by its `word` — **received**, **checked**, **going live**, **live** — never by
111
+ its `stage` name, and never a step no line said. Each call starts by printing the steps as they
112
+ stand: tell only what is new.
94
113
 
95
- Relay each step in its `word` — **received**, **checked**, **going live**, **live** — and nothing
96
- else: not the `stage` name, not a step you guessed. Read the result from the last line. **If the
97
- command was cut off while following**, the publication had already succeeded before the following
98
- began: say it is published, and follow it with `publisher follow --publication <publicationId>
99
- --timeout 90 --json` (the id is in the `sync` output's `publication`, or `publisher status`). Do
100
- not run `sync` again to find out. `--no-wait` skips the following, for when the user asks not to wait.
114
+ **If a command was cut off while following**, the publication had already succeeded before the
115
+ following began: say it is published, and follow it with `publisher follow <folder> --publication
116
+ <publicationId> --until-change --timeout 100 --json` (the id is in the `sync` output's
117
+ `publication`, or `publisher status`). Do not run `sync` again to find out. `--no-wait` skips the
118
+ following, for when the person asks not to wait.
101
119
 
102
120
  `state` says what happened; `hint` is one sentence you may pass on as it is.
103
121
 
@@ -108,7 +126,7 @@ not run `sync` again to find out. `--no-wait` skips the following, for when the
108
126
  checked, and this organisation does not deploy automatically. `"failed"`: it stopped at
109
127
  `failedAt`; the portal says why — do not guess the reason. `"stopped"`: a newer publication
110
128
  replaced it.
111
- - `"timed-out"`: still going; run `progress.next.command` as above. After about 15 minutes in
129
+ - `"moving"` or `"timed-out"`: still going; follow it as step 4 says. After about 15 minutes in
112
130
  all, stop and point them to the portal (`progress.portal`).
113
131
  - `"unauthorized"`: published, but the portal did not accept this sign-in; run `publisher login`
114
132
  before the next publish.
@@ -134,7 +152,28 @@ not run `sync` again to find out. `--no-wait` skips the following, for when the
134
152
  - Exit code 4: run `publisher doctor --json` and follow `next.action` (normally `reauthenticate`:
135
153
  run `next.command`). Never ask the user for a token.
136
154
 
137
- ## 5. Interrupted earlier?
155
+ ## 5. A folder that starts empty
156
+
157
+ On a new computer, or in a Claude Cowork task (whose sandbox starts empty every time), the app the
158
+ person means is not here yet. Find it, and bring it into a new folder:
159
+
160
+ ```bash
161
+ publisher list --json
162
+ ```
163
+
164
+ `apps[]` lists what they published, newest first, each with its `name` and a `checkout` command.
165
+ Ask which one only if the name does not settle it, then run its `checkout` command and work in that
166
+ folder. Never `sync` an empty new folder in its place: that makes a second app.
167
+
168
+ ## If the service cannot be reached
169
+
170
+ In a sandbox (Claude Cowork, Claude Code on the web) a `NETWORK` error, or `CONNECT tunnel failed,
171
+ response 403`, usually means the sandbox's network does not allow the platform. Tell the person that
172
+ their Claude settings must allow the platform's domain and its storage (`*.s3.<region>.amazonaws.com`)
173
+ under Settings → Capabilities, or that their organisation's owner must, on a Team or Enterprise plan.
174
+ Do not retry until they say it is done.
175
+
176
+ ## 6. Interrupted earlier?
138
177
 
139
178
  If a previous publish was interrupted (network loss, closed session), `publisher status . --json`
140
179
  resumes or resolves it. Always run it before starting a new sync when the last result was unknown.
@@ -12,7 +12,7 @@ result. Never run `git add`, `git commit`, `git push`, `git pull`, `git merge`,
12
12
  `git reset` or `git rebase` in the user's app folder.
13
13
 
14
14
  ```bash
15
- publisher sync . --json
15
+ publisher sync . --until-change --timeout 100 --json
16
16
  ```
17
17
 
18
18
  Read the step lines and `state` exactly as the `publish-app` skill describes (step 4): after a
@@ -40,7 +40,7 @@ publisher answer <question id>=<value> --json
40
40
 
41
41
  A value that is not an option is refused before anything is sent; `error.details.questions` lists
42
42
  the choices. Exit 4 means this sign-in may not answer (an owner or admin can); exit 3 means ask
43
- again. Then `publisher follow . --publication <id> --json` to see it go live.
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
45
  ## Just looking
46
46
 
@@ -63,9 +63,10 @@ publisher workspace-status . --json # state + hint; exit 2 means conflict
63
63
 
64
64
  ## A fresh copy
65
65
 
66
- When the user wants their app in a new folder (a new computer, a clean start):
66
+ When the user wants their app in a new folder (a new computer, a clean start, a Claude Cowork task):
67
67
 
68
68
  ```bash
69
+ publisher list --json # their apps, each with its checkout command
69
70
  publisher checkout <new-folder> --repo <repoId> --json # a NEW folder only; refuses a non-empty one
70
71
  ```
71
72
 
@@ -87,7 +88,7 @@ recorded locally, and the published version is still on the service.
87
88
 
88
89
  Each file in `conflicts[]` now holds both versions between `<<<<<<<`, `=======` and `>>>>>>>`
89
90
  lines. Edit each to what they chose, and remove those lines.
90
- 4. Run `publisher sync . --json`. It records the edit and publishes it. If a file still carries the
91
+ 4. Run `publisher sync . --until-change --timeout 100 --json`. It records the edit and publishes it. If a file still carries the
91
92
  marker lines it refuses, names the files (`error.details.marked`) and publishes nothing; finish
92
93
  the edit and run it again.
93
94
 
@@ -2564,13 +2564,14 @@ var LimitsSchema = object({
2564
2564
  maxTemporaryBytes: _default(positiveInt(), 3 * 512 * 1024 * 1024),
2565
2565
  /** wall-clock bound for a single validation git command */
2566
2566
  gitTimeoutMs: _default(positiveInt(), 3e5),
2567
- // ---- per-account bounds ----
2567
+ // ---- per-organisation bounds ----
2568
2568
  /**
2569
- * repositories one person may create, counted from the owner index; anyone may sign up and
2570
- * publish, so this bounds one account (docs/proposals/personal-organisations.md §8). Service
2571
- * principals are not counted. Defaulted so an older server reads as the default, not a failure.
2569
+ * ACTIVE repositories an organisation may hold when it sets no quota of its own in users
2570
+ * (ADR-0014 A1); anyone may sign up and publish, so this bounds one account's organisation.
2571
+ * Named `maxRepositoriesPerPrincipal` before 0.9.0, when it counted a creator's repositories.
2572
+ * Defaulted so an older server reads as the default, not a failure.
2572
2573
  */
2573
- maxRepositoriesPerPrincipal: _default(positiveInt(), 25)
2574
+ maxRepositoriesPerOrganisation: _default(positiveInt(), 25)
2574
2575
  });
2575
2576
  var DEFAULT_LIMITS = {
2576
2577
  maxPackBytes: 256 * 1024 * 1024,
@@ -2588,7 +2589,7 @@ var DEFAULT_LIMITS = {
2588
2589
  maxProcessMemoryBytes: 2 * 1024 * 1024 * 1024,
2589
2590
  maxTemporaryBytes: 3 * 512 * 1024 * 1024,
2590
2591
  gitTimeoutMs: 3e5,
2591
- maxRepositoriesPerPrincipal: 25
2592
+ maxRepositoriesPerOrganisation: 25
2592
2593
  };
2593
2594
 
2594
2595
  // ../protocol/src/storage-schema.ts
@@ -3511,12 +3512,6 @@ async function gitVersion() {
3511
3512
  cachedVersion = m[1];
3512
3513
  return cachedVersion;
3513
3514
  }
3514
- var MINIMUM_GIT_VERSION = "2.46.0";
3515
- async function assertGitVersion(minimum = MINIMUM_GIT_VERSION) {
3516
- const v = await gitVersion();
3517
- if (compareVersions(v, minimum) < 0) throw new PublisherError("GIT", `git ${v} is older than the minimum ${minimum}`);
3518
- return v;
3519
- }
3520
3515
  function compareVersions(a, b) {
3521
3516
  const pa = a.split(".").map(Number);
3522
3517
  const pb = b.split(".").map(Number);
@@ -4725,6 +4720,7 @@ async function followRun(input, opts) {
4725
4720
  let last = null;
4726
4721
  let lastError = null;
4727
4722
  let refreshed = false;
4723
+ let answered = false;
4728
4724
  for (; ; ) {
4729
4725
  const answer = await ask(fetchImpl, url2, await opts.token());
4730
4726
  if (answer.status === 401 && !refreshed && opts.refresh !== void 0) {
@@ -4742,15 +4738,19 @@ async function followRun(input, opts) {
4742
4738
  if (read2 === null) return { kind: "unavailable", reason: "no-stages" };
4743
4739
  if ("malformed" in read2) return { kind: "malformed", detail: read2.malformed };
4744
4740
  last = read2.stages;
4741
+ let moved = false;
4745
4742
  for (const entry of read2.stages) {
4746
4743
  if (entry.state === "pending" || seen.get(entry.stage) === entry.state) continue;
4747
4744
  seen.set(entry.stage, entry.state);
4748
4745
  opts.onStage?.(entry);
4746
+ moved = true;
4749
4747
  }
4750
4748
  if (isRecord2(run.run) && run.run["status"] === "awaiting-approval") return { kind: "waiting", stages: read2.stages };
4751
4749
  const end = ending(read2.stages);
4752
4750
  if (end !== null) return { kind: "settled", ending: end, failedAt: read2.stages.find((s) => s.state === "failed")?.word ?? null, stages: read2.stages };
4751
+ if (opts.untilChange === true && moved && answered) return { kind: "moving", stages: read2.stages };
4753
4752
  }
4753
+ answered = true;
4754
4754
  lastError = null;
4755
4755
  } else {
4756
4756
  lastError = answer.status === 0 ? "the portal could not be reached" : `the portal answered ${answer.status}`;
@@ -5880,6 +5880,108 @@ async function discoverEndpoint(portal, fetchImpl = fetch) {
5880
5880
  return endpoint;
5881
5881
  }
5882
5882
 
5883
+ // src/git-minimum.ts
5884
+ var CLIENT_GIT_MINIMUM = "2.43.0";
5885
+ function gitSupported(version2) {
5886
+ return compareVersions(version2, CLIENT_GIT_MINIMUM) >= 0;
5887
+ }
5888
+
5889
+ // src/follow.ts
5890
+ async function followPublication(p, endpoint, o, opts) {
5891
+ const derived = portalForEndpoint(endpoint);
5892
+ const confirmed = derived === null ? null : await confirmPortal(derived, endpoint, opts.fetch);
5893
+ const portal = confirmed === null ? null : derived;
5894
+ const elsewhere = (reason) => ({ kind: "unavailable", reason, portal, hint: inPortal(portal, "Published.", "Follow it") });
5895
+ if (portal === null || confirmed === null) return elsewhere("no-portal");
5896
+ if (!confirmed.terminal.includes(RUN_PROGRESS)) {
5897
+ const noRoute = { kind: "unavailable", reason: "no-route" };
5898
+ return { ...noRoute, portal, hint: progressHint(noRoute, portal) };
5899
+ }
5900
+ const refs = Object.entries(o.refs ?? {});
5901
+ if (refs.length !== 1) return elsewhere("several-refs");
5902
+ const [ref, commit] = refs[0];
5903
+ opts.log("Following it: received -> checked -> going live -> live. Ctrl-C stops watching; the publication is already saved.");
5904
+ const outcome = await p.followRun(
5905
+ { portal, repoId: o.repoId, publicationId: o.publicationId, ref, commit },
5906
+ { onStage: opts.onStage, ...opts.timeoutMs === void 0 ? {} : { timeoutMs: opts.timeoutMs }, ...opts.untilChange === true ? { untilChange: true } : {} }
5907
+ );
5908
+ const answerable = confirmed.terminal.includes(PREPARATION);
5909
+ const dir = shellWord(opts.dir);
5910
+ const next = outcome.kind === "timed-out" || outcome.kind === "moving" ? { action: "follow", command: `publisher follow ${dir} --publication ${o.publicationId}${opts.untilChange === true ? " --until-change" : ""} --json` } : outcome.kind === "waiting" && answerable ? { action: "question", command: `publisher question ${dir} --json` } : void 0;
5911
+ return { ...outcome, portal, hint: progressHint(outcome, portal, answerable), ...next === void 0 ? {} : { next } };
5912
+ }
5913
+ function followTimeoutMs(flag) {
5914
+ if (flag === void 0) return FOLLOW_TIMEOUT_MS;
5915
+ if (!/^[1-9][0-9]*$/.test(flag)) throw new PublisherError("INVALID_REQUEST", `--timeout takes a whole number of seconds, not ${JSON.stringify(flag)}`);
5916
+ return Math.min(Number(flag) * 1e3, FOLLOW_TIMEOUT_MS);
5917
+ }
5918
+ function shellWord(path) {
5919
+ return /^[A-Za-z0-9_./-]+$/.test(path) ? path : `'${path.replace(/'/g, `'\\''`)}'`;
5920
+ }
5921
+ function stageEmitter(json, quiet) {
5922
+ return (e) => {
5923
+ if (json) process.stdout.write(stageLine(e, true) + "\n");
5924
+ else if (!quiet) process.stderr.write(stageLine(e, false) + "\n");
5925
+ };
5926
+ }
5927
+ function stageLine(e, json) {
5928
+ return json ? JSON.stringify({ event: "stage", stage: e.stage, word: e.word, state: e.state, at: e.at }) : ` ${e.word}: ${e.state.replace("-", " ")}`;
5929
+ }
5930
+ function inPortal(portal, lead, verb) {
5931
+ return `${lead} ${verb} in the portal${portal === null ? "" : `: ${portal}`}.`;
5932
+ }
5933
+ function progressHint(r, portal, answerable = false) {
5934
+ switch (r.kind) {
5935
+ case "waiting":
5936
+ return answerable ? "It is waiting for an answer from you. Run publisher question to see it." : inPortal(portal, "It is waiting for an answer from you.", "Answer it");
5937
+ case "settled":
5938
+ switch (r.ending) {
5939
+ case "live":
5940
+ return "Your app is live.";
5941
+ case "not-deployed":
5942
+ return "Published and checked; this organisation does not deploy automatically.";
5943
+ case "failed":
5944
+ return inPortal(portal, `It stopped at "${r.failedAt ?? "a step"}".`, "See why");
5945
+ case "stopped":
5946
+ return "A newer publication replaced this one before it went live.";
5947
+ }
5948
+ break;
5949
+ case "moving": {
5950
+ const now = r.stages.find((s) => s.state === "in-progress") ?? r.stages.filter((s) => s.state !== "pending").at(-1);
5951
+ return `Now: ${now?.word ?? "received"}. Run next.command to wait for the next step.`;
5952
+ }
5953
+ case "timed-out":
5954
+ return inPortal(portal, `Still going${r.lastError === null ? "" : ` (${r.lastError})`}; run next.command to keep following.`, "Or follow it");
5955
+ case "unavailable":
5956
+ return inPortal(portal, "Published. This portal cannot show progress in the terminal yet.", "Follow it");
5957
+ case "unauthorized":
5958
+ return inPortal(portal, "Published, but the portal did not accept this sign-in (run publisher login).", "Follow it");
5959
+ case "forbidden":
5960
+ return inPortal(portal, "Published, but the portal would not show this sign-in the run.", "Follow it");
5961
+ case "malformed":
5962
+ return inPortal(portal, `Published, but the portal's answer about the run could not be read (${r.detail}).`, "Follow it");
5963
+ }
5964
+ }
5965
+
5966
+ // src/list.ts
5967
+ async function listApps(p, opts = {}) {
5968
+ const page = await p.listRepositories({ state: opts.archived ? "archived" : "active", limit: 200, ...opts.cursor ? { cursor: opts.cursor } : {} });
5969
+ const apps = page.repositories.map((r) => ({
5970
+ repoId: r.repoId,
5971
+ name: r.name,
5972
+ createdAt: r.createdAt,
5973
+ publishedAt: r.lastTransaction?.committedAt ?? null,
5974
+ archived: r.archived,
5975
+ checkout: `publisher checkout ${shellWord(folderFor(r.name))} --repo ${r.repoId} --json`
5976
+ })).sort((a, b) => (b.publishedAt ?? b.createdAt).localeCompare(a.publishedAt ?? a.createdAt));
5977
+ const hint = apps.length === 0 ? "You have not published an app from this sign-in yet." : "To work on one in a new folder, run its checkout command.";
5978
+ return { apps, nextCursor: page.nextCursor, hint };
5979
+ }
5980
+ function folderFor(name) {
5981
+ const slug2 = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
5982
+ return slug2 === "" ? "app" : slug2;
5983
+ }
5984
+
5883
5985
  // src/init.ts
5884
5986
  async function repositoryForInit(p, endpoint, name, opts = {}) {
5885
5987
  const portal = portalForEndpoint(endpoint);
@@ -5981,6 +6083,7 @@ var OPTIONS = {
5981
6083
  ours: { type: "string" },
5982
6084
  theirs: { type: "string" },
5983
6085
  to: { type: "string" },
6086
+ cursor: { type: "string" },
5984
6087
  ref: { type: "string", multiple: true },
5985
6088
  json: { type: "boolean" },
5986
6089
  quiet: { type: "boolean" },
@@ -5990,8 +6093,10 @@ var OPTIONS = {
5990
6093
  help: { type: "boolean" },
5991
6094
  version: { type: "boolean" },
5992
6095
  "set-default": { type: "boolean" },
6096
+ archived: { type: "boolean" },
5993
6097
  // A flag of its own, not a negation of --wait: there is no --wait.
5994
- "no-wait": { type: "boolean" }
6098
+ "no-wait": { type: "boolean" },
6099
+ "until-change": { type: "boolean" }
5995
6100
  };
5996
6101
  function parseArgs(argv) {
5997
6102
  try {
@@ -6057,69 +6162,6 @@ async function installSkills({ log }) {
6057
6162
  };
6058
6163
  }
6059
6164
 
6060
- // src/follow.ts
6061
- async function followPublication(p, endpoint, o, opts) {
6062
- const derived = portalForEndpoint(endpoint);
6063
- const confirmed = derived === null ? null : await confirmPortal(derived, endpoint, opts.fetch);
6064
- const portal = confirmed === null ? null : derived;
6065
- const elsewhere = (reason) => ({ kind: "unavailable", reason, portal, hint: inPortal(portal, "Published.", "Follow it") });
6066
- if (portal === null || confirmed === null) return elsewhere("no-portal");
6067
- if (!confirmed.terminal.includes(RUN_PROGRESS)) {
6068
- const noRoute = { kind: "unavailable", reason: "no-route" };
6069
- return { ...noRoute, portal, hint: progressHint(noRoute, portal) };
6070
- }
6071
- const refs = Object.entries(o.refs ?? {});
6072
- if (refs.length !== 1) return elsewhere("several-refs");
6073
- const [ref, commit] = refs[0];
6074
- opts.log("Following it: received -> checked -> going live -> live. Ctrl-C stops watching; the publication is already saved.");
6075
- const outcome = await p.followRun(
6076
- { portal, repoId: o.repoId, publicationId: o.publicationId, ref, commit },
6077
- { onStage: opts.onStage, ...opts.timeoutMs === void 0 ? {} : { timeoutMs: opts.timeoutMs } }
6078
- );
6079
- const answerable = confirmed.terminal.includes(PREPARATION);
6080
- const next = outcome.kind === "timed-out" ? { action: "follow", command: `publisher follow --publication ${o.publicationId} --json` } : outcome.kind === "waiting" && answerable ? { action: "question", command: "publisher question --json" } : void 0;
6081
- return { ...outcome, portal, hint: progressHint(outcome, portal, answerable), ...next === void 0 ? {} : { next } };
6082
- }
6083
- function followTimeoutMs(flag) {
6084
- if (flag === void 0) return FOLLOW_TIMEOUT_MS;
6085
- if (!/^[1-9][0-9]*$/.test(flag)) throw new PublisherError("INVALID_REQUEST", `--timeout takes a whole number of seconds, not ${JSON.stringify(flag)}`);
6086
- return Math.min(Number(flag) * 1e3, FOLLOW_TIMEOUT_MS);
6087
- }
6088
- function stageLine(e, json) {
6089
- return json ? JSON.stringify({ event: "stage", stage: e.stage, word: e.word, state: e.state, at: e.at }) : ` ${e.word}: ${e.state.replace("-", " ")}`;
6090
- }
6091
- function inPortal(portal, lead, verb) {
6092
- return `${lead} ${verb} in the portal${portal === null ? "" : `: ${portal}`}.`;
6093
- }
6094
- function progressHint(r, portal, answerable = false) {
6095
- switch (r.kind) {
6096
- case "waiting":
6097
- return answerable ? "It is waiting for an answer from you. Run publisher question to see it." : inPortal(portal, "It is waiting for an answer from you.", "Answer it");
6098
- case "settled":
6099
- switch (r.ending) {
6100
- case "live":
6101
- return "Your app is live.";
6102
- case "not-deployed":
6103
- return "Published and checked; this organisation does not deploy automatically.";
6104
- case "failed":
6105
- return inPortal(portal, `It stopped at "${r.failedAt ?? "a step"}".`, "See why");
6106
- case "stopped":
6107
- return "A newer publication replaced this one before it went live.";
6108
- }
6109
- break;
6110
- case "timed-out":
6111
- return inPortal(portal, `Still going${r.lastError === null ? "" : ` (${r.lastError})`}; run next.command to keep following.`, "Or follow it");
6112
- case "unavailable":
6113
- return inPortal(portal, "Published. This portal cannot show progress in the terminal yet.", "Follow it");
6114
- case "unauthorized":
6115
- return inPortal(portal, "Published, but the portal did not accept this sign-in (run publisher login).", "Follow it");
6116
- case "forbidden":
6117
- return inPortal(portal, "Published, but the portal would not show this sign-in the run.", "Follow it");
6118
- case "malformed":
6119
- return inPortal(portal, `Published, but the portal's answer about the run could not be read (${r.detail}).`, "Follow it");
6120
- }
6121
- }
6122
-
6123
6165
  // src/question.ts
6124
6166
  async function questionAt(p, endpoint, repoId, opts = {}) {
6125
6167
  const at = await portalAt(endpoint, opts);
@@ -6213,6 +6255,8 @@ Usage:
6213
6255
  a change, use sync
6214
6256
  publisher status [dir] [--publication <id>] resolve pending/interrupted publications
6215
6257
  publisher fetch [dir]
6258
+ publisher list [--archived] [--cursor <c>] the apps you published, newest first, each with the
6259
+ checkout command that brings it into a new folder
6216
6260
  publisher checkout <dir> --repo <id> [--branch <name>] create a NEW editable workspace (never overlays a folder)
6217
6261
  publisher workspace-status [dir] offline mapping + where the workspace stands (real three-way check)
6218
6262
  publisher sync [dir] [--message <text>] [--name <app>] THE way to publish: a folder linked to nothing first gets its
@@ -6228,7 +6272,7 @@ Usage:
6228
6272
  [--timeout <seconds>] follow for at most this long; if it is still going, the
6229
6273
  result's next.command picks it up
6230
6274
  publisher follow [dir] --publication <id> follow an accepted publication's run where a sync left it
6231
- [--timeout <seconds>]
6275
+ [--timeout <seconds>] [--until-change] --until-change (sync too): return at the next step, to tell it
6232
6276
  publisher question [dir] [--repo <id>] the question the app's preparation waits on, if any,
6233
6277
  with each option's value (answering needs owner or admin)
6234
6278
  publisher answer <question>=<value>... [--repo <id>] answer it from this folder; a value that is not one of
@@ -6277,10 +6321,7 @@ async function main(argv) {
6277
6321
  if (e.phase === "done") process.stderr.write("\n");
6278
6322
  };
6279
6323
  const dirArg = (i) => (0, import_node_path8.resolve)(cmd[i] ?? ".");
6280
- const emitStage = (e) => {
6281
- if (json) process.stdout.write(stageLine(e, true) + "\n");
6282
- else if (!quiet) process.stderr.write(stageLine(e, false) + "\n");
6283
- };
6324
+ const following = { onStage: stageEmitter(json, quiet), log, untilChange: flags["until-change"] === true };
6284
6325
  const resolutionInput = async (dir) => {
6285
6326
  let association;
6286
6327
  if (dir && await isGitRepo(dir)) association = (await readAssociation(dir))?.endpoint;
@@ -6353,7 +6394,7 @@ async function main(argv) {
6353
6394
  out({
6354
6395
  node: process.version,
6355
6396
  git,
6356
- gitOk: git ? await assertGitVersion().then(() => true).catch(() => false) : false,
6397
+ gitOk: git !== null && gitSupported(git),
6357
6398
  endpoint: s.resolution.kind === "resolved" ? s.resolution.endpoint : null,
6358
6399
  endpointSource: s.resolution.kind === "resolved" ? s.resolution.source : null,
6359
6400
  profile: s.resolution.kind === "resolved" ? s.resolution.profile ?? null : null,
@@ -6536,6 +6577,9 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
6536
6577
  out({ ok: true, repoId: r.repoId, generation: r.generation, remote: r.remote, refs: r.refs, tracking: r.tracking, bytes: r.bytes });
6537
6578
  return 0;
6538
6579
  }
6580
+ case "list":
6581
+ out(await listApps((await client()).p, { archived: flags["archived"] === true, cursor: str2(flags["cursor"]) }));
6582
+ return 0;
6539
6583
  case "checkout": {
6540
6584
  const dir = dirArg(1);
6541
6585
  const repoId = str2(flags["repo"]);
@@ -6565,7 +6609,7 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
6565
6609
  const linked = await isGitRepo(dir) && await readAssociation(dir) !== null ? null : await linkFolder(p, endpoint, dir, {});
6566
6610
  const r = await p.syncChanges({ directory: dir, markConflicts: flags["mark-conflicts"] === true, ...optStr(flags, "message") }, { onProgress: progress });
6567
6611
  const o = r.published;
6568
- const followed = o !== null && o.state === "accepted" && flags["no-wait"] !== true ? await followPublication(p, endpoint, o, { onStage: emitStage, log, timeoutMs }) : null;
6612
+ const followed = o !== null && o.state === "accepted" && flags["no-wait"] !== true ? await followPublication(p, endpoint, o, { ...following, timeoutMs, dir }) : null;
6569
6613
  out({
6570
6614
  ok: r.state === "published" || r.state === "up_to_date",
6571
6615
  state: r.state,
@@ -6604,7 +6648,7 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
6604
6648
  if (publication.state !== "accepted") {
6605
6649
  throw new PublisherError("LOCAL_STATE", `publication ${publicationId} is ${publication.state}, not accepted; run publisher status`);
6606
6650
  }
6607
- out({ publicationId, progress: await followPublication(p, endpoint, publication, { onStage: emitStage, log, timeoutMs }) });
6651
+ out({ publicationId, progress: await followPublication(p, endpoint, publication, { ...following, timeoutMs, dir }) });
6608
6652
  return 0;
6609
6653
  }
6610
6654
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnovate/shadow-publisher",
3
- "version": "0.8.4",
3
+ "version": "0.9.1",
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",