@coderook/cli 0.25.4 → 0.27.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/dist/cli/src/api.js +86 -0
- package/dist/cli/src/cli.js +267 -23
- package/dist/cli/src/git_remote.js +100 -15
- package/dist/cli/src/project_commands.js +67 -4
- package/dist/cli/src/service_commands.js +2 -1
- package/dist/cli/src/version_commands.js +652 -0
- package/dist/desktop-app/src/main/download.js +15 -6
- package/dist/desktop-app/src/main/secret_patterns.js +1 -0
- package/dist/desktop-app/src/main/tracks.js +76 -0
- package/dist/desktop-app/src/main/upload.js +5 -0
- package/dist/desktop-app/src/main/worktree.js +26 -8
- package/package.json +1 -1
- package/skills/coderook/SKILL.md +24 -0
|
@@ -229,6 +229,7 @@ class Downloader {
|
|
|
229
229
|
...(row.objectId ? { objectId: String(row.objectId) } : {}),
|
|
230
230
|
sha256: String(row.sha256 ?? ""),
|
|
231
231
|
sourceSize: Number(row.sourceSize ?? 0),
|
|
232
|
+
...(row.sealed === true ? { sealed: true } : {}),
|
|
232
233
|
...(packed?.objectId
|
|
233
234
|
? {
|
|
234
235
|
pack: {
|
|
@@ -298,7 +299,7 @@ class Downloader {
|
|
|
298
299
|
const packed = new Map();
|
|
299
300
|
for (const file of files) {
|
|
300
301
|
const id = file.pack?.objectId;
|
|
301
|
-
if (!id)
|
|
302
|
+
if (!id || file.sealed)
|
|
302
303
|
continue;
|
|
303
304
|
const group = packed.get(id) ?? [];
|
|
304
305
|
group.push(file);
|
|
@@ -323,7 +324,7 @@ class Downloader {
|
|
|
323
324
|
for (const file of files) {
|
|
324
325
|
this.check();
|
|
325
326
|
progress(file.path);
|
|
326
|
-
const packId = file.pack?.objectId;
|
|
327
|
+
const packId = file.sealed ? undefined : file.pack?.objectId;
|
|
327
328
|
if (packId && restoredPacks.has(packId))
|
|
328
329
|
continue;
|
|
329
330
|
if (packId) {
|
|
@@ -372,7 +373,14 @@ class Downloader {
|
|
|
372
373
|
}
|
|
373
374
|
async fetchInto(repositoryId, versionId, file, target) {
|
|
374
375
|
const whole = (0, node_crypto_1.createHash)("sha256");
|
|
375
|
-
|
|
376
|
+
/*
|
|
377
|
+
A sealed file is read whole, by path, whatever shape it is stored in.
|
|
378
|
+
|
|
379
|
+
Its pieces and its object are the covered bytes; only the per-path route
|
|
380
|
+
puts the real value back. Reading it any other way fetches something
|
|
381
|
+
that cannot match the digest this file was listed with.
|
|
382
|
+
*/
|
|
383
|
+
const pieces = file.sealed ? [] : (file.chunks ?? []);
|
|
376
384
|
// Captured so the generators below do not need `this` rebound.
|
|
377
385
|
const request = (route) => this.request(route);
|
|
378
386
|
const check = () => this.check();
|
|
@@ -399,7 +407,7 @@ class Downloader {
|
|
|
399
407
|
}
|
|
400
408
|
})()
|
|
401
409
|
: (async function* () {
|
|
402
|
-
const reply = await request(file.objectId
|
|
410
|
+
const reply = await request(file.objectId && !file.sealed
|
|
403
411
|
? `/v1/repositories/${repositoryId}/objects/${file.objectId}`
|
|
404
412
|
: `/v1/repositories/${repositoryId}/versions/${versionId}/file` +
|
|
405
413
|
`?path=${encodeURIComponent(file.path)}`);
|
|
@@ -456,7 +464,8 @@ class Downloader {
|
|
|
456
464
|
let bytes = 0;
|
|
457
465
|
const packed = new Map();
|
|
458
466
|
for (const file of files) {
|
|
459
|
-
|
|
467
|
+
/* Sealed members never come out of the pack — see `sealed` above. */
|
|
468
|
+
const id = file.sealed ? undefined : file.pack?.objectId;
|
|
460
469
|
if (!id)
|
|
461
470
|
continue;
|
|
462
471
|
const group = packed.get(id) ?? [];
|
|
@@ -479,7 +488,7 @@ class Downloader {
|
|
|
479
488
|
if (parts.some((part) => part === ".." || part.includes("\0"))) {
|
|
480
489
|
throw new Error(`That version contains an unsafe path: ${file.path}`);
|
|
481
490
|
}
|
|
482
|
-
const packId = file.pack?.objectId;
|
|
491
|
+
const packId = file.sealed ? undefined : file.pack?.objectId;
|
|
483
492
|
if (packId && restoredPacks.has(packId))
|
|
484
493
|
continue;
|
|
485
494
|
if (packId) {
|
|
@@ -54,6 +54,82 @@ class Tracks {
|
|
|
54
54
|
}
|
|
55
55
|
return (text ? JSON.parse(text) : {});
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* A project's saves, as the owner sees them.
|
|
59
|
+
*
|
|
60
|
+
* Every one of them, including the ones held for review and the ones whose
|
|
61
|
+
* files were taken down — this is the working history, not the published
|
|
62
|
+
* one, and the whole point of showing it here is deciding what becomes
|
|
63
|
+
* published.
|
|
64
|
+
*/
|
|
65
|
+
async versions(repositoryId) {
|
|
66
|
+
const body = await this.call(`/v1/repositories/${repositoryId}/versions`);
|
|
67
|
+
return body.versions ?? [];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* What the account says about this project, as distinct from this folder.
|
|
71
|
+
*
|
|
72
|
+
* The local record knows where a folder saves to and nothing about how the
|
|
73
|
+
* project is published — so the history window could show which saves were
|
|
74
|
+
* public without being able to say whether the project itself was, which is
|
|
75
|
+
* the half that decides whether anybody can reach them.
|
|
76
|
+
*/
|
|
77
|
+
async project(repositoryId) {
|
|
78
|
+
try {
|
|
79
|
+
const body = await this.call(`/v1/repositories/${repositoryId}`);
|
|
80
|
+
return { visibility: body.visibility ?? null };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* Not knowing is not worth failing the window for. */
|
|
84
|
+
return { visibility: null };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async labels(repositoryId) {
|
|
88
|
+
const body = await this.call(`/v1/repositories/${repositoryId}/labels`);
|
|
89
|
+
return body.labels ?? [];
|
|
90
|
+
}
|
|
91
|
+
async createLabel(repositoryId, name, colour) {
|
|
92
|
+
const body = await this.call(`/v1/repositories/${repositoryId}/labels`, { method: "POST", body: JSON.stringify({ name, colour }) });
|
|
93
|
+
return body.label;
|
|
94
|
+
}
|
|
95
|
+
async reviewVersion(repositoryId, versionId, decision, note) {
|
|
96
|
+
await this.call(`/v1/repositories/${repositoryId}/versions/${versionId}/review`, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
body: JSON.stringify({ decision, ...(note ? { note } : {}) }),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async takeVersionDown(repositoryId, versionId, reason) {
|
|
102
|
+
const query = reason ? `?reason=${encodeURIComponent(reason)}` : "";
|
|
103
|
+
await this.call(`/v1/repositories/${repositoryId}/versions/${versionId}/content${query}`, { method: "DELETE" });
|
|
104
|
+
}
|
|
105
|
+
/** Put the project back on an earlier save. Nothing is deleted. */
|
|
106
|
+
async undo(repositoryId, to) {
|
|
107
|
+
return await this.call(`/v1/repositories/${repositoryId}/undo`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
body: JSON.stringify(to ? { to } : {}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** Publish an older save's content again, as a new save. */
|
|
113
|
+
async restoreVersion(repositoryId, versionId, message) {
|
|
114
|
+
await this.call(`/v1/repositories/${repositoryId}/versions/${versionId}/restore`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
body: JSON.stringify({ message }),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Everything about a version, changed in one call.
|
|
121
|
+
*
|
|
122
|
+
* Named, hidden, pinned or labelled from the desktop app for the first time.
|
|
123
|
+
* All three clients call the same route with the same patch, which is what
|
|
124
|
+
* stops the next capability being reachable from one of them and not the
|
|
125
|
+
* others.
|
|
126
|
+
*/
|
|
127
|
+
async changeVersion(repositoryId, versionId, patch) {
|
|
128
|
+
await this.call(`/v1/repositories/${repositoryId}/versions/${versionId}`, {
|
|
129
|
+
method: "PATCH",
|
|
130
|
+
body: JSON.stringify(patch),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
57
133
|
/**
|
|
58
134
|
* Every line and merge this project has.
|
|
59
135
|
*
|
|
@@ -1163,6 +1163,10 @@ class Uploader {
|
|
|
1163
1163
|
...(request.track ? { track: request.track } : {}),
|
|
1164
1164
|
...(request.allowIgnored ? { allowIgnored: true } : {}),
|
|
1165
1165
|
...(request.allowSecrets ? { allowSecrets: true } : {}),
|
|
1166
|
+
...(request.linesAdded === undefined ? {} : { linesAdded: request.linesAdded }),
|
|
1167
|
+
...(request.linesRemoved === undefined
|
|
1168
|
+
? {}
|
|
1169
|
+
: { linesRemoved: request.linesRemoved }),
|
|
1166
1170
|
/*
|
|
1167
1171
|
Names this attempt so a retry after a lost connection is answered
|
|
1168
1172
|
with the version already made, rather than making a second one.
|
|
@@ -1233,6 +1237,7 @@ class Uploader {
|
|
|
1233
1237
|
repositoryId,
|
|
1234
1238
|
versionId: completed.version.id,
|
|
1235
1239
|
sequence: completed.version.sequence,
|
|
1240
|
+
...(completed.version.state === "held" ? { held: true } : {}),
|
|
1236
1241
|
...(completed.mergeTrack ? { mergeTrack: completed.mergeTrack } : {}),
|
|
1237
1242
|
...(completed.repeated ? { repeated: true } : {}),
|
|
1238
1243
|
sourceBytes: completed.version.sourceSize ?? sourceBytes,
|
|
@@ -999,9 +999,26 @@ async function uploadConcerns(root, include) {
|
|
|
999
999
|
* matches no service's format and so is invisible to the pattern scan — the
|
|
1000
1000
|
* only thing that identifies it is the name of the file it is sitting in.
|
|
1001
1001
|
*/
|
|
1002
|
+
/**
|
|
1003
|
+
* The `.env.<something>` files that exist to be committed.
|
|
1004
|
+
*
|
|
1005
|
+
* `.env.example` is the file a project is *supposed* to publish — it is the
|
|
1006
|
+
* documentation of which variables exist, with the values left empty. Refusing
|
|
1007
|
+
* to send it, and then advising it be added to .gitignore, is advice that
|
|
1008
|
+
* breaks the project for the next person who clones it.
|
|
1009
|
+
*
|
|
1010
|
+
* The same list the service uses when it decides what to cover, so a file is
|
|
1011
|
+
* not a template in one half of the system and a credential in the other.
|
|
1012
|
+
*/
|
|
1013
|
+
const TEMPLATE_SUFFIXES = [".example", ".sample", ".template", ".dist", ".defaults"];
|
|
1014
|
+
function isTemplateName(name) {
|
|
1015
|
+
return TEMPLATE_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
1016
|
+
}
|
|
1002
1017
|
function isCredentialByName(relativePath) {
|
|
1003
1018
|
const parts = relativePath.split("/");
|
|
1004
1019
|
const name = (parts[parts.length - 1] ?? "").toLowerCase();
|
|
1020
|
+
if (isTemplateName(name))
|
|
1021
|
+
return false;
|
|
1005
1022
|
return (SECRET_NAMES.has(name) ||
|
|
1006
1023
|
name.startsWith(".env.") ||
|
|
1007
1024
|
SECRET_SUFFIXES.some((suffix) => name.endsWith(suffix)) ||
|
|
@@ -1032,14 +1049,15 @@ async function detectSecrets(root) {
|
|
|
1032
1049
|
continue;
|
|
1033
1050
|
}
|
|
1034
1051
|
seen += 1;
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1052
|
+
/*
|
|
1053
|
+
Asked of the one function rather than repeated here. These were two
|
|
1054
|
+
copies of the same rule, which is how `.env.example` came to be
|
|
1055
|
+
refused by the command line long after the service had learned that
|
|
1056
|
+
templates are not credentials.
|
|
1057
|
+
*/
|
|
1058
|
+
const relative = node_path_1.default.relative(root, full).split(node_path_1.default.sep).join("/");
|
|
1059
|
+
if (isCredentialByName(relative))
|
|
1060
|
+
found.push(relative);
|
|
1043
1061
|
}
|
|
1044
1062
|
}
|
|
1045
1063
|
return found;
|
package/package.json
CHANGED
package/skills/coderook/SKILL.md
CHANGED
|
@@ -81,6 +81,28 @@ cbx submit --track spike -m "…"
|
|
|
81
81
|
Switching says where the next save goes and nothing else — no files move.
|
|
82
82
|
Run `cbx get` afterwards to bring that line's files in.
|
|
83
83
|
|
|
84
|
+
## Versions, and going back
|
|
85
|
+
|
|
86
|
+
Every save is a commit. A commit becomes a *version* — the thing the public
|
|
87
|
+
side of a project offers, and the thing a collaborator pulls — only when
|
|
88
|
+
somebody promotes it.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cbx promote # pick from the last ten and name one
|
|
92
|
+
cbx mark 41 --pin # pin, hide, rename or label one save
|
|
93
|
+
cbx labels add shipped green # the labels a project can wear
|
|
94
|
+
cbx undo # put the project back on the save before this one
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`undo` moves where the project stands. Nothing is deleted and nothing is
|
|
98
|
+
renumbered — the saves passed over stay in the history, and the next save
|
|
99
|
+
carries on from wherever it now stands. It is the answer to "I pushed the
|
|
100
|
+
wrong thing"; hiding a version stops strangers reading it but leaves the
|
|
101
|
+
project standing on it, so the next person to pull still lands on the mistake.
|
|
102
|
+
|
|
103
|
+
**Ask before `undo`, `promote` and `take-down`.** They change what other
|
|
104
|
+
people see. `cbx versions` first, so the person can say which save they mean.
|
|
105
|
+
|
|
84
106
|
## When two saves collide
|
|
85
107
|
|
|
86
108
|
If somebody saved while this folder was behind, the second save becomes a merge
|
|
@@ -121,6 +143,8 @@ cbx unbundle project.cbx ./restored
|
|
|
121
143
|
again and gains nothing.
|
|
122
144
|
- Do not guess a project name. `cbx projects` lists them; a folder that is
|
|
123
145
|
already linked needs no name at all.
|
|
146
|
+
- Do not run `cbx take-down`. It destroys the files in a version and they do
|
|
147
|
+
not come back. Say it exists and let its owner run it.
|
|
124
148
|
|
|
125
149
|
## If something refuses
|
|
126
150
|
|