archiver-ts 0.0.1

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 (57) hide show
  1. package/.draft/vibing.md +7 -0
  2. package/.prettierrc +5 -0
  3. package/.scripts/build.js +7 -0
  4. package/.scripts/e2e.js +3 -0
  5. package/.scripts/lines.js +53 -0
  6. package/.scripts/publish.js +5 -0
  7. package/.scripts/recover.js +154 -0
  8. package/README.dev.md +124 -0
  9. package/README.md +82 -0
  10. package/dist/index.js +2687 -0
  11. package/dist/index.js.map +1 -0
  12. package/package.json +52 -0
  13. package/rollup.config.mjs +46 -0
  14. package/src/commands/archive.ts +101 -0
  15. package/src/commands/cd-shell.ts +24 -0
  16. package/src/commands/check.ts +40 -0
  17. package/src/commands/command-utils.ts +50 -0
  18. package/src/commands/config.ts +119 -0
  19. package/src/commands/index.ts +26 -0
  20. package/src/commands/list-interactive.ts +201 -0
  21. package/src/commands/list.ts +118 -0
  22. package/src/commands/log.ts +102 -0
  23. package/src/commands/update.ts +78 -0
  24. package/src/commands/vault.ts +212 -0
  25. package/src/consts/defaults.ts +32 -0
  26. package/src/consts/enums.ts +15 -0
  27. package/src/consts/index.ts +7 -0
  28. package/src/consts/path-tree.ts +24 -0
  29. package/src/consts/update.ts +13 -0
  30. package/src/core/context.ts +281 -0
  31. package/src/core/initialize.ts +295 -0
  32. package/src/global.d.ts +76 -0
  33. package/src/index.ts +30 -0
  34. package/src/services/archive.ts +579 -0
  35. package/src/services/audit-logger.ts +33 -0
  36. package/src/services/check.ts +345 -0
  37. package/src/services/config.ts +71 -0
  38. package/src/services/context.ts +46 -0
  39. package/src/services/log.ts +83 -0
  40. package/src/services/update.ts +121 -0
  41. package/src/services/vault.ts +247 -0
  42. package/src/utils/date.ts +39 -0
  43. package/src/utils/fs.ts +61 -0
  44. package/src/utils/json.ts +70 -0
  45. package/src/utils/parse.ts +63 -0
  46. package/src/utils/prompt.ts +20 -0
  47. package/src/utils/terminal.ts +168 -0
  48. package/tests/commands/cd-marker.test.ts +13 -0
  49. package/tests/commands/list-interactive.test.ts +26 -0
  50. package/tests/core/initialize.test.ts +130 -0
  51. package/tests/e2e/cli.e2e.test.ts +104 -0
  52. package/tests/e2e/mock-cli.ts +42 -0
  53. package/tests/services/archive-workflow.test.ts +176 -0
  54. package/tests/utils/parse.test.ts +44 -0
  55. package/tsconfig.build.json +11 -0
  56. package/tsconfig.json +25 -0
  57. package/vitest.config.ts +16 -0
@@ -0,0 +1,7 @@
1
+ 优化:
2
+ 1、命令都写在index.ts里面太厚重了,建议分在不同文件,并创建文件夹保存它们;
3
+ 2、在service文件夹下最好写一个直接创建context的方法,避免service外面绕来绕去;
4
+
5
+ ---
6
+
7
+ 能否做成,arv list会展示list,然后可以按键盘上下高亮一个条目,然后左右键可以选择操作,是进入还是恢复档案,回车确认
package/.prettierrc ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "all",
4
+ "printWidth": 120
5
+ }
@@ -0,0 +1,7 @@
1
+ import { execSync } from 'node:child_process';
2
+
3
+ execSync('npx rimraf dist', { stdio: 'inherit' });
4
+ execSync('rollup -c', { stdio: 'inherit' });
5
+ if (process.argv.includes('--arv')) {
6
+ execSync('node ./dist/index.js', { stdio: 'inherit' });
7
+ }
@@ -0,0 +1,3 @@
1
+ import { execSync } from 'node:child_process';
2
+
3
+ execSync('vitest run tests/e2e', { stdio: 'inherit', env: { ...process.env, ARCHIVER_DIR_PARENT: process.cwd() } });
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const ROOT = path.resolve('.');
5
+
6
+ const IGNORE_DIRS = new Set(['node_modules', 'dist', 'tests']);
7
+
8
+ const EXTENSIONS = new Set(['.ts', '.tsx', '.css']);
9
+
10
+ let totalLines = 0;
11
+ let fileCount = 0;
12
+
13
+ function countLines(filePath) {
14
+ const content = fs.readFileSync(filePath, 'utf8');
15
+ return content.split(/\r\n|\n|\r/).length;
16
+ }
17
+
18
+ function walk(dir) {
19
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
20
+
21
+ for (const entry of entries) {
22
+ const fullPath = path.join(dir, entry.name);
23
+
24
+ if (entry.isDirectory()) {
25
+ if (IGNORE_DIRS.has(entry.name)) {
26
+ continue;
27
+ }
28
+ walk(fullPath);
29
+ continue;
30
+ }
31
+
32
+ if (!entry.isFile()) {
33
+ continue;
34
+ }
35
+
36
+ const ext = path.extname(entry.name);
37
+ if (!EXTENSIONS.has(ext)) {
38
+ continue;
39
+ }
40
+
41
+ const lines = countLines(fullPath);
42
+ totalLines += lines;
43
+ fileCount++;
44
+
45
+ console.log(`${lines.toString().padStart(7)} ${path.relative(ROOT, fullPath)}`);
46
+ }
47
+ }
48
+
49
+ // "lines": "clear && find ./packages -type f \\( -name \"*.ts\" -o -name \"*.tsx\" -o -name \"*.js\" -o -name \"*.mjs\" -o -name \"*.cjs\" \\) | xargs wc -l",
50
+
51
+ walk(ROOT);
52
+ console.log(`Files: ${fileCount} for ${[...EXTENSIONS].join(', ')}`);
53
+ console.log(`Total lines: ${totalLines}`);
@@ -0,0 +1,5 @@
1
+ import { execSync } from 'node:child_process';
2
+
3
+ execSync('npx rimraf dist', { stdio: 'inherit' });
4
+ execSync('rollup -c', { stdio: 'inherit' });
5
+ execSync('npm publish --access public', { stdio: 'inherit' });
@@ -0,0 +1,154 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { execSync } from 'node:child_process';
5
+
6
+ function shellQuote(value) {
7
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
8
+ }
9
+
10
+ function hasFlag(name) {
11
+ return process.argv.includes(name);
12
+ }
13
+
14
+ function readOption(name) {
15
+ const index = process.argv.indexOf(name);
16
+ if (index === -1) {
17
+ return undefined;
18
+ }
19
+ return process.argv[index + 1];
20
+ }
21
+
22
+ function statusIsArchived(rawStatus) {
23
+ return rawStatus === 'A' || rawStatus === 'Archived';
24
+ }
25
+
26
+ function normalizeEntry(raw) {
27
+ const id = Number(raw.id);
28
+ const vaultId = Number(raw.vid ?? raw.vaultId);
29
+ const item = String(raw.i ?? raw.item ?? '');
30
+ const directory = String(raw.d ?? raw.directory ?? '');
31
+ const status = String(raw.st ?? raw.status ?? '');
32
+ if (!Number.isInteger(id) || id <= 0) {
33
+ return undefined;
34
+ }
35
+ if (!Number.isInteger(vaultId) || vaultId < 0) {
36
+ return undefined;
37
+ }
38
+ if (!item || !directory) {
39
+ return undefined;
40
+ }
41
+ return { id, vaultId, item, directory, status };
42
+ }
43
+
44
+ function readJsonl(filePath) {
45
+ const content = fs.readFileSync(filePath, 'utf8');
46
+ if (!content.trim()) {
47
+ return [];
48
+ }
49
+
50
+ return content
51
+ .split(/\r?\n/)
52
+ .map((line) => line.trim())
53
+ .filter((line) => line.length > 0)
54
+ .map((line, index) => {
55
+ try {
56
+ return JSON.parse(line);
57
+ } catch (error) {
58
+ throw new Error(`Invalid JSON at ${filePath}:${index + 1}: ${error.message}`);
59
+ }
60
+ });
61
+ }
62
+
63
+ function readListTable(rootPath) {
64
+ const jsonlPath = path.join(rootPath, 'core', 'list.jsonl');
65
+ if (fs.existsSync(jsonlPath)) {
66
+ return readJsonl(jsonlPath);
67
+ }
68
+
69
+ const jsonPath = path.join(rootPath, 'core', 'list.json');
70
+ if (!fs.existsSync(jsonPath)) {
71
+ throw new Error(`Neither list.jsonl nor list.json exists under ${path.join(rootPath, 'core')}.`);
72
+ }
73
+
74
+ const parsed = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
75
+ return Array.isArray(parsed) ? parsed : [parsed];
76
+ }
77
+
78
+ function main() {
79
+ const apply = hasFlag('--apply');
80
+ const force = hasFlag('--force');
81
+
82
+ const rootPath = path.join(os.homedir(), '.archiver-rust');
83
+
84
+ const rows = readListTable(rootPath);
85
+ const entries = rows
86
+ .map((row) => normalizeEntry(row))
87
+ .filter((entry) => entry !== undefined)
88
+ .filter((entry) => statusIsArchived(entry.status))
89
+ .sort((a, b) => a.id - b.id);
90
+
91
+ if (entries.length === 0) {
92
+ console.log(`[INFO] No archived entries found in ${rootPath}.`);
93
+ return;
94
+ }
95
+
96
+ let listed = 0;
97
+ let restored = 0;
98
+ let skippedMissing = 0;
99
+ let skippedTargetExists = 0;
100
+ let failed = 0;
101
+
102
+ console.log(`[INFO] root=${rootPath}`);
103
+ console.log(`[INFO] mode=${apply ? 'apply' : 'dry-run'}${force ? ' (force enabled)' : ''}`);
104
+
105
+ for (const entry of entries) {
106
+ const sourcePath = path.join(rootPath, 'vaults', String(entry.vaultId), String(entry.id));
107
+ const targetPath = path.join(entry.directory, entry.item);
108
+
109
+ if (!fs.existsSync(sourcePath)) {
110
+ skippedMissing += 1;
111
+ console.log(`[SKIP][${entry.id}] source missing: ${sourcePath}`);
112
+ continue;
113
+ }
114
+
115
+ if (fs.existsSync(targetPath) && !force) {
116
+ skippedTargetExists += 1;
117
+ console.log(`[SKIP][${entry.id}] target exists: ${targetPath}`);
118
+ continue;
119
+ }
120
+
121
+ const mkdirCommand = `mkdir -p ${shellQuote(path.dirname(targetPath))}`;
122
+ const moveCommand = `mv ${shellQuote(sourcePath)} ${shellQuote(targetPath)}`;
123
+
124
+ listed += 1;
125
+ console.log(`[PLAN][${entry.id}] ${moveCommand}`);
126
+
127
+ if (!apply) {
128
+ continue;
129
+ }
130
+
131
+ try {
132
+ execSync(mkdirCommand, { stdio: 'inherit' });
133
+ if (force && fs.existsSync(targetPath)) {
134
+ const removeCommand = `rm -rf ${shellQuote(targetPath)}`;
135
+ execSync(removeCommand, { stdio: 'inherit' });
136
+ }
137
+ execSync(moveCommand, { stdio: 'inherit' });
138
+ restored += 1;
139
+ } catch (error) {
140
+ failed += 1;
141
+ console.error(`[FAIL][${entry.id}] ${error.message}`);
142
+ }
143
+ }
144
+
145
+ console.log(
146
+ `[INFO] listed=${listed}, restored=${restored}, skipped_missing=${skippedMissing}, skipped_target_exists=${skippedTargetExists}, failed=${failed}`,
147
+ );
148
+
149
+ if (apply && failed > 0) {
150
+ process.exitCode = 1;
151
+ }
152
+ }
153
+
154
+ main();
package/README.dev.md ADDED
@@ -0,0 +1,124 @@
1
+ # archiver-ts (Developer Guide)
2
+
3
+ This file is for contributors and maintainers.
4
+
5
+ If you are an end user, read `readme.md`.
6
+
7
+ ## Tech stack
8
+
9
+ - Node.js >= 20
10
+ - TypeScript (ESM)
11
+ - Commander for CLI
12
+ - Vitest for tests
13
+ - Rollup for build output
14
+
15
+ ## Quick start (dev)
16
+
17
+ ```bash
18
+ npm install
19
+ npm run check
20
+ npm test
21
+ ```
22
+
23
+ Run CLI from source:
24
+
25
+ ```bash
26
+ npm run dev -- --help
27
+ ```
28
+
29
+ Build:
30
+
31
+ ```bash
32
+ npm run build
33
+ ```
34
+
35
+ Run built CLI:
36
+
37
+ ```bash
38
+ node dist/index.js --help
39
+ ```
40
+
41
+ ## Repo scripts
42
+
43
+ - `npm run dev`: run CLI from `src/index.ts` via `tsx`
44
+ - `npm run check`: TypeScript type-check (`tsc --noEmit`)
45
+ - `npm test`: unit/e2e tests via Vitest
46
+ - `npm run e2e`: helper wrapper for e2e entry script
47
+ - `npm run build`: clean `dist` then build bundle with Rollup
48
+ - `npm run arv`: build and run built CLI
49
+ - `npm run pub`: publish helper script
50
+
51
+ ## Runtime and data root
52
+
53
+ Root resolution is implemented in `src/consts/path-tree.ts`:
54
+
55
+ - `NODE_ENV=production`: `ARCHIVER_PATH` or `~/.archiver`
56
+ - otherwise: `<cwd>/.archiver`
57
+
58
+ Current data layout:
59
+
60
+ ```text
61
+ .archiver/
62
+ config.jsonc
63
+ auto-incr.jsonc
64
+ list.jsonl
65
+ vaults.jsonl
66
+ log.jsonl
67
+ vaults/
68
+ <vaultId>/
69
+ <archiveId>/
70
+ <originalName>
71
+ ```
72
+
73
+ ## Architecture map
74
+
75
+ - `src/index.ts`: process entry, shell wrapper bootstrap, command parse
76
+ - `src/commands/*`: CLI command definitions and command-level flows
77
+ - `src/services/*`: business logic (archive, vault, config, log, update, check)
78
+ - `src/core/context.ts`: persistence and path helpers
79
+ - `src/core/initialize.ts`: shell wrapper auto-install
80
+ - `src/consts/*`: app constants/defaults/path tree
81
+ - `src/utils/*`: table rendering, parsing, fs/json/date helpers, prompt helpers
82
+ - `tests/*`: unit/integration/e2e tests
83
+
84
+ ## Command wiring notes
85
+
86
+ - Program wiring lives in `src/commands/index.ts`
87
+ - Main command groups:
88
+ - archive actions: `put`, `restore`, `move`, `cd`
89
+ - vault actions: `vault use/create/remove/recover/rename/list`
90
+ - views and maintenance: `list`, `log`, `config`, `update`, `check`
91
+
92
+ ## Important behavior notes
93
+
94
+ - Archive/restore/move use filesystem rename operations.
95
+ - Default vault is `@` (id `0`) and is protected.
96
+ - `list` plain mode prints names only; default vault entries omit vault prefix.
97
+ - Audit logs are appended into a single file: `log.jsonl`.
98
+ - Auto update check interval is 24h (`src/consts/update.ts`).
99
+ - Shell wrapper install is best-effort and should not block command execution.
100
+
101
+ ## Recovery helper
102
+
103
+ Legacy helper script:
104
+
105
+ ```bash
106
+ node .scripts/recover.js [--apply] [--force] [--root <path>]
107
+ ```
108
+
109
+ This script is for recovery from legacy rust data layout and is not part of normal CLI flows.
110
+
111
+ ## Testing
112
+
113
+ Run all tests:
114
+
115
+ ```bash
116
+ npm test
117
+ ```
118
+
119
+ Useful test focus:
120
+
121
+ - e2e CLI flow: `tests/e2e/cli.e2e.test.ts`
122
+ - archive/vault workflow: `tests/services/archive-workflow.test.ts`
123
+ - shell wrapper init: `tests/core/initialize.test.ts`
124
+
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # archiver
2
+
3
+ `archiver` (binary: `arv`) archives files/folders into a managed vault directory and lets you restore them later.
4
+
5
+ It uses filesystem move/rename semantics (no compression), keeps metadata in `.archiver`, and records audit logs.
6
+
7
+ ## Quick start
8
+
9
+ Install dependencies and build:
10
+
11
+ ```bash
12
+ npm install -g archiver-ts
13
+ ```
14
+
15
+ ## Common commands
16
+
17
+ Archive / restore:
18
+
19
+ ```bash
20
+ arv put <items...> [-v|--vault <vault>] [-m|--message <msg>] [-r|--remark <remark>]
21
+ arv restore <ids...>
22
+ arv move <ids...> --to <vault>
23
+ arv cd <archive-id | vault/archive-id> [--print]
24
+ ```
25
+
26
+ Vault management:
27
+
28
+ ```bash
29
+ arv vault use <name-or-id>
30
+ arv vault create <name> [-r|--remark <remark>] [-a|--activate]
31
+ arv vault remove <name-or-id>
32
+ arv vault recover <name-or-id>
33
+ arv vault rename <old> <new>
34
+ arv vault list [-a|--all]
35
+ ```
36
+
37
+ Query and maintenance:
38
+
39
+ ```bash
40
+ arv list [--restored] [--all] [--vault <vault>] [--no-interactive]
41
+ arv log [YYYYMM | YYYYMM-YYYYMM | all]
42
+ arv log --id <log-id>
43
+ arv config list [-c|--comment]
44
+ arv config alias <alias=path> [-r|--remove]
45
+ arv config update-check <on|off>
46
+ arv config vault-item-sep <separator>
47
+ arv update [--repo <owner/repo>] [--install]
48
+ arv check
49
+ ```
50
+
51
+ ## List output behavior
52
+
53
+ `arv list` plain mode prints names only (one per line):
54
+
55
+ - default vault (`@`, id `0`): show item name only
56
+ - non-default vault: show `<vaultName>(<vaultId>)<sep><item>`
57
+ - `<sep>` comes from `config vault-item-sep` (default `::`)
58
+
59
+ Example:
60
+
61
+ ```text
62
+ todo.txt
63
+ work(1)::report.pdf
64
+ ```
65
+
66
+ ## Shell wrapper note
67
+
68
+ On interactive terminal startup, `arv` may auto-install a shell wrapper function so `cd` can move your shell to archive slot paths.
69
+
70
+ Disable this behavior if needed:
71
+
72
+ ```bash
73
+ ARV_DISABLE_SHELL_INIT=1 arv <command>
74
+ ```
75
+
76
+ ## Useful aliases
77
+
78
+ - `list`: `l`, `ls`
79
+ - `log`: `lg`
80
+ - `config`: `c`, `cfg`
81
+ - `update`: `u`, `upd`
82
+ - `check`: `chk`