@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.7.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
- 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,
@@ -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 (!isRecord(run)) return { malformed: "a run that is not an object" };
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 (!isRecord(entry) || entry["stage"] !== stage) return { malformed: `stage ${index + 1} is not ${stage}` };
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 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) {
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 (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 };
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 (!isRecord(body) || !Array.isArray(body["runs"])) return { detail: "an answer with no list of runs" };
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 isRecord(value) {
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 = isRecord2(answer.body) ? answer.body["error"] : void 0;
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 (!isRecord2(body)) return { detail: "an answer that is not a preparation" };
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 read = [];
4851
+ const read2 = [];
4781
4852
  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) {
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 (!isRecord2(o) || typeof o["value"] !== "string" || typeof o["label"] !== "string") return { detail: "a question with an unreadable option" };
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
- read.push({ id: q["id"], text: q["text"], options });
4861
+ read2.push({ id: q["id"], text: q["text"], options });
4791
4862
  }
4792
4863
  let pub = null;
4793
- if (isRecord2(publication) && typeof publication["status"] === "string" && typeof publication["message"] === "string") {
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: read, publication: pub };
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 isRecord2(value) {
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 post = (path, fields) => postForm(base, path, fields, doFetch);
5741
- const started = await post("/v1/auth/device", { client_id: opts.clientId ?? "gitpub-cli" });
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 post("/v1/auth/token", { grant_type: DEVICE_CODE_GRANT, device_code: deviceCode });
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 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})`);
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 = read.questions.find((q) => q.id === id);
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: ${read.questions.map((q) => q.id).join(", ")}`, {
6044
- details: { questions: read.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: read.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: read.preparation.id, revision: read.preparation.revision, answers }));
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>] THE way to save and publish a change: records the folder's
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 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
- });
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,
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.0",
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",