@coderook/cli 0.16.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.
|
@@ -2,9 +2,17 @@
|
|
|
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.
|
|
6
|
-
"author": {
|
|
5
|
+
"version": "0.17.0",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "ACCA Gaming Productions",
|
|
8
|
+
"url": "https://coderook.com"
|
|
9
|
+
},
|
|
7
10
|
"homepage": "https://coderook.com/docs#assistants",
|
|
8
11
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
9
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"coderook",
|
|
14
|
+
"versioning",
|
|
15
|
+
"snapshots",
|
|
16
|
+
"backup"
|
|
17
|
+
]
|
|
10
18
|
}
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -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
|
-
|
|
777
|
-
|
|
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 = [];
|