@coderook/cli 0.15.0 → 0.17.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.
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "coderook",
3
+ "displayName": "CodeRook",
4
+ "description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, from Claude Code.",
5
+ "version": "0.17.0",
6
+ "author": {
7
+ "name": "ACCA Gaming Productions",
8
+ "url": "https://coderook.com"
9
+ },
10
+ "homepage": "https://coderook.com/docs#assistants",
11
+ "license": "SEE LICENSE IN LICENSE.txt",
12
+ "keywords": [
13
+ "coderook",
14
+ "versioning",
15
+ "snapshots",
16
+ "backup"
17
+ ]
18
+ }
package/README.md CHANGED
@@ -148,6 +148,25 @@ Sign in once per machine first — `coderook sign-in` — or set `CODEROOK_TOKEN
148
148
  in automation. The skill will not save anything without asking you first, and
149
149
  never deletes.
150
150
 
151
+ ## Installing it as a plugin instead
152
+
153
+ If you would rather not install the command line first, CodeRook publishes a
154
+ plugin marketplace of its own:
155
+
156
+ ```
157
+ /plugin marketplace add https://coderook.com/marketplace.json
158
+ /plugin install coderook@coderook
159
+ ```
160
+
161
+ That fetches the same package from npm and brings the skill with it. The
162
+ marketplace is a plain file on coderook.com rather than a git repository, so
163
+ there is no repository to clone and no second account anywhere.
164
+
165
+ One thing to know: a plugin install puts the package in Claude Code's plugin
166
+ folder, not on your `PATH`. The skill copes with that on its own by falling
167
+ back to `npx -y @coderook/cli`, but `npm install --global @coderook/cli` is
168
+ faster if you plan to run commands yourself as well.
169
+
151
170
  ## A structured connection instead
152
171
 
153
172
  The skill teaches Claude the command line, which needs no configuration. If you
@@ -352,6 +352,31 @@ async function commandSubmit(parsed) {
352
352
  const secrets = await (0, worktree_js_1.detectSecrets)(folder);
353
353
  const sending = new Set(files.map((file) => file.path));
354
354
  const exposed = secrets.filter((secret) => sending.has(secret));
355
+ /*
356
+ Whole folders that belong to a program rather than to the work.
357
+
358
+ The credential check answers by name, one file at a time, and cannot see
359
+ into a dependency folder at all — which is where a real cached login sat
360
+ while a project went up carrying a browser profile, an emulated database
361
+ and an unrelated work tree. Naming the folder is the only answer that
362
+ keeps working after the program next runs and rewrites what is inside it.
363
+ */
364
+ const shielded = (await (0, worktree_js_1.detectPrivateDirectories)(folder)).filter((finding) => [...sending].some((file) => file === finding.path || file.startsWith(`${finding.path}/`)));
365
+ if (shielded.length && !hasFlag(parsed, "allow-private")) {
366
+ console.log(red(`
367
+ ${shielded.length} folder${shielded.length === 1 ? "" : "s"} here belong${shielded.length === 1 ? "s" : ""} to a program, not to your project:`));
368
+ for (const finding of shielded) {
369
+ console.log(` ${finding.path} ${dim(`— ${finding.because}`)}`);
370
+ }
371
+ console.log(`
372
+ Nothing was sent. To leave them behind:`);
373
+ for (const finding of shielded) {
374
+ console.log(` ${accent(`echo "${finding.rule}" >> .gitignore`)}`);
375
+ }
376
+ console.error(`
377
+ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
378
+ return 1;
379
+ }
355
380
  if (exposed.length) {
356
381
  console.log(red(`\n${exposed.length} file${exposed.length === 1 ? " looks like a credential" : "s look like credentials"}:`));
357
382
  for (const secret of exposed.slice(0, 20))
@@ -18,6 +18,7 @@ exports.fileSizes = fileSizes;
18
18
  exports.fileDiff = fileDiff;
19
19
  exports.projectTree = projectTree;
20
20
  exports.evaluateRules = evaluateRules;
21
+ exports.detectPrivateDirectories = detectPrivateDirectories;
21
22
  exports.detectSecrets = detectSecrets;
22
23
  /** Reading a project folder: changed files, diffs, and rule measurement. */
23
24
  const node_child_process_1 = require("node:child_process");
@@ -773,8 +774,110 @@ function emptyEvaluation(truncated) {
773
774
  truncated,
774
775
  };
775
776
  }
776
- const SECRET_NAMES = new Set([".env", ".env.local", ".env.production"]);
777
- const SECRET_SUFFIXES = [".pem", ".key", ".p12", ".pfx"];
777
+ /*
778
+ Names that are a credential whatever is inside them.
779
+
780
+ The first three were the whole list, which meant the check answered for a
781
+ hand-written `.env` and for nothing a tool leaves behind. Every addition
782
+ below is something a program writes without being asked: a cloud CLI's
783
+ cached login, an SSH key, a registry token. Those are the ones that get
784
+ published, precisely because nobody chose to put them there.
785
+ */
786
+ const SECRET_NAMES = new Set([
787
+ ".env",
788
+ ".env.local",
789
+ ".env.production",
790
+ ".npmrc",
791
+ ".netrc",
792
+ ".git-credentials",
793
+ ".dockercfg",
794
+ ".pypirc",
795
+ "credentials",
796
+ "credentials.json",
797
+ "wrangler-account.json",
798
+ "id_rsa",
799
+ "id_dsa",
800
+ "id_ecdsa",
801
+ "id_ed25519",
802
+ ]);
803
+ const SECRET_SUFFIXES = [".pem", ".key", ".p12", ".pfx", ".keystore", ".jks"];
804
+ /*
805
+ Directories that are one program's private state, not somebody's project.
806
+
807
+ These are judged by directory rather than by file because that is how they
808
+ arrive: nobody adds `Local Storage/leveldb/000005.ldb`, they add a folder
809
+ and everything under it comes too. Matching the folder is also what lets
810
+ the answer be a rule — one line in .gitignore covers the lot, where a list
811
+ of the files inside it would go stale the moment the program ran again.
812
+ */
813
+ const PRIVATE_DIRECTORIES = [
814
+ {
815
+ match: /^(Local Storage|Session Storage|IndexedDB|Service Worker|Cache|Code Cache|GPUCache)$/i,
816
+ because: "a browser profile, which holds the sites you are signed in to",
817
+ },
818
+ { match: /^\.wrangler$/i, because: "local service state, including emulated databases" },
819
+ { match: /^\.aws$/i, because: "cloud credentials" },
820
+ { match: /^\.ssh$/i, because: "SSH keys" },
821
+ { match: /^\.gnupg$/i, because: "signing keys" },
822
+ { match: /^\.docker$/i, because: "registry logins" },
823
+ { match: /^\.terraform$/i, because: "infrastructure state" },
824
+ /*
825
+ Not private in itself, and included for a reason worth stating: the
826
+ credential check refuses to walk it, sensibly, because it is enormous and
827
+ none of it is yours. That makes it the one place a cached login can sit
828
+ and never be found by name — which is exactly where a real one was, at
829
+ `node_modules/.cache/wrangler/wrangler-account.json`. Naming the folder
830
+ covers everything the name check cannot reach into.
831
+ */
832
+ {
833
+ match: /^node_modules$/i,
834
+ because: "a dependency folder, which is rebuilt from your manifest and is where tools cache their logins",
835
+ },
836
+ ];
837
+ /**
838
+ * Folders in this project that belong to a program rather than to the work.
839
+ *
840
+ * Deliberately separate from `detectSecrets`. A credential is a refusal: it
841
+ * must not be published and the answer is to remove it. One of these is a
842
+ * question — the files are not dangerous in themselves, they are simply not
843
+ * the project, and the useful response is a rule rather than a refusal.
844
+ */
845
+ async function detectPrivateDirectories(root) {
846
+ const found = [];
847
+ const pending = [root];
848
+ let seen = 0;
849
+ while (pending.length && found.length < 25 && seen < 40_000) {
850
+ const directory = pending.pop();
851
+ let entries;
852
+ try {
853
+ entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
854
+ }
855
+ catch {
856
+ continue;
857
+ }
858
+ for (const entry of entries) {
859
+ seen += 1;
860
+ if (!entry.isDirectory())
861
+ continue;
862
+ if ((0, rules_js_1.isUncounted)(entry.name))
863
+ continue;
864
+ const full = node_path_1.default.join(directory, entry.name);
865
+ const relative = node_path_1.default.relative(root, full).split(node_path_1.default.sep).join("/");
866
+ const rule = PRIVATE_DIRECTORIES.find((candidate) => candidate.match.test(entry.name));
867
+ if (rule) {
868
+ found.push({
869
+ path: relative,
870
+ because: rule.because,
871
+ rule: `${relative}/`,
872
+ });
873
+ /* No need to walk into it; the whole folder is the answer. */
874
+ continue;
875
+ }
876
+ pending.push(full);
877
+ }
878
+ }
879
+ return found;
880
+ }
778
881
  /** Files the flow must ask about before the first upload. */
779
882
  async function detectSecrets(root) {
780
883
  const found = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "CodeRook from the command line, on any operating system",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "homepage": "https://coderook.com",
@@ -20,6 +20,7 @@
20
20
  "coderook": "dist/cli/src/cli.js"
21
21
  },
22
22
  "files": [
23
+ ".claude-plugin",
23
24
  "dist",
24
25
  "skills"
25
26
  ],
@@ -28,7 +29,7 @@
28
29
  "check": "tsc -p tsconfig.json --noEmit",
29
30
  "test": "tsc -p tsconfig.json && node --test --experimental-strip-types test/*.test.ts",
30
31
  "start": "node dist/cli/src/cli.js",
31
- "prepublishOnly": "npm run build",
32
+ "prepublishOnly": "npm run sync:plugin && npm run build",
32
33
  "test:e2e": "node test/e2e.mjs",
33
34
  "test:matrix": "node test/state-matrix.mjs",
34
35
  "test:attempt": "node test/attempt-identity.mjs",
@@ -40,7 +41,8 @@
40
41
  "test:floor": "node test/version-floor.mjs",
41
42
  "test:faultsubmit": "node test/fault-submit.mjs",
42
43
  "test:race": "node test/race-attempts.mjs",
43
- "test:runner": "node test/runner-live.mjs"
44
+ "test:runner": "node test/runner-live.mjs",
45
+ "sync:plugin": "node scripts/sync-plugin-version.mjs"
44
46
  },
45
47
  "devDependencies": {
46
48
  "@types/node": "24.10.1",
@@ -19,6 +19,12 @@ Everything here is the `coderook` command line. Run it with Bash.
19
19
  person needs to run `coderook sign-in` themselves — it takes a personal access
20
20
  token, so do not attempt it on their behalf.
21
21
 
22
+ If the command is not found at all, this machine has the skill but not the
23
+ command line. Either install it once with
24
+ `npm install --global @coderook/cli`, or put `npx -y @coderook/cli` where
25
+ `coderook` appears below — the commands are identical, npx is just slower
26
+ to start.
27
+
22
28
  ## Reading
23
29
 
24
30
  ```bash