@minnovate/shadow-publisher 0.7.0 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-publisher",
|
|
3
3
|
"displayName": "Git Publisher",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.3",
|
|
5
5
|
"description": "Publish an app folder's Git history to the serverless Git publishing backend and keep it updated (\"publish my app\", \"update my app\", \"fetch the engineered version\").",
|
|
6
6
|
"author": { "name": "git-publisher project" },
|
|
7
7
|
"keywords": ["git", "publish", "deploy", "serverless"]
|
|
@@ -56,24 +56,16 @@ changing their default.
|
|
|
56
56
|
|
|
57
57
|
## 2. First publication of a folder
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
There is no separate first step: run step 3 in the app folder, adding `--name "<app name>"` the
|
|
60
|
+
first time. A folder that is not published yet gets its application and repository from that same
|
|
61
|
+
`sync` (the portal makes the application first, in one request), and is then published.
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
If not associated:
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
publisher init . --name "<app name>" --json
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
`init` prepares the folder with an explicit file selection: it excludes `.git`, `node_modules`,
|
|
72
|
-
build caches, `.env*`, keys/certificates and files larger than 50 MB, and it respects
|
|
73
|
-
`.gitignore`. Show the user the `onboarding.excluded` list so they know what was left out (the
|
|
74
|
-
exclusion rules are a safety net, not a secret scanner). Then publish it with step 3.
|
|
63
|
+
The first `sync` prepares the folder with an explicit file selection: it excludes `.git`,
|
|
64
|
+
`node_modules`, build caches, `.env*`, keys/certificates and files larger than 50 MB, and it
|
|
65
|
+
respects `.gitignore`. Show the user the `linked` result and the excluded files it reports so they
|
|
66
|
+
know what was left out (the exclusion rules are a safety net, not a secret scanner).
|
|
75
67
|
|
|
76
|
-
## 3. Every
|
|
68
|
+
## 3. Every publish, the first one included
|
|
77
69
|
|
|
78
70
|
```bash
|
|
79
71
|
publisher sync . --message "<a few words on what changed>" --timeout 90 --json
|
package/dist/publisher.cjs
CHANGED
|
@@ -97,6 +97,11 @@ var ERROR_CODES = [
|
|
|
97
97
|
// code rather than FORBIDDEN: a caller can act on it (sign in again, choosing an organisation),
|
|
98
98
|
// and a test that could not tell it from "you may not" would prove nothing about the check.
|
|
99
99
|
"ORGANISATION_REQUIRED",
|
|
100
|
+
// A repository is created only by the platform's application maker (the portal), for a person,
|
|
101
|
+
// after it has made the application the repository will belong to. Its own code: a terminal that
|
|
102
|
+
// meets it is an older client calling this route directly, and can say "upgrade" rather than
|
|
103
|
+
// "you may not".
|
|
104
|
+
"APPLICATION_REQUIRED",
|
|
100
105
|
// repositories
|
|
101
106
|
"REPOSITORY_NOT_FOUND",
|
|
102
107
|
"REPOSITORY_CONFLICT",
|
|
@@ -139,6 +144,7 @@ var HTTP_STATUS_FOR_CODE = {
|
|
|
139
144
|
FORBIDDEN: 403,
|
|
140
145
|
REF_FORBIDDEN: 403,
|
|
141
146
|
ORGANISATION_REQUIRED: 403,
|
|
147
|
+
APPLICATION_REQUIRED: 403,
|
|
142
148
|
REPOSITORY_NOT_FOUND: 404,
|
|
143
149
|
REPOSITORY_CONFLICT: 409,
|
|
144
150
|
REPOSITORY_ARCHIVED: 409,
|
|
@@ -2666,6 +2672,11 @@ var OwnerMarker = object({
|
|
|
2666
2672
|
repoId: RepoId,
|
|
2667
2673
|
createdAt: IsoDate
|
|
2668
2674
|
});
|
|
2675
|
+
var RepositoryOrganisation = object({
|
|
2676
|
+
schemaVersion: SchemaVersion,
|
|
2677
|
+
repoId: RepoId,
|
|
2678
|
+
org: OrgId
|
|
2679
|
+
});
|
|
2669
2680
|
var MetaRecord = object({
|
|
2670
2681
|
schemaVersion: SchemaVersion,
|
|
2671
2682
|
repoId: RepoId,
|
|
@@ -2824,13 +2835,9 @@ var CapabilitiesResponse = object({
|
|
|
2824
2835
|
storageSchemaVersion: int(),
|
|
2825
2836
|
server: object({ name: string2(), version: string2(), git: string2() }),
|
|
2826
2837
|
/**
|
|
2827
|
-
* Either may be EMPTY, and the schema says so because the server can send it
|
|
2828
|
-
*
|
|
2829
|
-
* `url()`
|
|
2830
|
-
* deliberately empty when a deployment cannot determine its own public address — a client
|
|
2831
|
-
* reads that as "it did not say", which is the answer, and is why it must parse rather than
|
|
2832
|
-
* fail. Whoever publishes a value states its type; a schema that refuses what the producer
|
|
2833
|
-
* emits is a mismatch waiting to be found by a consumer.
|
|
2838
|
+
* Either may be EMPTY, and the schema says so because the server can send it: `control` is empty
|
|
2839
|
+
* when a deployment cannot determine its own public address, which a client reads as "it did not
|
|
2840
|
+
* say", so it must parse rather than fail. `url()` was rejected for that reason (#40).
|
|
2834
2841
|
*/
|
|
2835
2842
|
endpoints: object({
|
|
2836
2843
|
control: string2(),
|
|
@@ -2854,6 +2861,8 @@ var CreateRepositoryRequest = object({
|
|
|
2854
2861
|
objectFormat: _default(ObjectFormat, "sha1"),
|
|
2855
2862
|
grants: _default(array(Grant).check(_maxLength(200)), [])
|
|
2856
2863
|
});
|
|
2864
|
+
var RepositoryIdRequest = object({ creationId: CreationId });
|
|
2865
|
+
var RepositoryIdResponse = object({ repoId: RepoId });
|
|
2857
2866
|
var RepositoryInfo = object({
|
|
2858
2867
|
repoId: RepoId,
|
|
2859
2868
|
name: string2(),
|
|
@@ -2938,6 +2947,7 @@ var BundleBootstrapResponse = object({
|
|
|
2938
2947
|
// ../protocol/src/keys.ts
|
|
2939
2948
|
var PENDING_KEY_RE = new RegExp(`^pending/(${REPO_ID_RE.source.slice(1, -1)})/(${PUBLICATION_ID_RE.source.slice(1, -1)})$`);
|
|
2940
2949
|
var OWNER_KEY_RE = new RegExp(`^owners/(${PRINCIPAL_ID_RE.source.slice(1, -1)})/(${REPO_ID_RE.source.slice(1, -1)})$`);
|
|
2950
|
+
var REPOSITORY_ORGANISATION_KEY_RE = new RegExp(`^repository-organisations/(${REPO_ID_RE.source.slice(1, -1)})\\.json$`);
|
|
2941
2951
|
|
|
2942
2952
|
// ../protocol/src/index.ts
|
|
2943
2953
|
var MAX_WAIT_SECONDS = 5;
|
|
@@ -4499,6 +4509,69 @@ async function updateEntry(url2, change) {
|
|
|
4499
4509
|
await writeCredentials(file);
|
|
4500
4510
|
}
|
|
4501
4511
|
|
|
4512
|
+
// ../client/src/application.ts
|
|
4513
|
+
var APPLICATION_CREATION = "application-creation";
|
|
4514
|
+
var ATTEMPTS = 3;
|
|
4515
|
+
var REFUSALS = {
|
|
4516
|
+
"repository-limit": "LIMIT_EXCEEDED",
|
|
4517
|
+
"repository-conflict": "REPOSITORY_CONFLICT"
|
|
4518
|
+
};
|
|
4519
|
+
async function createApplication(input, opts) {
|
|
4520
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
4521
|
+
const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/applications`;
|
|
4522
|
+
const body = { creationId: input.creationId ?? newCreationId(), name: input.name, defaultBranch: input.defaultBranch ?? "main" };
|
|
4523
|
+
let refreshed = false;
|
|
4524
|
+
let lastOutage = "";
|
|
4525
|
+
for (let attempt = 0; attempt < ATTEMPTS; ) {
|
|
4526
|
+
const answer = await post(fetchImpl, url2, await opts.token(), body);
|
|
4527
|
+
if (answer.status === 401 && !refreshed && opts.refresh !== void 0) {
|
|
4528
|
+
refreshed = true;
|
|
4529
|
+
if (await opts.refresh()) continue;
|
|
4530
|
+
}
|
|
4531
|
+
if (answer.status === 200 || answer.status === 201) return read(answer.body, answer.status === 201);
|
|
4532
|
+
const code = isRecord(answer.body) && typeof answer.body["error"] === "string" ? answer.body["error"] : "";
|
|
4533
|
+
if (answer.status === 401) throw new PublisherError("UNAUTHORIZED", 'the portal did not accept this credential; run "publisher login" again');
|
|
4534
|
+
if (answer.status === 403) throw new PublisherError("FORBIDDEN", `the portal refused to make an application here (${code || "forbidden"})`);
|
|
4535
|
+
if (answer.status === 409 && REFUSALS[code] !== void 0)
|
|
4536
|
+
throw new PublisherError(REFUSALS[code], `the portal refused the repository (${code})`, { details: { portal: code } });
|
|
4537
|
+
if (answer.status === 400 || answer.status === 409)
|
|
4538
|
+
throw new PublisherError("INVALID_REQUEST", `the portal refused the request (${code || answer.status})`, { details: { portal: code } });
|
|
4539
|
+
lastOutage = answer.status === 0 ? "the portal could not be reached" : `the portal answered ${answer.status}`;
|
|
4540
|
+
attempt += 1;
|
|
4541
|
+
if (attempt < ATTEMPTS) await new Promise((resolve3) => setTimeout(resolve3, (opts.retryDelayMs ?? 1e3) * attempt));
|
|
4542
|
+
}
|
|
4543
|
+
throw new PublisherError("UNAVAILABLE", `could not make the application: ${lastOutage}; run init again`, { retryable: true });
|
|
4544
|
+
}
|
|
4545
|
+
function read(body, created) {
|
|
4546
|
+
const application = isRecord(body) ? body["application"] : void 0;
|
|
4547
|
+
const repository = RepositoryInfo.safeParse(isRecord(body) ? body["repository"] : void 0);
|
|
4548
|
+
if (!isRecord(application) || typeof application["appId"] !== "string" || !repository.success) {
|
|
4549
|
+
throw new PublisherError("UNAVAILABLE", "the portal answered with an application this version cannot read");
|
|
4550
|
+
}
|
|
4551
|
+
return { appId: application["appId"], created, repository: repository.data };
|
|
4552
|
+
}
|
|
4553
|
+
async function post(fetchImpl, url2, token, body) {
|
|
4554
|
+
try {
|
|
4555
|
+
const res = await fetchImpl(url2, {
|
|
4556
|
+
method: "POST",
|
|
4557
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json", "content-type": "application/json" },
|
|
4558
|
+
body: JSON.stringify(body),
|
|
4559
|
+
signal: AbortSignal.timeout(3e4)
|
|
4560
|
+
});
|
|
4561
|
+
const text = await res.text();
|
|
4562
|
+
try {
|
|
4563
|
+
return { status: res.status, body: text ? JSON.parse(text) : null };
|
|
4564
|
+
} catch {
|
|
4565
|
+
return { status: res.status, body: void 0 };
|
|
4566
|
+
}
|
|
4567
|
+
} catch {
|
|
4568
|
+
return { status: 0, body: null };
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
function isRecord(value) {
|
|
4572
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4502
4575
|
// ../client/src/transport.ts
|
|
4503
4576
|
function assertCredentialEndpoint(endpoint) {
|
|
4504
4577
|
let url2;
|
|
@@ -4615,14 +4688,14 @@ var RUN_STAGES = ["received", "running", "deploying", "live"];
|
|
|
4615
4688
|
var STAGE_WORDS = { received: "received", running: "checked", deploying: "going live", live: "live" };
|
|
4616
4689
|
var STATES = ["pending", "in-progress", "done", "failed", "skipped"];
|
|
4617
4690
|
function readStages(run) {
|
|
4618
|
-
if (!
|
|
4691
|
+
if (!isRecord2(run)) return { malformed: "a run that is not an object" };
|
|
4619
4692
|
if (!Object.hasOwn(run, "stages")) return null;
|
|
4620
4693
|
const stages = run["stages"];
|
|
4621
4694
|
if (!Array.isArray(stages) || stages.length !== RUN_STAGES.length) return { malformed: "stages that are not exactly four" };
|
|
4622
4695
|
const out = [];
|
|
4623
4696
|
for (const [index, entry] of stages.entries()) {
|
|
4624
4697
|
const stage = RUN_STAGES[index];
|
|
4625
|
-
if (!
|
|
4698
|
+
if (!isRecord2(entry) || entry["stage"] !== stage) return { malformed: `stage ${index + 1} is not ${stage}` };
|
|
4626
4699
|
const state = entry["state"];
|
|
4627
4700
|
if (!STATES.includes(state)) return { malformed: `stage ${stage} has an unknown state` };
|
|
4628
4701
|
const at = entry["at"];
|
|
@@ -4665,18 +4738,18 @@ async function followRun(input, opts) {
|
|
|
4665
4738
|
const run = soleRun(answer.body);
|
|
4666
4739
|
if ("detail" in run) return { kind: "malformed", detail: run.detail };
|
|
4667
4740
|
if (run.run !== null) {
|
|
4668
|
-
const
|
|
4669
|
-
if (
|
|
4670
|
-
if ("malformed" in
|
|
4671
|
-
last =
|
|
4672
|
-
for (const entry of
|
|
4741
|
+
const read2 = readStages(run.run);
|
|
4742
|
+
if (read2 === null) return { kind: "unavailable", reason: "no-stages" };
|
|
4743
|
+
if ("malformed" in read2) return { kind: "malformed", detail: read2.malformed };
|
|
4744
|
+
last = read2.stages;
|
|
4745
|
+
for (const entry of read2.stages) {
|
|
4673
4746
|
if (entry.state === "pending" || seen.get(entry.stage) === entry.state) continue;
|
|
4674
4747
|
seen.set(entry.stage, entry.state);
|
|
4675
4748
|
opts.onStage?.(entry);
|
|
4676
4749
|
}
|
|
4677
|
-
if (
|
|
4678
|
-
const end = ending(
|
|
4679
|
-
if (end !== null) return { kind: "settled", ending: end, failedAt:
|
|
4750
|
+
if (isRecord2(run.run) && run.run["status"] === "awaiting-approval") return { kind: "waiting", stages: read2.stages };
|
|
4751
|
+
const end = ending(read2.stages);
|
|
4752
|
+
if (end !== null) return { kind: "settled", ending: end, failedAt: read2.stages.find((s) => s.state === "failed")?.word ?? null, stages: read2.stages };
|
|
4680
4753
|
}
|
|
4681
4754
|
lastError = null;
|
|
4682
4755
|
} else {
|
|
@@ -4687,7 +4760,7 @@ async function followRun(input, opts) {
|
|
|
4687
4760
|
}
|
|
4688
4761
|
}
|
|
4689
4762
|
function soleRun(body) {
|
|
4690
|
-
if (!
|
|
4763
|
+
if (!isRecord2(body) || !Array.isArray(body["runs"])) return { detail: "an answer with no list of runs" };
|
|
4691
4764
|
const runs = body["runs"];
|
|
4692
4765
|
if (runs.length > 1) return { detail: "more than the one run this publication started" };
|
|
4693
4766
|
return { run: runs[0] ?? null };
|
|
@@ -4732,7 +4805,7 @@ async function confirmPortal(portal, endpoint, fetchImpl = fetch) {
|
|
|
4732
4805
|
return null;
|
|
4733
4806
|
}
|
|
4734
4807
|
}
|
|
4735
|
-
function
|
|
4808
|
+
function isRecord2(value) {
|
|
4736
4809
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4737
4810
|
}
|
|
4738
4811
|
|
|
@@ -4759,7 +4832,7 @@ async function call(input, opts, body) {
|
|
|
4759
4832
|
if (answer.status === 403) return { kind: "forbidden" };
|
|
4760
4833
|
if (answer.status === 409) return { kind: "moved-on" };
|
|
4761
4834
|
if (answer.status === 404) {
|
|
4762
|
-
const code =
|
|
4835
|
+
const code = isRecord3(answer.body) ? answer.body["error"] : void 0;
|
|
4763
4836
|
return code === "no-such-application" ? { kind: "no-application" } : { kind: "unavailable", detail: "the portal does not serve this address" };
|
|
4764
4837
|
}
|
|
4765
4838
|
if (answer.status === 200) {
|
|
@@ -4770,30 +4843,30 @@ async function call(input, opts, body) {
|
|
|
4770
4843
|
}
|
|
4771
4844
|
}
|
|
4772
4845
|
function readView(body) {
|
|
4773
|
-
if (!
|
|
4846
|
+
if (!isRecord3(body)) return { detail: "an answer that is not a preparation" };
|
|
4774
4847
|
const { id, revision, status, message, questions, publication } = body;
|
|
4775
4848
|
if (typeof id !== "string" || id === "") return { detail: "a preparation with no id" };
|
|
4776
4849
|
if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1) return { detail: "a preparation with no revision" };
|
|
4777
4850
|
if (!STATUSES.includes(status)) return { detail: "a preparation with an unknown status" };
|
|
4778
4851
|
if (typeof message !== "string") return { detail: "a preparation with no message" };
|
|
4779
4852
|
if (!Array.isArray(questions)) return { detail: "a preparation with no list of questions" };
|
|
4780
|
-
const
|
|
4853
|
+
const read2 = [];
|
|
4781
4854
|
for (const q of questions) {
|
|
4782
|
-
if (!
|
|
4855
|
+
if (!isRecord3(q) || typeof q["id"] !== "string" || typeof q["text"] !== "string" || !Array.isArray(q["options"]) || q["options"].length === 0) {
|
|
4783
4856
|
return { detail: "a question that cannot be answered" };
|
|
4784
4857
|
}
|
|
4785
4858
|
const options = [];
|
|
4786
4859
|
for (const o of q["options"]) {
|
|
4787
|
-
if (!
|
|
4860
|
+
if (!isRecord3(o) || typeof o["value"] !== "string" || typeof o["label"] !== "string") return { detail: "a question with an unreadable option" };
|
|
4788
4861
|
options.push({ value: o["value"], label: o["label"] });
|
|
4789
4862
|
}
|
|
4790
|
-
|
|
4863
|
+
read2.push({ id: q["id"], text: q["text"], options });
|
|
4791
4864
|
}
|
|
4792
4865
|
let pub = null;
|
|
4793
|
-
if (
|
|
4866
|
+
if (isRecord3(publication) && typeof publication["status"] === "string" && typeof publication["message"] === "string") {
|
|
4794
4867
|
pub = { status: publication["status"], message: publication["message"], ...typeof publication["url"] === "string" ? { url: publication["url"] } : {} };
|
|
4795
4868
|
}
|
|
4796
|
-
return { id, revision, status, message, questions:
|
|
4869
|
+
return { id, revision, status, message, questions: read2, publication: pub };
|
|
4797
4870
|
}
|
|
4798
4871
|
async function ask2(fetchImpl, url2, token, body) {
|
|
4799
4872
|
try {
|
|
@@ -4813,7 +4886,7 @@ async function ask2(fetchImpl, url2, token, body) {
|
|
|
4813
4886
|
return { status: 0, body: null };
|
|
4814
4887
|
}
|
|
4815
4888
|
}
|
|
4816
|
-
function
|
|
4889
|
+
function isRecord3(value) {
|
|
4817
4890
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4818
4891
|
}
|
|
4819
4892
|
|
|
@@ -5480,6 +5553,14 @@ var Publisher = class {
|
|
|
5480
5553
|
fetch: this.fetchImpl
|
|
5481
5554
|
});
|
|
5482
5555
|
}
|
|
5556
|
+
/**
|
|
5557
|
+
* A new application and its repository, made by the portal that serves this endpoint — the only
|
|
5558
|
+
* way a repository is created on a deployed stage (ADR-0016 A1). The caller confirms the portal's
|
|
5559
|
+
* `/config` lists `APPLICATION_CREATION` first, as it does for the other terminal routes.
|
|
5560
|
+
*/
|
|
5561
|
+
async createApplication(input) {
|
|
5562
|
+
return createApplication(input, this.portalCall());
|
|
5563
|
+
}
|
|
5483
5564
|
/**
|
|
5484
5565
|
* The question the repository's current preparation waits on, and its answer, at the portal that
|
|
5485
5566
|
* serves this endpoint — with this publisher's credential and its one refresh, as `followRun`.
|
|
@@ -5737,8 +5818,8 @@ async function deviceLogin(opts) {
|
|
|
5737
5818
|
if (typeof advertised !== "string" || advertised === "") throw new PublisherError("UNAVAILABLE", "this deployment does not offer device login; obtain a token another way");
|
|
5738
5819
|
const base = normalize(advertised);
|
|
5739
5820
|
assertCredentialEndpoint(base);
|
|
5740
|
-
const
|
|
5741
|
-
const started = await
|
|
5821
|
+
const post2 = (path, fields) => postForm(base, path, fields, doFetch);
|
|
5822
|
+
const started = await post2("/v1/auth/device", { client_id: opts.clientId ?? "gitpub-cli" });
|
|
5742
5823
|
if (started.status !== 200) throw new PublisherError("UNAVAILABLE", `starting the login failed (${started.status})`, { retryable: true });
|
|
5743
5824
|
const s = started.body;
|
|
5744
5825
|
const deviceCode = str(s.device_code);
|
|
@@ -5752,7 +5833,7 @@ async function deviceLogin(opts) {
|
|
|
5752
5833
|
for (; ; ) {
|
|
5753
5834
|
await sleep2(intervalMs);
|
|
5754
5835
|
if (now() >= deadline) throw new PublisherError("EXPIRED", "the login expired before it was approved");
|
|
5755
|
-
const polled = await
|
|
5836
|
+
const polled = await post2("/v1/auth/token", { grant_type: DEVICE_CODE_GRANT, device_code: deviceCode });
|
|
5756
5837
|
if (polled.status === 200) {
|
|
5757
5838
|
const token = str(polled.body["access_token"]);
|
|
5758
5839
|
if (!token) throw new PublisherError("UNAVAILABLE", "the login returned no credential");
|
|
@@ -5799,6 +5880,21 @@ async function discoverEndpoint(portal, fetchImpl = fetch) {
|
|
|
5799
5880
|
return endpoint;
|
|
5800
5881
|
}
|
|
5801
5882
|
|
|
5883
|
+
// src/init.ts
|
|
5884
|
+
async function repositoryForInit(p, endpoint, name, opts = {}) {
|
|
5885
|
+
const portal = portalForEndpoint(endpoint);
|
|
5886
|
+
if (portal === null) return { repoId: (await p.createRepository({ name, grants: [] })).repoId, appId: null };
|
|
5887
|
+
const confirmed = await confirmPortal(portal, endpoint, opts.fetch);
|
|
5888
|
+
if (confirmed === null) {
|
|
5889
|
+
throw new PublisherError("UNAVAILABLE", `could not confirm that ${portal} publishes to ${endpoint}; run init again`, { retryable: true });
|
|
5890
|
+
}
|
|
5891
|
+
if (!confirmed.terminal.includes(APPLICATION_CREATION)) {
|
|
5892
|
+
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`);
|
|
5893
|
+
}
|
|
5894
|
+
const made = await p.createApplication({ portal, name });
|
|
5895
|
+
return { repoId: made.repository.repoId, appId: made.appId };
|
|
5896
|
+
}
|
|
5897
|
+
|
|
5802
5898
|
// src/login.ts
|
|
5803
5899
|
async function verifiedCapabilities(publisher) {
|
|
5804
5900
|
const caps = await publisher.getCapabilities();
|
|
@@ -6034,22 +6130,22 @@ async function answerAt(p, endpoint, repoId, specs, opts = {}) {
|
|
|
6034
6130
|
const answers = parseSpecs(specs);
|
|
6035
6131
|
const at = await portalAt(endpoint, opts);
|
|
6036
6132
|
if ("reason" in at) return at;
|
|
6037
|
-
const
|
|
6038
|
-
if (
|
|
6039
|
-
if (
|
|
6133
|
+
const read2 = shown(at.portal, await p.readPreparation({ portal: at.portal, repoId }));
|
|
6134
|
+
if (read2.kind !== "view") return read2;
|
|
6135
|
+
if (read2.questions.length === 0) throw new PublisherError("LOCAL_STATE", `nothing is waiting for an answer (the preparation is ${read2.preparation.status})`);
|
|
6040
6136
|
for (const [id, value] of Object.entries(answers)) {
|
|
6041
|
-
const question =
|
|
6137
|
+
const question = read2.questions.find((q) => q.id === id);
|
|
6042
6138
|
if (question === void 0) {
|
|
6043
|
-
throw new PublisherError("INVALID_REQUEST", `no pending question "${id}"; pending: ${
|
|
6044
|
-
details: { questions:
|
|
6139
|
+
throw new PublisherError("INVALID_REQUEST", `no pending question "${id}"; pending: ${read2.questions.map((q) => q.id).join(", ")}`, {
|
|
6140
|
+
details: { questions: read2.questions }
|
|
6045
6141
|
});
|
|
6046
6142
|
}
|
|
6047
6143
|
const values = question.options.map((o) => o.value);
|
|
6048
6144
|
if (!values.includes(value)) {
|
|
6049
|
-
throw new PublisherError("INVALID_REQUEST", `"${value}" is not one of ${values.join(", ")} for "${id}"`, { details: { questions:
|
|
6145
|
+
throw new PublisherError("INVALID_REQUEST", `"${value}" is not one of ${values.join(", ")} for "${id}"`, { details: { questions: read2.questions } });
|
|
6050
6146
|
}
|
|
6051
6147
|
}
|
|
6052
|
-
return shown(at.portal, await p.answerPreparation({ portal: at.portal, repoId, id:
|
|
6148
|
+
return shown(at.portal, await p.answerPreparation({ portal: at.portal, repoId, id: read2.preparation.id, revision: read2.preparation.revision, answers }));
|
|
6053
6149
|
}
|
|
6054
6150
|
function parseSpecs(specs) {
|
|
6055
6151
|
if (specs.length === 0) throw new PublisherError("INVALID_REQUEST", "answer takes one or more question=value");
|
|
@@ -6119,7 +6215,8 @@ Usage:
|
|
|
6119
6215
|
publisher fetch [dir]
|
|
6120
6216
|
publisher checkout <dir> --repo <id> [--branch <name>] create a NEW editable workspace (never overlays a folder)
|
|
6121
6217
|
publisher workspace-status [dir] offline mapping + where the workspace stands (real three-way check)
|
|
6122
|
-
publisher sync [dir] [--message <text>]
|
|
6218
|
+
publisher sync [dir] [--message <text>] [--name <app>] THE way to publish: a folder linked to nothing first gets its
|
|
6219
|
+
application and repository (what init does); then it records the
|
|
6123
6220
|
changes (never .env, keys or node_modules), brings in what
|
|
6124
6221
|
was published meanwhile (fast-forward or a clean merge,
|
|
6125
6222
|
never a rewrite) and publishes. Stops on a conflict with
|
|
@@ -6212,6 +6309,31 @@ async function main(argv) {
|
|
|
6212
6309
|
const token = str2(flags["token"]);
|
|
6213
6310
|
return { endpoint, p: createPublisher({ endpoint, endpointSource: source, ...token ? { token } : {}, clientName: "git-publisher-cli", clientVersion: CLIENT_VERSION }) };
|
|
6214
6311
|
};
|
|
6312
|
+
const linkFolder = async (p, endpoint, dir, attach) => {
|
|
6313
|
+
let onboarding = null;
|
|
6314
|
+
if (!await isGitRepo(dir) || !await headCommit(dir)) {
|
|
6315
|
+
onboarding = await onboardFolder({ dir, ...optStr(flags, "message") });
|
|
6316
|
+
log(`prepared ${onboarding.committed.length} files (${onboarding.excluded.length} excluded)`);
|
|
6317
|
+
}
|
|
6318
|
+
let repoId = attach.repoId ?? null;
|
|
6319
|
+
let appId = null;
|
|
6320
|
+
if (repoId === null) {
|
|
6321
|
+
const made = await repositoryForInit(p, endpoint, str2(flags["name"]) ?? dir.split("/").pop() ?? "");
|
|
6322
|
+
repoId = made.repoId;
|
|
6323
|
+
appId = made.appId;
|
|
6324
|
+
} else {
|
|
6325
|
+
await p.getRepository(repoId);
|
|
6326
|
+
}
|
|
6327
|
+
await p.associate(dir, repoId);
|
|
6328
|
+
const workspace = await p.adoptWorkspace(dir).catch(() => null);
|
|
6329
|
+
return {
|
|
6330
|
+
repoId,
|
|
6331
|
+
appId,
|
|
6332
|
+
created: attach.repoId === void 0,
|
|
6333
|
+
onboarding: onboarding ? { commit: onboarding.commit, committed: onboarding.committed.length, excluded: onboarding.excluded } : null,
|
|
6334
|
+
workspace: workspace === null ? null : { branch: workspace.branch, baseOid: workspace.baseOid, baseGeneration: workspace.baseGeneration }
|
|
6335
|
+
};
|
|
6336
|
+
};
|
|
6215
6337
|
if (flags["version"] === true) {
|
|
6216
6338
|
process.stdout.write(`${packageVersion()}
|
|
6217
6339
|
`);
|
|
@@ -6345,33 +6467,9 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6345
6467
|
case "init": {
|
|
6346
6468
|
const dir = dirArg(1);
|
|
6347
6469
|
const { p, endpoint } = await client();
|
|
6348
|
-
const
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
onboarding = await onboardFolder({ dir, ...optStr(flags, "message") });
|
|
6352
|
-
log(`prepared ${onboarding.committed.length} files (${onboarding.excluded.length} excluded)`);
|
|
6353
|
-
}
|
|
6354
|
-
let repoId = str2(flags["repo"]);
|
|
6355
|
-
let created = false;
|
|
6356
|
-
if (!repoId) {
|
|
6357
|
-
const grants = [];
|
|
6358
|
-
const repo = await p.createRepository({ name: str2(flags["name"]) ?? dir.split("/").pop() ?? "", grants });
|
|
6359
|
-
repoId = repo.repoId;
|
|
6360
|
-
created = true;
|
|
6361
|
-
} else {
|
|
6362
|
-
await p.getRepository(repoId);
|
|
6363
|
-
}
|
|
6364
|
-
await p.associate(dir, repoId);
|
|
6365
|
-
const workspace = await p.adoptWorkspace(dir).catch(() => null);
|
|
6366
|
-
out({
|
|
6367
|
-
ok: true,
|
|
6368
|
-
repoId,
|
|
6369
|
-
created,
|
|
6370
|
-
endpoint,
|
|
6371
|
-
dir,
|
|
6372
|
-
onboarding: onboarding ? { commit: onboarding.commit, committed: onboarding.committed.length, excluded: onboarding.excluded } : null,
|
|
6373
|
-
workspace: workspace === null ? null : { branch: workspace.branch, baseOid: workspace.baseOid, baseGeneration: workspace.baseGeneration }
|
|
6374
|
-
});
|
|
6470
|
+
const repo = str2(flags["repo"]);
|
|
6471
|
+
const linked = await linkFolder(p, endpoint, dir, repo ? { repoId: repo } : {});
|
|
6472
|
+
out({ ok: true, ...linked, endpoint, dir });
|
|
6375
6473
|
return 0;
|
|
6376
6474
|
}
|
|
6377
6475
|
case "publish": {
|
|
@@ -6457,19 +6555,21 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6457
6555
|
}
|
|
6458
6556
|
/**
|
|
6459
6557
|
* The creator's one verb: record the folder's changes, bring in what the service has, publish.
|
|
6460
|
-
* `--apply` is
|
|
6461
|
-
*
|
|
6558
|
+
* `--apply` is still accepted, so a `sync --apply` written down anywhere does the whole round
|
|
6559
|
+
* trip rather than failing.
|
|
6462
6560
|
*/
|
|
6463
6561
|
case "sync": {
|
|
6464
6562
|
const dir = dirArg(1);
|
|
6465
6563
|
const timeoutMs = followTimeoutMs(str2(flags["timeout"]));
|
|
6466
6564
|
const { p, endpoint } = await client(dir);
|
|
6565
|
+
const linked = await isGitRepo(dir) && await readAssociation(dir) !== null ? null : await linkFolder(p, endpoint, dir, {});
|
|
6467
6566
|
const r = await p.syncChanges({ directory: dir, markConflicts: flags["mark-conflicts"] === true, ...optStr(flags, "message") }, { onProgress: progress });
|
|
6468
6567
|
const o = r.published;
|
|
6469
6568
|
const followed = o !== null && o.state === "accepted" && flags["no-wait"] !== true ? await followPublication(p, endpoint, o, { onStage: emitStage, log, timeoutMs }) : null;
|
|
6470
6569
|
out({
|
|
6471
6570
|
ok: r.state === "published" || r.state === "up_to_date",
|
|
6472
6571
|
state: r.state,
|
|
6572
|
+
linked: linked === null ? null : { repoId: linked.repoId, appId: linked.appId, created: linked.created },
|
|
6473
6573
|
recorded: r.recorded,
|
|
6474
6574
|
merged: r.reconciled.applied,
|
|
6475
6575
|
conflicts: r.reconciled.before.conflicts,
|