@minnovate/shadow-publisher 0.2.0 → 0.5.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 +124 -0
- package/dist/claude/plugin/skills/sync-app/SKILL.md +70 -0
- package/dist/publisher.cjs +488 -153
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -4,11 +4,18 @@ Publish an app's Git history to the serverless Git publishing backend.
|
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install -g @minnovate/shadow-publisher
|
|
7
|
-
publisher
|
|
8
|
-
publisher login
|
|
7
|
+
publisher skills install # adds the publisher's skills to Claude Code
|
|
9
8
|
```
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
Then ask Claude Code to publish your app. Without Claude Code:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
publisher login # signs you in through your browser
|
|
14
|
+
publisher init . --name my-app
|
|
15
|
+
publisher sync # after every change: saves it and publishes it; no git needed
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Needs Node 22.12 or later and git 2.46 or later on `PATH`. The package is one bundled file, and the
|
|
19
|
+
Claude Code skills beside it, with no runtime dependencies.
|
|
13
20
|
|
|
14
21
|
Apache-2.0 licensed; see `LICENSE`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shadow-publisher",
|
|
3
|
+
"owner": { "name": "Minnovate" },
|
|
4
|
+
"description": "The publisher's Claude Code skills: publish an app and keep it in sync, without git.",
|
|
5
|
+
"plugins": [
|
|
6
|
+
{
|
|
7
|
+
"name": "git-publisher",
|
|
8
|
+
"source": "./plugin",
|
|
9
|
+
"description": "Publish an app folder and keep it updated (\"publish my app\", \"save my changes\", \"get the latest version\")."
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-publisher",
|
|
3
|
+
"displayName": "Git Publisher",
|
|
4
|
+
"version": "0.5.0",
|
|
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
|
+
"author": { "name": "git-publisher project" },
|
|
7
|
+
"keywords": ["git", "publish", "deploy", "serverless"]
|
|
8
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Git Publisher plugin for Claude Code
|
|
2
|
+
|
|
3
|
+
Thin adapter: two skills (`publish-app`, `sync-app`) that drive the `publisher` CLI
|
|
4
|
+
(`@minnovate/shadow-publisher`, `packages/cli`) from PATH. All publication, retry, resume and merge
|
|
5
|
+
logic lives in the shared client library; the skills only orchestrate, and a creator's changes are
|
|
6
|
+
saved and published with `publisher sync`, never with git.
|
|
7
|
+
|
|
8
|
+
It ships inside the npm package, and `publisher skills install` installs it into Claude Code.
|
|
9
|
+
|
|
10
|
+
Runtime requirements in the environment where Claude Code runs: the publisher installed
|
|
11
|
+
(`npm install -g @minnovate/shadow-publisher`, Node.js 22), Git ≥ 2.46 on PATH, outbound HTTPS to
|
|
12
|
+
the portal's domain (sign-in and publishing at `/publish`) and the S3 bucket host for direct
|
|
13
|
+
uploads/downloads.
|
|
14
|
+
|
|
15
|
+
See `integrations/claude/README.md` for installation, authentication, uninstall and the verified
|
|
16
|
+
environments.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: publish-app
|
|
3
|
+
description: Publish or update the user's app (a folder) with the publisher. Use when the user says "publish my app", "update my app", "save my changes", "push my changes", "save this version online", or asks to resolve an interrupted publication. Handles first-time setup, every later update, interruption recovery and conflicts by running the `publisher` command; never uses git to commit or push.
|
|
4
|
+
allowed-tools: Bash(publisher *), Bash(git status *), Bash(git diff *), Bash(git log *)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Publish or update the user's app
|
|
8
|
+
|
|
9
|
+
You publish the user's app folder with the `publisher` command. All saving, merging, publishing,
|
|
10
|
+
retry and resume logic lives in it; your job is to run it, read its `--json` output, and tell the
|
|
11
|
+
user what happened in plain language. Never explain S3, packs, commits, sessions or tokens.
|
|
12
|
+
|
|
13
|
+
**`publisher sync` is how a change is saved and published. Never run `git add`, `git commit`,
|
|
14
|
+
`git push` or `git stash` in the user's app folder**, and never add a git remote. `sync` records the
|
|
15
|
+
folder's changes itself — and leaves out `.env` files, keys, `node_modules` and anything larger than
|
|
16
|
+
50 MB, which a hand-made commit would not. You may read with `git status`, `git diff` and `git log`
|
|
17
|
+
to describe a change; nothing else.
|
|
18
|
+
|
|
19
|
+
Every command accepts `--json` and prints exactly one JSON object on stdout. Exit codes:
|
|
20
|
+
`0` success · `1` local/usage error · `2` rejected or conflict · `3` outcome not yet known · `4` authentication.
|
|
21
|
+
|
|
22
|
+
## 1. Preflight (run once)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
publisher --version
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If the command is not found, tell the user the publisher is not installed and that one command
|
|
29
|
+
installs it: `npm install -g @minnovate/shadow-publisher` (it needs Node.js 22). Stop there.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
publisher doctor --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `gitOk:false` → tell the user Git 2.46 or later must be installed; stop.
|
|
36
|
+
- Otherwise do exactly what **`next.action`** says. `doctor` works this out for you, and the same
|
|
37
|
+
`next` object rides on `error.details` of any command that cannot resolve a connection — so you
|
|
38
|
+
never have to interpret `credentials`, `endpoint` or `profiles` yourself.
|
|
39
|
+
|
|
40
|
+
| `next.action` | What it means | What to do |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `"ready"` | Signed in and pointed somewhere. | **Nothing. Go to step 2 and do not mention any of this.** |
|
|
43
|
+
| `"sign-in"` | Not connected to anything yet. | **Ask them nothing** — `next.ask` is `null`. Just run `next.command` (`publisher login --json`). It finds the service, opens their browser and prints a code for them to confirm; tell them to expect that, and wait. Use `--portal <url>` only if they say they are on a different platform. |
|
|
44
|
+
| `"choose-profile"` | Several places are set up and none is marked usual. | Show `next.profiles` and ask `next.ask`. Then run `next.command` with the name they picked. |
|
|
45
|
+
| `"reauthenticate"` | The place is known; the credential is gone or expired. | Just run `next.command`. `next.ask` is `null` — do **not** ask them for anything; the browser sign-in asks them itself. |
|
|
46
|
+
| `"bind-endpoint"` | A token is set in the environment, but nothing said where it may go. | **Stop** and tell them publishing is blocked by a token set in this shell. `next.command` is `null` on purpose: never set `GIT_PUBLISHER_ENDPOINT` or pass `--endpoint` yourself — that is exactly the choice the check leaves to whoever set the token. |
|
|
47
|
+
|
|
48
|
+
**Never ask the user for a token, an endpoint, a URL or a credential — in any words.** They are
|
|
49
|
+
publishing an app; they do not know what those are, and you no longer need them: `login` discovers
|
|
50
|
+
where publishing lives and signs them in through their browser. `next.ask` is `null` whenever
|
|
51
|
+
nothing needs asking, which is now every case except choosing between connections they already
|
|
52
|
+
have; when it is not null it is already phrased for them, so pass it on rather than rewriting it.
|
|
53
|
+
|
|
54
|
+
If they name an environment ("publish to dev"), pass `--profile dev` on each command rather than
|
|
55
|
+
changing their default.
|
|
56
|
+
|
|
57
|
+
## 2. First publication of a folder
|
|
58
|
+
|
|
59
|
+
Run in the app folder (the folder the user is working in):
|
|
60
|
+
|
|
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.
|
|
75
|
+
|
|
76
|
+
## 3. Every update — and the first publish after `init`
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
publisher sync . --message "<a few words on what changed>" --json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
That one command records what changed in the folder, brings in anything published meanwhile (a
|
|
83
|
+
fast-forward or a clean merge — it never rewrites the user's work), and publishes the result. Write
|
|
84
|
+
the message in the user's words ("Add a contact page"), not git's.
|
|
85
|
+
|
|
86
|
+
## 4. Reading the result
|
|
87
|
+
|
|
88
|
+
`state` says what happened; `hint` is one sentence you may pass on as it is.
|
|
89
|
+
|
|
90
|
+
- `"published"` → the app was published. Mention what was recorded (`recorded.committed`) and, if
|
|
91
|
+
`recorded.excluded` is not empty, what was left out and why. Then tell them the portal shows the
|
|
92
|
+
app moving through **received → checked → going live → live**, and that it appears there within
|
|
93
|
+
a few minutes. (The publisher cannot follow those steps from here yet.)
|
|
94
|
+
- `"up_to_date"` → nothing new to publish; the service already has this version.
|
|
95
|
+
- `"conflict"` → the user's change and a published change touched the same lines of
|
|
96
|
+
`conflicts[]`. **Nothing was published and nothing was lost.** Do not resolve it on your own and
|
|
97
|
+
never publish with `--force`. Follow the `sync-app` skill's conflict section.
|
|
98
|
+
- `"rejected"` with `publication.rejection.code`:
|
|
99
|
+
- `NON_FAST_FORWARD` / `STALE_GENERATION` / `EXPECTED_OLD_MISMATCH`: somebody published in the
|
|
100
|
+
moment between the merge and the publish. Run `publisher sync . --json` once more
|
|
101
|
+
(`publication.next.command` says the same).
|
|
102
|
+
- `REF_FORBIDDEN`: the user may not publish this branch; tell them which branch and stop.
|
|
103
|
+
- `MISSING_OBJECTS`: the local copy is incomplete (for example a shallow clone). Tell the user to
|
|
104
|
+
publish from a complete copy: `publisher checkout <new folder> --repo <id>`.
|
|
105
|
+
- `LIMIT_EXCEEDED`, `PACK_INVALID`: quote the message; suggest removing very large files from the
|
|
106
|
+
folder if the message mentions size.
|
|
107
|
+
- `"unknown"` / exit code 3: the publication was sent but its outcome is not known yet. Run
|
|
108
|
+
`publisher status . --json`; it waits for and resolves interrupted publications without creating
|
|
109
|
+
duplicates. Repeat once if it still says `finalizing`.
|
|
110
|
+
- Exit code 4: run `publisher doctor --json` and follow `next.action` (normally `reauthenticate`:
|
|
111
|
+
run `next.command`). Never ask the user for a token.
|
|
112
|
+
|
|
113
|
+
## 5. Interrupted earlier?
|
|
114
|
+
|
|
115
|
+
If a previous publish was interrupted (network loss, closed session), `publisher status . --json`
|
|
116
|
+
resumes or resolves it. Always run it before starting a new sync when the last result was unknown.
|
|
117
|
+
|
|
118
|
+
## Rules
|
|
119
|
+
|
|
120
|
+
- `publisher sync` is the only way a change is saved and published. Never `git add`, `git commit`,
|
|
121
|
+
`git push`, `git stash`, `git reset`, `git rebase`, and never modify remotes.
|
|
122
|
+
- Never print tokens or signed URLs. The publisher already redacts them.
|
|
123
|
+
- Never publish with `--force` unless the user explicitly asks to overwrite the published history
|
|
124
|
+
and understands the other side's changes will be discarded.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sync-app
|
|
3
|
+
description: Bring the published (possibly engineered) version of the user's app into their folder, together with their own changes, and publish the result. Use when the user says "get the latest version", "sync my app", "sync with the engineering changes", when a publish was refused because somebody else published first, or when a sync reported a conflict. Never overwrites local work and never uses git to commit.
|
|
4
|
+
allowed-tools: Bash(publisher *), Bash(git status *), Bash(git log *), Bash(git diff *)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Keep the user's app in step with what is published
|
|
8
|
+
|
|
9
|
+
**`publisher sync` does all of it**: it records the user's changes, brings in what was published
|
|
10
|
+
meanwhile (a fast-forward, or a clean merge that never rewrites their work), and publishes the
|
|
11
|
+
result. Never run `git add`, `git commit`, `git push`, `git pull`, `git merge`, `git stash`,
|
|
12
|
+
`git reset` or `git rebase` in the user's app folder.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
publisher sync . --json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Read `state` exactly as the `publish-app` skill describes (step 4). `merged` says whether anything
|
|
19
|
+
published elsewhere was brought in (`"fast-forward"` or `"merge"`); mention it when it is not null.
|
|
20
|
+
|
|
21
|
+
## Just looking
|
|
22
|
+
|
|
23
|
+
To say where the folder stands without changing anything:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
publisher workspace-status . --json # state + hint; exit 2 means conflict
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| state | meaning | what to do |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| `up_to_date` | identical to the published version | nothing |
|
|
32
|
+
| `local_changes` | the user changed something the service does not have yet | `publisher sync` |
|
|
33
|
+
| `remote_ahead` | something was published elsewhere; the folder is behind | `publisher sync` |
|
|
34
|
+
| `diverged_clean_merge` | both changed, and the changes combine cleanly | `publisher sync` |
|
|
35
|
+
| `conflict` | both changed the same lines (`conflicts[]`) | see below |
|
|
36
|
+
| `detached` | the folder is not on the branch it publishes | ask the user before anything else |
|
|
37
|
+
|
|
38
|
+
`pending[]` lists interrupted publications; run `publisher status . --json` first when it is not empty.
|
|
39
|
+
|
|
40
|
+
## A fresh copy
|
|
41
|
+
|
|
42
|
+
When the user wants their app in a new folder (a new computer, a clean start):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
publisher checkout <new-folder> --repo <repoId> --json # a NEW folder only; refuses a non-empty one
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Conflicts
|
|
49
|
+
|
|
50
|
+
A `conflict` means the user's version and the published one both changed the same lines of the
|
|
51
|
+
files in `conflicts[]`. **Nothing was published and nothing was lost**: the user's change is
|
|
52
|
+
recorded locally, and the published version is still on the service.
|
|
53
|
+
|
|
54
|
+
1. List the files and say, in plain words, what each side changed there (`git diff` and `git log`
|
|
55
|
+
may help you describe it).
|
|
56
|
+
2. Ask the user which version to keep for each file, or whether to combine them. Do not decide for
|
|
57
|
+
them and never publish with `--force` — that discards the other side's work.
|
|
58
|
+
3. Only after they decide, put both versions into the files, marked:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
publisher sync . --mark-conflicts --json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Each file in `conflicts[]` now holds both versions between `<<<<<<<`, `=======` and `>>>>>>>`
|
|
65
|
+
lines. Edit each to what they chose, and remove those lines.
|
|
66
|
+
4. Run `publisher sync . --json`. It records the edit and publishes it. If a file still carries the
|
|
67
|
+
marker lines it refuses, names the files (`error.details.marked`) and publishes nothing; finish
|
|
68
|
+
the edit and run it again.
|
|
69
|
+
|
|
70
|
+
No git command is needed at any point, including here.
|
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),
|
|
@@ -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,17 +4493,205 @@ async function updateEntry(url2, change) {
|
|
|
4419
4493
|
await writeCredentials(file);
|
|
4420
4494
|
}
|
|
4421
4495
|
|
|
4496
|
+
// ../client/src/onboard.ts
|
|
4497
|
+
var import_promises6 = require("node:fs/promises");
|
|
4498
|
+
var import_node_path5 = require("node:path");
|
|
4499
|
+
var DEFAULT_EXCLUDED_DIRS = [
|
|
4500
|
+
".git",
|
|
4501
|
+
"node_modules",
|
|
4502
|
+
".venv",
|
|
4503
|
+
"venv",
|
|
4504
|
+
"__pycache__",
|
|
4505
|
+
".cache",
|
|
4506
|
+
".next",
|
|
4507
|
+
".nuxt",
|
|
4508
|
+
".turbo",
|
|
4509
|
+
".parcel-cache",
|
|
4510
|
+
".aws",
|
|
4511
|
+
".ssh",
|
|
4512
|
+
".gnupg",
|
|
4513
|
+
".idea",
|
|
4514
|
+
".terraform",
|
|
4515
|
+
".pytest_cache",
|
|
4516
|
+
".mypy_cache",
|
|
4517
|
+
"coverage",
|
|
4518
|
+
".DS_Store"
|
|
4519
|
+
];
|
|
4520
|
+
var DEFAULT_EXCLUDED_FILES = [
|
|
4521
|
+
/^\.env(\..*)?$/,
|
|
4522
|
+
/\.(pem|key|p12|pfx|jks|keystore)$/i,
|
|
4523
|
+
/^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/,
|
|
4524
|
+
/^\.npmrc$/,
|
|
4525
|
+
/^\.pypirc$/,
|
|
4526
|
+
/^\.netrc$/,
|
|
4527
|
+
/\.(log|tmp|swp)$/i,
|
|
4528
|
+
/^\.DS_Store$/,
|
|
4529
|
+
/^Thumbs\.db$/i,
|
|
4530
|
+
/^credentials(\.json)?$/i,
|
|
4531
|
+
/^service-account.*\.json$/i,
|
|
4532
|
+
/^\.git(modules|attributes)?$/
|
|
4533
|
+
];
|
|
4534
|
+
async function onboardFolder(opts) {
|
|
4535
|
+
const dir = opts.dir;
|
|
4536
|
+
const policy = opts.policy ?? {};
|
|
4537
|
+
const maxBytes = policy.maxFileBytes ?? 50 * 1024 * 1024;
|
|
4538
|
+
let initialized = false;
|
|
4539
|
+
if (await isGitRepo(dir)) {
|
|
4540
|
+
const head = await headCommit(dir);
|
|
4541
|
+
if (head && !policy.allowExistingHistory) {
|
|
4542
|
+
throw new PublisherError("LOCAL_STATE", "folder is already a Git repository with history; commit your changes and use publish instead of init");
|
|
4543
|
+
}
|
|
4544
|
+
} else {
|
|
4545
|
+
await initWorkRepo(dir, opts.branch ?? "main");
|
|
4546
|
+
initialized = true;
|
|
4547
|
+
}
|
|
4548
|
+
const excluded = [];
|
|
4549
|
+
const candidates = [];
|
|
4550
|
+
const extra = policy.exclude ?? [];
|
|
4551
|
+
const walk = async (d) => {
|
|
4552
|
+
for (const e of await (0, import_promises6.readdir)(d, { withFileTypes: true })) {
|
|
4553
|
+
if (opts.signal?.aborted) throw new PublisherError("CANCELLED", "cancelled");
|
|
4554
|
+
const full = (0, import_node_path5.join)(d, e.name);
|
|
4555
|
+
const rel = (0, import_node_path5.relative)(dir, full).split(import_node_path5.sep).join("/");
|
|
4556
|
+
if (e.isSymbolicLink()) {
|
|
4557
|
+
excluded.push({ path: rel, reason: "symbolic link" });
|
|
4558
|
+
continue;
|
|
4559
|
+
}
|
|
4560
|
+
if (e.isDirectory()) {
|
|
4561
|
+
if (excludedDir(rel, e.name, extra)) {
|
|
4562
|
+
excluded.push({ path: rel + "/", reason: "excluded by policy" });
|
|
4563
|
+
continue;
|
|
4564
|
+
}
|
|
4565
|
+
await walk(full);
|
|
4566
|
+
continue;
|
|
4567
|
+
}
|
|
4568
|
+
if (!e.isFile()) continue;
|
|
4569
|
+
if (excludedFile(rel, e.name, extra)) {
|
|
4570
|
+
excluded.push({ path: rel, reason: "excluded by policy" });
|
|
4571
|
+
continue;
|
|
4572
|
+
}
|
|
4573
|
+
const st = await (0, import_promises6.stat)(full);
|
|
4574
|
+
if (st.size > maxBytes) {
|
|
4575
|
+
excluded.push({ path: rel, reason: `larger than ${maxBytes} bytes` });
|
|
4576
|
+
continue;
|
|
4577
|
+
}
|
|
4578
|
+
candidates.push(rel);
|
|
4579
|
+
}
|
|
4580
|
+
};
|
|
4581
|
+
await walk(dir);
|
|
4582
|
+
const ignored = await checkIgnore(dir, candidates);
|
|
4583
|
+
const selected = candidates.filter((p) => !ignored.has(p)).sort();
|
|
4584
|
+
for (const p of ignored) excluded.push({ path: p, reason: "ignored by .gitignore" });
|
|
4585
|
+
if (selected.length === 0) return { repoDir: dir, commit: null, committed: [], excluded, initialized };
|
|
4586
|
+
const commit = await commitPaths(dir, selected, {
|
|
4587
|
+
message: opts.message ?? "Initial publication",
|
|
4588
|
+
...opts.authorName ? { authorName: opts.authorName } : {},
|
|
4589
|
+
...opts.authorEmail ? { authorEmail: opts.authorEmail } : {},
|
|
4590
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
4591
|
+
});
|
|
4592
|
+
return { repoDir: dir, commit, committed: selected, excluded, initialized };
|
|
4593
|
+
}
|
|
4594
|
+
function excludedDir(rel, name, extra) {
|
|
4595
|
+
return DEFAULT_EXCLUDED_DIRS.includes(name) || extra.some((p) => matches(rel, name, p));
|
|
4596
|
+
}
|
|
4597
|
+
function excludedFile(rel, name, extra) {
|
|
4598
|
+
return DEFAULT_EXCLUDED_FILES.some((re) => re.test(name)) || extra.some((p) => matches(rel, name, p));
|
|
4599
|
+
}
|
|
4600
|
+
async function recordChanges(opts) {
|
|
4601
|
+
const dir = opts.dir;
|
|
4602
|
+
if (!await isGitRepo(dir)) throw new PublisherError("LOCAL_STATE", "this folder is not published yet; run publisher init first");
|
|
4603
|
+
const maxBytes = opts.policy?.maxFileBytes ?? 50 * 1024 * 1024;
|
|
4604
|
+
const extra = opts.policy?.exclude ?? [];
|
|
4605
|
+
const excluded = [];
|
|
4606
|
+
const selected = [];
|
|
4607
|
+
const entries = await statusPorcelain(dir);
|
|
4608
|
+
if (await mergeInProgress(dir)) {
|
|
4609
|
+
const marked = [];
|
|
4610
|
+
for (const { code, path } of entries) {
|
|
4611
|
+
if (!UNMERGED.has(code)) continue;
|
|
4612
|
+
const text = await (0, import_promises6.readFile)((0, import_node_path5.join)(dir, path), "utf8").catch(() => "");
|
|
4613
|
+
if (/^<{7} /m.test(text) && /^>{7} /m.test(text)) marked.push(path);
|
|
4614
|
+
}
|
|
4615
|
+
if (marked.length)
|
|
4616
|
+
throw new PublisherError(
|
|
4617
|
+
"LOCAL_STATE",
|
|
4618
|
+
`still marked as conflicting: ${marked.join(", ")}; keep one version (or both) in each, remove the <<<<<<< ======= >>>>>>> lines, then sync again`,
|
|
4619
|
+
{ details: { marked } }
|
|
4620
|
+
);
|
|
4621
|
+
}
|
|
4622
|
+
for (const { code, path } of entries) {
|
|
4623
|
+
if (code !== "??") {
|
|
4624
|
+
selected.push(path);
|
|
4625
|
+
continue;
|
|
4626
|
+
}
|
|
4627
|
+
const parts = path.split("/");
|
|
4628
|
+
const name = parts[parts.length - 1];
|
|
4629
|
+
const dirs = parts.slice(0, -1);
|
|
4630
|
+
const inExcludedDir = dirs.findIndex((d, i) => excludedDir(dirs.slice(0, i + 1).join("/"), d, extra));
|
|
4631
|
+
if (inExcludedDir >= 0) {
|
|
4632
|
+
excluded.push({ path: dirs.slice(0, inExcludedDir + 1).join("/") + "/", reason: "excluded by policy" });
|
|
4633
|
+
continue;
|
|
4634
|
+
}
|
|
4635
|
+
if (excludedFile(path, name, extra)) {
|
|
4636
|
+
excluded.push({ path, reason: "excluded by policy" });
|
|
4637
|
+
continue;
|
|
4638
|
+
}
|
|
4639
|
+
const st = await (0, import_promises6.lstat)((0, import_node_path5.join)(dir, path));
|
|
4640
|
+
if (st.isSymbolicLink()) {
|
|
4641
|
+
excluded.push({ path, reason: "symbolic link" });
|
|
4642
|
+
continue;
|
|
4643
|
+
}
|
|
4644
|
+
if (st.size > maxBytes) {
|
|
4645
|
+
excluded.push({ path, reason: `larger than ${maxBytes} bytes` });
|
|
4646
|
+
continue;
|
|
4647
|
+
}
|
|
4648
|
+
selected.push(path);
|
|
4649
|
+
}
|
|
4650
|
+
const unique = dedupe(excluded);
|
|
4651
|
+
if (selected.length === 0) return { repoDir: dir, commit: null, committed: [], excluded: unique, initialized: false };
|
|
4652
|
+
const commit = await commitPaths(dir, selected.sort(), {
|
|
4653
|
+
message: opts.message ?? "Update",
|
|
4654
|
+
...opts.authorName ? { authorName: opts.authorName } : {},
|
|
4655
|
+
...opts.authorEmail ? { authorEmail: opts.authorEmail } : {},
|
|
4656
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
4657
|
+
});
|
|
4658
|
+
return { repoDir: dir, commit, committed: selected, excluded: unique, initialized: false };
|
|
4659
|
+
}
|
|
4660
|
+
var UNMERGED = /* @__PURE__ */ new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
|
|
4661
|
+
async function mergeInProgress(dir) {
|
|
4662
|
+
return (await runGit(["rev-parse", "-q", "--verify", "MERGE_HEAD"], { cwd: dir, allowFailure: true })).code === 0;
|
|
4663
|
+
}
|
|
4664
|
+
function dedupe(excluded) {
|
|
4665
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4666
|
+
return excluded.filter((e) => seen.has(e.path) ? false : (seen.add(e.path), true));
|
|
4667
|
+
}
|
|
4668
|
+
function matches(rel, base, pattern) {
|
|
4669
|
+
if (pattern.includes("*")) {
|
|
4670
|
+
const re = new RegExp(
|
|
4671
|
+
"^" + pattern.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
|
|
4672
|
+
);
|
|
4673
|
+
return re.test(base) || re.test(rel);
|
|
4674
|
+
}
|
|
4675
|
+
return base === pattern || rel === pattern || rel.startsWith(pattern.replace(/\/$/, "") + "/");
|
|
4676
|
+
}
|
|
4677
|
+
async function checkIgnore(dir, paths) {
|
|
4678
|
+
if (paths.length === 0) return /* @__PURE__ */ new Set();
|
|
4679
|
+
const r = await runGit(["check-ignore", "--stdin", "-z"], { cwd: dir, input: paths.join("\0") + "\0", allowFailure: true });
|
|
4680
|
+
if (r.code !== 0) return /* @__PURE__ */ new Set();
|
|
4681
|
+
return new Set(r.stdout.toString("utf8").split("\0").filter(Boolean));
|
|
4682
|
+
}
|
|
4683
|
+
|
|
4422
4684
|
// ../client/src/fetch.ts
|
|
4423
4685
|
var import_node_fs5 = require("node:fs");
|
|
4424
|
-
var
|
|
4686
|
+
var import_promises8 = require("node:fs/promises");
|
|
4425
4687
|
var import_node_os3 = require("node:os");
|
|
4426
|
-
var
|
|
4427
|
-
var
|
|
4688
|
+
var import_node_path6 = require("node:path");
|
|
4689
|
+
var import_promises9 = require("node:stream/promises");
|
|
4428
4690
|
var import_node_stream2 = require("node:stream");
|
|
4429
4691
|
var import_node_crypto4 = require("node:crypto");
|
|
4430
4692
|
|
|
4431
4693
|
// ../client/src/publish.ts
|
|
4432
|
-
var
|
|
4694
|
+
var import_promises7 = require("node:fs/promises");
|
|
4433
4695
|
|
|
4434
4696
|
// ../client/src/transport.ts
|
|
4435
4697
|
function assertCredentialEndpoint(endpoint) {
|
|
@@ -4614,7 +4876,7 @@ async function publish(ctx, repoId, remote, opts, call) {
|
|
|
4614
4876
|
} catch (e) {
|
|
4615
4877
|
if (!(e instanceof PublisherError && e.code === "LOCAL_STATE")) throw e;
|
|
4616
4878
|
}
|
|
4617
|
-
if (remaining.length === 0) return toOutcome(outcome, true);
|
|
4879
|
+
if (remaining.length === 0) return toOutcome(outcome, true, pending2.refUpdates);
|
|
4618
4880
|
return publish(ctx, repoId, remote, opts, call);
|
|
4619
4881
|
}
|
|
4620
4882
|
}
|
|
@@ -4690,7 +4952,7 @@ async function runPublication(ctx, repoId, remote, repoDir, store2, pending, opt
|
|
|
4690
4952
|
}
|
|
4691
4953
|
if (opts.wait === false) {
|
|
4692
4954
|
summary = summary ?? await ctx.getStatus(repoId, pending.publicationId, call);
|
|
4693
|
-
return toOutcome(summary, resumed);
|
|
4955
|
+
return toOutcome(summary, resumed, pending.refUpdates);
|
|
4694
4956
|
}
|
|
4695
4957
|
progress({ phase: "validate", message: "waiting for validation" });
|
|
4696
4958
|
summary = await waitForOutcome(ctx, repoId, pending.publicationId, opts.waitTimeoutMs ?? 10 * 6e4, call);
|
|
@@ -4700,9 +4962,9 @@ async function runPublication(ctx, repoId, remote, repoDir, store2, pending, opt
|
|
|
4700
4962
|
progress({ phase: "done", message: `published generation ${summary.resultGeneration}` });
|
|
4701
4963
|
} else if (summary.state === "rejected") {
|
|
4702
4964
|
await store2.remove(pending.publicationId);
|
|
4703
|
-
if (pending.pack) await (0,
|
|
4965
|
+
if (pending.pack) await (0, import_promises7.rm)(pending.pack.path, { force: true });
|
|
4704
4966
|
}
|
|
4705
|
-
return toOutcome(summary, resumed);
|
|
4967
|
+
return toOutcome(summary, resumed, pending.refUpdates);
|
|
4706
4968
|
}
|
|
4707
4969
|
async function waitForOutcome(ctx, repoId, publicationId, timeoutMs, call) {
|
|
4708
4970
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -4749,7 +5011,7 @@ async function resolvePending(ctx, repoDir, remote, pending, call, store2) {
|
|
|
4749
5011
|
}
|
|
4750
5012
|
return summary;
|
|
4751
5013
|
}
|
|
4752
|
-
function toOutcome(s, resumed) {
|
|
5014
|
+
function toOutcome(s, resumed, updates) {
|
|
4753
5015
|
return {
|
|
4754
5016
|
publicationId: s.publicationId,
|
|
4755
5017
|
repoId: s.repoId,
|
|
@@ -4758,10 +5020,23 @@ function toOutcome(s, resumed) {
|
|
|
4758
5020
|
resultGeneration: s.resultGeneration,
|
|
4759
5021
|
refs: s.refs,
|
|
4760
5022
|
rejection: s.rejection,
|
|
5023
|
+
next: publishNext(s.rejection, updates[0]?.ref ?? null),
|
|
4761
5024
|
resumed,
|
|
4762
5025
|
summary: s
|
|
4763
5026
|
};
|
|
4764
5027
|
}
|
|
5028
|
+
var REF_MOVED = /* @__PURE__ */ new Set(["NON_FAST_FORWARD", "EXPECTED_OLD_MISMATCH", "STALE_GENERATION"]);
|
|
5029
|
+
function publishNext(rejection, published) {
|
|
5030
|
+
if (rejection === null || !REF_MOVED.has(rejection.code)) return null;
|
|
5031
|
+
const named = rejection.details?.["ref"];
|
|
5032
|
+
const ref = typeof named === "string" ? named : published;
|
|
5033
|
+
const name = ref?.replace(/^refs\/heads\//, "") ?? "main";
|
|
5034
|
+
return {
|
|
5035
|
+
action: "sync-and-publish",
|
|
5036
|
+
ask: `${name} moved since your last fetch: someone else published to it. Sync brings those changes in without rewriting yours, and publishes the result.`,
|
|
5037
|
+
command: `publisher sync`
|
|
5038
|
+
};
|
|
5039
|
+
}
|
|
4765
5040
|
async function defaultUpdate(repoDir) {
|
|
4766
5041
|
const branch = await currentBranch(repoDir);
|
|
4767
5042
|
if (!branch) throw new PublisherError("LOCAL_STATE", "detached HEAD: specify which ref to publish");
|
|
@@ -4807,15 +5082,15 @@ function trackingRef(remote, ref) {
|
|
|
4807
5082
|
// ../client/src/fetch.ts
|
|
4808
5083
|
async function installedManifest(gd) {
|
|
4809
5084
|
try {
|
|
4810
|
-
const parsed = JSON.parse(await (0,
|
|
5085
|
+
const parsed = JSON.parse(await (0, import_promises8.readFile)((0, import_node_path6.join)(gd, "publisher", "installed.json"), "utf8"));
|
|
4811
5086
|
return new Set(Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : []);
|
|
4812
5087
|
} catch {
|
|
4813
5088
|
return /* @__PURE__ */ new Set();
|
|
4814
5089
|
}
|
|
4815
5090
|
}
|
|
4816
5091
|
async function writeManifest(gd, installed) {
|
|
4817
|
-
await (0,
|
|
4818
|
-
await (0,
|
|
5092
|
+
await (0, import_promises8.mkdir)((0, import_node_path6.join)(gd, "publisher"), { recursive: true });
|
|
5093
|
+
await (0, import_promises8.writeFile)((0, import_node_path6.join)(gd, "publisher", "installed.json"), JSON.stringify(installed));
|
|
4819
5094
|
}
|
|
4820
5095
|
async function fetchRepository(ctx, repoId, opts, call) {
|
|
4821
5096
|
const remote = opts.remote ?? "publisher";
|
|
@@ -4862,13 +5137,14 @@ async function haveAll(repoDir, refs) {
|
|
|
4862
5137
|
async function install(ctx, gd, artifacts, call) {
|
|
4863
5138
|
if (artifacts.length === 0) return 0;
|
|
4864
5139
|
const progress = call.onProgress ?? (() => void 0);
|
|
4865
|
-
const tmp = await (0,
|
|
5140
|
+
const tmp = await (0, import_promises8.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "gitpub-fetch-"));
|
|
4866
5141
|
let bytes = 0;
|
|
5142
|
+
const total = artifacts.reduce((sum, a) => sum + a.size, 0);
|
|
4867
5143
|
try {
|
|
4868
5144
|
for (const a of artifacts) {
|
|
4869
|
-
const file = (0,
|
|
5145
|
+
const file = (0, import_node_path6.join)(tmp, `${a.sha256}.${a.kind}`);
|
|
4870
5146
|
bytes += await download(ctx.fetchImpl, a.url, file, a.sha256, a.size, call);
|
|
4871
|
-
progress({ phase: "fetch", message: `installing ${a.kind}`, bytes, total
|
|
5147
|
+
progress({ phase: "fetch", message: `installing ${a.kind}`, bytes, total });
|
|
4872
5148
|
if (a.kind === "bundle") {
|
|
4873
5149
|
await bundleUnbundleObjects(gd, file);
|
|
4874
5150
|
} else {
|
|
@@ -4876,7 +5152,7 @@ async function install(ctx, gd, artifacts, call) {
|
|
|
4876
5152
|
}
|
|
4877
5153
|
}
|
|
4878
5154
|
} finally {
|
|
4879
|
-
await (0,
|
|
5155
|
+
await (0, import_promises8.rm)(tmp, { recursive: true, force: true });
|
|
4880
5156
|
}
|
|
4881
5157
|
return bytes;
|
|
4882
5158
|
}
|
|
@@ -4916,7 +5192,7 @@ async function download(fetchImpl, url2, dest, sha256, size, call) {
|
|
|
4916
5192
|
controller.enqueue(chunk);
|
|
4917
5193
|
}
|
|
4918
5194
|
});
|
|
4919
|
-
await (0,
|
|
5195
|
+
await (0, import_promises9.pipeline)(import_node_stream2.Readable.fromWeb(res.body.pipeThrough(counted)), (0, import_node_fs5.createWriteStream)(dest));
|
|
4920
5196
|
if (n !== size || hash.digest("hex") !== sha256) throw new PublisherError("NETWORK", "downloaded artifact failed checksum verification", { retryable: true });
|
|
4921
5197
|
return n;
|
|
4922
5198
|
}
|
|
@@ -4924,6 +5200,11 @@ async function download(fetchImpl, url2, dest, sha256, size, call) {
|
|
|
4924
5200
|
// ../client/src/publisher.ts
|
|
4925
5201
|
var CLIENT_NAME = "git-publisher-client";
|
|
4926
5202
|
var CLIENT_VERSION = "0.1.0";
|
|
5203
|
+
async function markConflicts(dir, remote) {
|
|
5204
|
+
const who = { GIT_AUTHOR_NAME: "Publisher", GIT_AUTHOR_EMAIL: "publisher@localhost", GIT_COMMITTER_NAME: "Publisher", GIT_COMMITTER_EMAIL: "publisher@localhost" };
|
|
5205
|
+
const r = await runGit(["merge", "--no-ff", "--no-commit", "--no-verify", remote], { cwd: dir, env: { extra: who }, allowFailure: true });
|
|
5206
|
+
if (r.code !== 0 && !await mergeInProgress(dir)) throw new PublisherError("GIT", `could not mark the conflicts: ${r.stderr.trim()}`);
|
|
5207
|
+
}
|
|
4927
5208
|
var Publisher = class {
|
|
4928
5209
|
constructor(opts) {
|
|
4929
5210
|
this.opts = opts;
|
|
@@ -5022,6 +5303,10 @@ var Publisher = class {
|
|
|
5022
5303
|
async checkout(options, call = {}) {
|
|
5023
5304
|
return checkoutWorkspace(this, options, call);
|
|
5024
5305
|
}
|
|
5306
|
+
/** Make an associated folder a workspace (idempotent); `init` and `sync` both call it. */
|
|
5307
|
+
async adoptWorkspace(directory, call = {}) {
|
|
5308
|
+
return adoptWorkspace(this, directory, call);
|
|
5309
|
+
}
|
|
5025
5310
|
/** Offline: the persisted workspace mapping, or null. */
|
|
5026
5311
|
async workspace(directory) {
|
|
5027
5312
|
return getWorkspaceMapping(directory);
|
|
@@ -5034,6 +5319,37 @@ var Publisher = class {
|
|
|
5034
5319
|
async sync(options, call = {}) {
|
|
5035
5320
|
return syncWorkspace(this, options, call);
|
|
5036
5321
|
}
|
|
5322
|
+
/**
|
|
5323
|
+
* A creator's whole round trip in one call, so nobody runs git: record their changes, bring in
|
|
5324
|
+
* what the service has (fast-forward or a clean merge, never a rewrite), and publish. It stops,
|
|
5325
|
+
* having published nothing, on a conflict; the recorded commit stays, so nothing is lost.
|
|
5326
|
+
*
|
|
5327
|
+
* Everything that can refuse without touching the folder refuses first: a folder with no
|
|
5328
|
+
* association (or one pinned elsewhere) and a HEAD off the workspace branch would otherwise get
|
|
5329
|
+
* a commit and then an error. `syncWorkspace` itself still never commits or publishes; this is
|
|
5330
|
+
* the composition, and the only one.
|
|
5331
|
+
*/
|
|
5332
|
+
async syncChanges(options, call = {}) {
|
|
5333
|
+
const dir = options.directory;
|
|
5334
|
+
await this.resolveRepo(dir);
|
|
5335
|
+
const branch = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
|
|
5336
|
+
if (!branch) throw new PublisherError("LOCAL_STATE", "HEAD is detached; check out the branch this folder publishes, then run sync again");
|
|
5337
|
+
const mapped = await getWorkspaceMapping(dir);
|
|
5338
|
+
if (mapped && mapped.branch !== branch)
|
|
5339
|
+
throw new PublisherError("LOCAL_STATE", `branch ${branch} is checked out, but this folder publishes ${mapped.branch}; check it out, then run sync again`);
|
|
5340
|
+
const recorded = await recordChanges({ dir, ...options.message ? { message: options.message } : {} });
|
|
5341
|
+
const reconciled = await syncWorkspace(this, { directory: dir, apply: true }, call);
|
|
5342
|
+
const base = { recorded: { commit: recorded.commit, committed: recorded.committed, excluded: recorded.excluded }, reconciled };
|
|
5343
|
+
if (reconciled.after === "conflict") {
|
|
5344
|
+
const remote = reconciled.before.remote?.oid ?? null;
|
|
5345
|
+
if (options.markConflicts && remote) await markConflicts(dir, remote);
|
|
5346
|
+
return { ...base, state: "conflict", published: null, marked: options.markConflicts === true && remote !== null };
|
|
5347
|
+
}
|
|
5348
|
+
const tip = await revParse(dir, `refs/heads/${branch}`);
|
|
5349
|
+
if (tip === null || tip === (reconciled.before.remote?.oid ?? null)) return { ...base, state: "up_to_date", published: null, marked: false };
|
|
5350
|
+
const published = await this.publish({ repoDir: dir }, call);
|
|
5351
|
+
return { ...base, state: published.state === "accepted" ? "published" : published.state === "rejected" ? "rejected" : "unknown", published, marked: false };
|
|
5352
|
+
}
|
|
5037
5353
|
/** Resolve interrupted publications recorded locally without creating duplicates. */
|
|
5038
5354
|
async resumePublications(repoDir, call = {}) {
|
|
5039
5355
|
const { remote } = await this.resolveRepo(repoDir);
|
|
@@ -5051,7 +5367,7 @@ var Publisher = class {
|
|
|
5051
5367
|
call.onProgress?.({ phase: "validate", message: `waiting for ${p.publicationId}` });
|
|
5052
5368
|
s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...call, waitSeconds: MAX_WAIT_SECONDS }, store2);
|
|
5053
5369
|
}
|
|
5054
|
-
out.push(toOutcome(s, true));
|
|
5370
|
+
out.push(toOutcome(s, true, p.refUpdates));
|
|
5055
5371
|
}
|
|
5056
5372
|
for (const o of out) await noteAcceptedPublication(repoDir, o);
|
|
5057
5373
|
return out;
|
|
@@ -5087,120 +5403,6 @@ function createPublisher(opts) {
|
|
|
5087
5403
|
return new Publisher(opts);
|
|
5088
5404
|
}
|
|
5089
5405
|
|
|
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
5406
|
// ../client/src/migrate.ts
|
|
5205
5407
|
async function fetchCapabilities(endpoint, fetchImpl = fetch, signal) {
|
|
5206
5408
|
const res = await fetchImpl(`${normalize(endpoint)}/v1/capabilities`, { method: "GET", ...signal ? { signal } : {} });
|
|
@@ -5420,7 +5622,9 @@ var OPTIONS = {
|
|
|
5420
5622
|
quiet: { type: "boolean" },
|
|
5421
5623
|
force: { type: "boolean" },
|
|
5422
5624
|
apply: { type: "boolean" },
|
|
5625
|
+
"mark-conflicts": { type: "boolean" },
|
|
5423
5626
|
help: { type: "boolean" },
|
|
5627
|
+
version: { type: "boolean" },
|
|
5424
5628
|
"set-default": { type: "boolean" },
|
|
5425
5629
|
// A flag of its own, not a negation of --wait: there is no --wait.
|
|
5426
5630
|
"no-wait": { type: "boolean" }
|
|
@@ -5434,10 +5638,67 @@ function parseArgs(argv) {
|
|
|
5434
5638
|
}
|
|
5435
5639
|
}
|
|
5436
5640
|
|
|
5641
|
+
// src/skills.ts
|
|
5642
|
+
var import_node_child_process3 = require("node:child_process");
|
|
5643
|
+
var import_node_fs6 = require("node:fs");
|
|
5644
|
+
var import_node_path7 = require("node:path");
|
|
5645
|
+
var MARKETPLACE = "shadow-publisher";
|
|
5646
|
+
var PLUGIN = "git-publisher";
|
|
5647
|
+
function packageRoot() {
|
|
5648
|
+
return (0, import_node_path7.join)((0, import_node_path7.dirname)((0, import_node_fs6.realpathSync)(process.argv[1])), "..");
|
|
5649
|
+
}
|
|
5650
|
+
function packageVersion() {
|
|
5651
|
+
try {
|
|
5652
|
+
return JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path7.join)(packageRoot(), "package.json"), "utf8")).version;
|
|
5653
|
+
} catch {
|
|
5654
|
+
return "unknown";
|
|
5655
|
+
}
|
|
5656
|
+
}
|
|
5657
|
+
function claude(args) {
|
|
5658
|
+
return new Promise((resolve3) => {
|
|
5659
|
+
let stdout = "";
|
|
5660
|
+
let stderr = "";
|
|
5661
|
+
const child = (0, import_node_child_process3.spawn)("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5662
|
+
child.stdout.on("data", (d) => stdout += d.toString("utf8"));
|
|
5663
|
+
child.stderr.on("data", (d) => stderr += d.toString("utf8"));
|
|
5664
|
+
child.on("error", (e) => resolve3({ code: null, stdout, stderr: e.message, missing: e.code === "ENOENT" }));
|
|
5665
|
+
child.on("close", (code) => resolve3({ code, stdout, stderr, missing: false }));
|
|
5666
|
+
});
|
|
5667
|
+
}
|
|
5668
|
+
async function installSkills({ log }) {
|
|
5669
|
+
const source = (0, import_node_path7.join)(packageRoot(), "dist", "claude");
|
|
5670
|
+
if (!(0, import_node_fs6.existsSync)((0, import_node_path7.join)(source, ".claude-plugin", "marketplace.json"))) {
|
|
5671
|
+
throw new PublisherError("LOCAL_STATE", `this publisher carries no skills at ${source}; reinstall it with: npm install -g @minnovate/shadow-publisher`);
|
|
5672
|
+
}
|
|
5673
|
+
const step = async (what, args) => {
|
|
5674
|
+
log(what);
|
|
5675
|
+
const r = await claude(args);
|
|
5676
|
+
if (r.missing) {
|
|
5677
|
+
throw new PublisherError(
|
|
5678
|
+
"LOCAL_STATE",
|
|
5679
|
+
'Claude Code is not installed, or "claude" is not on PATH. Install it from https://claude.com/claude-code, then run: publisher skills install'
|
|
5680
|
+
);
|
|
5681
|
+
}
|
|
5682
|
+
if (r.code !== 0) throw new PublisherError("LOCAL_STATE", `"claude ${args.slice(0, 3).join(" ")}" failed: ${(r.stderr || r.stdout).trim()}`);
|
|
5683
|
+
};
|
|
5684
|
+
await step("Adding the publisher to Claude Code\u2026", ["plugin", "marketplace", "add", source, "--scope", "user"]);
|
|
5685
|
+
await step("Installing its skills\u2026", ["plugin", "install", `${PLUGIN}@${MARKETPLACE}`, "--scope", "user"]);
|
|
5686
|
+
return {
|
|
5687
|
+
ok: true,
|
|
5688
|
+
plugin: `${PLUGIN}@${MARKETPLACE}`,
|
|
5689
|
+
marketplace: MARKETPLACE,
|
|
5690
|
+
source,
|
|
5691
|
+
version: packageVersion(),
|
|
5692
|
+
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."
|
|
5693
|
+
};
|
|
5694
|
+
}
|
|
5695
|
+
|
|
5437
5696
|
// src/main.ts
|
|
5438
5697
|
var USAGE = `publisher \u2014 publish an app's Git history to the serverless Git backend
|
|
5439
5698
|
|
|
5440
5699
|
Usage:
|
|
5700
|
+
publisher skills install add the publisher's skills to Claude Code (user scope);
|
|
5701
|
+
run it again after updating the publisher
|
|
5441
5702
|
publisher login sign in. With NO arguments it asks the
|
|
5442
5703
|
portal where publishing lives, opens a
|
|
5443
5704
|
browser to confirm who you are, and asks
|
|
@@ -5447,19 +5708,29 @@ Usage:
|
|
|
5447
5708
|
[--profile <name>] [--set-default] [--token <token>]
|
|
5448
5709
|
publisher logout (--endpoint <url> | --profile <name>)
|
|
5449
5710
|
publisher profile list | profile use <name> | profile remove <name>
|
|
5450
|
-
publisher init [dir] [--name <name>] [--repo <id>] create (or attach to) a repository
|
|
5451
|
-
|
|
5711
|
+
publisher init [dir] [--name <name>] [--repo <id>] create (or attach to) a repository, associate this folder
|
|
5712
|
+
and record it as a workspace (so sync works here)
|
|
5713
|
+
publisher publish [dir] [--ref <ref>]... [--force] [--no-wait] publish commits as they are; to save and publish
|
|
5714
|
+
a change, use sync
|
|
5452
5715
|
publisher status [dir] [--publication <id>] resolve pending/interrupted publications
|
|
5453
5716
|
publisher fetch [dir]
|
|
5454
5717
|
publisher checkout <dir> --repo <id> [--branch <name>] create a NEW editable workspace (never overlays a folder)
|
|
5455
5718
|
publisher workspace-status [dir] offline mapping + where the workspace stands (real three-way check)
|
|
5456
|
-
publisher sync [dir] [--
|
|
5719
|
+
publisher sync [dir] [--message <text>] THE way to save and publish a change: records the folder's
|
|
5720
|
+
changes (never .env, keys or node_modules), brings in what
|
|
5721
|
+
was published meanwhile (fast-forward or a clean merge,
|
|
5722
|
+
never a rewrite) and publishes. Stops on a conflict with
|
|
5723
|
+
nothing published and nothing lost. No git needed
|
|
5724
|
+
[--mark-conflicts] on a conflict, put both versions in each file, marked,
|
|
5725
|
+
to edit; the next sync records and publishes the edit
|
|
5457
5726
|
publisher merge-check [dir] --ours <ref> --theirs <ref> [--apply]
|
|
5458
5727
|
publisher info [dir] repository state as seen by the server
|
|
5459
5728
|
publisher doctor environment check (node, git, credentials, association)
|
|
5460
5729
|
publisher migrate [dir] [--to <url>] re-point a folder whose service moved (asks the service where)
|
|
5461
5730
|
|
|
5462
|
-
Global flags: --endpoint <url>, --profile <name>, --token, --json, --quiet
|
|
5731
|
+
Global flags: --endpoint <url>, --profile <name>, --token, --json, --quiet, --version
|
|
5732
|
+
|
|
5733
|
+
A creator never needs git: init once, then sync after every change.
|
|
5463
5734
|
|
|
5464
5735
|
Which endpoint a command uses, first hit wins:
|
|
5465
5736
|
--endpoint <url> an explicit URL; needs no stored profile
|
|
@@ -5495,7 +5766,7 @@ async function main(argv) {
|
|
|
5495
5766
|
process.stderr.write(`\r[${e.phase}] ${e.message}${pct} `);
|
|
5496
5767
|
if (e.phase === "done") process.stderr.write("\n");
|
|
5497
5768
|
};
|
|
5498
|
-
const dirArg = (i) => (0,
|
|
5769
|
+
const dirArg = (i) => (0, import_node_path8.resolve)(cmd[i] ?? ".");
|
|
5499
5770
|
const resolutionInput = async (dir) => {
|
|
5500
5771
|
let association;
|
|
5501
5772
|
if (dir && await isGitRepo(dir)) association = (await readAssociation(dir))?.endpoint;
|
|
@@ -5524,6 +5795,11 @@ async function main(argv) {
|
|
|
5524
5795
|
const token = str2(flags["token"]);
|
|
5525
5796
|
return { endpoint, p: createPublisher({ endpoint, endpointSource: source, ...token ? { token } : {}, clientName: "git-publisher-cli", clientVersion: CLIENT_VERSION }) };
|
|
5526
5797
|
};
|
|
5798
|
+
if (flags["version"] === true) {
|
|
5799
|
+
process.stdout.write(`${packageVersion()}
|
|
5800
|
+
`);
|
|
5801
|
+
return 0;
|
|
5802
|
+
}
|
|
5527
5803
|
switch (flags["help"] === true ? "help" : cmd[0]) {
|
|
5528
5804
|
case void 0:
|
|
5529
5805
|
case "help":
|
|
@@ -5669,19 +5945,30 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
5669
5945
|
await p.getRepository(repoId);
|
|
5670
5946
|
}
|
|
5671
5947
|
await p.associate(dir, repoId);
|
|
5948
|
+
const workspace = await p.adoptWorkspace(dir).catch(() => null);
|
|
5672
5949
|
out({
|
|
5673
5950
|
ok: true,
|
|
5674
5951
|
repoId,
|
|
5675
5952
|
created,
|
|
5676
5953
|
endpoint,
|
|
5677
5954
|
dir,
|
|
5678
|
-
onboarding: onboarding ? { commit: onboarding.commit, committed: onboarding.committed.length, excluded: onboarding.excluded } : null
|
|
5955
|
+
onboarding: onboarding ? { commit: onboarding.commit, committed: onboarding.committed.length, excluded: onboarding.excluded } : null,
|
|
5956
|
+
workspace: workspace === null ? null : { branch: workspace.branch, baseOid: workspace.baseOid, baseGeneration: workspace.baseGeneration }
|
|
5679
5957
|
});
|
|
5680
5958
|
return 0;
|
|
5681
5959
|
}
|
|
5682
5960
|
case "publish": {
|
|
5683
5961
|
let finishPublish2 = function(o) {
|
|
5684
|
-
out({
|
|
5962
|
+
out({
|
|
5963
|
+
state: o.state,
|
|
5964
|
+
publicationId: o.publicationId,
|
|
5965
|
+
repoId: o.repoId,
|
|
5966
|
+
generation: o.resultGeneration,
|
|
5967
|
+
refs: o.refs,
|
|
5968
|
+
rejection: o.rejection,
|
|
5969
|
+
resumed: o.resumed,
|
|
5970
|
+
...o.next === null ? {} : { next: o.next }
|
|
5971
|
+
});
|
|
5685
5972
|
return o.state === "accepted" ? 0 : o.state === "rejected" ? 2 : 3;
|
|
5686
5973
|
};
|
|
5687
5974
|
var finishPublish = finishPublish2;
|
|
@@ -5717,7 +6004,13 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
5717
6004
|
const resolved = await p.resumePublications(dir, { onProgress: progress });
|
|
5718
6005
|
out({
|
|
5719
6006
|
pending: pending.map((x) => x.publicationId),
|
|
5720
|
-
resolved: resolved.map((o) => ({
|
|
6007
|
+
resolved: resolved.map((o) => ({
|
|
6008
|
+
publicationId: o.publicationId,
|
|
6009
|
+
state: o.state,
|
|
6010
|
+
generation: o.resultGeneration,
|
|
6011
|
+
rejection: o.rejection,
|
|
6012
|
+
...o.next === null ? {} : { next: o.next }
|
|
6013
|
+
}))
|
|
5721
6014
|
});
|
|
5722
6015
|
return resolved.every((o) => o.state === "accepted") ? 0 : resolved.some((o) => o.state === "rejected") ? 2 : 3;
|
|
5723
6016
|
}
|
|
@@ -5745,12 +6038,40 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
5745
6038
|
out(s);
|
|
5746
6039
|
return s.state === "conflict" ? 2 : 0;
|
|
5747
6040
|
}
|
|
6041
|
+
/**
|
|
6042
|
+
* The creator's one verb: record the folder's changes, bring in what the service has, publish.
|
|
6043
|
+
* `--apply` is what this was before it recorded and published, and is still accepted, so a
|
|
6044
|
+
* `sync --apply` written down anywhere does the whole round trip rather than failing.
|
|
6045
|
+
*/
|
|
5748
6046
|
case "sync": {
|
|
5749
6047
|
const dir = dirArg(1);
|
|
5750
6048
|
const { p } = await client(dir);
|
|
5751
|
-
const r = await p.
|
|
5752
|
-
|
|
5753
|
-
|
|
6049
|
+
const r = await p.syncChanges({ directory: dir, markConflicts: flags["mark-conflicts"] === true, ...optStr(flags, "message") }, { onProgress: progress });
|
|
6050
|
+
const o = r.published;
|
|
6051
|
+
out({
|
|
6052
|
+
ok: r.state === "published" || r.state === "up_to_date",
|
|
6053
|
+
state: r.state,
|
|
6054
|
+
recorded: r.recorded,
|
|
6055
|
+
merged: r.reconciled.applied,
|
|
6056
|
+
conflicts: r.reconciled.before.conflicts,
|
|
6057
|
+
marked: r.marked,
|
|
6058
|
+
publication: o === null ? null : {
|
|
6059
|
+
state: o.state,
|
|
6060
|
+
publicationId: o.publicationId,
|
|
6061
|
+
generation: o.resultGeneration,
|
|
6062
|
+
refs: o.refs,
|
|
6063
|
+
rejection: o.rejection,
|
|
6064
|
+
...o.next === null ? {} : { next: o.next }
|
|
6065
|
+
},
|
|
6066
|
+
hint: syncHint(r.state, r.reconciled.before.conflicts, r.marked)
|
|
6067
|
+
});
|
|
6068
|
+
return r.state === "published" || r.state === "up_to_date" ? 0 : r.state === "unknown" ? 3 : 2;
|
|
6069
|
+
}
|
|
6070
|
+
case "skills": {
|
|
6071
|
+
if (cmd[1] !== "install") throw new PublisherError("INVALID_REQUEST", `unknown skills command: ${cmd[1] ?? "(none)"}; expected install`);
|
|
6072
|
+
const r = await installSkills({ log });
|
|
6073
|
+
out(r);
|
|
6074
|
+
return 0;
|
|
5754
6075
|
}
|
|
5755
6076
|
case "merge-check": {
|
|
5756
6077
|
const dir = dirArg(1);
|
|
@@ -5796,6 +6117,20 @@ function demand(r, input) {
|
|
|
5796
6117
|
throw new PublisherError("LOCAL_STATE", `not connected to a publishing service yet.${run}`, { details });
|
|
5797
6118
|
}
|
|
5798
6119
|
}
|
|
6120
|
+
function syncHint(state, conflicts, marked) {
|
|
6121
|
+
switch (state) {
|
|
6122
|
+
case "published":
|
|
6123
|
+
return "Your changes are saved and published.";
|
|
6124
|
+
case "up_to_date":
|
|
6125
|
+
return "Nothing new to publish: the service already has this version.";
|
|
6126
|
+
case "conflict":
|
|
6127
|
+
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.`;
|
|
6128
|
+
case "rejected":
|
|
6129
|
+
return "The service refused the publication; see publication.rejection.";
|
|
6130
|
+
default:
|
|
6131
|
+
return "The publication was sent but its outcome is not known yet; run publisher status.";
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
5799
6134
|
function formatHuman(v) {
|
|
5800
6135
|
return JSON.stringify(v, null, 2);
|
|
5801
6136
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minnovate/shadow-publisher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"publisher": "./dist/publisher.cjs"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
|
-
"dist/publisher.cjs"
|
|
14
|
+
"dist/publisher.cjs",
|
|
15
|
+
"dist/claude"
|
|
15
16
|
],
|
|
16
17
|
"publishConfig": {
|
|
17
18
|
"access": "public"
|
|
@@ -19,8 +20,8 @@
|
|
|
19
20
|
"scripts": {
|
|
20
21
|
"build": "tsc -b tsconfig.json && pnpm run bundle",
|
|
21
22
|
"test": "vitest run --project unit --root ../.. packages/cli/",
|
|
22
|
-
"bundle": "esbuild src/main.ts --bundle --platform=node --target=node22 --format=cjs --outfile=dist/publisher.cjs --banner:js=\"#!/usr/bin/env node\" --log-level=warning",
|
|
23
|
-
"
|
|
23
|
+
"bundle": "esbuild src/main.ts --bundle --platform=node --target=node22 --format=cjs --outfile=dist/publisher.cjs --banner:js=\"#!/usr/bin/env node\" --log-level=warning && pnpm run skills",
|
|
24
|
+
"skills": "rm -rf dist/claude && mkdir -p dist/claude && cp -R ../../integrations/claude/.claude-plugin ../../integrations/claude/plugin dist/claude/",
|
|
24
25
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|