@minnovate/shadow-publisher 0.3.0 → 0.6.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.
- package/README.md +11 -4
- package/dist/claude/.claude-plugin/marketplace.json +12 -0
- package/dist/claude/plugin/.claude-plugin/plugin.json +8 -0
- package/dist/claude/plugin/README.md +16 -0
- package/dist/claude/plugin/skills/publish-app/SKILL.md +156 -0
- package/dist/claude/plugin/skills/sync-app/SKILL.md +72 -0
- package/dist/publisher.cjs +728 -169
- package/package.json +5 -4
package/dist/publisher.cjs
CHANGED
|
@@ -28,7 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
));
|
|
29
29
|
|
|
30
30
|
// src/main.ts
|
|
31
|
-
var
|
|
31
|
+
var import_node_path8 = require("node:path");
|
|
32
32
|
|
|
33
33
|
// ../protocol/src/versions.ts
|
|
34
34
|
var PROTOCOL_VERSION = 1;
|
|
@@ -44,6 +44,7 @@ var PUBLICATION_ID_RE = new RegExp(`^pub_${ULID_BODY}$`);
|
|
|
44
44
|
var CREATION_ID_RE = new RegExp(`^${ULID_BODY}$`);
|
|
45
45
|
var TRANSACTION_ID_RE = new RegExp(`^(pub|cmp)_${ULID_BODY}$`);
|
|
46
46
|
var PRINCIPAL_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._@-]{0,127}$/;
|
|
47
|
+
var ORG_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
47
48
|
var SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
48
49
|
function ulid(now = Date.now()) {
|
|
49
50
|
let time3 = now;
|
|
@@ -92,6 +93,10 @@ var ERROR_CODES = [
|
|
|
92
93
|
"UNAUTHORIZED",
|
|
93
94
|
"FORBIDDEN",
|
|
94
95
|
"REF_FORBIDDEN",
|
|
96
|
+
// The credential names no organisation, so there is nothing to publish a repository FOR. Its own
|
|
97
|
+
// code rather than FORBIDDEN: a caller can act on it (sign in again, choosing an organisation),
|
|
98
|
+
// and a test that could not tell it from "you may not" would prove nothing about the check.
|
|
99
|
+
"ORGANISATION_REQUIRED",
|
|
95
100
|
// repositories
|
|
96
101
|
"REPOSITORY_NOT_FOUND",
|
|
97
102
|
"REPOSITORY_CONFLICT",
|
|
@@ -133,6 +138,7 @@ var HTTP_STATUS_FOR_CODE = {
|
|
|
133
138
|
UNAUTHORIZED: 401,
|
|
134
139
|
FORBIDDEN: 403,
|
|
135
140
|
REF_FORBIDDEN: 403,
|
|
141
|
+
ORGANISATION_REQUIRED: 403,
|
|
136
142
|
REPOSITORY_NOT_FOUND: 404,
|
|
137
143
|
REPOSITORY_CONFLICT: 409,
|
|
138
144
|
REPOSITORY_ARCHIVED: 409,
|
|
@@ -2585,6 +2591,7 @@ var PublicationId = string2().check(_regex(PUBLICATION_ID_RE));
|
|
|
2585
2591
|
var TransactionId = string2().check(_regex(TRANSACTION_ID_RE));
|
|
2586
2592
|
var CreationId = string2().check(_regex(CREATION_ID_RE));
|
|
2587
2593
|
var PrincipalId = string2().check(_regex(PRINCIPAL_ID_RE));
|
|
2594
|
+
var OrgId = string2().check(_regex(ORG_ID_RE));
|
|
2588
2595
|
var Sha256Hex = string2().check(_regex(SHA256_HEX_RE));
|
|
2589
2596
|
var Oid = string2().check(_regex(/^[0-9a-f]{40}$|^[0-9a-f]{64}$/));
|
|
2590
2597
|
var IsoDate = iso_exports.datetime({ offset: true });
|
|
@@ -2666,7 +2673,19 @@ var MetaRecord = object({
|
|
|
2666
2673
|
objectFormat: ObjectFormat,
|
|
2667
2674
|
createdAt: IsoDate,
|
|
2668
2675
|
createdBy: PrincipalId,
|
|
2669
|
-
creationId: CreationId
|
|
2676
|
+
creationId: CreationId,
|
|
2677
|
+
/**
|
|
2678
|
+
* The organisation that published this repository, and what `repoId` is derived from: "that
|
|
2679
|
+
* organisation publishes a repository" is the product's sentence.
|
|
2680
|
+
*
|
|
2681
|
+
* `null` is what a record written BEFORE repositories were org-scoped reads as, and every such
|
|
2682
|
+
* record is still readable, cloneable and publishable — `tests/compat/fixtures/schema-v1` is one,
|
|
2683
|
+
* and the promise that it stays readable is why this field is not required. What `null` must
|
|
2684
|
+
* never do is default: the worker refuses to ANNOUNCE a publication it cannot attribute
|
|
2685
|
+
* (`packages/worker/src/publish.ts`), because a consumer that guessed would create an
|
|
2686
|
+
* application in an organisation nobody chose. Every repository created since carries one.
|
|
2687
|
+
*/
|
|
2688
|
+
org: _default(nullable(OrgId), null)
|
|
2670
2689
|
});
|
|
2671
2690
|
var GrantPattern = string2().check(
|
|
2672
2691
|
_minLength(1),
|
|
@@ -3566,12 +3585,12 @@ async function resolveRevs(dir, revs, opts = {}) {
|
|
|
3566
3585
|
const out = revs.map(
|
|
3567
3586
|
(r) => r.startsWith("-") ? null : r.includes("\0") ? new PublisherError("INVALID_REQUEST", "NUL in git argument") : null
|
|
3568
3587
|
);
|
|
3569
|
-
const
|
|
3570
|
-
if (
|
|
3571
|
-
const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input:
|
|
3588
|
+
const ask3 = revs.flatMap((r, i) => out[i] === null && !r.startsWith("-") ? [i] : []);
|
|
3589
|
+
if (ask3.length === 0) return out;
|
|
3590
|
+
const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input: ask3.map((i) => revs[i]).join("\0") + "\0", ...opts });
|
|
3572
3591
|
const records = stdout.toString("utf8").split("\0");
|
|
3573
3592
|
const misses = [];
|
|
3574
|
-
for (const [j, i] of
|
|
3593
|
+
for (const [j, i] of ask3.entries()) {
|
|
3575
3594
|
const record2 = records[j] ?? "";
|
|
3576
3595
|
if (record2 && !/ (missing|ambiguous)$/.test(record2)) out[i] = record2;
|
|
3577
3596
|
else if (!revs[i].includes("^{")) misses.push(i);
|
|
@@ -3971,6 +3990,47 @@ async function noteAcceptedPublication(directory, outcome, now = /* @__PURE__ */
|
|
|
3971
3990
|
if (!oid) return;
|
|
3972
3991
|
await advanceWorkspaceBase(directory, { oid, generation: outcome.resultGeneration }, now);
|
|
3973
3992
|
}
|
|
3993
|
+
async function adoptWorkspace(client, directory, call = {}, now = /* @__PURE__ */ new Date()) {
|
|
3994
|
+
const dir = (0, import_node_path3.resolve)(directory);
|
|
3995
|
+
const existing = await getWorkspaceMapping(dir);
|
|
3996
|
+
if (existing) return existing;
|
|
3997
|
+
const section = await getConfigSection(dir, "publisher").catch(() => null);
|
|
3998
|
+
const a = section ? associationFrom(section) : null;
|
|
3999
|
+
if (!a) throw new PublisherError("LOCAL_STATE", "this folder is not associated with a published repository; run init first");
|
|
4000
|
+
const branch = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
|
|
4001
|
+
if (!branch) throw new PublisherError("LOCAL_STATE", "HEAD is detached; check out the branch this folder publishes, then run sync again");
|
|
4002
|
+
const repo = await client.getRepository(a.repoId, call);
|
|
4003
|
+
const remoteOid = repo.refs[`refs/heads/${branch}`] ?? null;
|
|
4004
|
+
const tip = await revParse(dir, `refs/heads/${branch}`);
|
|
4005
|
+
let baseOid = null;
|
|
4006
|
+
let baseGeneration = 0;
|
|
4007
|
+
if (remoteOid) {
|
|
4008
|
+
if (!await objectExists(dir, remoteOid)) await client.fetch({ repoDir: dir, repoId: a.repoId, remote: a.remote }, call);
|
|
4009
|
+
const haveRemote = await objectExists(dir, remoteOid);
|
|
4010
|
+
if (!tip) {
|
|
4011
|
+
baseOid = haveRemote ? remoteOid : null;
|
|
4012
|
+
baseGeneration = haveRemote ? repo.generation : 0;
|
|
4013
|
+
} else if (haveRemote && (remoteOid === tip || await isAncestor(dir, remoteOid, tip))) {
|
|
4014
|
+
baseOid = remoteOid;
|
|
4015
|
+
baseGeneration = repo.generation;
|
|
4016
|
+
} else if (haveRemote) {
|
|
4017
|
+
const r = await runGit(["merge-base", tip, remoteOid], { cwd: dir, allowFailure: true });
|
|
4018
|
+
const shared = r.code === 0 ? r.stdout.toString("utf8").trim() : "";
|
|
4019
|
+
baseOid = shared === "" ? null : shared;
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
await writeMapping(dir, {
|
|
4023
|
+
endpoint: a.endpoint,
|
|
4024
|
+
repoId: a.repoId,
|
|
4025
|
+
remote: a.remote,
|
|
4026
|
+
branch,
|
|
4027
|
+
baseOid,
|
|
4028
|
+
baseGeneration,
|
|
4029
|
+
checkedOutAt: now.toISOString(),
|
|
4030
|
+
baseAdvancedAt: now.toISOString()
|
|
4031
|
+
});
|
|
4032
|
+
return await getWorkspaceMapping(dir);
|
|
4033
|
+
}
|
|
3974
4034
|
async function isEmptyDir(p) {
|
|
3975
4035
|
try {
|
|
3976
4036
|
return (await (0, import_promises4.readdir)(p)).length === 0;
|
|
@@ -4065,7 +4125,16 @@ async function inspectWorkspace(client, directory, call = {}) {
|
|
|
4065
4125
|
const dir = (0, import_node_path3.resolve)(directory);
|
|
4066
4126
|
const mapping = await getWorkspaceMapping(dir);
|
|
4067
4127
|
const pending = await listPending(dir).catch(() => []);
|
|
4068
|
-
if (!mapping)
|
|
4128
|
+
if (!mapping)
|
|
4129
|
+
return {
|
|
4130
|
+
state: "unassociated",
|
|
4131
|
+
mapping: null,
|
|
4132
|
+
local: null,
|
|
4133
|
+
remote: null,
|
|
4134
|
+
pending,
|
|
4135
|
+
conflicts: [],
|
|
4136
|
+
hint: "not a workspace yet; run sync here (an associated folder adopts itself), or checkout to create one"
|
|
4137
|
+
};
|
|
4069
4138
|
const branchRef = `refs/heads/${mapping.branch}`;
|
|
4070
4139
|
const tip = await revParse(dir, branchRef);
|
|
4071
4140
|
const head = await revParse(dir, "HEAD");
|
|
@@ -4088,27 +4157,31 @@ async function inspectWorkspace(client, directory, call = {}) {
|
|
|
4088
4157
|
);
|
|
4089
4158
|
}
|
|
4090
4159
|
if (!remoteOid) {
|
|
4091
|
-
if (!tip) return done(hasLocalWork ? "local_changes" : "up_to_date", hasLocalWork ? `${work};
|
|
4092
|
-
return done("local_changes", "the server has no such branch yet;
|
|
4160
|
+
if (!tip) return done(hasLocalWork ? "local_changes" : "up_to_date", hasLocalWork ? `${work}; publisher sync records and publishes them` : "empty on both sides");
|
|
4161
|
+
return done("local_changes", "the server has no such branch yet; publisher sync creates it");
|
|
4093
4162
|
}
|
|
4094
4163
|
if (!tip) return done("remote_ahead", hasLocalWork ? `local branch is unborn and there are ${work}; move them aside, then sync` : "local branch is unborn; sync to fast-forward");
|
|
4095
4164
|
if (!await objectExists(dir, remoteOid)) {
|
|
4096
4165
|
await client.fetch({ repoDir: dir, repoId: mapping.repoId, remote: mapping.remote }, call);
|
|
4097
4166
|
if (!await objectExists(dir, remoteOid)) return done("remote_ahead", "server tip could not be fetched; retry");
|
|
4098
4167
|
}
|
|
4099
|
-
if (remoteOid === tip)
|
|
4168
|
+
if (remoteOid === tip)
|
|
4169
|
+
return done(hasLocalWork ? "local_changes" : "up_to_date", hasLocalWork ? `${work}; publisher sync records and publishes them` : "in sync with the server");
|
|
4100
4170
|
if (await isAncestor(dir, remoteOid, tip))
|
|
4101
|
-
return done(
|
|
4171
|
+
return done(
|
|
4172
|
+
"local_changes",
|
|
4173
|
+
hasLocalWork ? `local commits and ${work}; publisher sync records and publishes them` : "local commits not yet published; publisher sync publishes them"
|
|
4174
|
+
);
|
|
4102
4175
|
if (await isAncestor(dir, tip, remoteOid))
|
|
4103
4176
|
return done(
|
|
4104
4177
|
"remote_ahead",
|
|
4105
|
-
hasLocalWork ? `server moved ahead and there are ${work};
|
|
4178
|
+
hasLocalWork ? `server moved ahead and there are ${work}; publisher sync records them, brings the server's changes in and publishes` : "server moved ahead; publisher sync brings it in"
|
|
4106
4179
|
);
|
|
4107
4180
|
const merge2 = await mergeRemote({ repoDir: dir, ours: branchRef, theirs: remoteOid, apply: false });
|
|
4108
4181
|
if (merge2.ok)
|
|
4109
4182
|
return done(
|
|
4110
4183
|
"diverged_clean_merge",
|
|
4111
|
-
hasLocalWork ? `both sides changed, merge is clean;
|
|
4184
|
+
hasLocalWork ? `both sides changed, merge is clean; publisher sync records the ${work}, merges and publishes` : "both sides changed, merge is clean; publisher sync merges and publishes"
|
|
4112
4185
|
);
|
|
4113
4186
|
return done(
|
|
4114
4187
|
"conflict",
|
|
@@ -4118,6 +4191,7 @@ async function inspectWorkspace(client, directory, call = {}) {
|
|
|
4118
4191
|
}
|
|
4119
4192
|
async function syncWorkspace(client, opts, call = {}, now = /* @__PURE__ */ new Date()) {
|
|
4120
4193
|
const dir = (0, import_node_path3.resolve)(opts.directory);
|
|
4194
|
+
await adoptWorkspace(client, dir, call, now);
|
|
4121
4195
|
const before = await inspectWorkspace(client, dir, call);
|
|
4122
4196
|
if (!before.mapping) throw new PublisherError("LOCAL_STATE", "not a workspace; run checkout");
|
|
4123
4197
|
const mapping = before.mapping;
|
|
@@ -4125,7 +4199,7 @@ async function syncWorkspace(client, opts, call = {}, now = /* @__PURE__ */ new
|
|
|
4125
4199
|
let applied = null;
|
|
4126
4200
|
if (before.state === "detached") throw new PublisherError("LOCAL_STATE", before.hint);
|
|
4127
4201
|
if (opts.apply && before.remote?.oid && (before.state === "remote_ahead" || before.state === "diverged_clean_merge")) {
|
|
4128
|
-
if (before.local?.dirty) throw new PublisherError("LOCAL_STATE", "the working tree has uncommitted changes;
|
|
4202
|
+
if (before.local?.dirty) throw new PublisherError("LOCAL_STATE", "the working tree has uncommitted changes; record them first (publisher sync does)");
|
|
4129
4203
|
if (!before.local?.tip) {
|
|
4130
4204
|
await runGit(["checkout", "-q", "-B", mapping.branch, before.remote.oid], { cwd: dir });
|
|
4131
4205
|
applied = "fast-forward";
|
|
@@ -4419,18 +4493,6 @@ async function updateEntry(url2, change) {
|
|
|
4419
4493
|
await writeCredentials(file);
|
|
4420
4494
|
}
|
|
4421
4495
|
|
|
4422
|
-
// ../client/src/fetch.ts
|
|
4423
|
-
var import_node_fs5 = require("node:fs");
|
|
4424
|
-
var import_promises7 = require("node:fs/promises");
|
|
4425
|
-
var import_node_os3 = require("node:os");
|
|
4426
|
-
var import_node_path5 = require("node:path");
|
|
4427
|
-
var import_promises8 = require("node:stream/promises");
|
|
4428
|
-
var import_node_stream2 = require("node:stream");
|
|
4429
|
-
var import_node_crypto4 = require("node:crypto");
|
|
4430
|
-
|
|
4431
|
-
// ../client/src/publish.ts
|
|
4432
|
-
var import_promises6 = require("node:fs/promises");
|
|
4433
|
-
|
|
4434
4496
|
// ../client/src/transport.ts
|
|
4435
4497
|
function assertCredentialEndpoint(endpoint) {
|
|
4436
4498
|
let url2;
|
|
@@ -4536,6 +4598,328 @@ function sleep(ms, signal) {
|
|
|
4536
4598
|
});
|
|
4537
4599
|
}
|
|
4538
4600
|
|
|
4601
|
+
// ../client/src/run-progress.ts
|
|
4602
|
+
var RUN_STAGES = ["received", "running", "deploying", "live"];
|
|
4603
|
+
var STAGE_WORDS = { received: "received", running: "checked", deploying: "going live", live: "live" };
|
|
4604
|
+
var STATES = ["pending", "in-progress", "done", "failed", "skipped"];
|
|
4605
|
+
function readStages(run) {
|
|
4606
|
+
if (!isRecord(run)) return { malformed: "a run that is not an object" };
|
|
4607
|
+
if (!Object.hasOwn(run, "stages")) return null;
|
|
4608
|
+
const stages = run["stages"];
|
|
4609
|
+
if (!Array.isArray(stages) || stages.length !== RUN_STAGES.length) return { malformed: "stages that are not exactly four" };
|
|
4610
|
+
const out = [];
|
|
4611
|
+
for (const [index, entry] of stages.entries()) {
|
|
4612
|
+
const stage = RUN_STAGES[index];
|
|
4613
|
+
if (!isRecord(entry) || entry["stage"] !== stage) return { malformed: `stage ${index + 1} is not ${stage}` };
|
|
4614
|
+
const state = entry["state"];
|
|
4615
|
+
if (!STATES.includes(state)) return { malformed: `stage ${stage} has an unknown state` };
|
|
4616
|
+
const at = entry["at"];
|
|
4617
|
+
if (typeof at !== "string" && at !== null) return { malformed: `stage ${stage} has an unreadable time` };
|
|
4618
|
+
out.push({ stage, word: STAGE_WORDS[stage], state, at });
|
|
4619
|
+
}
|
|
4620
|
+
return { stages: out };
|
|
4621
|
+
}
|
|
4622
|
+
function ending(stages) {
|
|
4623
|
+
if (stages.some((s) => s.state === "pending" || s.state === "in-progress")) return null;
|
|
4624
|
+
if (stages.some((s) => s.state === "failed")) return "failed";
|
|
4625
|
+
if (stages[3].state === "done") return "live";
|
|
4626
|
+
if (stages[1].state === "done" && stages[2].state === "skipped") return "not-deployed";
|
|
4627
|
+
return "stopped";
|
|
4628
|
+
}
|
|
4629
|
+
var POLL_INTERVAL_MS = 5e3;
|
|
4630
|
+
var FOLLOW_TIMEOUT_MS = 15 * 6e4;
|
|
4631
|
+
async function followRun(input, opts) {
|
|
4632
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
4633
|
+
const sleep2 = opts.sleep ?? sleep;
|
|
4634
|
+
const now = opts.now ?? Date.now;
|
|
4635
|
+
const interval = opts.intervalMs ?? POLL_INTERVAL_MS;
|
|
4636
|
+
const deadline = now() + (opts.timeoutMs ?? FOLLOW_TIMEOUT_MS);
|
|
4637
|
+
const query = new URLSearchParams({ publication: input.publicationId, ref: input.ref, commit: input.commit });
|
|
4638
|
+
const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/repositories/${encodeURIComponent(input.repoId)}/runs?${query}`;
|
|
4639
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4640
|
+
let last = null;
|
|
4641
|
+
let lastError = null;
|
|
4642
|
+
let refreshed = false;
|
|
4643
|
+
for (; ; ) {
|
|
4644
|
+
const answer = await ask(fetchImpl, url2, await opts.token());
|
|
4645
|
+
if (answer.status === 401 && !refreshed && opts.refresh !== void 0) {
|
|
4646
|
+
refreshed = true;
|
|
4647
|
+
if (await opts.refresh()) continue;
|
|
4648
|
+
}
|
|
4649
|
+
if (answer.status === 401 && isRecord(answer.body) && typeof answer.body["error"] === "string") return { kind: "unauthorized" };
|
|
4650
|
+
if (answer.status === 404 || answer.status === 401) return { kind: "unavailable", reason: "no-route" };
|
|
4651
|
+
if (answer.status === 403) return { kind: "forbidden" };
|
|
4652
|
+
if (answer.status === 200) {
|
|
4653
|
+
const run = soleRun(answer.body);
|
|
4654
|
+
if ("detail" in run) return { kind: "malformed", detail: run.detail };
|
|
4655
|
+
if (run.run !== null) {
|
|
4656
|
+
const read = readStages(run.run);
|
|
4657
|
+
if (read === null) return { kind: "unavailable", reason: "no-stages" };
|
|
4658
|
+
if ("malformed" in read) return { kind: "malformed", detail: read.malformed };
|
|
4659
|
+
last = read.stages;
|
|
4660
|
+
for (const entry of read.stages) {
|
|
4661
|
+
if (entry.state === "pending" || seen.get(entry.stage) === entry.state) continue;
|
|
4662
|
+
seen.set(entry.stage, entry.state);
|
|
4663
|
+
opts.onStage?.(entry);
|
|
4664
|
+
}
|
|
4665
|
+
const end = ending(read.stages);
|
|
4666
|
+
if (end !== null) return { kind: "settled", ending: end, failedAt: read.stages.find((s) => s.state === "failed")?.word ?? null, stages: read.stages };
|
|
4667
|
+
}
|
|
4668
|
+
lastError = null;
|
|
4669
|
+
} else {
|
|
4670
|
+
lastError = answer.status === 0 ? "the portal could not be reached" : `the portal answered ${answer.status}`;
|
|
4671
|
+
}
|
|
4672
|
+
if (now() + interval > deadline) return { kind: "timed-out", stages: last, lastError };
|
|
4673
|
+
await sleep2(interval);
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
function soleRun(body) {
|
|
4677
|
+
if (!isRecord(body) || !Array.isArray(body["runs"])) return { detail: "an answer with no list of runs" };
|
|
4678
|
+
const runs = body["runs"];
|
|
4679
|
+
if (runs.length > 1) return { detail: "more than the one run this publication started" };
|
|
4680
|
+
return { run: runs[0] ?? null };
|
|
4681
|
+
}
|
|
4682
|
+
async function ask(fetchImpl, url2, token) {
|
|
4683
|
+
try {
|
|
4684
|
+
const res = await fetchImpl(url2, { headers: { authorization: `Bearer ${token}`, accept: "application/json" }, signal: AbortSignal.timeout(3e4) });
|
|
4685
|
+
const text = await res.text();
|
|
4686
|
+
let body = null;
|
|
4687
|
+
try {
|
|
4688
|
+
body = text ? JSON.parse(text) : null;
|
|
4689
|
+
} catch {
|
|
4690
|
+
if (res.status === 200) return { status: 200, body: void 0 };
|
|
4691
|
+
}
|
|
4692
|
+
return { status: res.status, body };
|
|
4693
|
+
} catch {
|
|
4694
|
+
return { status: 0, body: null };
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
function portalForEndpoint(endpoint) {
|
|
4698
|
+
let url2;
|
|
4699
|
+
try {
|
|
4700
|
+
url2 = new URL(endpoint);
|
|
4701
|
+
} catch {
|
|
4702
|
+
return null;
|
|
4703
|
+
}
|
|
4704
|
+
if (url2.protocol !== "https:" || url2.pathname.replace(/\/+$/, "") !== "/publish") return null;
|
|
4705
|
+
return url2.origin;
|
|
4706
|
+
}
|
|
4707
|
+
async function confirmPortal(portal, endpoint, fetchImpl = fetch) {
|
|
4708
|
+
try {
|
|
4709
|
+
const res = await fetchImpl(`${portal}/api/v1/config`, { signal: AbortSignal.timeout(1e4) });
|
|
4710
|
+
if (!res.ok) return false;
|
|
4711
|
+
const body = await res.json();
|
|
4712
|
+
const named = body?.publishingEndpoint;
|
|
4713
|
+
const bare = (u) => u.replace(/\/+$/, "");
|
|
4714
|
+
return typeof named === "string" && bare(named) === bare(endpoint);
|
|
4715
|
+
} catch {
|
|
4716
|
+
return false;
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
function isRecord(value) {
|
|
4720
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4721
|
+
}
|
|
4722
|
+
|
|
4723
|
+
// ../client/src/onboard.ts
|
|
4724
|
+
var import_promises6 = require("node:fs/promises");
|
|
4725
|
+
var import_node_path5 = require("node:path");
|
|
4726
|
+
var DEFAULT_EXCLUDED_DIRS = [
|
|
4727
|
+
".git",
|
|
4728
|
+
"node_modules",
|
|
4729
|
+
".venv",
|
|
4730
|
+
"venv",
|
|
4731
|
+
"__pycache__",
|
|
4732
|
+
".cache",
|
|
4733
|
+
".next",
|
|
4734
|
+
".nuxt",
|
|
4735
|
+
".turbo",
|
|
4736
|
+
".parcel-cache",
|
|
4737
|
+
".aws",
|
|
4738
|
+
".ssh",
|
|
4739
|
+
".gnupg",
|
|
4740
|
+
".idea",
|
|
4741
|
+
".terraform",
|
|
4742
|
+
".pytest_cache",
|
|
4743
|
+
".mypy_cache",
|
|
4744
|
+
"coverage",
|
|
4745
|
+
".DS_Store"
|
|
4746
|
+
];
|
|
4747
|
+
var DEFAULT_EXCLUDED_FILES = [
|
|
4748
|
+
/^\.env(\..*)?$/,
|
|
4749
|
+
/\.(pem|key|p12|pfx|jks|keystore)$/i,
|
|
4750
|
+
/^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/,
|
|
4751
|
+
/^\.npmrc$/,
|
|
4752
|
+
/^\.pypirc$/,
|
|
4753
|
+
/^\.netrc$/,
|
|
4754
|
+
/\.(log|tmp|swp)$/i,
|
|
4755
|
+
/^\.DS_Store$/,
|
|
4756
|
+
/^Thumbs\.db$/i,
|
|
4757
|
+
/^credentials(\.json)?$/i,
|
|
4758
|
+
/^service-account.*\.json$/i,
|
|
4759
|
+
/^\.git(modules|attributes)?$/
|
|
4760
|
+
];
|
|
4761
|
+
async function onboardFolder(opts) {
|
|
4762
|
+
const dir = opts.dir;
|
|
4763
|
+
const policy = opts.policy ?? {};
|
|
4764
|
+
const maxBytes = policy.maxFileBytes ?? 50 * 1024 * 1024;
|
|
4765
|
+
let initialized = false;
|
|
4766
|
+
if (await isGitRepo(dir)) {
|
|
4767
|
+
const head = await headCommit(dir);
|
|
4768
|
+
if (head && !policy.allowExistingHistory) {
|
|
4769
|
+
throw new PublisherError("LOCAL_STATE", "folder is already a Git repository with history; commit your changes and use publish instead of init");
|
|
4770
|
+
}
|
|
4771
|
+
} else {
|
|
4772
|
+
await initWorkRepo(dir, opts.branch ?? "main");
|
|
4773
|
+
initialized = true;
|
|
4774
|
+
}
|
|
4775
|
+
const excluded = [];
|
|
4776
|
+
const candidates = [];
|
|
4777
|
+
const extra = policy.exclude ?? [];
|
|
4778
|
+
const walk = async (d) => {
|
|
4779
|
+
for (const e of await (0, import_promises6.readdir)(d, { withFileTypes: true })) {
|
|
4780
|
+
if (opts.signal?.aborted) throw new PublisherError("CANCELLED", "cancelled");
|
|
4781
|
+
const full = (0, import_node_path5.join)(d, e.name);
|
|
4782
|
+
const rel = (0, import_node_path5.relative)(dir, full).split(import_node_path5.sep).join("/");
|
|
4783
|
+
if (e.isSymbolicLink()) {
|
|
4784
|
+
excluded.push({ path: rel, reason: "symbolic link" });
|
|
4785
|
+
continue;
|
|
4786
|
+
}
|
|
4787
|
+
if (e.isDirectory()) {
|
|
4788
|
+
if (excludedDir(rel, e.name, extra)) {
|
|
4789
|
+
excluded.push({ path: rel + "/", reason: "excluded by policy" });
|
|
4790
|
+
continue;
|
|
4791
|
+
}
|
|
4792
|
+
await walk(full);
|
|
4793
|
+
continue;
|
|
4794
|
+
}
|
|
4795
|
+
if (!e.isFile()) continue;
|
|
4796
|
+
if (excludedFile(rel, e.name, extra)) {
|
|
4797
|
+
excluded.push({ path: rel, reason: "excluded by policy" });
|
|
4798
|
+
continue;
|
|
4799
|
+
}
|
|
4800
|
+
const st = await (0, import_promises6.stat)(full);
|
|
4801
|
+
if (st.size > maxBytes) {
|
|
4802
|
+
excluded.push({ path: rel, reason: `larger than ${maxBytes} bytes` });
|
|
4803
|
+
continue;
|
|
4804
|
+
}
|
|
4805
|
+
candidates.push(rel);
|
|
4806
|
+
}
|
|
4807
|
+
};
|
|
4808
|
+
await walk(dir);
|
|
4809
|
+
const ignored = await checkIgnore(dir, candidates);
|
|
4810
|
+
const selected = candidates.filter((p) => !ignored.has(p)).sort();
|
|
4811
|
+
for (const p of ignored) excluded.push({ path: p, reason: "ignored by .gitignore" });
|
|
4812
|
+
if (selected.length === 0) return { repoDir: dir, commit: null, committed: [], excluded, initialized };
|
|
4813
|
+
const commit = await commitPaths(dir, selected, {
|
|
4814
|
+
message: opts.message ?? "Initial publication",
|
|
4815
|
+
...opts.authorName ? { authorName: opts.authorName } : {},
|
|
4816
|
+
...opts.authorEmail ? { authorEmail: opts.authorEmail } : {},
|
|
4817
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
4818
|
+
});
|
|
4819
|
+
return { repoDir: dir, commit, committed: selected, excluded, initialized };
|
|
4820
|
+
}
|
|
4821
|
+
function excludedDir(rel, name, extra) {
|
|
4822
|
+
return DEFAULT_EXCLUDED_DIRS.includes(name) || extra.some((p) => matches(rel, name, p));
|
|
4823
|
+
}
|
|
4824
|
+
function excludedFile(rel, name, extra) {
|
|
4825
|
+
return DEFAULT_EXCLUDED_FILES.some((re) => re.test(name)) || extra.some((p) => matches(rel, name, p));
|
|
4826
|
+
}
|
|
4827
|
+
async function recordChanges(opts) {
|
|
4828
|
+
const dir = opts.dir;
|
|
4829
|
+
if (!await isGitRepo(dir)) throw new PublisherError("LOCAL_STATE", "this folder is not published yet; run publisher init first");
|
|
4830
|
+
const maxBytes = opts.policy?.maxFileBytes ?? 50 * 1024 * 1024;
|
|
4831
|
+
const extra = opts.policy?.exclude ?? [];
|
|
4832
|
+
const excluded = [];
|
|
4833
|
+
const selected = [];
|
|
4834
|
+
const entries = await statusPorcelain(dir);
|
|
4835
|
+
if (await mergeInProgress(dir)) {
|
|
4836
|
+
const marked = [];
|
|
4837
|
+
for (const { code, path } of entries) {
|
|
4838
|
+
if (!UNMERGED.has(code)) continue;
|
|
4839
|
+
const text = await (0, import_promises6.readFile)((0, import_node_path5.join)(dir, path), "utf8").catch(() => "");
|
|
4840
|
+
if (/^<{7} /m.test(text) && /^>{7} /m.test(text)) marked.push(path);
|
|
4841
|
+
}
|
|
4842
|
+
if (marked.length)
|
|
4843
|
+
throw new PublisherError(
|
|
4844
|
+
"LOCAL_STATE",
|
|
4845
|
+
`still marked as conflicting: ${marked.join(", ")}; keep one version (or both) in each, remove the <<<<<<< ======= >>>>>>> lines, then sync again`,
|
|
4846
|
+
{ details: { marked } }
|
|
4847
|
+
);
|
|
4848
|
+
}
|
|
4849
|
+
for (const { code, path } of entries) {
|
|
4850
|
+
if (code !== "??") {
|
|
4851
|
+
selected.push(path);
|
|
4852
|
+
continue;
|
|
4853
|
+
}
|
|
4854
|
+
const parts = path.split("/");
|
|
4855
|
+
const name = parts[parts.length - 1];
|
|
4856
|
+
const dirs = parts.slice(0, -1);
|
|
4857
|
+
const inExcludedDir = dirs.findIndex((d, i) => excludedDir(dirs.slice(0, i + 1).join("/"), d, extra));
|
|
4858
|
+
if (inExcludedDir >= 0) {
|
|
4859
|
+
excluded.push({ path: dirs.slice(0, inExcludedDir + 1).join("/") + "/", reason: "excluded by policy" });
|
|
4860
|
+
continue;
|
|
4861
|
+
}
|
|
4862
|
+
if (excludedFile(path, name, extra)) {
|
|
4863
|
+
excluded.push({ path, reason: "excluded by policy" });
|
|
4864
|
+
continue;
|
|
4865
|
+
}
|
|
4866
|
+
const st = await (0, import_promises6.lstat)((0, import_node_path5.join)(dir, path));
|
|
4867
|
+
if (st.isSymbolicLink()) {
|
|
4868
|
+
excluded.push({ path, reason: "symbolic link" });
|
|
4869
|
+
continue;
|
|
4870
|
+
}
|
|
4871
|
+
if (st.size > maxBytes) {
|
|
4872
|
+
excluded.push({ path, reason: `larger than ${maxBytes} bytes` });
|
|
4873
|
+
continue;
|
|
4874
|
+
}
|
|
4875
|
+
selected.push(path);
|
|
4876
|
+
}
|
|
4877
|
+
const unique = dedupe(excluded);
|
|
4878
|
+
if (selected.length === 0) return { repoDir: dir, commit: null, committed: [], excluded: unique, initialized: false };
|
|
4879
|
+
const commit = await commitPaths(dir, selected.sort(), {
|
|
4880
|
+
message: opts.message ?? "Update",
|
|
4881
|
+
...opts.authorName ? { authorName: opts.authorName } : {},
|
|
4882
|
+
...opts.authorEmail ? { authorEmail: opts.authorEmail } : {},
|
|
4883
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
4884
|
+
});
|
|
4885
|
+
return { repoDir: dir, commit, committed: selected, excluded: unique, initialized: false };
|
|
4886
|
+
}
|
|
4887
|
+
var UNMERGED = /* @__PURE__ */ new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
|
|
4888
|
+
async function mergeInProgress(dir) {
|
|
4889
|
+
return (await runGit(["rev-parse", "-q", "--verify", "MERGE_HEAD"], { cwd: dir, allowFailure: true })).code === 0;
|
|
4890
|
+
}
|
|
4891
|
+
function dedupe(excluded) {
|
|
4892
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4893
|
+
return excluded.filter((e) => seen.has(e.path) ? false : (seen.add(e.path), true));
|
|
4894
|
+
}
|
|
4895
|
+
function matches(rel, base, pattern) {
|
|
4896
|
+
if (pattern.includes("*")) {
|
|
4897
|
+
const re = new RegExp(
|
|
4898
|
+
"^" + pattern.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
|
|
4899
|
+
);
|
|
4900
|
+
return re.test(base) || re.test(rel);
|
|
4901
|
+
}
|
|
4902
|
+
return base === pattern || rel === pattern || rel.startsWith(pattern.replace(/\/$/, "") + "/");
|
|
4903
|
+
}
|
|
4904
|
+
async function checkIgnore(dir, paths) {
|
|
4905
|
+
if (paths.length === 0) return /* @__PURE__ */ new Set();
|
|
4906
|
+
const r = await runGit(["check-ignore", "--stdin", "-z"], { cwd: dir, input: paths.join("\0") + "\0", allowFailure: true });
|
|
4907
|
+
if (r.code !== 0) return /* @__PURE__ */ new Set();
|
|
4908
|
+
return new Set(r.stdout.toString("utf8").split("\0").filter(Boolean));
|
|
4909
|
+
}
|
|
4910
|
+
|
|
4911
|
+
// ../client/src/fetch.ts
|
|
4912
|
+
var import_node_fs5 = require("node:fs");
|
|
4913
|
+
var import_promises8 = require("node:fs/promises");
|
|
4914
|
+
var import_node_os3 = require("node:os");
|
|
4915
|
+
var import_node_path6 = require("node:path");
|
|
4916
|
+
var import_promises9 = require("node:stream/promises");
|
|
4917
|
+
var import_node_stream2 = require("node:stream");
|
|
4918
|
+
var import_node_crypto4 = require("node:crypto");
|
|
4919
|
+
|
|
4920
|
+
// ../client/src/publish.ts
|
|
4921
|
+
var import_promises7 = require("node:fs/promises");
|
|
4922
|
+
|
|
4539
4923
|
// ../client/src/upload.ts
|
|
4540
4924
|
var import_node_fs4 = require("node:fs");
|
|
4541
4925
|
var import_node_stream = require("node:stream");
|
|
@@ -4614,7 +4998,7 @@ async function publish(ctx, repoId, remote, opts, call) {
|
|
|
4614
4998
|
} catch (e) {
|
|
4615
4999
|
if (!(e instanceof PublisherError && e.code === "LOCAL_STATE")) throw e;
|
|
4616
5000
|
}
|
|
4617
|
-
if (remaining.length === 0) return toOutcome(outcome, true);
|
|
5001
|
+
if (remaining.length === 0) return toOutcome(outcome, true, pending2.refUpdates);
|
|
4618
5002
|
return publish(ctx, repoId, remote, opts, call);
|
|
4619
5003
|
}
|
|
4620
5004
|
}
|
|
@@ -4690,7 +5074,7 @@ async function runPublication(ctx, repoId, remote, repoDir, store2, pending, opt
|
|
|
4690
5074
|
}
|
|
4691
5075
|
if (opts.wait === false) {
|
|
4692
5076
|
summary = summary ?? await ctx.getStatus(repoId, pending.publicationId, call);
|
|
4693
|
-
return toOutcome(summary, resumed);
|
|
5077
|
+
return toOutcome(summary, resumed, pending.refUpdates);
|
|
4694
5078
|
}
|
|
4695
5079
|
progress({ phase: "validate", message: "waiting for validation" });
|
|
4696
5080
|
summary = await waitForOutcome(ctx, repoId, pending.publicationId, opts.waitTimeoutMs ?? 10 * 6e4, call);
|
|
@@ -4700,9 +5084,9 @@ async function runPublication(ctx, repoId, remote, repoDir, store2, pending, opt
|
|
|
4700
5084
|
progress({ phase: "done", message: `published generation ${summary.resultGeneration}` });
|
|
4701
5085
|
} else if (summary.state === "rejected") {
|
|
4702
5086
|
await store2.remove(pending.publicationId);
|
|
4703
|
-
if (pending.pack) await (0,
|
|
5087
|
+
if (pending.pack) await (0, import_promises7.rm)(pending.pack.path, { force: true });
|
|
4704
5088
|
}
|
|
4705
|
-
return toOutcome(summary, resumed);
|
|
5089
|
+
return toOutcome(summary, resumed, pending.refUpdates);
|
|
4706
5090
|
}
|
|
4707
5091
|
async function waitForOutcome(ctx, repoId, publicationId, timeoutMs, call) {
|
|
4708
5092
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -4749,7 +5133,7 @@ async function resolvePending(ctx, repoDir, remote, pending, call, store2) {
|
|
|
4749
5133
|
}
|
|
4750
5134
|
return summary;
|
|
4751
5135
|
}
|
|
4752
|
-
function toOutcome(s, resumed) {
|
|
5136
|
+
function toOutcome(s, resumed, updates) {
|
|
4753
5137
|
return {
|
|
4754
5138
|
publicationId: s.publicationId,
|
|
4755
5139
|
repoId: s.repoId,
|
|
@@ -4758,10 +5142,23 @@ function toOutcome(s, resumed) {
|
|
|
4758
5142
|
resultGeneration: s.resultGeneration,
|
|
4759
5143
|
refs: s.refs,
|
|
4760
5144
|
rejection: s.rejection,
|
|
5145
|
+
next: publishNext(s.rejection, updates[0]?.ref ?? null),
|
|
4761
5146
|
resumed,
|
|
4762
5147
|
summary: s
|
|
4763
5148
|
};
|
|
4764
5149
|
}
|
|
5150
|
+
var REF_MOVED = /* @__PURE__ */ new Set(["NON_FAST_FORWARD", "EXPECTED_OLD_MISMATCH", "STALE_GENERATION"]);
|
|
5151
|
+
function publishNext(rejection, published) {
|
|
5152
|
+
if (rejection === null || !REF_MOVED.has(rejection.code)) return null;
|
|
5153
|
+
const named = rejection.details?.["ref"];
|
|
5154
|
+
const ref = typeof named === "string" ? named : published;
|
|
5155
|
+
const name = ref?.replace(/^refs\/heads\//, "") ?? "main";
|
|
5156
|
+
return {
|
|
5157
|
+
action: "sync-and-publish",
|
|
5158
|
+
ask: `${name} moved since your last fetch: someone else published to it. Sync brings those changes in without rewriting yours, and publishes the result.`,
|
|
5159
|
+
command: `publisher sync`
|
|
5160
|
+
};
|
|
5161
|
+
}
|
|
4765
5162
|
async function defaultUpdate(repoDir) {
|
|
4766
5163
|
const branch = await currentBranch(repoDir);
|
|
4767
5164
|
if (!branch) throw new PublisherError("LOCAL_STATE", "detached HEAD: specify which ref to publish");
|
|
@@ -4807,15 +5204,15 @@ function trackingRef(remote, ref) {
|
|
|
4807
5204
|
// ../client/src/fetch.ts
|
|
4808
5205
|
async function installedManifest(gd) {
|
|
4809
5206
|
try {
|
|
4810
|
-
const parsed = JSON.parse(await (0,
|
|
5207
|
+
const parsed = JSON.parse(await (0, import_promises8.readFile)((0, import_node_path6.join)(gd, "publisher", "installed.json"), "utf8"));
|
|
4811
5208
|
return new Set(Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : []);
|
|
4812
5209
|
} catch {
|
|
4813
5210
|
return /* @__PURE__ */ new Set();
|
|
4814
5211
|
}
|
|
4815
5212
|
}
|
|
4816
5213
|
async function writeManifest(gd, installed) {
|
|
4817
|
-
await (0,
|
|
4818
|
-
await (0,
|
|
5214
|
+
await (0, import_promises8.mkdir)((0, import_node_path6.join)(gd, "publisher"), { recursive: true });
|
|
5215
|
+
await (0, import_promises8.writeFile)((0, import_node_path6.join)(gd, "publisher", "installed.json"), JSON.stringify(installed));
|
|
4819
5216
|
}
|
|
4820
5217
|
async function fetchRepository(ctx, repoId, opts, call) {
|
|
4821
5218
|
const remote = opts.remote ?? "publisher";
|
|
@@ -4862,13 +5259,14 @@ async function haveAll(repoDir, refs) {
|
|
|
4862
5259
|
async function install(ctx, gd, artifacts, call) {
|
|
4863
5260
|
if (artifacts.length === 0) return 0;
|
|
4864
5261
|
const progress = call.onProgress ?? (() => void 0);
|
|
4865
|
-
const tmp = await (0,
|
|
5262
|
+
const tmp = await (0, import_promises8.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "gitpub-fetch-"));
|
|
4866
5263
|
let bytes = 0;
|
|
5264
|
+
const total = artifacts.reduce((sum, a) => sum + a.size, 0);
|
|
4867
5265
|
try {
|
|
4868
5266
|
for (const a of artifacts) {
|
|
4869
|
-
const file = (0,
|
|
5267
|
+
const file = (0, import_node_path6.join)(tmp, `${a.sha256}.${a.kind}`);
|
|
4870
5268
|
bytes += await download(ctx.fetchImpl, a.url, file, a.sha256, a.size, call);
|
|
4871
|
-
progress({ phase: "fetch", message: `installing ${a.kind}`, bytes, total
|
|
5269
|
+
progress({ phase: "fetch", message: `installing ${a.kind}`, bytes, total });
|
|
4872
5270
|
if (a.kind === "bundle") {
|
|
4873
5271
|
await bundleUnbundleObjects(gd, file);
|
|
4874
5272
|
} else {
|
|
@@ -4876,7 +5274,7 @@ async function install(ctx, gd, artifacts, call) {
|
|
|
4876
5274
|
}
|
|
4877
5275
|
}
|
|
4878
5276
|
} finally {
|
|
4879
|
-
await (0,
|
|
5277
|
+
await (0, import_promises8.rm)(tmp, { recursive: true, force: true });
|
|
4880
5278
|
}
|
|
4881
5279
|
return bytes;
|
|
4882
5280
|
}
|
|
@@ -4916,7 +5314,7 @@ async function download(fetchImpl, url2, dest, sha256, size, call) {
|
|
|
4916
5314
|
controller.enqueue(chunk);
|
|
4917
5315
|
}
|
|
4918
5316
|
});
|
|
4919
|
-
await (0,
|
|
5317
|
+
await (0, import_promises9.pipeline)(import_node_stream2.Readable.fromWeb(res.body.pipeThrough(counted)), (0, import_node_fs5.createWriteStream)(dest));
|
|
4920
5318
|
if (n !== size || hash.digest("hex") !== sha256) throw new PublisherError("NETWORK", "downloaded artifact failed checksum verification", { retryable: true });
|
|
4921
5319
|
return n;
|
|
4922
5320
|
}
|
|
@@ -4924,6 +5322,11 @@ async function download(fetchImpl, url2, dest, sha256, size, call) {
|
|
|
4924
5322
|
// ../client/src/publisher.ts
|
|
4925
5323
|
var CLIENT_NAME = "git-publisher-client";
|
|
4926
5324
|
var CLIENT_VERSION = "0.1.0";
|
|
5325
|
+
async function markConflicts(dir, remote) {
|
|
5326
|
+
const who = { GIT_AUTHOR_NAME: "Publisher", GIT_AUTHOR_EMAIL: "publisher@localhost", GIT_COMMITTER_NAME: "Publisher", GIT_COMMITTER_EMAIL: "publisher@localhost" };
|
|
5327
|
+
const r = await runGit(["merge", "--no-ff", "--no-commit", "--no-verify", remote], { cwd: dir, env: { extra: who }, allowFailure: true });
|
|
5328
|
+
if (r.code !== 0 && !await mergeInProgress(dir)) throw new PublisherError("GIT", `could not mark the conflicts: ${r.stderr.trim()}`);
|
|
5329
|
+
}
|
|
4927
5330
|
var Publisher = class {
|
|
4928
5331
|
constructor(opts) {
|
|
4929
5332
|
this.opts = opts;
|
|
@@ -4950,6 +5353,19 @@ var Publisher = class {
|
|
|
4950
5353
|
fetchImpl;
|
|
4951
5354
|
tokenFn;
|
|
4952
5355
|
clientInfo;
|
|
5356
|
+
/**
|
|
5357
|
+
* Follow the run an accepted publication started, at the portal that serves this endpoint, with
|
|
5358
|
+
* this publisher's own credential and its one refresh — the portal verifies the same token the
|
|
5359
|
+
* control API does.
|
|
5360
|
+
*/
|
|
5361
|
+
async followRun(input, opts = {}) {
|
|
5362
|
+
return followRun(input, {
|
|
5363
|
+
...opts,
|
|
5364
|
+
token: this.tokenFn,
|
|
5365
|
+
refresh: async () => !this.opts.token && refreshStoredCredential(this.endpoint, { fetch: this.fetchImpl }),
|
|
5366
|
+
fetch: this.fetchImpl
|
|
5367
|
+
});
|
|
5368
|
+
}
|
|
4953
5369
|
async getCapabilities(call = {}) {
|
|
4954
5370
|
const r = await this.transport.request("GET", "/v1/capabilities", void 0, call);
|
|
4955
5371
|
return r.body;
|
|
@@ -5022,6 +5438,10 @@ var Publisher = class {
|
|
|
5022
5438
|
async checkout(options, call = {}) {
|
|
5023
5439
|
return checkoutWorkspace(this, options, call);
|
|
5024
5440
|
}
|
|
5441
|
+
/** Make an associated folder a workspace (idempotent); `init` and `sync` both call it. */
|
|
5442
|
+
async adoptWorkspace(directory, call = {}) {
|
|
5443
|
+
return adoptWorkspace(this, directory, call);
|
|
5444
|
+
}
|
|
5025
5445
|
/** Offline: the persisted workspace mapping, or null. */
|
|
5026
5446
|
async workspace(directory) {
|
|
5027
5447
|
return getWorkspaceMapping(directory);
|
|
@@ -5034,6 +5454,37 @@ var Publisher = class {
|
|
|
5034
5454
|
async sync(options, call = {}) {
|
|
5035
5455
|
return syncWorkspace(this, options, call);
|
|
5036
5456
|
}
|
|
5457
|
+
/**
|
|
5458
|
+
* A creator's whole round trip in one call, so nobody runs git: record their changes, bring in
|
|
5459
|
+
* what the service has (fast-forward or a clean merge, never a rewrite), and publish. It stops,
|
|
5460
|
+
* having published nothing, on a conflict; the recorded commit stays, so nothing is lost.
|
|
5461
|
+
*
|
|
5462
|
+
* Everything that can refuse without touching the folder refuses first: a folder with no
|
|
5463
|
+
* association (or one pinned elsewhere) and a HEAD off the workspace branch would otherwise get
|
|
5464
|
+
* a commit and then an error. `syncWorkspace` itself still never commits or publishes; this is
|
|
5465
|
+
* the composition, and the only one.
|
|
5466
|
+
*/
|
|
5467
|
+
async syncChanges(options, call = {}) {
|
|
5468
|
+
const dir = options.directory;
|
|
5469
|
+
await this.resolveRepo(dir);
|
|
5470
|
+
const branch = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
|
|
5471
|
+
if (!branch) throw new PublisherError("LOCAL_STATE", "HEAD is detached; check out the branch this folder publishes, then run sync again");
|
|
5472
|
+
const mapped = await getWorkspaceMapping(dir);
|
|
5473
|
+
if (mapped && mapped.branch !== branch)
|
|
5474
|
+
throw new PublisherError("LOCAL_STATE", `branch ${branch} is checked out, but this folder publishes ${mapped.branch}; check it out, then run sync again`);
|
|
5475
|
+
const recorded = await recordChanges({ dir, ...options.message ? { message: options.message } : {} });
|
|
5476
|
+
const reconciled = await syncWorkspace(this, { directory: dir, apply: true }, call);
|
|
5477
|
+
const base = { recorded: { commit: recorded.commit, committed: recorded.committed, excluded: recorded.excluded }, reconciled };
|
|
5478
|
+
if (reconciled.after === "conflict") {
|
|
5479
|
+
const remote = reconciled.before.remote?.oid ?? null;
|
|
5480
|
+
if (options.markConflicts && remote) await markConflicts(dir, remote);
|
|
5481
|
+
return { ...base, state: "conflict", published: null, marked: options.markConflicts === true && remote !== null };
|
|
5482
|
+
}
|
|
5483
|
+
const tip = await revParse(dir, `refs/heads/${branch}`);
|
|
5484
|
+
if (tip === null || tip === (reconciled.before.remote?.oid ?? null)) return { ...base, state: "up_to_date", published: null, marked: false };
|
|
5485
|
+
const published = await this.publish({ repoDir: dir }, call);
|
|
5486
|
+
return { ...base, state: published.state === "accepted" ? "published" : published.state === "rejected" ? "rejected" : "unknown", published, marked: false };
|
|
5487
|
+
}
|
|
5037
5488
|
/** Resolve interrupted publications recorded locally without creating duplicates. */
|
|
5038
5489
|
async resumePublications(repoDir, call = {}) {
|
|
5039
5490
|
const { remote } = await this.resolveRepo(repoDir);
|
|
@@ -5051,7 +5502,7 @@ var Publisher = class {
|
|
|
5051
5502
|
call.onProgress?.({ phase: "validate", message: `waiting for ${p.publicationId}` });
|
|
5052
5503
|
s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...call, waitSeconds: MAX_WAIT_SECONDS }, store2);
|
|
5053
5504
|
}
|
|
5054
|
-
out.push(toOutcome(s, true));
|
|
5505
|
+
out.push(toOutcome(s, true, p.refUpdates));
|
|
5055
5506
|
}
|
|
5056
5507
|
for (const o of out) await noteAcceptedPublication(repoDir, o);
|
|
5057
5508
|
return out;
|
|
@@ -5087,120 +5538,6 @@ function createPublisher(opts) {
|
|
|
5087
5538
|
return new Publisher(opts);
|
|
5088
5539
|
}
|
|
5089
5540
|
|
|
5090
|
-
// ../client/src/onboard.ts
|
|
5091
|
-
var import_promises9 = require("node:fs/promises");
|
|
5092
|
-
var import_node_path6 = require("node:path");
|
|
5093
|
-
var DEFAULT_EXCLUDED_DIRS = [
|
|
5094
|
-
".git",
|
|
5095
|
-
"node_modules",
|
|
5096
|
-
".venv",
|
|
5097
|
-
"venv",
|
|
5098
|
-
"__pycache__",
|
|
5099
|
-
".cache",
|
|
5100
|
-
".next",
|
|
5101
|
-
".nuxt",
|
|
5102
|
-
".turbo",
|
|
5103
|
-
".parcel-cache",
|
|
5104
|
-
".aws",
|
|
5105
|
-
".ssh",
|
|
5106
|
-
".gnupg",
|
|
5107
|
-
".idea",
|
|
5108
|
-
".terraform",
|
|
5109
|
-
".pytest_cache",
|
|
5110
|
-
".mypy_cache",
|
|
5111
|
-
"coverage",
|
|
5112
|
-
".DS_Store"
|
|
5113
|
-
];
|
|
5114
|
-
var DEFAULT_EXCLUDED_FILES = [
|
|
5115
|
-
/^\.env(\..*)?$/,
|
|
5116
|
-
/\.(pem|key|p12|pfx|jks|keystore)$/i,
|
|
5117
|
-
/^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/,
|
|
5118
|
-
/^\.npmrc$/,
|
|
5119
|
-
/^\.pypirc$/,
|
|
5120
|
-
/^\.netrc$/,
|
|
5121
|
-
/\.(log|tmp|swp)$/i,
|
|
5122
|
-
/^\.DS_Store$/,
|
|
5123
|
-
/^Thumbs\.db$/i,
|
|
5124
|
-
/^credentials(\.json)?$/i,
|
|
5125
|
-
/^service-account.*\.json$/i,
|
|
5126
|
-
/^\.git(modules|attributes)?$/
|
|
5127
|
-
];
|
|
5128
|
-
async function onboardFolder(opts) {
|
|
5129
|
-
const dir = opts.dir;
|
|
5130
|
-
const policy = opts.policy ?? {};
|
|
5131
|
-
const maxBytes = policy.maxFileBytes ?? 50 * 1024 * 1024;
|
|
5132
|
-
let initialized = false;
|
|
5133
|
-
if (await isGitRepo(dir)) {
|
|
5134
|
-
const head = await headCommit(dir);
|
|
5135
|
-
if (head && !policy.allowExistingHistory) {
|
|
5136
|
-
throw new PublisherError("LOCAL_STATE", "folder is already a Git repository with history; commit your changes and use publish instead of init");
|
|
5137
|
-
}
|
|
5138
|
-
} else {
|
|
5139
|
-
await initWorkRepo(dir, opts.branch ?? "main");
|
|
5140
|
-
initialized = true;
|
|
5141
|
-
}
|
|
5142
|
-
const excluded = [];
|
|
5143
|
-
const candidates = [];
|
|
5144
|
-
const extra = policy.exclude ?? [];
|
|
5145
|
-
const walk = async (d) => {
|
|
5146
|
-
for (const e of await (0, import_promises9.readdir)(d, { withFileTypes: true })) {
|
|
5147
|
-
if (opts.signal?.aborted) throw new PublisherError("CANCELLED", "cancelled");
|
|
5148
|
-
const full = (0, import_node_path6.join)(d, e.name);
|
|
5149
|
-
const rel = (0, import_node_path6.relative)(dir, full).split(import_node_path6.sep).join("/");
|
|
5150
|
-
if (e.isSymbolicLink()) {
|
|
5151
|
-
excluded.push({ path: rel, reason: "symbolic link" });
|
|
5152
|
-
continue;
|
|
5153
|
-
}
|
|
5154
|
-
if (e.isDirectory()) {
|
|
5155
|
-
if (DEFAULT_EXCLUDED_DIRS.includes(e.name) || extra.some((p) => matches(rel, e.name, p))) {
|
|
5156
|
-
excluded.push({ path: rel + "/", reason: "excluded by policy" });
|
|
5157
|
-
continue;
|
|
5158
|
-
}
|
|
5159
|
-
await walk(full);
|
|
5160
|
-
continue;
|
|
5161
|
-
}
|
|
5162
|
-
if (!e.isFile()) continue;
|
|
5163
|
-
if (DEFAULT_EXCLUDED_FILES.some((re) => re.test(e.name)) || extra.some((p) => matches(rel, e.name, p))) {
|
|
5164
|
-
excluded.push({ path: rel, reason: "excluded by policy" });
|
|
5165
|
-
continue;
|
|
5166
|
-
}
|
|
5167
|
-
const st = await (0, import_promises9.stat)(full);
|
|
5168
|
-
if (st.size > maxBytes) {
|
|
5169
|
-
excluded.push({ path: rel, reason: `larger than ${maxBytes} bytes` });
|
|
5170
|
-
continue;
|
|
5171
|
-
}
|
|
5172
|
-
candidates.push(rel);
|
|
5173
|
-
}
|
|
5174
|
-
};
|
|
5175
|
-
await walk(dir);
|
|
5176
|
-
const ignored = await checkIgnore(dir, candidates);
|
|
5177
|
-
const selected = candidates.filter((p) => !ignored.has(p)).sort();
|
|
5178
|
-
for (const p of ignored) excluded.push({ path: p, reason: "ignored by .gitignore" });
|
|
5179
|
-
if (selected.length === 0) return { repoDir: dir, commit: null, committed: [], excluded, initialized };
|
|
5180
|
-
const commit = await commitPaths(dir, selected, {
|
|
5181
|
-
message: opts.message ?? "Initial publication",
|
|
5182
|
-
...opts.authorName ? { authorName: opts.authorName } : {},
|
|
5183
|
-
...opts.authorEmail ? { authorEmail: opts.authorEmail } : {},
|
|
5184
|
-
...opts.signal ? { signal: opts.signal } : {}
|
|
5185
|
-
});
|
|
5186
|
-
return { repoDir: dir, commit, committed: selected, excluded, initialized };
|
|
5187
|
-
}
|
|
5188
|
-
function matches(rel, base, pattern) {
|
|
5189
|
-
if (pattern.includes("*")) {
|
|
5190
|
-
const re = new RegExp(
|
|
5191
|
-
"^" + pattern.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
|
|
5192
|
-
);
|
|
5193
|
-
return re.test(base) || re.test(rel);
|
|
5194
|
-
}
|
|
5195
|
-
return base === pattern || rel === pattern || rel.startsWith(pattern.replace(/\/$/, "") + "/");
|
|
5196
|
-
}
|
|
5197
|
-
async function checkIgnore(dir, paths) {
|
|
5198
|
-
if (paths.length === 0) return /* @__PURE__ */ new Set();
|
|
5199
|
-
const r = await runGit(["check-ignore", "--stdin", "-z"], { cwd: dir, input: paths.join("\0") + "\0", allowFailure: true });
|
|
5200
|
-
if (r.code !== 0) return /* @__PURE__ */ new Set();
|
|
5201
|
-
return new Set(r.stdout.toString("utf8").split("\0").filter(Boolean));
|
|
5202
|
-
}
|
|
5203
|
-
|
|
5204
5541
|
// ../client/src/migrate.ts
|
|
5205
5542
|
async function fetchCapabilities(endpoint, fetchImpl = fetch, signal) {
|
|
5206
5543
|
const res = await fetchImpl(`${normalize(endpoint)}/v1/capabilities`, { method: "GET", ...signal ? { signal } : {} });
|
|
@@ -5354,7 +5691,7 @@ function resolveChoice(answer, names) {
|
|
|
5354
5691
|
}
|
|
5355
5692
|
return names.find((n) => n.toLowerCase() === a.toLowerCase()) ?? null;
|
|
5356
5693
|
}
|
|
5357
|
-
async function
|
|
5694
|
+
async function ask2(question) {
|
|
5358
5695
|
const rl = (0, import_promises10.createInterface)({ input: process.stdin, output: process.stderr });
|
|
5359
5696
|
try {
|
|
5360
5697
|
return await rl.question(question);
|
|
@@ -5372,7 +5709,7 @@ ${purpose}
|
|
|
5372
5709
|
names.forEach((n, i) => process.stderr.write(` ${i + 1}) ${n}
|
|
5373
5710
|
`));
|
|
5374
5711
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
5375
|
-
const answer = await
|
|
5712
|
+
const answer = await ask2(`
|
|
5376
5713
|
Which one? [1-${names.length}, or a name] `);
|
|
5377
5714
|
if (answer === null) return null;
|
|
5378
5715
|
const chosen = resolveChoice(answer, names);
|
|
@@ -5388,7 +5725,7 @@ Name this connection, so you never have to type ${endpoint} again.
|
|
|
5388
5725
|
(for example: prod, dev, staging)
|
|
5389
5726
|
`);
|
|
5390
5727
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
5391
|
-
const answer = await
|
|
5728
|
+
const answer = await ask2(suggested ? `
|
|
5392
5729
|
Profile name [${suggested}]: ` : `
|
|
5393
5730
|
Profile name: `);
|
|
5394
5731
|
if (answer === null) return null;
|
|
@@ -5411,6 +5748,7 @@ var OPTIONS = {
|
|
|
5411
5748
|
repo: { type: "string" },
|
|
5412
5749
|
message: { type: "string" },
|
|
5413
5750
|
publication: { type: "string" },
|
|
5751
|
+
timeout: { type: "string" },
|
|
5414
5752
|
branch: { type: "string" },
|
|
5415
5753
|
ours: { type: "string" },
|
|
5416
5754
|
theirs: { type: "string" },
|
|
@@ -5420,7 +5758,9 @@ var OPTIONS = {
|
|
|
5420
5758
|
quiet: { type: "boolean" },
|
|
5421
5759
|
force: { type: "boolean" },
|
|
5422
5760
|
apply: { type: "boolean" },
|
|
5761
|
+
"mark-conflicts": { type: "boolean" },
|
|
5423
5762
|
help: { type: "boolean" },
|
|
5763
|
+
version: { type: "boolean" },
|
|
5424
5764
|
"set-default": { type: "boolean" },
|
|
5425
5765
|
// A flag of its own, not a negation of --wait: there is no --wait.
|
|
5426
5766
|
"no-wait": { type: "boolean" }
|
|
@@ -5434,10 +5774,122 @@ function parseArgs(argv) {
|
|
|
5434
5774
|
}
|
|
5435
5775
|
}
|
|
5436
5776
|
|
|
5777
|
+
// src/skills.ts
|
|
5778
|
+
var import_node_child_process3 = require("node:child_process");
|
|
5779
|
+
var import_node_fs6 = require("node:fs");
|
|
5780
|
+
var import_node_path7 = require("node:path");
|
|
5781
|
+
var MARKETPLACE = "shadow-publisher";
|
|
5782
|
+
var PLUGIN = "git-publisher";
|
|
5783
|
+
function packageRoot() {
|
|
5784
|
+
return (0, import_node_path7.join)((0, import_node_path7.dirname)((0, import_node_fs6.realpathSync)(process.argv[1])), "..");
|
|
5785
|
+
}
|
|
5786
|
+
function packageVersion() {
|
|
5787
|
+
try {
|
|
5788
|
+
return JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path7.join)(packageRoot(), "package.json"), "utf8")).version;
|
|
5789
|
+
} catch {
|
|
5790
|
+
return "unknown";
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
function claude(args) {
|
|
5794
|
+
return new Promise((resolve3) => {
|
|
5795
|
+
let stdout = "";
|
|
5796
|
+
let stderr = "";
|
|
5797
|
+
const child = (0, import_node_child_process3.spawn)("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5798
|
+
child.stdout.on("data", (d) => stdout += d.toString("utf8"));
|
|
5799
|
+
child.stderr.on("data", (d) => stderr += d.toString("utf8"));
|
|
5800
|
+
child.on("error", (e) => resolve3({ code: null, stdout, stderr: e.message, missing: e.code === "ENOENT" }));
|
|
5801
|
+
child.on("close", (code) => resolve3({ code, stdout, stderr, missing: false }));
|
|
5802
|
+
});
|
|
5803
|
+
}
|
|
5804
|
+
async function installSkills({ log }) {
|
|
5805
|
+
const source = (0, import_node_path7.join)(packageRoot(), "dist", "claude");
|
|
5806
|
+
if (!(0, import_node_fs6.existsSync)((0, import_node_path7.join)(source, ".claude-plugin", "marketplace.json"))) {
|
|
5807
|
+
throw new PublisherError("LOCAL_STATE", `this publisher carries no skills at ${source}; reinstall it with: npm install -g @minnovate/shadow-publisher`);
|
|
5808
|
+
}
|
|
5809
|
+
const step = async (what, args) => {
|
|
5810
|
+
log(what);
|
|
5811
|
+
const r = await claude(args);
|
|
5812
|
+
if (r.missing) {
|
|
5813
|
+
throw new PublisherError(
|
|
5814
|
+
"LOCAL_STATE",
|
|
5815
|
+
'Claude Code is not installed, or "claude" is not on PATH. Install it from https://claude.com/claude-code, then run: publisher skills install'
|
|
5816
|
+
);
|
|
5817
|
+
}
|
|
5818
|
+
if (r.code !== 0) throw new PublisherError("LOCAL_STATE", `"claude ${args.slice(0, 3).join(" ")}" failed: ${(r.stderr || r.stdout).trim()}`);
|
|
5819
|
+
};
|
|
5820
|
+
await step("Adding the publisher to Claude Code\u2026", ["plugin", "marketplace", "add", source, "--scope", "user"]);
|
|
5821
|
+
await step("Installing its skills\u2026", ["plugin", "install", `${PLUGIN}@${MARKETPLACE}`, "--scope", "user"]);
|
|
5822
|
+
return {
|
|
5823
|
+
ok: true,
|
|
5824
|
+
plugin: `${PLUGIN}@${MARKETPLACE}`,
|
|
5825
|
+
marketplace: MARKETPLACE,
|
|
5826
|
+
source,
|
|
5827
|
+
version: packageVersion(),
|
|
5828
|
+
message: "The publisher skills are installed in Claude Code for your user. Start a new Claude Code session (or run /reload-plugins in an open one) and ask it to publish your app."
|
|
5829
|
+
};
|
|
5830
|
+
}
|
|
5831
|
+
|
|
5832
|
+
// src/follow.ts
|
|
5833
|
+
async function followPublication(p, endpoint, o, opts) {
|
|
5834
|
+
const derived = portalForEndpoint(endpoint);
|
|
5835
|
+
const portal = derived !== null && await confirmPortal(derived, endpoint, opts.fetch) ? derived : null;
|
|
5836
|
+
const elsewhere = (reason) => ({ kind: "unavailable", reason, portal, hint: inPortal(portal, "Published.", "Follow it") });
|
|
5837
|
+
if (portal === null) return elsewhere("no-portal");
|
|
5838
|
+
const refs = Object.entries(o.refs ?? {});
|
|
5839
|
+
if (refs.length !== 1) return elsewhere("several-refs");
|
|
5840
|
+
const [ref, commit] = refs[0];
|
|
5841
|
+
opts.log("Following it: received -> checked -> going live -> live. Ctrl-C stops watching; the publication is already saved.");
|
|
5842
|
+
const outcome = await p.followRun(
|
|
5843
|
+
{ portal, repoId: o.repoId, publicationId: o.publicationId, ref, commit },
|
|
5844
|
+
{ onStage: opts.onStage, ...opts.timeoutMs === void 0 ? {} : { timeoutMs: opts.timeoutMs } }
|
|
5845
|
+
);
|
|
5846
|
+
const next = outcome.kind === "timed-out" ? { action: "follow", command: `publisher follow --publication ${o.publicationId} --json` } : void 0;
|
|
5847
|
+
return { ...outcome, portal, hint: progressHint(outcome, portal), ...next === void 0 ? {} : { next } };
|
|
5848
|
+
}
|
|
5849
|
+
function followTimeoutMs(flag) {
|
|
5850
|
+
if (flag === void 0) return FOLLOW_TIMEOUT_MS;
|
|
5851
|
+
if (!/^[1-9][0-9]*$/.test(flag)) throw new PublisherError("INVALID_REQUEST", `--timeout takes a whole number of seconds, not ${JSON.stringify(flag)}`);
|
|
5852
|
+
return Math.min(Number(flag) * 1e3, FOLLOW_TIMEOUT_MS);
|
|
5853
|
+
}
|
|
5854
|
+
function stageLine(e, json) {
|
|
5855
|
+
return json ? JSON.stringify({ event: "stage", stage: e.stage, word: e.word, state: e.state, at: e.at }) : ` ${e.word}: ${e.state.replace("-", " ")}`;
|
|
5856
|
+
}
|
|
5857
|
+
function inPortal(portal, lead, verb) {
|
|
5858
|
+
return `${lead} ${verb} in the portal${portal === null ? "" : `: ${portal}`}.`;
|
|
5859
|
+
}
|
|
5860
|
+
function progressHint(r, portal) {
|
|
5861
|
+
switch (r.kind) {
|
|
5862
|
+
case "settled":
|
|
5863
|
+
switch (r.ending) {
|
|
5864
|
+
case "live":
|
|
5865
|
+
return "Your app is live.";
|
|
5866
|
+
case "not-deployed":
|
|
5867
|
+
return "Published and checked; this organisation does not deploy automatically.";
|
|
5868
|
+
case "failed":
|
|
5869
|
+
return inPortal(portal, `It stopped at "${r.failedAt ?? "a step"}".`, "See why");
|
|
5870
|
+
case "stopped":
|
|
5871
|
+
return "A newer publication replaced this one before it went live.";
|
|
5872
|
+
}
|
|
5873
|
+
break;
|
|
5874
|
+
case "timed-out":
|
|
5875
|
+
return inPortal(portal, `Still going${r.lastError === null ? "" : ` (${r.lastError})`}; run next.command to keep following.`, "Or follow it");
|
|
5876
|
+
case "unavailable":
|
|
5877
|
+
return inPortal(portal, "Published. This portal cannot show progress in the terminal yet.", "Follow it");
|
|
5878
|
+
case "unauthorized":
|
|
5879
|
+
return inPortal(portal, "Published, but the portal did not accept this sign-in (run publisher login).", "Follow it");
|
|
5880
|
+
case "forbidden":
|
|
5881
|
+
return inPortal(portal, "Published, but the portal would not show this sign-in the run.", "Follow it");
|
|
5882
|
+
case "malformed":
|
|
5883
|
+
return inPortal(portal, `Published, but the portal's answer about the run could not be read (${r.detail}).`, "Follow it");
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
|
|
5437
5887
|
// src/main.ts
|
|
5438
5888
|
var USAGE = `publisher \u2014 publish an app's Git history to the serverless Git backend
|
|
5439
5889
|
|
|
5440
5890
|
Usage:
|
|
5891
|
+
publisher skills install add the publisher's skills to Claude Code (user scope);
|
|
5892
|
+
run it again after updating the publisher
|
|
5441
5893
|
publisher login sign in. With NO arguments it asks the
|
|
5442
5894
|
portal where publishing lives, opens a
|
|
5443
5895
|
browser to confirm who you are, and asks
|
|
@@ -5447,19 +5899,35 @@ Usage:
|
|
|
5447
5899
|
[--profile <name>] [--set-default] [--token <token>]
|
|
5448
5900
|
publisher logout (--endpoint <url> | --profile <name>)
|
|
5449
5901
|
publisher profile list | profile use <name> | profile remove <name>
|
|
5450
|
-
publisher init [dir] [--name <name>] [--repo <id>] create (or attach to) a repository
|
|
5451
|
-
|
|
5902
|
+
publisher init [dir] [--name <name>] [--repo <id>] create (or attach to) a repository, associate this folder
|
|
5903
|
+
and record it as a workspace (so sync works here)
|
|
5904
|
+
publisher publish [dir] [--ref <ref>]... [--force] [--no-wait] publish commits as they are; to save and publish
|
|
5905
|
+
a change, use sync
|
|
5452
5906
|
publisher status [dir] [--publication <id>] resolve pending/interrupted publications
|
|
5453
5907
|
publisher fetch [dir]
|
|
5454
5908
|
publisher checkout <dir> --repo <id> [--branch <name>] create a NEW editable workspace (never overlays a folder)
|
|
5455
5909
|
publisher workspace-status [dir] offline mapping + where the workspace stands (real three-way check)
|
|
5456
|
-
publisher sync [dir] [--
|
|
5910
|
+
publisher sync [dir] [--message <text>] THE way to save and publish a change: records the folder's
|
|
5911
|
+
changes (never .env, keys or node_modules), brings in what
|
|
5912
|
+
was published meanwhile (fast-forward or a clean merge,
|
|
5913
|
+
never a rewrite) and publishes. Stops on a conflict with
|
|
5914
|
+
nothing published and nothing lost. No git needed
|
|
5915
|
+
[--mark-conflicts] on a conflict, put both versions in each file, marked,
|
|
5916
|
+
to edit; the next sync records and publishes the edit
|
|
5917
|
+
[--no-wait] don't follow the app going live afterwards (received ->
|
|
5918
|
+
checked -> going live -> live, up to 15 minutes)
|
|
5919
|
+
[--timeout <seconds>] follow for at most this long; if it is still going, the
|
|
5920
|
+
result's next.command picks it up
|
|
5921
|
+
publisher follow [dir] --publication <id> follow an accepted publication's run where a sync left it
|
|
5922
|
+
[--timeout <seconds>]
|
|
5457
5923
|
publisher merge-check [dir] --ours <ref> --theirs <ref> [--apply]
|
|
5458
5924
|
publisher info [dir] repository state as seen by the server
|
|
5459
5925
|
publisher doctor environment check (node, git, credentials, association)
|
|
5460
5926
|
publisher migrate [dir] [--to <url>] re-point a folder whose service moved (asks the service where)
|
|
5461
5927
|
|
|
5462
|
-
Global flags: --endpoint <url>, --profile <name>, --token, --json, --quiet
|
|
5928
|
+
Global flags: --endpoint <url>, --profile <name>, --token, --json, --quiet, --version
|
|
5929
|
+
|
|
5930
|
+
A creator never needs git: init once, then sync after every change.
|
|
5463
5931
|
|
|
5464
5932
|
Which endpoint a command uses, first hit wins:
|
|
5465
5933
|
--endpoint <url> an explicit URL; needs no stored profile
|
|
@@ -5495,7 +5963,11 @@ async function main(argv) {
|
|
|
5495
5963
|
process.stderr.write(`\r[${e.phase}] ${e.message}${pct} `);
|
|
5496
5964
|
if (e.phase === "done") process.stderr.write("\n");
|
|
5497
5965
|
};
|
|
5498
|
-
const dirArg = (i) => (0,
|
|
5966
|
+
const dirArg = (i) => (0, import_node_path8.resolve)(cmd[i] ?? ".");
|
|
5967
|
+
const emitStage = (e) => {
|
|
5968
|
+
if (json) process.stdout.write(stageLine(e, true) + "\n");
|
|
5969
|
+
else if (!quiet) process.stderr.write(stageLine(e, false) + "\n");
|
|
5970
|
+
};
|
|
5499
5971
|
const resolutionInput = async (dir) => {
|
|
5500
5972
|
let association;
|
|
5501
5973
|
if (dir && await isGitRepo(dir)) association = (await readAssociation(dir))?.endpoint;
|
|
@@ -5524,6 +5996,11 @@ async function main(argv) {
|
|
|
5524
5996
|
const token = str2(flags["token"]);
|
|
5525
5997
|
return { endpoint, p: createPublisher({ endpoint, endpointSource: source, ...token ? { token } : {}, clientName: "git-publisher-cli", clientVersion: CLIENT_VERSION }) };
|
|
5526
5998
|
};
|
|
5999
|
+
if (flags["version"] === true) {
|
|
6000
|
+
process.stdout.write(`${packageVersion()}
|
|
6001
|
+
`);
|
|
6002
|
+
return 0;
|
|
6003
|
+
}
|
|
5527
6004
|
switch (flags["help"] === true ? "help" : cmd[0]) {
|
|
5528
6005
|
case void 0:
|
|
5529
6006
|
case "help":
|
|
@@ -5669,19 +6146,30 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
5669
6146
|
await p.getRepository(repoId);
|
|
5670
6147
|
}
|
|
5671
6148
|
await p.associate(dir, repoId);
|
|
6149
|
+
const workspace = await p.adoptWorkspace(dir).catch(() => null);
|
|
5672
6150
|
out({
|
|
5673
6151
|
ok: true,
|
|
5674
6152
|
repoId,
|
|
5675
6153
|
created,
|
|
5676
6154
|
endpoint,
|
|
5677
6155
|
dir,
|
|
5678
|
-
onboarding: onboarding ? { commit: onboarding.commit, committed: onboarding.committed.length, excluded: onboarding.excluded } : null
|
|
6156
|
+
onboarding: onboarding ? { commit: onboarding.commit, committed: onboarding.committed.length, excluded: onboarding.excluded } : null,
|
|
6157
|
+
workspace: workspace === null ? null : { branch: workspace.branch, baseOid: workspace.baseOid, baseGeneration: workspace.baseGeneration }
|
|
5679
6158
|
});
|
|
5680
6159
|
return 0;
|
|
5681
6160
|
}
|
|
5682
6161
|
case "publish": {
|
|
5683
6162
|
let finishPublish2 = function(o) {
|
|
5684
|
-
out({
|
|
6163
|
+
out({
|
|
6164
|
+
state: o.state,
|
|
6165
|
+
publicationId: o.publicationId,
|
|
6166
|
+
repoId: o.repoId,
|
|
6167
|
+
generation: o.resultGeneration,
|
|
6168
|
+
refs: o.refs,
|
|
6169
|
+
rejection: o.rejection,
|
|
6170
|
+
resumed: o.resumed,
|
|
6171
|
+
...o.next === null ? {} : { next: o.next }
|
|
6172
|
+
});
|
|
5685
6173
|
return o.state === "accepted" ? 0 : o.state === "rejected" ? 2 : 3;
|
|
5686
6174
|
};
|
|
5687
6175
|
var finishPublish = finishPublish2;
|
|
@@ -5717,7 +6205,13 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
5717
6205
|
const resolved = await p.resumePublications(dir, { onProgress: progress });
|
|
5718
6206
|
out({
|
|
5719
6207
|
pending: pending.map((x) => x.publicationId),
|
|
5720
|
-
resolved: resolved.map((o) => ({
|
|
6208
|
+
resolved: resolved.map((o) => ({
|
|
6209
|
+
publicationId: o.publicationId,
|
|
6210
|
+
state: o.state,
|
|
6211
|
+
generation: o.resultGeneration,
|
|
6212
|
+
rejection: o.rejection,
|
|
6213
|
+
...o.next === null ? {} : { next: o.next }
|
|
6214
|
+
}))
|
|
5721
6215
|
});
|
|
5722
6216
|
return resolved.every((o) => o.state === "accepted") ? 0 : resolved.some((o) => o.state === "rejected") ? 2 : 3;
|
|
5723
6217
|
}
|
|
@@ -5745,12 +6239,63 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
5745
6239
|
out(s);
|
|
5746
6240
|
return s.state === "conflict" ? 2 : 0;
|
|
5747
6241
|
}
|
|
6242
|
+
/**
|
|
6243
|
+
* The creator's one verb: record the folder's changes, bring in what the service has, publish.
|
|
6244
|
+
* `--apply` is what this was before it recorded and published, and is still accepted, so a
|
|
6245
|
+
* `sync --apply` written down anywhere does the whole round trip rather than failing.
|
|
6246
|
+
*/
|
|
5748
6247
|
case "sync": {
|
|
5749
6248
|
const dir = dirArg(1);
|
|
5750
|
-
const
|
|
5751
|
-
const
|
|
5752
|
-
|
|
5753
|
-
|
|
6249
|
+
const timeoutMs = followTimeoutMs(str2(flags["timeout"]));
|
|
6250
|
+
const { p, endpoint } = await client(dir);
|
|
6251
|
+
const r = await p.syncChanges({ directory: dir, markConflicts: flags["mark-conflicts"] === true, ...optStr(flags, "message") }, { onProgress: progress });
|
|
6252
|
+
const o = r.published;
|
|
6253
|
+
const followed = o !== null && o.state === "accepted" && flags["no-wait"] !== true ? await followPublication(p, endpoint, o, { onStage: emitStage, log, timeoutMs }) : null;
|
|
6254
|
+
out({
|
|
6255
|
+
ok: r.state === "published" || r.state === "up_to_date",
|
|
6256
|
+
state: r.state,
|
|
6257
|
+
recorded: r.recorded,
|
|
6258
|
+
merged: r.reconciled.applied,
|
|
6259
|
+
conflicts: r.reconciled.before.conflicts,
|
|
6260
|
+
marked: r.marked,
|
|
6261
|
+
publication: o === null ? null : {
|
|
6262
|
+
state: o.state,
|
|
6263
|
+
publicationId: o.publicationId,
|
|
6264
|
+
generation: o.resultGeneration,
|
|
6265
|
+
refs: o.refs,
|
|
6266
|
+
rejection: o.rejection,
|
|
6267
|
+
...o.next === null ? {} : { next: o.next }
|
|
6268
|
+
},
|
|
6269
|
+
hint: syncHint(r.state, r.reconciled.before.conflicts, r.marked),
|
|
6270
|
+
progress: followed
|
|
6271
|
+
});
|
|
6272
|
+
return r.state === "published" || r.state === "up_to_date" ? 0 : r.state === "unknown" ? 3 : 2;
|
|
6273
|
+
}
|
|
6274
|
+
/**
|
|
6275
|
+
* Pick up following a run where a bounded `sync --timeout` (or an interrupted one) left it. The
|
|
6276
|
+
* ref and commit come from git-publisher's own record of the publication, so the command needs
|
|
6277
|
+
* only its id. Each call reports the stages as they now stand, from the first.
|
|
6278
|
+
*/
|
|
6279
|
+
case "follow": {
|
|
6280
|
+
const dir = dirArg(1);
|
|
6281
|
+
const publicationId = str2(flags["publication"]);
|
|
6282
|
+
if (!publicationId) throw new PublisherError("INVALID_REQUEST", "follow requires --publication <id>");
|
|
6283
|
+
const timeoutMs = followTimeoutMs(str2(flags["timeout"]));
|
|
6284
|
+
const { p, endpoint } = await client(dir);
|
|
6285
|
+
const repoId = str2(flags["repo"]) ?? (await readAssociation(dir))?.repoId;
|
|
6286
|
+
if (!repoId) throw new PublisherError("LOCAL_STATE", "no repository association; pass --repo");
|
|
6287
|
+
const publication = await p.getPublicationStatus(repoId, publicationId);
|
|
6288
|
+
if (publication.state !== "accepted") {
|
|
6289
|
+
throw new PublisherError("LOCAL_STATE", `publication ${publicationId} is ${publication.state}, not accepted; run publisher status`);
|
|
6290
|
+
}
|
|
6291
|
+
out({ publicationId, progress: await followPublication(p, endpoint, publication, { onStage: emitStage, log, timeoutMs }) });
|
|
6292
|
+
return 0;
|
|
6293
|
+
}
|
|
6294
|
+
case "skills": {
|
|
6295
|
+
if (cmd[1] !== "install") throw new PublisherError("INVALID_REQUEST", `unknown skills command: ${cmd[1] ?? "(none)"}; expected install`);
|
|
6296
|
+
const r = await installSkills({ log });
|
|
6297
|
+
out(r);
|
|
6298
|
+
return 0;
|
|
5754
6299
|
}
|
|
5755
6300
|
case "merge-check": {
|
|
5756
6301
|
const dir = dirArg(1);
|
|
@@ -5796,6 +6341,20 @@ function demand(r, input) {
|
|
|
5796
6341
|
throw new PublisherError("LOCAL_STATE", `not connected to a publishing service yet.${run}`, { details });
|
|
5797
6342
|
}
|
|
5798
6343
|
}
|
|
6344
|
+
function syncHint(state, conflicts, marked) {
|
|
6345
|
+
switch (state) {
|
|
6346
|
+
case "published":
|
|
6347
|
+
return "Your changes are saved and published.";
|
|
6348
|
+
case "up_to_date":
|
|
6349
|
+
return "Nothing new to publish: the service already has this version.";
|
|
6350
|
+
case "conflict":
|
|
6351
|
+
return marked ? `Both versions are now in ${conflicts.join(", ")}, between <<<<<<< and >>>>>>> lines. Edit each to what should be kept, remove those lines, then run publisher sync.` : `Your changes and the published version both changed ${conflicts.join(", ")}. Nothing was published and nothing was lost; decide which version to keep.`;
|
|
6352
|
+
case "rejected":
|
|
6353
|
+
return "The service refused the publication; see publication.rejection.";
|
|
6354
|
+
default:
|
|
6355
|
+
return "The publication was sent but its outcome is not known yet; run publisher status.";
|
|
6356
|
+
}
|
|
6357
|
+
}
|
|
5799
6358
|
function formatHuman(v) {
|
|
5800
6359
|
return JSON.stringify(v, null, 2);
|
|
5801
6360
|
}
|