@coderook/cli 0.22.2 → 0.24.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/.claude-plugin/plugin.json +1 -1
- package/README.md +342 -220
- package/dist/cli/src/api.js +27 -2
- package/dist/cli/src/cli.js +323 -87
- package/dist/cli/src/git_history.js +188 -0
- package/dist/cli/src/git_remote.js +1118 -0
- package/dist/cli/src/git_remote_bin.js +22 -0
- package/dist/cli/src/help.js +8 -8
- package/dist/cli/src/import_command.js +150 -0
- package/dist/cli/src/licence_commands.js +5 -5
- package/dist/cli/src/mcp.js +1 -1
- package/dist/cli/src/publish.js +37 -0
- package/dist/cli/src/registry.js +1 -1
- package/dist/cli/src/runner.js +2 -2
- package/dist/cli/src/service_commands.js +53 -3
- package/dist/cli/src/skill_command.js +3 -3
- package/dist/cli/src/track_commands.js +7 -7
- package/dist/desktop-app/src/main/tracks.js +12 -2
- package/dist/desktop-app/src/main/upload.js +11 -1
- package/package.json +53 -51
- package/skills/coderook/SKILL.md +130 -130
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The decisions the git remote helper makes, with nothing plugged in.
|
|
4
|
+
*
|
|
5
|
+
* Ordering a version graph, reading and writing the commit marker, parsing a
|
|
6
|
+
* remote URL, quoting a path for fast-import: none of it needs a network, a
|
|
7
|
+
* token, an upload engine or a git binary, and all of it is where a mistake is
|
|
8
|
+
* silent rather than loud. Kept apart so a test can exercise it directly —
|
|
9
|
+
* importing the helper itself drags in the uploader and the downloader, which
|
|
10
|
+
* is why this file exists at all.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.TRAILER = exports.EMPTY_TREE = void 0;
|
|
14
|
+
exports.parseRemoteUrl = parseRemoteUrl;
|
|
15
|
+
exports.commitFromMessage = commitFromMessage;
|
|
16
|
+
exports.messageWithCommit = messageWithCommit;
|
|
17
|
+
exports.messageWithoutMarker = messageWithoutMarker;
|
|
18
|
+
exports.tagOf = tagOf;
|
|
19
|
+
exports.branchOf = branchOf;
|
|
20
|
+
exports.versionsInOrder = versionsInOrder;
|
|
21
|
+
exports.stamp = stamp;
|
|
22
|
+
exports.importRef = importRef;
|
|
23
|
+
exports.quotePath = quotePath;
|
|
24
|
+
/**
|
|
25
|
+
* Git's constant hash for the empty tree.
|
|
26
|
+
*
|
|
27
|
+
* Diffing a root commit against this gives the same "everything is added"
|
|
28
|
+
* answer as a normal parent diff, so the first commit is not a special case
|
|
29
|
+
* with its own code path to get wrong.
|
|
30
|
+
*/
|
|
31
|
+
exports.EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
32
|
+
/**
|
|
33
|
+
* The line that ties a CodeRook version back to the git commit it came from.
|
|
34
|
+
*
|
|
35
|
+
* Written into the version message because it has to survive somewhere both
|
|
36
|
+
* machines can see. A file under `.git/` would be invisible to a colleague and
|
|
37
|
+
* to this same person on another laptop, and the consequence of losing the
|
|
38
|
+
* mapping is not a missing feature — it is re-pushing a history the project
|
|
39
|
+
* already holds, as a second copy. `git-svn` and `git-p4` put their marker in
|
|
40
|
+
* the message for the same reason.
|
|
41
|
+
*/
|
|
42
|
+
exports.TRAILER = "CodeRook-Git-Commit:";
|
|
43
|
+
/** `coderook://project`, `coderook://owner/project`, `coderook::project`. */
|
|
44
|
+
function parseRemoteUrl(url) {
|
|
45
|
+
let rest = url.trim();
|
|
46
|
+
for (const prefix of ["coderook://", "coderook::", "coderook:"]) {
|
|
47
|
+
if (rest.startsWith(prefix)) {
|
|
48
|
+
rest = rest.slice(prefix.length);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
rest = rest.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
53
|
+
// An owner segment is accepted and ignored: a token already says who this
|
|
54
|
+
// is, and silently pushing to a different account than the URL named would
|
|
55
|
+
// be a worse outcome than not supporting the form at all.
|
|
56
|
+
const parts = rest.split("/").filter(Boolean);
|
|
57
|
+
const slug = parts[parts.length - 1] ?? "";
|
|
58
|
+
if (!slug)
|
|
59
|
+
throw new Error(`Not a CodeRook remote URL: ${url}`);
|
|
60
|
+
return { slug };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pull the git commit id back out of a version message, if it carries one.
|
|
64
|
+
*
|
|
65
|
+
* The *last* marker wins. A message can hold more than one after a round trip
|
|
66
|
+
* — clone a project, push it back, and the message carries the marker it was
|
|
67
|
+
* imported with plus the one this publish adds. Taking the first would name
|
|
68
|
+
* the commit from the original repository rather than the one just published,
|
|
69
|
+
* and every incremental push after that would compute its range from a commit
|
|
70
|
+
* this repository has never heard of.
|
|
71
|
+
*/
|
|
72
|
+
function commitFromMessage(message) {
|
|
73
|
+
const all = [
|
|
74
|
+
...message.matchAll(new RegExp(`${exports.TRAILER}\\s*([0-9a-f]{40})\\b`, "g")),
|
|
75
|
+
];
|
|
76
|
+
return all.length ? all[all.length - 1][1] : null;
|
|
77
|
+
}
|
|
78
|
+
/** Attach the marker to a commit message without disturbing what it says. */
|
|
79
|
+
function messageWithCommit(message, sha) {
|
|
80
|
+
const body = message.replace(/\s+$/, "");
|
|
81
|
+
return `${body}\n\n${exports.TRAILER} ${sha}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The message as a person wrote it, with our bookkeeping taken back out.
|
|
85
|
+
*
|
|
86
|
+
* Used when rebuilding git commits: the marker names the commit a version came
|
|
87
|
+
* *from*, which is not the commit being created here, so leaving it in would
|
|
88
|
+
* stamp every imported commit with a false identity.
|
|
89
|
+
*/
|
|
90
|
+
function messageWithoutMarker(message) {
|
|
91
|
+
return message
|
|
92
|
+
.replace(new RegExp(`^\\s*${exports.TRAILER}\\s*[0-9a-f]{40}\\s*$`, "gm"), "")
|
|
93
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
94
|
+
.trim();
|
|
95
|
+
}
|
|
96
|
+
/** `refs/tags/v1.0` -> `v1.0`. Anything else is not a tag. */
|
|
97
|
+
function tagOf(ref) {
|
|
98
|
+
return ref.startsWith("refs/tags/") ? ref.slice("refs/tags/".length) : null;
|
|
99
|
+
}
|
|
100
|
+
/** `refs/heads/main` -> `main`. Anything else is not a branch. */
|
|
101
|
+
function branchOf(ref) {
|
|
102
|
+
return ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Every version reachable from these heads, oldest first.
|
|
106
|
+
*
|
|
107
|
+
* A plain sort by sequence would look right and be wrong: sequences are handed
|
|
108
|
+
* out per project as versions are made, so two lines interleave in a way that
|
|
109
|
+
* has nothing to do with ancestry. Walking `parentVersionIds` and emitting a
|
|
110
|
+
* version only once all of its parents have been emitted is what makes a
|
|
111
|
+
* commit's parents exist by the time git is told about them — fast-import
|
|
112
|
+
* refuses a mark it has not seen, so getting this wrong fails loudly, which is
|
|
113
|
+
* the one mercy of it.
|
|
114
|
+
*/
|
|
115
|
+
function versionsInOrder(heads, all) {
|
|
116
|
+
const byId = new Map(all.map((version) => [version.id, version]));
|
|
117
|
+
// Everything the heads can reach. Iterative rather than recursive: a long
|
|
118
|
+
// project is thousands of versions deep and a stack is not.
|
|
119
|
+
const reachable = new Set();
|
|
120
|
+
const pending = [...heads];
|
|
121
|
+
while (pending.length) {
|
|
122
|
+
const id = pending.pop();
|
|
123
|
+
if (!id || reachable.has(id) || !byId.has(id))
|
|
124
|
+
continue;
|
|
125
|
+
reachable.add(id);
|
|
126
|
+
for (const parent of byId.get(id).parentVersionIds)
|
|
127
|
+
pending.push(parent);
|
|
128
|
+
}
|
|
129
|
+
const emitted = new Set();
|
|
130
|
+
const order = [];
|
|
131
|
+
/*
|
|
132
|
+
Depth-first with an explicit stack, visiting parents before the child. The
|
|
133
|
+
`expanded` flag is what separates "I have queued this one's parents" from
|
|
134
|
+
"its parents are done", without which a diamond history emits the join
|
|
135
|
+
twice.
|
|
136
|
+
*/
|
|
137
|
+
const stack = heads
|
|
138
|
+
.filter((id) => reachable.has(id))
|
|
139
|
+
.map((id) => ({ id, expanded: false }));
|
|
140
|
+
while (stack.length) {
|
|
141
|
+
const frame = stack.pop();
|
|
142
|
+
if (emitted.has(frame.id))
|
|
143
|
+
continue;
|
|
144
|
+
const version = byId.get(frame.id);
|
|
145
|
+
if (!version)
|
|
146
|
+
continue;
|
|
147
|
+
if (frame.expanded) {
|
|
148
|
+
emitted.add(frame.id);
|
|
149
|
+
order.push(version);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
stack.push({ id: frame.id, expanded: true });
|
|
153
|
+
for (const parent of version.parentVersionIds) {
|
|
154
|
+
if (reachable.has(parent) && !emitted.has(parent)) {
|
|
155
|
+
stack.push({ id: parent, expanded: false });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return order;
|
|
160
|
+
}
|
|
161
|
+
/** Seconds since the epoch, for a fast-import person stamp. */
|
|
162
|
+
function stamp(when) {
|
|
163
|
+
const at = Date.parse(when);
|
|
164
|
+
return Number.isFinite(at) ? Math.floor(at / 1000) : Math.floor(Date.now() / 1000);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Where an imported branch is written.
|
|
168
|
+
*
|
|
169
|
+
* Into the helper's own namespace rather than `refs/heads/*`, matching the
|
|
170
|
+
* `refspec` advertised in `capabilities`. Writing straight to `refs/heads/*`
|
|
171
|
+
* would have this remote overwrite local branches of the same name on every
|
|
172
|
+
* fetch.
|
|
173
|
+
*/
|
|
174
|
+
function importRef(ref) {
|
|
175
|
+
const branch = branchOf(ref);
|
|
176
|
+
return branch ? `refs/coderook/${branch}` : ref;
|
|
177
|
+
}
|
|
178
|
+
/** fast-import wants a path C-quoted when it holds a quote or a newline. */
|
|
179
|
+
function quotePath(filePath) {
|
|
180
|
+
const normalised = filePath.replaceAll("\\", "/");
|
|
181
|
+
if (!/["\n\r]/.test(normalised))
|
|
182
|
+
return normalised;
|
|
183
|
+
return `"${normalised
|
|
184
|
+
.replaceAll("\\", "\\\\")
|
|
185
|
+
.replaceAll('"', '\\"')
|
|
186
|
+
.replaceAll("\n", "\\n")
|
|
187
|
+
.replaceAll("\r", "\\r")}"`;
|
|
188
|
+
}
|