@dbarjs/dead-drop 0.1.0 → 0.2.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.
Files changed (2) hide show
  1. package/dist/cli.mjs +63 -27
  2. package/package.json +4 -2
package/dist/cli.mjs CHANGED
@@ -3,8 +3,10 @@ import { fileURLToPath } from 'node:url';
3
3
  import { defineCommand, runMain } from 'citty';
4
4
  import { readPackageJSON } from 'pkg-types';
5
5
  import { consola } from 'consola';
6
- import { join, relative, resolve, sep, dirname } from 'pathe';
7
- import { stat, readdir, readFile, mkdir, writeFile } from 'node:fs/promises';
6
+ import { join, dirname, resolve, sep } from 'pathe';
7
+ import { stat, readFile, mkdir, writeFile } from 'node:fs/promises';
8
+ import { glob } from 'tinyglobby';
9
+ import ignore from 'ignore';
8
10
  import { ofetch } from 'ofetch';
9
11
 
10
12
  const SALT_BYTES = 16;
@@ -57,34 +59,60 @@ function fromBase64(b64) {
57
59
  return new Uint8Array(Buffer.from(b64, "base64"));
58
60
  }
59
61
 
60
- const IGNORED_DIRS = /* @__PURE__ */ new Set([
61
- "node_modules",
62
- ".git",
63
- ".nuxt",
64
- ".output",
65
- ".nitro",
66
- ".cache",
67
- "dist"
68
- ]);
69
- async function* walkDirectory(rootDir) {
62
+ function isUnderGitDir(relPath) {
63
+ return relPath === ".git" || relPath.startsWith(".git/");
64
+ }
65
+ function isIgnored(relPath, cascade) {
66
+ let ignored = false;
67
+ for (const { dir, ig } of cascade) {
68
+ if (dir !== "" && relPath !== dir && !relPath.startsWith(`${dir}/`)) continue;
69
+ const sub = dir === "" ? relPath : relPath.slice(dir.length + 1);
70
+ if (sub === "") continue;
71
+ const { ignored: hit, unignored: rescued } = ig.test(sub);
72
+ if (hit) ignored = true;
73
+ else if (rescued) ignored = false;
74
+ }
75
+ return ignored;
76
+ }
77
+ async function buildCascade(rootDir, gitignorePaths) {
78
+ const ordered = [...gitignorePaths].sort(
79
+ (a, b) => a.split("/").length - b.split("/").length
80
+ );
81
+ const cascade = [];
82
+ for (const relPath of ordered) {
83
+ if (isIgnored(relPath, cascade)) continue;
84
+ const text = await readFile(join(rootDir, relPath), "utf8");
85
+ const parent = dirname(relPath);
86
+ cascade.push({ dir: parent === "." ? "" : parent, ig: ignore().add(text) });
87
+ }
88
+ return cascade;
89
+ }
90
+ async function* walkDirectory(rootDir, options = {}) {
70
91
  const root = await stat(rootDir);
71
92
  if (!root.isDirectory()) {
72
93
  throw new Error(`${rootDir} is not a directory`);
73
94
  }
74
- yield* walkInto(rootDir, rootDir);
75
- }
76
- async function* walkInto(rootDir, current) {
77
- const entries = await readdir(current, { withFileTypes: true });
78
- for (const entry of entries) {
79
- if (IGNORED_DIRS.has(entry.name)) continue;
80
- const absPath = join(current, entry.name);
81
- if (entry.isDirectory()) {
82
- yield* walkInto(rootDir, absPath);
83
- } else if (entry.isFile()) {
84
- const relPath = relative(rootDir, absPath);
85
- const bytes = new Uint8Array(await readFile(absPath));
86
- yield { absPath, relPath, bytes };
87
- }
95
+ const respectGitignore = options.respectGitignore ?? true;
96
+ let relPaths = await glob("**/*", {
97
+ cwd: rootDir,
98
+ dot: true,
99
+ onlyFiles: true,
100
+ expandDirectories: false,
101
+ ignore: [".git", ".git/**"]
102
+ });
103
+ relPaths = relPaths.filter((relPath) => !isUnderGitDir(relPath));
104
+ if (respectGitignore) {
105
+ const gitignorePaths = relPaths.filter(
106
+ (relPath) => relPath === ".gitignore" || relPath.endsWith("/.gitignore")
107
+ );
108
+ const cascade = await buildCascade(rootDir, gitignorePaths);
109
+ relPaths = relPaths.filter((relPath) => !isIgnored(relPath, cascade));
110
+ }
111
+ relPaths.sort();
112
+ for (const relPath of relPaths) {
113
+ const absPath = join(rootDir, relPath);
114
+ const bytes = new Uint8Array(await readFile(absPath));
115
+ yield { absPath, relPath, bytes };
88
116
  }
89
117
  }
90
118
 
@@ -214,12 +242,20 @@ const copy = defineCommand({
214
242
  api: {
215
243
  type: "string",
216
244
  description: "API base URL (overrides $DEAD_DROP_API_URL, default http://localhost:3000)."
245
+ },
246
+ gitignore: {
247
+ type: "boolean",
248
+ default: true,
249
+ description: "Copy every file, even those matched by .gitignore."
217
250
  }
218
251
  },
219
252
  async run({ args }) {
220
253
  const rootDir = resolve(args.directory);
221
254
  const baseUrl = resolveBaseUrl(args.api);
222
255
  const api = createApi(baseUrl);
256
+ if (args.gitignore) {
257
+ consola.log("Respecting .gitignore (use --no-gitignore to include all files).");
258
+ }
223
259
  const passphrase = await promptPassphrase();
224
260
  if (!passphrase) {
225
261
  consola.error("Passphrase required.");
@@ -234,7 +270,7 @@ const copy = defineCommand({
234
270
  };
235
271
  let dropId = null;
236
272
  let count = 0;
237
- for await (const file of walkDirectory(rootDir)) {
273
+ for await (const file of walkDirectory(rootDir, { respectGitignore: args.gitignore })) {
238
274
  const { ciphertext, nonce } = await encryptBytes(file.bytes, key);
239
275
  const contentHash = await sha256Hex(file.bytes);
240
276
  const entry = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbarjs/dead-drop",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Privacy-first dead-drop CLI: copy a project directory; the recipient cuts it once.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,9 +18,11 @@
18
18
  "dependencies": {
19
19
  "citty": "^0.1.6",
20
20
  "consola": "^3.4.2",
21
+ "ignore": "^7.0.5",
21
22
  "ofetch": "^1.5.1",
22
23
  "pathe": "^1.1.2",
23
- "pkg-types": "^1.3.1"
24
+ "pkg-types": "^1.3.1",
25
+ "tinyglobby": "^0.2.16"
24
26
  },
25
27
  "devDependencies": {
26
28
  "@types/node": "^25.8.0",