@coderook/cli 0.31.9 → 0.31.10

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.
@@ -2,7 +2,7 @@
2
2
  "name": "coderook",
3
3
  "displayName": "CodeRook",
4
4
  "description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, from Claude Code.",
5
- "version": "0.31.9",
5
+ "version": "0.31.10",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
package/README.md CHANGED
@@ -118,6 +118,50 @@ on a decision. `cbx merges` lists them and `cbx merge <ref>` walks
118
118
  through it; `cbx tracks` shows them beside the ordinary lines, because
119
119
  somebody looking for where they can save needs to see the one they cannot.
120
120
 
121
+ ## Proposals
122
+
123
+ A line gets back to main by being proposed. The proposal is checked and
124
+ reviewed before it lands, and it follows the line: saving onto it afterwards
125
+ changes what is proposed rather than needing a second one.
126
+
127
+ ```
128
+ cbx propose -m "Faster encoder" offer this folder's line to main
129
+ cbx proposals what is open (--all for the rest)
130
+ cbx merge M-3 look at one: reviews, checks, decisions
131
+ cbx merge M-3 --ask sam,priya ask people to review it
132
+ cbx merge M-3 --apply land it, once nothing is in the way
133
+ ```
134
+
135
+ `--into` proposes into a line other than main, and `--from` proposes a line
136
+ other than the one this folder is on. "Fixes #12" in the title or body closes
137
+ issue 12 when it lands. The same proposals are on the website's Merges page
138
+ and behind **propose** in the desktop application.
139
+
140
+ A line can require approvals, and an approval from whoever proposed it does
141
+ not count. Checks come from actions: `cbx actions --on-proposal Tests` runs
142
+ Tests whenever a line is proposed or saved onto, and its result is a required
143
+ check. `--on-save` runs an action on every save, and `--by-hand` only when
144
+ somebody starts it.
145
+
146
+ A **protected** line refuses saves made straight onto it — from `cbx submit`,
147
+ the desktop application and `git push` alike — so the way in is a line of your
148
+ own and a proposal. It also cannot be put back on an earlier save, and a merge
149
+ cannot land on it over a conflict or a failing check.
150
+
151
+ ## Who can reach a project
152
+
153
+ cbx visibility # what it is now, and what each level means
154
+ cbx visibility public # list it on your profile
155
+ cbx visibility unlisted my-project # link only: kept off your profile and out of search
156
+ cbx visibility private # back to you and the people you invite
157
+
158
+ Making a project public or unlisted asks first and says what becomes readable.
159
+ In a script there is nobody to ask, so pass `--yes` (or `-y`); without it the
160
+ command refuses rather than waiting. Going private never asks.
161
+
162
+ A public project's page offers only its named versions that have not been
163
+ hidden (see `cbx promote` and `cbx mark --hide`).
164
+
121
165
  ## Ignore rules
122
166
 
123
167
  ```
@@ -141,6 +185,11 @@ and the desktop application all read one file. A `!` line puts something back.
141
185
  Personal exclusions that should not reach a collaborator belong in
142
186
  `.git/info/exclude`.
143
187
 
188
+ In a git repository, a file git already tracks is sent even if a rule matches
189
+ it — the same exception git makes, so a broad rule written after the fact does
190
+ not quietly drop committed source. Git's own `.git` directory is never sent,
191
+ and the service refuses a save with any path inside one.
192
+
144
193
  ## Moving a repository here
145
194
 
146
195
  `cbx import` takes a snapshot — the files as they are now, one version, no
@@ -167,7 +216,7 @@ exists.
167
216
 
168
217
  The project is created **private** whatever the source was — publishing
169
218
  somebody's code as a side effect of moving it is not recoverable. Pass
170
- `--visibility same` to mirror the source, or `--public`.
219
+ `--visibility same` to mirror the source, or `--visibility public`.
171
220
 
172
221
  A private repository needs a read token in the environment — `GITHUB_TOKEN`,
173
222
  `GITLAB_TOKEN` or `FORGE_TOKEN`. It is never taken as a flag, never stored,
@@ -254,7 +303,8 @@ second implementation of them that happens to agree.
254
303
 
255
304
  **What does not appear on that list**, because git has no such idea and
256
305
  inventing one would be pretending: `projects`, `delete`, `people`, `watch`,
257
- `issues`, `actions`, `runs`, `logs`, `runner`, `tokens`, `licence`, `ai`. Use
306
+ `issues`, `propose`, `proposals`, `actions`, `runs`, `logs`, `runner`,
307
+ `tokens`, `licence`, `ai`. Use
258
308
  them by name; nothing about them changes when a version arrives through git.
259
309
 
260
310
  `git commit` has no CodeRook equivalent either, and should not: it is local to
@@ -278,9 +328,13 @@ deliberately *not* used, since nobody wrote it about the release.
278
328
 
279
329
  Cloning reverses it: every release comes back as a tag on the right commit.
280
330
 
331
+ From the command line, `cbx release v1.0` names the newest save the same way,
332
+ and `cbx notes 41 --edit` writes its release notes.
333
+
281
334
  Assets are separate and unaffected. They attach to a version from a run's
282
- artifacts, so a release made by pushing a tag simply has none until something
283
- adds them — which is what a release without a build has always looked like.
335
+ artifacts or with `cbx attach <file>`, so a release made by pushing a tag
336
+ simply has none until something adds them — which is what a release without a
337
+ build has always looked like.
284
338
 
285
339
  ## Bundles
286
340
 
@@ -2267,6 +2267,36 @@ const SPECS = [
2267
2267
  ],
2268
2268
  run: project_commands_js_1.commandAi,
2269
2269
  },
2270
+ {
2271
+ /*
2272
+ Not `publish`, which already reaches `submit` and would read as a
2273
+ release. This is who can reach the project, and it is named for that.
2274
+ */
2275
+ name: "visibility",
2276
+ group: "Your projects",
2277
+ summary: "who can reach a project: private, link only or public",
2278
+ usage: "visibility [level] [project]",
2279
+ detail: "The level is public, unlisted or private. With none, says what the\n" +
2280
+ "project is set to now.\n\n" +
2281
+ "Private is you and the people you invite. Unlisted, which the website\n" +
2282
+ "calls link only, keeps it off your profile and out of search. Public\n" +
2283
+ "lists it on your profile, and its page offers the saves somebody named\n" +
2284
+ "and did not hide.\n\n" +
2285
+ "Opening a project up asks first and says what becomes readable: any\n" +
2286
+ "account signed in to CodeRook that has its link can open every save\n" +
2287
+ "through the API, not only the named versions. Going private asks\n" +
2288
+ "nothing.",
2289
+ options: [
2290
+ { flags: "-y, --yes", description: "open it up without asking, for scripts" },
2291
+ ],
2292
+ examples: [
2293
+ "cbx visibility",
2294
+ "cbx visibility public",
2295
+ "cbx visibility unlisted my-project --yes",
2296
+ "cbx visibility private my-project",
2297
+ ],
2298
+ run: project_commands_js_1.commandVisibility,
2299
+ },
2270
2300
  {
2271
2301
  name: "issues",
2272
2302
  group: "Your projects",
@@ -2715,6 +2745,8 @@ const SPECS = [
2715
2745
  { flags: "--theirs", description: "keep theirs" },
2716
2746
  { flags: "--both", description: "keep both" },
2717
2747
  { flags: "--drop", description: "abandon the upload" },
2748
+ { flags: "--path <file>", description: "decide one file rather than every one outstanding" },
2749
+ { flags: "--cancel", description: "call the merge off; your upload stays stored" },
2718
2750
  { flags: "--ask <usernames>", description: "ask people to review it, comma separated" },
2719
2751
  ],
2720
2752
  run: commandMerge,
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.DamagedLinkError = exports.credentials = exports.DEFAULT_API = void 0;
7
7
  exports.apiOrigin = apiOrigin;
8
+ exports.siteOrigin = siteOrigin;
8
9
  exports.configDirectory = configDirectory;
9
10
  exports.storeToken = storeToken;
10
11
  exports.loadToken = loadToken;
@@ -33,6 +34,26 @@ exports.DEFAULT_API = "https://api.coderook.com";
33
34
  function apiOrigin() {
34
35
  return (process.env.CODEROOK_API_URL || exports.DEFAULT_API).replace(/\/+$/, "");
35
36
  }
37
+ /**
38
+ * The website that goes with the service, for a link somebody can open.
39
+ *
40
+ * Derived rather than configured: the service answers on `api.` in front of
41
+ * the site's own host, so taking that off gives the address people use. A
42
+ * service anywhere else — a local one on a port — has no site this can name,
43
+ * and null says so rather than printing a link that goes nowhere.
44
+ */
45
+ function siteOrigin(api = apiOrigin()) {
46
+ try {
47
+ const url = new URL(api);
48
+ if (!url.hostname.startsWith("api."))
49
+ return null;
50
+ url.hostname = url.hostname.slice("api.".length);
51
+ return url.origin;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
36
57
  /** The per-user configuration directory, per platform convention. */
37
58
  function configDirectory() {
38
59
  if (process.env.CODEROOK_CONFIG_DIR)
@@ -14,8 +14,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.resolveProject = resolveProject;
16
16
  exports.commandAi = commandAi;
17
+ exports.readLevel = readLevel;
18
+ exports.splitVisibility = splitVisibility;
19
+ exports.offeredPublicly = offeredPublicly;
20
+ exports.commandVisibility = commandVisibility;
17
21
  exports.commandVersions = commandVersions;
18
22
  const node_process_1 = __importDefault(require("node:process"));
23
+ const promises_1 = require("node:readline/promises");
19
24
  const api_js_1 = require("./api.js");
20
25
  const config_js_1 = require("./config.js");
21
26
  const dim = (value) => `${value}`;
@@ -121,6 +126,207 @@ async function commandAi(parsed) {
121
126
  console.log(dim("one that refuses model training."));
122
127
  return 0;
123
128
  }
129
+ /**
130
+ * Who can reach a project, in the order the site shows them.
131
+ *
132
+ * The service calls the middle one `unlisted` and the website calls it "Link
133
+ * only"; both are accepted, and the prose uses the website's words.
134
+ */
135
+ const LEVELS = [
136
+ ["private", "private", "you and the people you invite"],
137
+ [
138
+ "unlisted",
139
+ "link only",
140
+ "not listed anywhere; any account signed in with its link can read it",
141
+ ],
142
+ [
143
+ "public",
144
+ "public",
145
+ "listed on your profile and in search; its page offers the named versions",
146
+ ],
147
+ ];
148
+ const wordFor = (level) => LEVELS.find(([name]) => name === level)?.[1] ?? level;
149
+ /** A typed word as a level, or null when it is not one. */
150
+ function readLevel(word) {
151
+ const text = (word ?? "").trim().toLowerCase();
152
+ if (text === "link" || text === "link-only")
153
+ return "unlisted";
154
+ const found = LEVELS.find(([name]) => name === text);
155
+ return found ? found[0] : null;
156
+ }
157
+ /**
158
+ * `[level] [project]`, both optional.
159
+ *
160
+ * The level is recognised by being one, so `cbx visibility my-project` asks
161
+ * and `cbx visibility public` changes. Two words where the first is not a
162
+ * level is a mistyped level rather than a project, and is said so — reading
163
+ * it as a project would answer "no project matching" to somebody who typed
164
+ * "pubic".
165
+ */
166
+ function splitVisibility(positional) {
167
+ const [first, second] = positional;
168
+ const level = readLevel(first);
169
+ if (level)
170
+ return { level, project: second };
171
+ if (second !== undefined) {
172
+ return {
173
+ error: `"${first}" is not a visibility. Pick public, unlisted or private.`,
174
+ };
175
+ }
176
+ return { project: first };
177
+ }
178
+ /**
179
+ * The versions a public page offers, by the service's own rule.
180
+ *
181
+ * Named, not hidden, finished arriving and not taken down — the four things
182
+ * `public_rule.ts` asks. Counted here so the question before a project goes
183
+ * public can say what the page will show rather than describe it.
184
+ */
185
+ function offeredPublicly(saved) {
186
+ return saved.filter((one) => Boolean(one.name) &&
187
+ one.visibility === "public" &&
188
+ one.state === "verified" &&
189
+ !one.removedAt);
190
+ }
191
+ /**
192
+ * The public page's address, once it answers.
193
+ *
194
+ * Asked of the service rather than assembled, because the page lives under
195
+ * the owner's name and the owner is not always the person running this — a
196
+ * collaborator can open up somebody else's project. A link is printed only
197
+ * when the public side has confirmed it is the same project.
198
+ */
199
+ async function publicAddress(project) {
200
+ const site = (0, config_js_1.siteOrigin)();
201
+ if (!site)
202
+ return null;
203
+ const me = await (0, api_js_1.whoami)().catch(() => null);
204
+ if (!me?.username)
205
+ return null;
206
+ const found = await (0, api_js_1.findPublicProject)(me.username, project.slug);
207
+ if (found?.id !== project.id)
208
+ return null;
209
+ return `${site}/${encodeURIComponent(me.username)}/${encodeURIComponent(project.slug)}`;
210
+ }
211
+ /** A plain yes or no, where anything but yes is no. */
212
+ async function agreed(question) {
213
+ const prompt = (0, promises_1.createInterface)({
214
+ input: node_process_1.default.stdin,
215
+ output: node_process_1.default.stdout,
216
+ });
217
+ const typed = await prompt.question(question);
218
+ prompt.close();
219
+ return /^y(es)?$/i.test(typed.trim());
220
+ }
221
+ /**
222
+ * Show or change who can reach a project.
223
+ *
224
+ * Opening one up asks first and says what becomes readable, because that is
225
+ * the step that cannot be taken back — somebody may already have read it by
226
+ * the time it is private again. Closing one asks nothing: it takes nothing
227
+ * from anybody who should have it.
228
+ */
229
+ async function commandVisibility(parsed) {
230
+ const asked = splitVisibility(parsed.positional);
231
+ if (asked.error) {
232
+ console.error(red(asked.error));
233
+ return 1;
234
+ }
235
+ const project = await resolveProject(asked.project);
236
+ if (!project)
237
+ return 1;
238
+ const current = readLevel(project.visibility) ?? "private";
239
+ if (!asked.level) {
240
+ console.log(`${bold(project.name)} is ${accent(wordFor(current))}.`);
241
+ for (const [level, , meaning] of LEVELS) {
242
+ const mark = level === current ? accent("›") : " ";
243
+ console.log(` ${mark} ${level.padEnd(9)} ${dim(meaning)}`);
244
+ }
245
+ if (current === "public") {
246
+ const address = await publicAddress(project);
247
+ if (address)
248
+ console.log(`\n ${address}`);
249
+ }
250
+ console.log("");
251
+ console.log(dim(`Change it with: cbx visibility <level>` +
252
+ (asked.project ? ` ${asked.project}` : "")));
253
+ return 0;
254
+ }
255
+ const target = asked.level;
256
+ if (target === current) {
257
+ const address = target === "public" ? await publicAddress(project) : null;
258
+ console.log(`${project.name} is already ${wordFor(target)}` +
259
+ (address ? `: ${address}` : "."));
260
+ return 0;
261
+ }
262
+ const sure = parsed.flags.get("yes") === true || parsed.flags.get("y") === true;
263
+ const asking = target !== "private" && !sure;
264
+ /*
265
+ Refused rather than asked when nobody can answer. A prompt on a pipe
266
+ either hangs a script for ever or reads an empty line as no, and neither
267
+ tells whoever wrote the script what to do about it.
268
+ */
269
+ if (asking && !node_process_1.default.stdin.isTTY) {
270
+ console.error(red(`Making ${project.name} ${wordFor(target)} needs a yes, and nobody is here to give one.`));
271
+ console.error(dim(" Run it in a terminal to be asked, or add --yes."));
272
+ return 1;
273
+ }
274
+ const offered = target === "public" ? offeredPublicly(await (0, api_js_1.versions)(project.id)) : [];
275
+ if (asking) {
276
+ if (target === "public") {
277
+ console.log(`Making ${bold(project.name)} public lists it on your profile and in search.`);
278
+ if (offered.length) {
279
+ const names = offered.slice(0, 5).map((one) => one.name).join(", ");
280
+ const more = offered.length > 5 ? ` and ${offered.length - 5} more` : "";
281
+ console.log(` Its page offers ${offered.length} named version${offered.length === 1 ? "" : "s"}: ${names}${more}.`);
282
+ }
283
+ else {
284
+ console.log(" Its page will be empty until a save is named — cbx promote does that.");
285
+ }
286
+ console.log(dim(" Saves nobody named, and versions you hid, are not on the page."));
287
+ }
288
+ else {
289
+ console.log(`Making ${bold(project.name)} link only keeps it off your profile and out of search.`);
290
+ }
291
+ /*
292
+ Said because it is true today, not because it is the design. The public
293
+ page follows the named-versions rule; the signed-in routes let any
294
+ account read an unlisted or public project whole, commits and hidden
295
+ versions included. Somebody deciding whether to open a project up
296
+ should be deciding about that.
297
+ */
298
+ console.log(dim(" Any account signed in to CodeRook that has its link can open every save\n" +
299
+ " through the API, not only the named versions. Check the history holds\n" +
300
+ " nothing you would not share."));
301
+ if (!(await agreed(`Make ${project.name} ${wordFor(target)}? [y/N] `))) {
302
+ console.log(dim(`Nothing changed. ${project.name} is still ${wordFor(current)}.`));
303
+ return 1;
304
+ }
305
+ }
306
+ try {
307
+ await (0, api_js_1.updateProject)(project.id, { visibility: target });
308
+ }
309
+ catch (error) {
310
+ console.error(red(error instanceof Error ? error.message : String(error)));
311
+ return 1;
312
+ }
313
+ if (target === "public") {
314
+ const address = await publicAddress(project);
315
+ console.log(green(`${project.name} is now public`) + (address ? `: ${address}` : "."));
316
+ console.log(dim(offered.length
317
+ ? ` Its page offers ${offered.length} named version${offered.length === 1 ? "" : "s"}.`
318
+ : " Its page is empty until a save is named — cbx promote does that."));
319
+ }
320
+ else if (target === "unlisted") {
321
+ console.log(green(`${project.name} is now link only.`));
322
+ console.log(dim(" It is not listed anywhere; any account signed in with its link can read it."));
323
+ }
324
+ else {
325
+ console.log(green(`${project.name} is now private.`));
326
+ console.log(dim(" Only you and the people you invite can reach it."));
327
+ }
328
+ return 0;
329
+ }
124
330
  /** Every saved version of a project, newest first. */
125
331
  async function commandVersions(parsed) {
126
332
  const project = await resolveProject(parsed.positional[0]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.31.9",
3
+ "version": "0.31.10",
4
4
  "description": "The command line for CodeRook (coderook.com): version control and project hosting for projects of any size. Every save is a compressed snapshot, private or public, never used to train AI.",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "homepage": "https://coderook.com",
@@ -103,6 +103,18 @@ project standing on it, so the next person to pull still lands on the mistake.
103
103
  **Ask before `undo`, `promote` and `take-down`.** They change what other
104
104
  people see. `cbx versions` first, so the person can say which save they mean.
105
105
 
106
+ ## Who can reach a project
107
+
108
+ ```bash
109
+ cbx visibility # what it is now: private, unlisted or public
110
+ cbx visibility public --yes # list it on the owner's profile
111
+ cbx visibility private # back to the people invited
112
+ ```
113
+
114
+ **Never make a project public or unlisted unless the person has asked for
115
+ exactly that.** `--yes` skips the question the command would otherwise ask
116
+ them, so only pass it once they have answered it. Going private is always safe.
117
+
106
118
  ## When two saves collide
107
119
 
108
120
  If somebody saved while this folder was behind, the second save becomes a merge