@minnovate/shadow-publisher 0.7.0 → 0.8.0
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.0",
|
|
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,
|
|
@@ -2854,6 +2860,8 @@ var CreateRepositoryRequest = object({
|
|
|
2854
2860
|
objectFormat: _default(ObjectFormat, "sha1"),
|
|
2855
2861
|
grants: _default(array(Grant).check(_maxLength(200)), [])
|
|
2856
2862
|
});
|
|
2863
|
+
var RepositoryIdRequest = object({ creationId: CreationId });
|
|
2864
|
+
var RepositoryIdResponse = object({ repoId: RepoId });
|
|
2857
2865
|
var RepositoryInfo = object({
|
|
2858
2866
|
repoId: RepoId,
|
|
2859
2867
|
name: string2(),
|
|
@@ -4499,6 +4507,69 @@ async function updateEntry(url2, change) {
|
|
|
4499
4507
|
await writeCredentials(file);
|
|
4500
4508
|
}
|
|
4501
4509
|
|
|
4510
|
+
// ../client/src/application.ts
|
|
4511
|
+
var APPLICATION_CREATION = "application-creation";
|
|
4512
|
+
var ATTEMPTS = 3;
|
|
4513
|
+
var REFUSALS = {
|
|
4514
|
+
"repository-limit": "LIMIT_EXCEEDED",
|
|
4515
|
+
"repository-conflict": "REPOSITORY_CONFLICT"
|
|
4516
|
+
};
|
|
4517
|
+
async function createApplication(input, opts) {
|
|
4518
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
4519
|
+
const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/applications`;
|
|
4520
|
+
const body = { creationId: input.creationId ?? newCreationId(), name: input.name, defaultBranch: input.defaultBranch ?? "main" };
|
|
4521
|
+
let refreshed = false;
|
|
4522
|
+
let lastOutage = "";
|
|
4523
|
+
for (let attempt = 0; attempt < ATTEMPTS; ) {
|
|
4524
|
+
const answer = await post(fetchImpl, url2, await opts.token(), body);
|
|
4525
|
+
if (answer.status === 401 && !refreshed && opts.refresh !== void 0) {
|
|
4526
|
+
refreshed = true;
|
|
4527
|
+
if (await opts.refresh()) continue;
|
|
4528
|
+
}
|
|
4529
|
+
if (answer.status === 200 || answer.status === 201) return read(answer.body, answer.status === 201);
|
|
4530
|
+
const code = isRecord(answer.body) && typeof answer.body["error"] === "string" ? answer.body["error"] : "";
|
|
4531
|
+
if (answer.status === 401) throw new PublisherError("UNAUTHORIZED", 'the portal did not accept this credential; run "publisher login" again');
|
|
4532
|
+
if (answer.status === 403) throw new PublisherError("FORBIDDEN", `the portal refused to make an application here (${code || "forbidden"})`);
|
|
4533
|
+
if (answer.status === 409 && REFUSALS[code] !== void 0)
|
|
4534
|
+
throw new PublisherError(REFUSALS[code], `the portal refused the repository (${code})`, { details: { portal: code } });
|
|
4535
|
+
if (answer.status === 400 || answer.status === 409)
|
|
4536
|
+
throw new PublisherError("INVALID_REQUEST", `the portal refused the request (${code || answer.status})`, { details: { portal: code } });
|
|
4537
|
+
lastOutage = answer.status === 0 ? "the portal could not be reached" : `the portal answered ${answer.status}`;
|
|
4538
|
+
attempt += 1;
|
|
4539
|
+
if (attempt < ATTEMPTS) await new Promise((resolve3) => setTimeout(resolve3, (opts.retryDelayMs ?? 1e3) * attempt));
|
|
4540
|
+
}
|
|
4541
|
+
throw new PublisherError("UNAVAILABLE", `could not make the application: ${lastOutage}; run init again`, { retryable: true });
|
|
4542
|
+
}
|
|
4543
|
+
function read(body, created) {
|
|
4544
|
+
const application = isRecord(body) ? body["application"] : void 0;
|
|
4545
|
+
const repository = RepositoryInfo.safeParse(isRecord(body) ? body["repository"] : void 0);
|
|
4546
|
+
if (!isRecord(application) || typeof application["appId"] !== "string" || !repository.success) {
|
|
4547
|
+
throw new PublisherError("UNAVAILABLE", "the portal answered with an application this version cannot read");
|
|
4548
|
+
}
|
|
4549
|
+
return { appId: application["appId"], created, repository: repository.data };
|
|
4550
|
+
}
|
|
4551
|
+
async function post(fetchImpl, url2, token, body) {
|
|
4552
|
+
try {
|
|
4553
|
+
const res = await fetchImpl(url2, {
|
|
4554
|
+
method: "POST",
|
|
4555
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json", "content-type": "application/json" },
|
|
4556
|
+
body: JSON.stringify(body),
|
|
4557
|
+
signal: AbortSignal.timeout(3e4)
|
|
4558
|
+
});
|
|
4559
|
+
const text = await res.text();
|
|
4560
|
+
try {
|
|
4561
|
+
return { status: res.status, body: text ? JSON.parse(text) : null };
|
|
4562
|
+
} catch {
|
|
4563
|
+
return { status: res.status, body: void 0 };
|
|
4564
|
+
}
|
|
4565
|
+
} catch {
|
|
4566
|
+
return { status: 0, body: null };
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
function isRecord(value) {
|
|
4570
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4502
4573
|
// ../client/src/transport.ts
|
|
4503
4574
|
function assertCredentialEndpoint(endpoint) {
|
|
4504
4575
|
let url2;
|
|
@@ -4615,14 +4686,14 @@ var RUN_STAGES = ["received", "running", "deploying", "live"];
|
|
|
4615
4686
|
var STAGE_WORDS = { received: "received", running: "checked", deploying: "going live", live: "live" };
|
|
4616
4687
|
var STATES = ["pending", "in-progress", "done", "failed", "skipped"];
|
|
4617
4688
|
function readStages(run) {
|
|
4618
|
-
if (!
|
|
4689
|
+
if (!isRecord2(run)) return { malformed: "a run that is not an object" };
|
|
4619
4690
|
if (!Object.hasOwn(run, "stages")) return null;
|
|
4620
4691
|
const stages = run["stages"];
|
|
4621
4692
|
if (!Array.isArray(stages) || stages.length !== RUN_STAGES.length) return { malformed: "stages that are not exactly four" };
|
|
4622
4693
|
const out = [];
|
|
4623
4694
|
for (const [index, entry] of stages.entries()) {
|
|
4624
4695
|
const stage = RUN_STAGES[index];
|
|
4625
|
-
if (!
|
|
4696
|
+
if (!isRecord2(entry) || entry["stage"] !== stage) return { malformed: `stage ${index + 1} is not ${stage}` };
|
|
4626
4697
|
const state = entry["state"];
|
|
4627
4698
|
if (!STATES.includes(state)) return { malformed: `stage ${stage} has an unknown state` };
|
|
4628
4699
|
const at = entry["at"];
|
|
@@ -4665,18 +4736,18 @@ async function followRun(input, opts) {
|
|
|
4665
4736
|
const run = soleRun(answer.body);
|
|
4666
4737
|
if ("detail" in run) return { kind: "malformed", detail: run.detail };
|
|
4667
4738
|
if (run.run !== null) {
|
|
4668
|
-
const
|
|
4669
|
-
if (
|
|
4670
|
-
if ("malformed" in
|
|
4671
|
-
last =
|
|
4672
|
-
for (const entry of
|
|
4739
|
+
const read2 = readStages(run.run);
|
|
4740
|
+
if (read2 === null) return { kind: "unavailable", reason: "no-stages" };
|
|
4741
|
+
if ("malformed" in read2) return { kind: "malformed", detail: read2.malformed };
|
|
4742
|
+
last = read2.stages;
|
|
4743
|
+
for (const entry of read2.stages) {
|
|
4673
4744
|
if (entry.state === "pending" || seen.get(entry.stage) === entry.state) continue;
|
|
4674
4745
|
seen.set(entry.stage, entry.state);
|
|
4675
4746
|
opts.onStage?.(entry);
|
|
4676
4747
|
}
|
|
4677
|
-
if (
|
|
4678
|
-
const end = ending(
|
|
4679
|
-
if (end !== null) return { kind: "settled", ending: end, failedAt:
|
|
4748
|
+
if (isRecord2(run.run) && run.run["status"] === "awaiting-approval") return { kind: "waiting", stages: read2.stages };
|
|
4749
|
+
const end = ending(read2.stages);
|
|
4750
|
+
if (end !== null) return { kind: "settled", ending: end, failedAt: read2.stages.find((s) => s.state === "failed")?.word ?? null, stages: read2.stages };
|
|
4680
4751
|
}
|
|
4681
4752
|
lastError = null;
|
|
4682
4753
|
} else {
|
|
@@ -4687,7 +4758,7 @@ async function followRun(input, opts) {
|
|
|
4687
4758
|
}
|
|
4688
4759
|
}
|
|
4689
4760
|
function soleRun(body) {
|
|
4690
|
-
if (!
|
|
4761
|
+
if (!isRecord2(body) || !Array.isArray(body["runs"])) return { detail: "an answer with no list of runs" };
|
|
4691
4762
|
const runs = body["runs"];
|
|
4692
4763
|
if (runs.length > 1) return { detail: "more than the one run this publication started" };
|
|
4693
4764
|
return { run: runs[0] ?? null };
|
|
@@ -4732,7 +4803,7 @@ async function confirmPortal(portal, endpoint, fetchImpl = fetch) {
|
|
|
4732
4803
|
return null;
|
|
4733
4804
|
}
|
|
4734
4805
|
}
|
|
4735
|
-
function
|
|
4806
|
+
function isRecord2(value) {
|
|
4736
4807
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4737
4808
|
}
|
|
4738
4809
|
|
|
@@ -4759,7 +4830,7 @@ async function call(input, opts, body) {
|
|
|
4759
4830
|
if (answer.status === 403) return { kind: "forbidden" };
|
|
4760
4831
|
if (answer.status === 409) return { kind: "moved-on" };
|
|
4761
4832
|
if (answer.status === 404) {
|
|
4762
|
-
const code =
|
|
4833
|
+
const code = isRecord3(answer.body) ? answer.body["error"] : void 0;
|
|
4763
4834
|
return code === "no-such-application" ? { kind: "no-application" } : { kind: "unavailable", detail: "the portal does not serve this address" };
|
|
4764
4835
|
}
|
|
4765
4836
|
if (answer.status === 200) {
|
|
@@ -4770,30 +4841,30 @@ async function call(input, opts, body) {
|
|
|
4770
4841
|
}
|
|
4771
4842
|
}
|
|
4772
4843
|
function readView(body) {
|
|
4773
|
-
if (!
|
|
4844
|
+
if (!isRecord3(body)) return { detail: "an answer that is not a preparation" };
|
|
4774
4845
|
const { id, revision, status, message, questions, publication } = body;
|
|
4775
4846
|
if (typeof id !== "string" || id === "") return { detail: "a preparation with no id" };
|
|
4776
4847
|
if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1) return { detail: "a preparation with no revision" };
|
|
4777
4848
|
if (!STATUSES.includes(status)) return { detail: "a preparation with an unknown status" };
|
|
4778
4849
|
if (typeof message !== "string") return { detail: "a preparation with no message" };
|
|
4779
4850
|
if (!Array.isArray(questions)) return { detail: "a preparation with no list of questions" };
|
|
4780
|
-
const
|
|
4851
|
+
const read2 = [];
|
|
4781
4852
|
for (const q of questions) {
|
|
4782
|
-
if (!
|
|
4853
|
+
if (!isRecord3(q) || typeof q["id"] !== "string" || typeof q["text"] !== "string" || !Array.isArray(q["options"]) || q["options"].length === 0) {
|
|
4783
4854
|
return { detail: "a question that cannot be answered" };
|
|
4784
4855
|
}
|
|
4785
4856
|
const options = [];
|
|
4786
4857
|
for (const o of q["options"]) {
|
|
4787
|
-
if (!
|
|
4858
|
+
if (!isRecord3(o) || typeof o["value"] !== "string" || typeof o["label"] !== "string") return { detail: "a question with an unreadable option" };
|
|
4788
4859
|
options.push({ value: o["value"], label: o["label"] });
|
|
4789
4860
|
}
|
|
4790
|
-
|
|
4861
|
+
read2.push({ id: q["id"], text: q["text"], options });
|
|
4791
4862
|
}
|
|
4792
4863
|
let pub = null;
|
|
4793
|
-
if (
|
|
4864
|
+
if (isRecord3(publication) && typeof publication["status"] === "string" && typeof publication["message"] === "string") {
|
|
4794
4865
|
pub = { status: publication["status"], message: publication["message"], ...typeof publication["url"] === "string" ? { url: publication["url"] } : {} };
|
|
4795
4866
|
}
|
|
4796
|
-
return { id, revision, status, message, questions:
|
|
4867
|
+
return { id, revision, status, message, questions: read2, publication: pub };
|
|
4797
4868
|
}
|
|
4798
4869
|
async function ask2(fetchImpl, url2, token, body) {
|
|
4799
4870
|
try {
|
|
@@ -4813,7 +4884,7 @@ async function ask2(fetchImpl, url2, token, body) {
|
|
|
4813
4884
|
return { status: 0, body: null };
|
|
4814
4885
|
}
|
|
4815
4886
|
}
|
|
4816
|
-
function
|
|
4887
|
+
function isRecord3(value) {
|
|
4817
4888
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4818
4889
|
}
|
|
4819
4890
|
|
|
@@ -5485,6 +5556,14 @@ var Publisher = class {
|
|
|
5485
5556
|
* serves this endpoint — with this publisher's credential and its one refresh, as `followRun`.
|
|
5486
5557
|
* The caller confirms the portal's `/config` lists `PREPARATION` first.
|
|
5487
5558
|
*/
|
|
5559
|
+
/**
|
|
5560
|
+
* A new application and its repository, made by the portal that serves this endpoint — the only
|
|
5561
|
+
* way a repository is created on a deployed stage (ADR-0016 A1). The caller confirms the portal's
|
|
5562
|
+
* `/config` lists `APPLICATION_CREATION` first, as it does for the other terminal routes.
|
|
5563
|
+
*/
|
|
5564
|
+
async createApplication(input) {
|
|
5565
|
+
return createApplication(input, this.portalCall());
|
|
5566
|
+
}
|
|
5488
5567
|
async readPreparation(input) {
|
|
5489
5568
|
return readPreparation(input, this.portalCall());
|
|
5490
5569
|
}
|
|
@@ -5737,8 +5816,8 @@ async function deviceLogin(opts) {
|
|
|
5737
5816
|
if (typeof advertised !== "string" || advertised === "") throw new PublisherError("UNAVAILABLE", "this deployment does not offer device login; obtain a token another way");
|
|
5738
5817
|
const base = normalize(advertised);
|
|
5739
5818
|
assertCredentialEndpoint(base);
|
|
5740
|
-
const
|
|
5741
|
-
const started = await
|
|
5819
|
+
const post2 = (path, fields) => postForm(base, path, fields, doFetch);
|
|
5820
|
+
const started = await post2("/v1/auth/device", { client_id: opts.clientId ?? "gitpub-cli" });
|
|
5742
5821
|
if (started.status !== 200) throw new PublisherError("UNAVAILABLE", `starting the login failed (${started.status})`, { retryable: true });
|
|
5743
5822
|
const s = started.body;
|
|
5744
5823
|
const deviceCode = str(s.device_code);
|
|
@@ -5752,7 +5831,7 @@ async function deviceLogin(opts) {
|
|
|
5752
5831
|
for (; ; ) {
|
|
5753
5832
|
await sleep2(intervalMs);
|
|
5754
5833
|
if (now() >= deadline) throw new PublisherError("EXPIRED", "the login expired before it was approved");
|
|
5755
|
-
const polled = await
|
|
5834
|
+
const polled = await post2("/v1/auth/token", { grant_type: DEVICE_CODE_GRANT, device_code: deviceCode });
|
|
5756
5835
|
if (polled.status === 200) {
|
|
5757
5836
|
const token = str(polled.body["access_token"]);
|
|
5758
5837
|
if (!token) throw new PublisherError("UNAVAILABLE", "the login returned no credential");
|
|
@@ -5799,6 +5878,21 @@ async function discoverEndpoint(portal, fetchImpl = fetch) {
|
|
|
5799
5878
|
return endpoint;
|
|
5800
5879
|
}
|
|
5801
5880
|
|
|
5881
|
+
// src/init.ts
|
|
5882
|
+
async function repositoryForInit(p, endpoint, name, opts = {}) {
|
|
5883
|
+
const portal = portalForEndpoint(endpoint);
|
|
5884
|
+
if (portal === null) return { repoId: (await p.createRepository({ name, grants: [] })).repoId, appId: null };
|
|
5885
|
+
const confirmed = await confirmPortal(portal, endpoint, opts.fetch);
|
|
5886
|
+
if (confirmed === null) {
|
|
5887
|
+
throw new PublisherError("UNAVAILABLE", `could not confirm that ${portal} publishes to ${endpoint}; run init again`, { retryable: true });
|
|
5888
|
+
}
|
|
5889
|
+
if (!confirmed.terminal.includes(APPLICATION_CREATION)) {
|
|
5890
|
+
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`);
|
|
5891
|
+
}
|
|
5892
|
+
const made = await p.createApplication({ portal, name });
|
|
5893
|
+
return { repoId: made.repository.repoId, appId: made.appId };
|
|
5894
|
+
}
|
|
5895
|
+
|
|
5802
5896
|
// src/login.ts
|
|
5803
5897
|
async function verifiedCapabilities(publisher) {
|
|
5804
5898
|
const caps = await publisher.getCapabilities();
|
|
@@ -6034,22 +6128,22 @@ async function answerAt(p, endpoint, repoId, specs, opts = {}) {
|
|
|
6034
6128
|
const answers = parseSpecs(specs);
|
|
6035
6129
|
const at = await portalAt(endpoint, opts);
|
|
6036
6130
|
if ("reason" in at) return at;
|
|
6037
|
-
const
|
|
6038
|
-
if (
|
|
6039
|
-
if (
|
|
6131
|
+
const read2 = shown(at.portal, await p.readPreparation({ portal: at.portal, repoId }));
|
|
6132
|
+
if (read2.kind !== "view") return read2;
|
|
6133
|
+
if (read2.questions.length === 0) throw new PublisherError("LOCAL_STATE", `nothing is waiting for an answer (the preparation is ${read2.preparation.status})`);
|
|
6040
6134
|
for (const [id, value] of Object.entries(answers)) {
|
|
6041
|
-
const question =
|
|
6135
|
+
const question = read2.questions.find((q) => q.id === id);
|
|
6042
6136
|
if (question === void 0) {
|
|
6043
|
-
throw new PublisherError("INVALID_REQUEST", `no pending question "${id}"; pending: ${
|
|
6044
|
-
details: { questions:
|
|
6137
|
+
throw new PublisherError("INVALID_REQUEST", `no pending question "${id}"; pending: ${read2.questions.map((q) => q.id).join(", ")}`, {
|
|
6138
|
+
details: { questions: read2.questions }
|
|
6045
6139
|
});
|
|
6046
6140
|
}
|
|
6047
6141
|
const values = question.options.map((o) => o.value);
|
|
6048
6142
|
if (!values.includes(value)) {
|
|
6049
|
-
throw new PublisherError("INVALID_REQUEST", `"${value}" is not one of ${values.join(", ")} for "${id}"`, { details: { questions:
|
|
6143
|
+
throw new PublisherError("INVALID_REQUEST", `"${value}" is not one of ${values.join(", ")} for "${id}"`, { details: { questions: read2.questions } });
|
|
6050
6144
|
}
|
|
6051
6145
|
}
|
|
6052
|
-
return shown(at.portal, await p.answerPreparation({ portal: at.portal, repoId, id:
|
|
6146
|
+
return shown(at.portal, await p.answerPreparation({ portal: at.portal, repoId, id: read2.preparation.id, revision: read2.preparation.revision, answers }));
|
|
6053
6147
|
}
|
|
6054
6148
|
function parseSpecs(specs) {
|
|
6055
6149
|
if (specs.length === 0) throw new PublisherError("INVALID_REQUEST", "answer takes one or more question=value");
|
|
@@ -6119,7 +6213,8 @@ Usage:
|
|
|
6119
6213
|
publisher fetch [dir]
|
|
6120
6214
|
publisher checkout <dir> --repo <id> [--branch <name>] create a NEW editable workspace (never overlays a folder)
|
|
6121
6215
|
publisher workspace-status [dir] offline mapping + where the workspace stands (real three-way check)
|
|
6122
|
-
publisher sync [dir] [--message <text>]
|
|
6216
|
+
publisher sync [dir] [--message <text>] [--name <app>] THE way to publish: a folder linked to nothing first gets its
|
|
6217
|
+
application and repository (what init does); then it records the
|
|
6123
6218
|
changes (never .env, keys or node_modules), brings in what
|
|
6124
6219
|
was published meanwhile (fast-forward or a clean merge,
|
|
6125
6220
|
never a rewrite) and publishes. Stops on a conflict with
|
|
@@ -6212,6 +6307,31 @@ async function main(argv) {
|
|
|
6212
6307
|
const token = str2(flags["token"]);
|
|
6213
6308
|
return { endpoint, p: createPublisher({ endpoint, endpointSource: source, ...token ? { token } : {}, clientName: "git-publisher-cli", clientVersion: CLIENT_VERSION }) };
|
|
6214
6309
|
};
|
|
6310
|
+
const linkFolder = async (p, endpoint, dir, attach) => {
|
|
6311
|
+
let onboarding = null;
|
|
6312
|
+
if (!await isGitRepo(dir) || !await headCommit(dir)) {
|
|
6313
|
+
onboarding = await onboardFolder({ dir, ...optStr(flags, "message") });
|
|
6314
|
+
log(`prepared ${onboarding.committed.length} files (${onboarding.excluded.length} excluded)`);
|
|
6315
|
+
}
|
|
6316
|
+
let repoId = attach.repoId ?? null;
|
|
6317
|
+
let appId = null;
|
|
6318
|
+
if (repoId === null) {
|
|
6319
|
+
const made = await repositoryForInit(p, endpoint, str2(flags["name"]) ?? dir.split("/").pop() ?? "");
|
|
6320
|
+
repoId = made.repoId;
|
|
6321
|
+
appId = made.appId;
|
|
6322
|
+
} else {
|
|
6323
|
+
await p.getRepository(repoId);
|
|
6324
|
+
}
|
|
6325
|
+
await p.associate(dir, repoId);
|
|
6326
|
+
const workspace = await p.adoptWorkspace(dir).catch(() => null);
|
|
6327
|
+
return {
|
|
6328
|
+
repoId,
|
|
6329
|
+
appId,
|
|
6330
|
+
created: attach.repoId === void 0,
|
|
6331
|
+
onboarding: onboarding ? { commit: onboarding.commit, committed: onboarding.committed.length, excluded: onboarding.excluded } : null,
|
|
6332
|
+
workspace: workspace === null ? null : { branch: workspace.branch, baseOid: workspace.baseOid, baseGeneration: workspace.baseGeneration }
|
|
6333
|
+
};
|
|
6334
|
+
};
|
|
6215
6335
|
if (flags["version"] === true) {
|
|
6216
6336
|
process.stdout.write(`${packageVersion()}
|
|
6217
6337
|
`);
|
|
@@ -6345,33 +6465,9 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6345
6465
|
case "init": {
|
|
6346
6466
|
const dir = dirArg(1);
|
|
6347
6467
|
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
|
-
});
|
|
6468
|
+
const repo = str2(flags["repo"]);
|
|
6469
|
+
const linked = await linkFolder(p, endpoint, dir, repo ? { repoId: repo } : {});
|
|
6470
|
+
out({ ok: true, ...linked, endpoint, dir });
|
|
6375
6471
|
return 0;
|
|
6376
6472
|
}
|
|
6377
6473
|
case "publish": {
|
|
@@ -6464,12 +6560,14 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6464
6560
|
const dir = dirArg(1);
|
|
6465
6561
|
const timeoutMs = followTimeoutMs(str2(flags["timeout"]));
|
|
6466
6562
|
const { p, endpoint } = await client(dir);
|
|
6563
|
+
const linked = await isGitRepo(dir) && await readAssociation(dir) !== null ? null : await linkFolder(p, endpoint, dir, {});
|
|
6467
6564
|
const r = await p.syncChanges({ directory: dir, markConflicts: flags["mark-conflicts"] === true, ...optStr(flags, "message") }, { onProgress: progress });
|
|
6468
6565
|
const o = r.published;
|
|
6469
6566
|
const followed = o !== null && o.state === "accepted" && flags["no-wait"] !== true ? await followPublication(p, endpoint, o, { onStage: emitStage, log, timeoutMs }) : null;
|
|
6470
6567
|
out({
|
|
6471
6568
|
ok: r.state === "published" || r.state === "up_to_date",
|
|
6472
6569
|
state: r.state,
|
|
6570
|
+
linked: linked === null ? null : { repoId: linked.repoId, appId: linked.appId, created: linked.created },
|
|
6473
6571
|
recorded: r.recorded,
|
|
6474
6572
|
merged: r.reconciled.applied,
|
|
6475
6573
|
conflicts: r.reconciled.before.conflicts,
|