@devkitvault/recall 1.2.9 → 1.2.11
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.
- package/README.md +27 -25
- package/dist/commands/import.js +155 -45
- package/dist/commands/sync.js +143 -32
- package/dist/lib/config.js +1 -1
- package/dist/lib/local-collections.js +617 -0
- package/dist/lib/local-vault.js +12 -2
- package/dist/lib/shell-history.js +94 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# @devkitvault/recall
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
I kept losing useful shell commands. So I built a small vault for the ones I actually reuse.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**recall** saves commands locally, lets you search and run them by name, and optionally syncs when you want the same vault on another machine.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Free:** local vault only — no account, nothing uploads (`~/.recall/commands.json`)
|
|
8
|
+
- **Pro / Team:** `recall sync` + Ask (prints a suggestion; never runs it)
|
|
9
|
+
|
|
10
|
+
Product: [recall.devkitvault.com](https://recall.devkitvault.com) · Docs: [docs](https://recall.devkitvault.com/docs)
|
|
8
11
|
|
|
9
12
|
## Requirements
|
|
10
13
|
|
|
@@ -23,44 +26,43 @@ recall --version
|
|
|
23
26
|
recall doctor
|
|
24
27
|
```
|
|
25
28
|
|
|
26
|
-
## Quick start (local
|
|
29
|
+
## Quick start (Free — local)
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
No account needed.
|
|
29
32
|
|
|
30
33
|
```sh
|
|
31
|
-
recall save "docker compose up -d"
|
|
34
|
+
recall save "docker compose up -d" -n up -t docker
|
|
32
35
|
recall save --last -n useful -t docker
|
|
36
|
+
recall import history -n 20
|
|
33
37
|
recall list
|
|
34
38
|
recall search docker
|
|
35
|
-
recall run
|
|
39
|
+
recall run up
|
|
36
40
|
```
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
`recall import history` reads your shell history file (zsh / bash / PowerShell) and lets you pick what to keep. It does not sit in the background recording keystrokes.
|
|
39
43
|
|
|
40
|
-
`recall save --last` (or `-L`)
|
|
44
|
+
`recall save --last` (or `-L`) takes the last history line. Not supported in Windows `cmd.exe`. On bash, if it’s empty: `history -a && recall save --last`.
|
|
41
45
|
|
|
42
46
|
```sh
|
|
43
47
|
recall playbook save deploy "docker compose up -d" "pnpm migrate"
|
|
44
48
|
recall playbook run deploy --dry-run
|
|
45
49
|
```
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
`recall playbook run` executes steps. Ask never does.
|
|
48
52
|
|
|
49
53
|
## Ask (Pro / Team)
|
|
50
54
|
|
|
51
|
-
Natural-language Ask prints a suggestion and never runs it. Requires a Pro or Team plan.
|
|
52
|
-
|
|
53
55
|
```sh
|
|
54
56
|
recall auth login
|
|
55
57
|
recall "find the 10 biggest files in this directory"
|
|
56
58
|
rec "that docker command from last week"
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
Ask only prints. You run the command yourself.
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
No-args `recall` opens an Ask session (`Recall >`). `/save [name]` stores the last suggestion. `/exit` leaves.
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
## Sync (Pro / Team)
|
|
64
66
|
|
|
65
67
|
```sh
|
|
66
68
|
recall auth login
|
|
@@ -70,12 +72,13 @@ recall sync --push # local-only → cloud
|
|
|
70
72
|
recall sync --status
|
|
71
73
|
recall whoami
|
|
72
74
|
```
|
|
73
|
-
|
|
75
|
+
|
|
76
|
+
## Native binary (optional)
|
|
74
77
|
|
|
75
78
|
- **macOS / Linux:** `curl -fsSL https://devkitvault.com/recall/install.sh | sh`
|
|
76
79
|
- **Windows:** `irm https://devkitvault.com/recall/install.ps1 | iex`
|
|
77
80
|
|
|
78
|
-
See [
|
|
81
|
+
See [install docs](https://recall.devkitvault.com/docs/install).
|
|
79
82
|
|
|
80
83
|
## Update
|
|
81
84
|
|
|
@@ -90,18 +93,17 @@ recall doctor
|
|
|
90
93
|
recall whoami
|
|
91
94
|
```
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
Issues: [github.com/devkitvaultorg/recall-cli/issues](https://github.com/devkitvaultorg/recall-cli/issues)
|
|
94
97
|
|
|
95
98
|
## Links
|
|
96
99
|
|
|
97
|
-
|
|
|
100
|
+
| | |
|
|
98
101
|
| --------- | ----------------------------------------------------------------------------------------- |
|
|
99
|
-
| Website
|
|
100
|
-
| Docs
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
| VS Code
|
|
104
|
-
| Issues | [github.com/devkitvault/recall-cli](https://github.com/devkitvault/recall-cli/issues) |
|
|
102
|
+
| Website | [recall.devkitvault.com](https://recall.devkitvault.com) |
|
|
103
|
+
| Docs | [recall.devkitvault.com/docs](https://recall.devkitvault.com/docs) |
|
|
104
|
+
| Source | [github.com/devkitvaultorg/recall-cli](https://github.com/devkitvaultorg/recall-cli) |
|
|
105
|
+
| npm | [@devkitvault/recall](https://www.npmjs.com/package/@devkitvault/recall) |
|
|
106
|
+
| VS Code | [Marketplace](https://marketplace.visualstudio.com/items?itemName=devkitvault.recall-cmd) |
|
|
105
107
|
|
|
106
108
|
## License
|
|
107
109
|
|
package/dist/commands/import.js
CHANGED
|
@@ -7,19 +7,22 @@ exports.importCommand = void 0;
|
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const commander_1 = require("commander");
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
-
const
|
|
10
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const local_vault_1 = require("../lib/local-vault");
|
|
13
|
+
const tags_1 = require("../lib/tags");
|
|
14
|
+
const shell_history_1 = require("../lib/shell-history");
|
|
14
15
|
function parseJSON(content) {
|
|
15
16
|
const data = JSON.parse(content);
|
|
16
17
|
if (!Array.isArray(data))
|
|
17
18
|
throw new Error('JSON must be an array of commands');
|
|
18
19
|
return data.map((item) => ({
|
|
19
|
-
command: item.command,
|
|
20
|
-
name: item.name,
|
|
21
|
-
tags: item.tags
|
|
22
|
-
|
|
20
|
+
command: String(item.command ?? ''),
|
|
21
|
+
name: typeof item.name === 'string' ? item.name : undefined,
|
|
22
|
+
tags: Array.isArray(item.tags)
|
|
23
|
+
? item.tags.filter((t) => typeof t === 'string')
|
|
24
|
+
: undefined,
|
|
25
|
+
})).filter((e) => e.command.trim());
|
|
23
26
|
}
|
|
24
27
|
function parseSh(content) {
|
|
25
28
|
const lines = content.split('\n');
|
|
@@ -28,14 +31,13 @@ function parseSh(content) {
|
|
|
28
31
|
let currentTags = [];
|
|
29
32
|
for (const raw of lines) {
|
|
30
33
|
const line = raw.trim();
|
|
31
|
-
// skip empty lines and shebang
|
|
32
34
|
if (!line || line.startsWith('#!'))
|
|
33
35
|
continue;
|
|
34
36
|
if (line.startsWith('# tags:')) {
|
|
35
37
|
currentTags = line
|
|
36
38
|
.replace('# tags:', '')
|
|
37
39
|
.split(',')
|
|
38
|
-
.map(t => t.trim())
|
|
40
|
+
.map((t) => t.trim())
|
|
39
41
|
.filter(Boolean);
|
|
40
42
|
continue;
|
|
41
43
|
}
|
|
@@ -43,24 +45,59 @@ function parseSh(content) {
|
|
|
43
45
|
currentName = line.replace('#', '').trim();
|
|
44
46
|
continue;
|
|
45
47
|
}
|
|
46
|
-
// it's a command
|
|
47
48
|
entries.push({
|
|
48
49
|
command: line,
|
|
49
50
|
name: currentName,
|
|
50
|
-
tags: currentTags,
|
|
51
|
+
tags: currentTags.length ? currentTags : undefined,
|
|
51
52
|
});
|
|
52
|
-
// reset
|
|
53
53
|
currentName = undefined;
|
|
54
54
|
currentTags = [];
|
|
55
55
|
}
|
|
56
56
|
return entries;
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
function commandAlreadyLocal(command, name) {
|
|
59
|
+
if (name && (0, local_vault_1.findLocalByName)(name))
|
|
60
|
+
return true;
|
|
61
|
+
return (0, local_vault_1.listLocalCommands)().some((c) => c.command === command);
|
|
62
|
+
}
|
|
63
|
+
function suggestName(command, used) {
|
|
64
|
+
const base = command
|
|
65
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
66
|
+
.replace(/^-|-$/g, '')
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.slice(0, 32) || 'cmd';
|
|
69
|
+
let name = base;
|
|
70
|
+
let n = 2;
|
|
71
|
+
while (used.has(name) || (0, local_vault_1.findLocalByName)(name)) {
|
|
72
|
+
name = `${base.slice(0, 28)}-${n}`;
|
|
73
|
+
n++;
|
|
74
|
+
}
|
|
75
|
+
used.add(name);
|
|
76
|
+
return name;
|
|
77
|
+
}
|
|
78
|
+
async function importEntries(entries, opts) {
|
|
79
|
+
let imported = 0;
|
|
80
|
+
let skipped = 0;
|
|
81
|
+
const usedNames = new Set();
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (opts.skipDuplicates && commandAlreadyLocal(entry.command, entry.name)) {
|
|
84
|
+
skipped++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const name = entry.name
|
|
88
|
+
?? (opts.nameThem ? suggestName(entry.command, usedNames) : undefined);
|
|
89
|
+
if (name)
|
|
90
|
+
usedNames.add(name);
|
|
91
|
+
(0, local_vault_1.saveLocalCommand)({
|
|
92
|
+
command: entry.command,
|
|
93
|
+
name,
|
|
94
|
+
tags: entry.tags,
|
|
95
|
+
});
|
|
96
|
+
imported++;
|
|
97
|
+
}
|
|
98
|
+
return { imported, skipped };
|
|
99
|
+
}
|
|
100
|
+
async function importFromFile(file, opts) {
|
|
64
101
|
const filePath = path_1.default.resolve(file);
|
|
65
102
|
if (!fs_1.default.existsSync(filePath)) {
|
|
66
103
|
console.error(chalk_1.default.red(`\n File not found: ${filePath}\n`));
|
|
@@ -77,7 +114,6 @@ exports.importCommand = new commander_1.Command('import')
|
|
|
77
114
|
entries = parseSh(content);
|
|
78
115
|
}
|
|
79
116
|
else {
|
|
80
|
-
// try JSON first, then sh
|
|
81
117
|
try {
|
|
82
118
|
entries = parseJSON(content);
|
|
83
119
|
}
|
|
@@ -87,40 +123,114 @@ exports.importCommand = new commander_1.Command('import')
|
|
|
87
123
|
}
|
|
88
124
|
}
|
|
89
125
|
catch (err) {
|
|
90
|
-
|
|
126
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
127
|
+
console.error(chalk_1.default.red(`\n Failed to parse file: ${message}\n`));
|
|
91
128
|
process.exit(1);
|
|
92
129
|
}
|
|
93
130
|
if (!entries.length) {
|
|
94
131
|
console.log(chalk_1.default.dim('\n No commands found in file.\n'));
|
|
95
132
|
return;
|
|
96
133
|
}
|
|
97
|
-
console.log(chalk_1.default.dim(`\n Found ${entries.length} command${entries.length === 1 ? '' : 's'}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
console.log(chalk_1.default.dim(`\n Found ${entries.length} command${entries.length === 1 ? '' : 's'} in file\n`));
|
|
135
|
+
const { imported, skipped } = await importEntries(entries, {
|
|
136
|
+
skipDuplicates: opts.skipDuplicates,
|
|
137
|
+
nameThem: false,
|
|
138
|
+
});
|
|
139
|
+
console.log(` ${chalk_1.default.green(`${imported} imported`)} ${chalk_1.default.yellow(`${skipped} skipped`)}`);
|
|
140
|
+
console.log(chalk_1.default.dim(` Vault: ${local_vault_1.VAULT_FILE}\n`));
|
|
141
|
+
}
|
|
142
|
+
exports.importCommand = new commander_1.Command('import')
|
|
143
|
+
.description('Import commands into your local vault')
|
|
144
|
+
.argument('[file]', 'JSON or shell script to import (or use: recall import history)')
|
|
145
|
+
.option('-s, --skip-duplicates', 'Skip commands that already exist locally', true)
|
|
146
|
+
.addHelpText('after', `
|
|
147
|
+
Examples:
|
|
148
|
+
$ recall import history
|
|
149
|
+
$ recall import history -n 20 -y
|
|
150
|
+
$ recall import ./backup.json
|
|
151
|
+
`)
|
|
152
|
+
.action(async (file, opts) => {
|
|
153
|
+
if (!file) {
|
|
154
|
+
console.log(chalk_1.default.dim('\n Usage:'));
|
|
155
|
+
console.log(chalk_1.default.dim(' recall import history # from shell history (curated)'));
|
|
156
|
+
console.log(chalk_1.default.dim(' recall import <file> # from JSON or .sh\n'));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
await importFromFile(file, opts);
|
|
160
|
+
});
|
|
161
|
+
exports.importCommand
|
|
162
|
+
.command('history')
|
|
163
|
+
.description('Import curated commands from shell history into the local vault (not live capture)')
|
|
164
|
+
.option('-n, --limit <count>', 'Max unique recent commands to consider', '40')
|
|
165
|
+
.option('-y, --yes', 'Import all candidates without prompting')
|
|
166
|
+
.option('-i, --interactive', 'Choose which commands to import (default unless --yes)')
|
|
167
|
+
.option('--dry-run', 'Show candidates only; do not write')
|
|
168
|
+
.option('-t, --tags <tags>', 'Tags applied to imported commands')
|
|
169
|
+
.option('--min-length <n>', 'Skip commands shorter than this', '3')
|
|
170
|
+
.option('--no-names', 'Do not auto-generate names')
|
|
171
|
+
.option('--no-skip-duplicates', 'Import even if the same command text already exists')
|
|
172
|
+
.action(async (opts) => {
|
|
173
|
+
const limit = Math.max(1, parseInt(String(opts.limit), 10) || 40);
|
|
174
|
+
const minLength = Math.max(1, parseInt(String(opts.minLength), 10) || 3);
|
|
175
|
+
const tags = (0, tags_1.parseTags)(opts.tags);
|
|
176
|
+
let commands;
|
|
177
|
+
let source;
|
|
178
|
+
let shell;
|
|
179
|
+
try {
|
|
180
|
+
const result = (0, shell_history_1.readHistoryCommands)({ limit, minLength });
|
|
181
|
+
commands = result.commands;
|
|
182
|
+
source = result.source;
|
|
183
|
+
shell = result.shell;
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
if (err instanceof shell_history_1.LastCommandError) {
|
|
187
|
+
console.log(chalk_1.default.red(`\n ${err.message}\n`));
|
|
188
|
+
process.exit(1);
|
|
121
189
|
}
|
|
190
|
+
throw err;
|
|
122
191
|
}
|
|
123
192
|
console.log();
|
|
124
|
-
console.log(
|
|
193
|
+
console.log(chalk_1.default.dim(` Shell: ${shell} · ${source}`));
|
|
194
|
+
console.log(chalk_1.default.dim(` ${commands.length} unique candidate${commands.length === 1 ? '' : 's'} (newest first)\n`));
|
|
195
|
+
let selected = commands;
|
|
196
|
+
const interactive = !opts.yes && (opts.interactive || !opts.dryRun);
|
|
197
|
+
if (opts.yes) {
|
|
198
|
+
selected = commands;
|
|
199
|
+
}
|
|
200
|
+
else if (interactive && !opts.dryRun) {
|
|
201
|
+
const { picked } = await inquirer_1.default.prompt([
|
|
202
|
+
{
|
|
203
|
+
type: 'checkbox',
|
|
204
|
+
name: 'picked',
|
|
205
|
+
message: 'Select commands to save locally',
|
|
206
|
+
pageSize: 16,
|
|
207
|
+
choices: commands.map((c) => ({
|
|
208
|
+
name: c.length > 80 ? `${c.slice(0, 77)}…` : c,
|
|
209
|
+
value: c,
|
|
210
|
+
checked: false,
|
|
211
|
+
})),
|
|
212
|
+
},
|
|
213
|
+
]);
|
|
214
|
+
selected = picked;
|
|
215
|
+
if (!selected.length) {
|
|
216
|
+
console.log(chalk_1.default.dim('\n Nothing selected.\n'));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (opts.dryRun) {
|
|
221
|
+
for (const [i, c] of selected.entries()) {
|
|
222
|
+
console.log(` ${chalk_1.default.dim(String(i + 1).padStart(2))}. ${c}`);
|
|
223
|
+
}
|
|
224
|
+
console.log(chalk_1.default.dim(`\n Dry run — nothing written. Drop --dry-run to import.\n`));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const entries = selected.map((command) => ({ command, tags }));
|
|
228
|
+
const { imported, skipped } = await importEntries(entries, {
|
|
229
|
+
skipDuplicates: opts.skipDuplicates !== false,
|
|
230
|
+
nameThem: opts.names !== false,
|
|
231
|
+
});
|
|
125
232
|
console.log();
|
|
233
|
+
console.log(` ${chalk_1.default.green(`${imported} imported`)} ${chalk_1.default.yellow(`${skipped} skipped`)}`);
|
|
234
|
+
console.log(chalk_1.default.dim(` Vault: ${local_vault_1.VAULT_FILE}`));
|
|
235
|
+
console.log(chalk_1.default.dim(' Sync to cloud (Pro): recall sync\n'));
|
|
126
236
|
});
|
package/dist/commands/sync.js
CHANGED
|
@@ -10,29 +10,47 @@ const commander_1 = require("commander");
|
|
|
10
10
|
const ora_1 = __importDefault(require("ora"));
|
|
11
11
|
const api_1 = require("../lib/api");
|
|
12
12
|
const auth_1 = require("../lib/auth");
|
|
13
|
+
const local_collections_1 = require("../lib/local-collections");
|
|
13
14
|
const local_vault_1 = require("../lib/local-vault");
|
|
14
15
|
exports.syncCommand = new commander_1.Command('sync')
|
|
15
16
|
.description('Sync local vault ↔ cloud (Pro/Team)')
|
|
16
17
|
.option('-s, --status', 'Show local vault + last sync status')
|
|
17
18
|
.option('--pull', 'Pull cloud → local only')
|
|
18
|
-
.option('--push', 'Push local-only
|
|
19
|
+
.option('--push', 'Push local-only items → cloud only')
|
|
19
20
|
.action(async (opts) => {
|
|
20
21
|
const vault = (0, local_vault_1.readVault)();
|
|
21
22
|
if (opts.status) {
|
|
23
|
+
const groups = (0, local_collections_1.listLocalGroups)().length;
|
|
24
|
+
const templates = (0, local_collections_1.listLocalTemplates)().length;
|
|
25
|
+
const snippets = (0, local_collections_1.listLocalSnippets)().length;
|
|
26
|
+
const envSets = (0, local_collections_1.listLocalEnvSets)().length;
|
|
27
|
+
const aliases = (0, local_collections_1.listLocalAliases)().length;
|
|
22
28
|
console.log();
|
|
23
|
-
console.log(
|
|
24
|
-
console.log(`
|
|
29
|
+
console.log(chalk_1.default.white(' Local vault (Free soft caps)'));
|
|
30
|
+
console.log(` Commands: ${chalk_1.default.white(vault.commands.length)}/${local_collections_1.LOCAL_SOFT_CAPS.commands}`);
|
|
31
|
+
console.log(` Groups: ${chalk_1.default.white(groups)}/${local_collections_1.LOCAL_SOFT_CAPS.groups}`);
|
|
32
|
+
console.log(` Templates: ${chalk_1.default.white(templates)}/${local_collections_1.LOCAL_SOFT_CAPS.templates}`);
|
|
33
|
+
console.log(` Snippets: ${chalk_1.default.white(snippets)}/${local_collections_1.LOCAL_SOFT_CAPS.snippets}`);
|
|
34
|
+
console.log(` Env sets: ${chalk_1.default.white(envSets)}/${local_collections_1.LOCAL_SOFT_CAPS.envSets}`);
|
|
35
|
+
console.log(` Aliases: ${chalk_1.default.white(aliases)}/${local_collections_1.LOCAL_SOFT_CAPS.aliases}`);
|
|
36
|
+
console.log(` Vault file: ${chalk_1.default.dim(local_vault_1.VAULT_FILE)}`);
|
|
25
37
|
if (!vault.syncedAt) {
|
|
26
38
|
console.log(chalk_1.default.dim(' Never synced to cloud.'));
|
|
27
39
|
}
|
|
28
40
|
else {
|
|
29
41
|
const ago = Math.round((Date.now() - new Date(vault.syncedAt).getTime()) / 1000 / 60);
|
|
30
|
-
console.log(` Last synced:
|
|
42
|
+
console.log(` Last synced: ${chalk_1.default.white(ago < 1 ? 'just now' : `${ago} minutes ago`)}`);
|
|
31
43
|
}
|
|
32
|
-
const pending = (0, local_vault_1.localCommandsNeedingPush)().length
|
|
44
|
+
const pending = (0, local_vault_1.localCommandsNeedingPush)().length +
|
|
45
|
+
(0, local_collections_1.groupsNeedingPush)().length +
|
|
46
|
+
(0, local_collections_1.templatesNeedingPush)().length +
|
|
47
|
+
(0, local_collections_1.snippetsNeedingPush)().length +
|
|
48
|
+
(0, local_collections_1.envSetsNeedingPush)().length +
|
|
49
|
+
(0, local_collections_1.aliasesNeedingPush)().length;
|
|
33
50
|
if (pending) {
|
|
34
|
-
console.log(` Not on cloud:
|
|
51
|
+
console.log(` Not on cloud: ${chalk_1.default.yellow(String(pending))} (recall sync --push)`);
|
|
35
52
|
}
|
|
53
|
+
console.log(chalk_1.default.dim(' Sync requires Pro/Team. Free cloud quota is always 0.'));
|
|
36
54
|
console.log();
|
|
37
55
|
return;
|
|
38
56
|
}
|
|
@@ -44,9 +62,11 @@ exports.syncCommand = new commander_1.Command('sync')
|
|
|
44
62
|
const { user } = await api_1.ApiClient.get('/auth/me', token);
|
|
45
63
|
const plan = (user.plan ?? 'free');
|
|
46
64
|
spinner.stop();
|
|
65
|
+
// Anti-scam: never sync on Free — cloud Free quotas are 0
|
|
47
66
|
if (plan !== 'pro' && plan !== 'team') {
|
|
48
67
|
console.log(chalk_1.default.yellow('\n Cloud sync requires Pro or Team.'));
|
|
49
68
|
console.log(chalk_1.default.dim(' Local vault still works without sync.'));
|
|
69
|
+
console.log(chalk_1.default.dim(' Free accounts cannot push to cloud (anti-abuse).'));
|
|
50
70
|
console.log(chalk_1.default.dim(' Upgrade: recall upgrade\n'));
|
|
51
71
|
process.exit(1);
|
|
52
72
|
}
|
|
@@ -64,9 +84,56 @@ exports.syncCommand = new commander_1.Command('sync')
|
|
|
64
84
|
const pullSpinner = (0, ora_1.default)('Pulling from cloud...').start();
|
|
65
85
|
try {
|
|
66
86
|
const { commands } = await api_1.ApiClient.get('/commands', token);
|
|
67
|
-
const
|
|
68
|
-
pulled
|
|
69
|
-
|
|
87
|
+
const cmdMerge = (0, local_vault_1.mergeCloudIntoLocal)(commands);
|
|
88
|
+
pulled += cmdMerge.added + cmdMerge.updated;
|
|
89
|
+
try {
|
|
90
|
+
const { groups } = await api_1.ApiClient.get('/groups', token);
|
|
91
|
+
const g = (0, local_collections_1.mergeCloudGroups)(groups ?? []);
|
|
92
|
+
pulled += g.added + g.updated;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* Free/legacy — ignore */
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const { templates } = await api_1.ApiClient.get('/templates', token);
|
|
99
|
+
const t = (0, local_collections_1.mergeCloudTemplates)(templates ?? []);
|
|
100
|
+
pulled += t.added + t.updated;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* ignore */
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const { snippets } = await api_1.ApiClient.get('/snippets', token);
|
|
107
|
+
const s = (0, local_collections_1.mergeCloudSnippets)(snippets ?? []);
|
|
108
|
+
pulled += s.added + s.updated;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* ignore */
|
|
112
|
+
}
|
|
113
|
+
// Env list omits vars — fetch each by name
|
|
114
|
+
try {
|
|
115
|
+
const { envSets } = await api_1.ApiClient.get('/env', token);
|
|
116
|
+
const full = [];
|
|
117
|
+
for (const e of envSets ?? []) {
|
|
118
|
+
try {
|
|
119
|
+
const row = await api_1.ApiClient.get(`/env/${encodeURIComponent(e.name)}`, token);
|
|
120
|
+
full.push({
|
|
121
|
+
id: row.id,
|
|
122
|
+
name: row.name,
|
|
123
|
+
vars: row.vars ?? {},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
/* skip */
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const em = (0, local_collections_1.mergeCloudEnvSets)(full);
|
|
131
|
+
pulled += em.added + em.updated;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* ignore */
|
|
135
|
+
}
|
|
136
|
+
pullSpinner.succeed(chalk_1.default.green(`Pulled cloud vault (≈${pulled} changes)`));
|
|
70
137
|
}
|
|
71
138
|
catch (err) {
|
|
72
139
|
pullSpinner.fail(chalk_1.default.red('Pull failed'));
|
|
@@ -77,32 +144,76 @@ exports.syncCommand = new commander_1.Command('sync')
|
|
|
77
144
|
}
|
|
78
145
|
}
|
|
79
146
|
if (doPush) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
pushSpinner.succeed(chalk_1.default.green(`Pushed ${pushed} command${pushed === 1 ? '' : 's'} to cloud`));
|
|
147
|
+
const pushSpinner = (0, ora_1.default)('Pushing local items...').start();
|
|
148
|
+
try {
|
|
149
|
+
for (const cmd of (0, local_vault_1.localCommandsNeedingPush)()) {
|
|
150
|
+
const saved = await api_1.ApiClient.post('/commands', {
|
|
151
|
+
command: cmd.command,
|
|
152
|
+
name: cmd.name,
|
|
153
|
+
tags: cmd.tags,
|
|
154
|
+
}, token);
|
|
155
|
+
if (saved?.id)
|
|
156
|
+
(0, local_vault_1.attachCloudId)(cmd.id, saved.id);
|
|
157
|
+
pushed++;
|
|
158
|
+
}
|
|
159
|
+
for (const g of (0, local_collections_1.groupsNeedingPush)()) {
|
|
160
|
+
const saved = await api_1.ApiClient.post('/groups', { name: g.name }, token);
|
|
161
|
+
if (saved?.id)
|
|
162
|
+
(0, local_collections_1.attachGroupCloudId)(g.id, saved.id);
|
|
163
|
+
pushed++;
|
|
98
164
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
165
|
+
for (const t of (0, local_collections_1.templatesNeedingPush)()) {
|
|
166
|
+
const saved = await api_1.ApiClient.post('/templates', { name: t.name, template: t.template, tags: t.tags ?? [] }, token);
|
|
167
|
+
if (saved?.id)
|
|
168
|
+
(0, local_collections_1.attachTemplateCloudId)(t.id, saved.id);
|
|
169
|
+
pushed++;
|
|
170
|
+
}
|
|
171
|
+
for (const s of (0, local_collections_1.snippetsNeedingPush)()) {
|
|
172
|
+
const saved = await api_1.ApiClient.post('/snippets', { name: s.name, content: s.content, tags: s.tags ?? [] }, token);
|
|
173
|
+
if (saved?.id)
|
|
174
|
+
(0, local_collections_1.attachSnippetCloudId)(s.id, saved.id);
|
|
175
|
+
pushed++;
|
|
176
|
+
}
|
|
177
|
+
for (const e of (0, local_collections_1.envSetsNeedingPush)()) {
|
|
178
|
+
const saved = await api_1.ApiClient.post('/env', { name: e.name, vars: e.vars }, token);
|
|
179
|
+
// Env upsert may return without id — treat name as synced marker via cloudId if present
|
|
180
|
+
if (saved?.id)
|
|
181
|
+
(0, local_collections_1.attachEnvCloudId)(e.id, saved.id);
|
|
182
|
+
else if (saved?.name)
|
|
183
|
+
(0, local_collections_1.attachEnvCloudId)(e.id, saved.name);
|
|
184
|
+
pushed++;
|
|
185
|
+
}
|
|
186
|
+
// Aliases need cloud commandId — resolve via local command's cloudId
|
|
187
|
+
for (const a of (0, local_collections_1.aliasesNeedingPush)()) {
|
|
188
|
+
const localCmd = (0, local_vault_1.listLocalCommands)().find((c) => c.id === a.localCommandId);
|
|
189
|
+
const cloudCommandId = localCmd?.cloudId ??
|
|
190
|
+
(localCmd?.name ? (0, local_vault_1.findLocalByName)(localCmd.name)?.cloudId : undefined);
|
|
191
|
+
if (!cloudCommandId) {
|
|
192
|
+
continue; // skip until command is synced
|
|
103
193
|
}
|
|
104
|
-
|
|
194
|
+
const saved = await api_1.ApiClient.post('/aliases', { alias: a.alias, commandId: cloudCommandId }, token);
|
|
195
|
+
if (saved?.id)
|
|
196
|
+
(0, local_collections_1.attachAliasCloudId)(a.id, saved.id);
|
|
197
|
+
else
|
|
198
|
+
(0, local_collections_1.attachAliasCloudId)(a.id, a.alias);
|
|
199
|
+
pushed++;
|
|
200
|
+
}
|
|
201
|
+
if (!pushed) {
|
|
202
|
+
pushSpinner.succeed(chalk_1.default.dim('Nothing new to push'));
|
|
105
203
|
}
|
|
204
|
+
else {
|
|
205
|
+
pushSpinner.succeed(chalk_1.default.green(`Pushed ${pushed} item${pushed === 1 ? '' : 's'} to cloud`));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
pushSpinner.fail(chalk_1.default.red('Push failed'));
|
|
210
|
+
if (err instanceof api_1.ApiError && err.status === 403) {
|
|
211
|
+
console.log(chalk_1.default.yellow('\n Cloud save blocked (plan limit or Free). Local vault unchanged.\n'));
|
|
212
|
+
}
|
|
213
|
+
else if (err instanceof Error) {
|
|
214
|
+
console.log(chalk_1.default.red(`\n ${err.message}\n`));
|
|
215
|
+
}
|
|
216
|
+
process.exit(1);
|
|
106
217
|
}
|
|
107
218
|
}
|
|
108
219
|
console.log();
|
package/dist/lib/config.js
CHANGED
|
@@ -11,7 +11,7 @@ const os_1 = __importDefault(require("os"));
|
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.recall');
|
|
13
13
|
const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
|
|
14
|
-
exports.APP_VERSION = '1.2.
|
|
14
|
+
exports.APP_VERSION = '1.2.11';
|
|
15
15
|
exports.ENVIRONMENTS = {
|
|
16
16
|
production: 'https://api.devkitvault.com',
|
|
17
17
|
local: 'http://127.0.0.1:3001',
|