@minnovate/shadow-publisher 0.9.0 → 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.9.
|
|
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,
|
|
29
|
-
|
|
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.
|
|
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`.
|
|
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. |
|
|
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
|
|
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.
|
|
96
|
+
## 4. Telling them how it goes, step by step
|
|
79
97
|
|
|
80
|
-
After a publish, the
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
`progress.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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;
|
|
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.
|
|
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
|
|
package/dist/publisher.cjs
CHANGED
|
@@ -3512,12 +3512,6 @@ async function gitVersion() {
|
|
|
3512
3512
|
cachedVersion = m[1];
|
|
3513
3513
|
return cachedVersion;
|
|
3514
3514
|
}
|
|
3515
|
-
var MINIMUM_GIT_VERSION = "2.46.0";
|
|
3516
|
-
async function assertGitVersion(minimum = MINIMUM_GIT_VERSION) {
|
|
3517
|
-
const v = await gitVersion();
|
|
3518
|
-
if (compareVersions(v, minimum) < 0) throw new PublisherError("GIT", `git ${v} is older than the minimum ${minimum}`);
|
|
3519
|
-
return v;
|
|
3520
|
-
}
|
|
3521
3515
|
function compareVersions(a, b) {
|
|
3522
3516
|
const pa = a.split(".").map(Number);
|
|
3523
3517
|
const pb = b.split(".").map(Number);
|
|
@@ -4726,6 +4720,7 @@ async function followRun(input, opts) {
|
|
|
4726
4720
|
let last = null;
|
|
4727
4721
|
let lastError = null;
|
|
4728
4722
|
let refreshed = false;
|
|
4723
|
+
let answered = false;
|
|
4729
4724
|
for (; ; ) {
|
|
4730
4725
|
const answer = await ask(fetchImpl, url2, await opts.token());
|
|
4731
4726
|
if (answer.status === 401 && !refreshed && opts.refresh !== void 0) {
|
|
@@ -4743,15 +4738,19 @@ async function followRun(input, opts) {
|
|
|
4743
4738
|
if (read2 === null) return { kind: "unavailable", reason: "no-stages" };
|
|
4744
4739
|
if ("malformed" in read2) return { kind: "malformed", detail: read2.malformed };
|
|
4745
4740
|
last = read2.stages;
|
|
4741
|
+
let moved = false;
|
|
4746
4742
|
for (const entry of read2.stages) {
|
|
4747
4743
|
if (entry.state === "pending" || seen.get(entry.stage) === entry.state) continue;
|
|
4748
4744
|
seen.set(entry.stage, entry.state);
|
|
4749
4745
|
opts.onStage?.(entry);
|
|
4746
|
+
moved = true;
|
|
4750
4747
|
}
|
|
4751
4748
|
if (isRecord2(run.run) && run.run["status"] === "awaiting-approval") return { kind: "waiting", stages: read2.stages };
|
|
4752
4749
|
const end = ending(read2.stages);
|
|
4753
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 };
|
|
4754
4752
|
}
|
|
4753
|
+
answered = true;
|
|
4755
4754
|
lastError = null;
|
|
4756
4755
|
} else {
|
|
4757
4756
|
lastError = answer.status === 0 ? "the portal could not be reached" : `the portal answered ${answer.status}`;
|
|
@@ -5881,6 +5880,108 @@ async function discoverEndpoint(portal, fetchImpl = fetch) {
|
|
|
5881
5880
|
return endpoint;
|
|
5882
5881
|
}
|
|
5883
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
|
+
|
|
5884
5985
|
// src/init.ts
|
|
5885
5986
|
async function repositoryForInit(p, endpoint, name, opts = {}) {
|
|
5886
5987
|
const portal = portalForEndpoint(endpoint);
|
|
@@ -5982,6 +6083,7 @@ var OPTIONS = {
|
|
|
5982
6083
|
ours: { type: "string" },
|
|
5983
6084
|
theirs: { type: "string" },
|
|
5984
6085
|
to: { type: "string" },
|
|
6086
|
+
cursor: { type: "string" },
|
|
5985
6087
|
ref: { type: "string", multiple: true },
|
|
5986
6088
|
json: { type: "boolean" },
|
|
5987
6089
|
quiet: { type: "boolean" },
|
|
@@ -5991,8 +6093,10 @@ var OPTIONS = {
|
|
|
5991
6093
|
help: { type: "boolean" },
|
|
5992
6094
|
version: { type: "boolean" },
|
|
5993
6095
|
"set-default": { type: "boolean" },
|
|
6096
|
+
archived: { type: "boolean" },
|
|
5994
6097
|
// A flag of its own, not a negation of --wait: there is no --wait.
|
|
5995
|
-
"no-wait": { type: "boolean" }
|
|
6098
|
+
"no-wait": { type: "boolean" },
|
|
6099
|
+
"until-change": { type: "boolean" }
|
|
5996
6100
|
};
|
|
5997
6101
|
function parseArgs(argv) {
|
|
5998
6102
|
try {
|
|
@@ -6058,69 +6162,6 @@ async function installSkills({ log }) {
|
|
|
6058
6162
|
};
|
|
6059
6163
|
}
|
|
6060
6164
|
|
|
6061
|
-
// src/follow.ts
|
|
6062
|
-
async function followPublication(p, endpoint, o, opts) {
|
|
6063
|
-
const derived = portalForEndpoint(endpoint);
|
|
6064
|
-
const confirmed = derived === null ? null : await confirmPortal(derived, endpoint, opts.fetch);
|
|
6065
|
-
const portal = confirmed === null ? null : derived;
|
|
6066
|
-
const elsewhere = (reason) => ({ kind: "unavailable", reason, portal, hint: inPortal(portal, "Published.", "Follow it") });
|
|
6067
|
-
if (portal === null || confirmed === null) return elsewhere("no-portal");
|
|
6068
|
-
if (!confirmed.terminal.includes(RUN_PROGRESS)) {
|
|
6069
|
-
const noRoute = { kind: "unavailable", reason: "no-route" };
|
|
6070
|
-
return { ...noRoute, portal, hint: progressHint(noRoute, portal) };
|
|
6071
|
-
}
|
|
6072
|
-
const refs = Object.entries(o.refs ?? {});
|
|
6073
|
-
if (refs.length !== 1) return elsewhere("several-refs");
|
|
6074
|
-
const [ref, commit] = refs[0];
|
|
6075
|
-
opts.log("Following it: received -> checked -> going live -> live. Ctrl-C stops watching; the publication is already saved.");
|
|
6076
|
-
const outcome = await p.followRun(
|
|
6077
|
-
{ portal, repoId: o.repoId, publicationId: o.publicationId, ref, commit },
|
|
6078
|
-
{ onStage: opts.onStage, ...opts.timeoutMs === void 0 ? {} : { timeoutMs: opts.timeoutMs } }
|
|
6079
|
-
);
|
|
6080
|
-
const answerable = confirmed.terminal.includes(PREPARATION);
|
|
6081
|
-
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;
|
|
6082
|
-
return { ...outcome, portal, hint: progressHint(outcome, portal, answerable), ...next === void 0 ? {} : { next } };
|
|
6083
|
-
}
|
|
6084
|
-
function followTimeoutMs(flag) {
|
|
6085
|
-
if (flag === void 0) return FOLLOW_TIMEOUT_MS;
|
|
6086
|
-
if (!/^[1-9][0-9]*$/.test(flag)) throw new PublisherError("INVALID_REQUEST", `--timeout takes a whole number of seconds, not ${JSON.stringify(flag)}`);
|
|
6087
|
-
return Math.min(Number(flag) * 1e3, FOLLOW_TIMEOUT_MS);
|
|
6088
|
-
}
|
|
6089
|
-
function stageLine(e, json) {
|
|
6090
|
-
return json ? JSON.stringify({ event: "stage", stage: e.stage, word: e.word, state: e.state, at: e.at }) : ` ${e.word}: ${e.state.replace("-", " ")}`;
|
|
6091
|
-
}
|
|
6092
|
-
function inPortal(portal, lead, verb) {
|
|
6093
|
-
return `${lead} ${verb} in the portal${portal === null ? "" : `: ${portal}`}.`;
|
|
6094
|
-
}
|
|
6095
|
-
function progressHint(r, portal, answerable = false) {
|
|
6096
|
-
switch (r.kind) {
|
|
6097
|
-
case "waiting":
|
|
6098
|
-
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");
|
|
6099
|
-
case "settled":
|
|
6100
|
-
switch (r.ending) {
|
|
6101
|
-
case "live":
|
|
6102
|
-
return "Your app is live.";
|
|
6103
|
-
case "not-deployed":
|
|
6104
|
-
return "Published and checked; this organisation does not deploy automatically.";
|
|
6105
|
-
case "failed":
|
|
6106
|
-
return inPortal(portal, `It stopped at "${r.failedAt ?? "a step"}".`, "See why");
|
|
6107
|
-
case "stopped":
|
|
6108
|
-
return "A newer publication replaced this one before it went live.";
|
|
6109
|
-
}
|
|
6110
|
-
break;
|
|
6111
|
-
case "timed-out":
|
|
6112
|
-
return inPortal(portal, `Still going${r.lastError === null ? "" : ` (${r.lastError})`}; run next.command to keep following.`, "Or follow it");
|
|
6113
|
-
case "unavailable":
|
|
6114
|
-
return inPortal(portal, "Published. This portal cannot show progress in the terminal yet.", "Follow it");
|
|
6115
|
-
case "unauthorized":
|
|
6116
|
-
return inPortal(portal, "Published, but the portal did not accept this sign-in (run publisher login).", "Follow it");
|
|
6117
|
-
case "forbidden":
|
|
6118
|
-
return inPortal(portal, "Published, but the portal would not show this sign-in the run.", "Follow it");
|
|
6119
|
-
case "malformed":
|
|
6120
|
-
return inPortal(portal, `Published, but the portal's answer about the run could not be read (${r.detail}).`, "Follow it");
|
|
6121
|
-
}
|
|
6122
|
-
}
|
|
6123
|
-
|
|
6124
6165
|
// src/question.ts
|
|
6125
6166
|
async function questionAt(p, endpoint, repoId, opts = {}) {
|
|
6126
6167
|
const at = await portalAt(endpoint, opts);
|
|
@@ -6214,6 +6255,8 @@ Usage:
|
|
|
6214
6255
|
a change, use sync
|
|
6215
6256
|
publisher status [dir] [--publication <id>] resolve pending/interrupted publications
|
|
6216
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
|
|
6217
6260
|
publisher checkout <dir> --repo <id> [--branch <name>] create a NEW editable workspace (never overlays a folder)
|
|
6218
6261
|
publisher workspace-status [dir] offline mapping + where the workspace stands (real three-way check)
|
|
6219
6262
|
publisher sync [dir] [--message <text>] [--name <app>] THE way to publish: a folder linked to nothing first gets its
|
|
@@ -6229,7 +6272,7 @@ Usage:
|
|
|
6229
6272
|
[--timeout <seconds>] follow for at most this long; if it is still going, the
|
|
6230
6273
|
result's next.command picks it up
|
|
6231
6274
|
publisher follow [dir] --publication <id> follow an accepted publication's run where a sync left it
|
|
6232
|
-
[--timeout <seconds>]
|
|
6275
|
+
[--timeout <seconds>] [--until-change] --until-change (sync too): return at the next step, to tell it
|
|
6233
6276
|
publisher question [dir] [--repo <id>] the question the app's preparation waits on, if any,
|
|
6234
6277
|
with each option's value (answering needs owner or admin)
|
|
6235
6278
|
publisher answer <question>=<value>... [--repo <id>] answer it from this folder; a value that is not one of
|
|
@@ -6278,10 +6321,7 @@ async function main(argv) {
|
|
|
6278
6321
|
if (e.phase === "done") process.stderr.write("\n");
|
|
6279
6322
|
};
|
|
6280
6323
|
const dirArg = (i) => (0, import_node_path8.resolve)(cmd[i] ?? ".");
|
|
6281
|
-
const
|
|
6282
|
-
if (json) process.stdout.write(stageLine(e, true) + "\n");
|
|
6283
|
-
else if (!quiet) process.stderr.write(stageLine(e, false) + "\n");
|
|
6284
|
-
};
|
|
6324
|
+
const following = { onStage: stageEmitter(json, quiet), log, untilChange: flags["until-change"] === true };
|
|
6285
6325
|
const resolutionInput = async (dir) => {
|
|
6286
6326
|
let association;
|
|
6287
6327
|
if (dir && await isGitRepo(dir)) association = (await readAssociation(dir))?.endpoint;
|
|
@@ -6354,7 +6394,7 @@ async function main(argv) {
|
|
|
6354
6394
|
out({
|
|
6355
6395
|
node: process.version,
|
|
6356
6396
|
git,
|
|
6357
|
-
gitOk: git
|
|
6397
|
+
gitOk: git !== null && gitSupported(git),
|
|
6358
6398
|
endpoint: s.resolution.kind === "resolved" ? s.resolution.endpoint : null,
|
|
6359
6399
|
endpointSource: s.resolution.kind === "resolved" ? s.resolution.source : null,
|
|
6360
6400
|
profile: s.resolution.kind === "resolved" ? s.resolution.profile ?? null : null,
|
|
@@ -6537,6 +6577,9 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6537
6577
|
out({ ok: true, repoId: r.repoId, generation: r.generation, remote: r.remote, refs: r.refs, tracking: r.tracking, bytes: r.bytes });
|
|
6538
6578
|
return 0;
|
|
6539
6579
|
}
|
|
6580
|
+
case "list":
|
|
6581
|
+
out(await listApps((await client()).p, { archived: flags["archived"] === true, cursor: str2(flags["cursor"]) }));
|
|
6582
|
+
return 0;
|
|
6540
6583
|
case "checkout": {
|
|
6541
6584
|
const dir = dirArg(1);
|
|
6542
6585
|
const repoId = str2(flags["repo"]);
|
|
@@ -6566,7 +6609,7 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6566
6609
|
const linked = await isGitRepo(dir) && await readAssociation(dir) !== null ? null : await linkFolder(p, endpoint, dir, {});
|
|
6567
6610
|
const r = await p.syncChanges({ directory: dir, markConflicts: flags["mark-conflicts"] === true, ...optStr(flags, "message") }, { onProgress: progress });
|
|
6568
6611
|
const o = r.published;
|
|
6569
|
-
const followed = o !== null && o.state === "accepted" && flags["no-wait"] !== true ? await followPublication(p, endpoint, o, {
|
|
6612
|
+
const followed = o !== null && o.state === "accepted" && flags["no-wait"] !== true ? await followPublication(p, endpoint, o, { ...following, timeoutMs, dir }) : null;
|
|
6570
6613
|
out({
|
|
6571
6614
|
ok: r.state === "published" || r.state === "up_to_date",
|
|
6572
6615
|
state: r.state,
|
|
@@ -6605,7 +6648,7 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6605
6648
|
if (publication.state !== "accepted") {
|
|
6606
6649
|
throw new PublisherError("LOCAL_STATE", `publication ${publicationId} is ${publication.state}, not accepted; run publisher status`);
|
|
6607
6650
|
}
|
|
6608
|
-
out({ publicationId, progress: await followPublication(p, endpoint, publication, {
|
|
6651
|
+
out({ publicationId, progress: await followPublication(p, endpoint, publication, { ...following, timeoutMs, dir }) });
|
|
6609
6652
|
return 0;
|
|
6610
6653
|
}
|
|
6611
6654
|
/**
|