@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.7.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
- Run in the app folder (the folder the user is working in):
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
- ```bash
62
- publisher info --json # exit 1 with LOCAL_STATE "not associated" means: not published yet
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 update — and the first publish after `init`
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
@@ -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()` claimed otherwise while `control` was already
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 (!isRecord(run)) return { malformed: "a run that is not an object" };
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 (!isRecord(entry) || entry["stage"] !== stage) return { malformed: `stage ${index + 1} is not ${stage}` };
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 read = readStages(run.run);
4669
- if (read === null) return { kind: "unavailable", reason: "no-stages" };
4670
- if ("malformed" in read) return { kind: "malformed", detail: read.malformed };
4671
- last = read.stages;
4672
- for (const entry of read.stages) {
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 (isRecord(run.run) && run.run["status"] === "awaiting-approval") return { kind: "waiting", stages: read.stages };
4678
- const end = ending(read.stages);
4679
- if (end !== null) return { kind: "settled", ending: end, failedAt: read.stages.find((s) => s.state === "failed")?.word ?? null, stages: read.stages };
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 (!isRecord(body) || !Array.isArray(body["runs"])) return { detail: "an answer with no list of runs" };
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 isRecord(value) {
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 = isRecord2(answer.body) ? answer.body["error"] : void 0;
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 (!isRecord2(body)) return { detail: "an answer that is not a preparation" };
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 read = [];
4853
+ const read2 = [];
4781
4854
  for (const q of questions) {
4782
- if (!isRecord2(q) || typeof q["id"] !== "string" || typeof q["text"] !== "string" || !Array.isArray(q["options"]) || q["options"].length === 0) {
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 (!isRecord2(o) || typeof o["value"] !== "string" || typeof o["label"] !== "string") return { detail: "a question with an unreadable option" };
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
- read.push({ id: q["id"], text: q["text"], options });
4863
+ read2.push({ id: q["id"], text: q["text"], options });
4791
4864
  }
4792
4865
  let pub = null;
4793
- if (isRecord2(publication) && typeof publication["status"] === "string" && typeof publication["message"] === "string") {
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: read, publication: pub };
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 isRecord2(value) {
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 post = (path, fields) => postForm(base, path, fields, doFetch);
5741
- const started = await post("/v1/auth/device", { client_id: opts.clientId ?? "gitpub-cli" });
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 post("/v1/auth/token", { grant_type: DEVICE_CODE_GRANT, device_code: deviceCode });
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 read = shown(at.portal, await p.readPreparation({ portal: at.portal, repoId }));
6038
- if (read.kind !== "view") return read;
6039
- if (read.questions.length === 0) throw new PublisherError("LOCAL_STATE", `nothing is waiting for an answer (the preparation is ${read.preparation.status})`);
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 = read.questions.find((q) => q.id === id);
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: ${read.questions.map((q) => q.id).join(", ")}`, {
6044
- details: { questions: read.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: read.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: read.preparation.id, revision: read.preparation.revision, answers }));
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>] THE way to save and publish a change: records the folder's
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 existing = await isGitRepo(dir);
6349
- let onboarding = null;
6350
- if (!existing || !await headCommit(dir)) {
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 what this was before it recorded and published, and is still accepted, so a
6461
- * `sync --apply` written down anywhere does the whole round trip rather than failing.
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnovate/shadow-publisher",
3
- "version": "0.7.0",
3
+ "version": "0.8.3",
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",