@devkitvault/recall 1.2.10 → 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 -26
- 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/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,45 +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
|
|
33
|
-
recall import history
|
|
36
|
+
recall import history -n 20
|
|
34
37
|
recall list
|
|
35
38
|
recall search docker
|
|
36
|
-
recall run
|
|
39
|
+
recall run up
|
|
37
40
|
```
|
|
38
41
|
|
|
39
|
-
`recall import history` reads your shell history file (zsh/bash/PowerShell)
|
|
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.
|
|
40
43
|
|
|
41
|
-
`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`.
|
|
42
45
|
|
|
43
46
|
```sh
|
|
44
47
|
recall playbook save deploy "docker compose up -d" "pnpm migrate"
|
|
45
48
|
recall playbook run deploy --dry-run
|
|
46
49
|
```
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
`recall playbook run` executes steps. Ask never does.
|
|
49
52
|
|
|
50
53
|
## Ask (Pro / Team)
|
|
51
54
|
|
|
52
|
-
Natural-language Ask prints a suggestion and never runs it. Requires a Pro or Team plan.
|
|
53
|
-
|
|
54
55
|
```sh
|
|
55
56
|
recall auth login
|
|
56
57
|
recall "find the 10 biggest files in this directory"
|
|
57
58
|
rec "that docker command from last week"
|
|
58
59
|
```
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
Ask only prints. You run the command yourself.
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
No-args `recall` opens an Ask session (`Recall >`). `/save [name]` stores the last suggestion. `/exit` leaves.
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
## Sync (Pro / Team)
|
|
65
66
|
|
|
66
67
|
```sh
|
|
67
68
|
recall auth login
|
|
@@ -71,12 +72,13 @@ recall sync --push # local-only → cloud
|
|
|
71
72
|
recall sync --status
|
|
72
73
|
recall whoami
|
|
73
74
|
```
|
|
74
|
-
|
|
75
|
+
|
|
76
|
+
## Native binary (optional)
|
|
75
77
|
|
|
76
78
|
- **macOS / Linux:** `curl -fsSL https://devkitvault.com/recall/install.sh | sh`
|
|
77
79
|
- **Windows:** `irm https://devkitvault.com/recall/install.ps1 | iex`
|
|
78
80
|
|
|
79
|
-
See [
|
|
81
|
+
See [install docs](https://recall.devkitvault.com/docs/install).
|
|
80
82
|
|
|
81
83
|
## Update
|
|
82
84
|
|
|
@@ -91,18 +93,17 @@ recall doctor
|
|
|
91
93
|
recall whoami
|
|
92
94
|
```
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
Issues: [github.com/devkitvaultorg/recall-cli/issues](https://github.com/devkitvaultorg/recall-cli/issues)
|
|
95
97
|
|
|
96
98
|
## Links
|
|
97
99
|
|
|
98
|
-
|
|
|
100
|
+
| | |
|
|
99
101
|
| --------- | ----------------------------------------------------------------------------------------- |
|
|
100
|
-
| Website
|
|
101
|
-
| Docs
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
| VS Code
|
|
105
|
-
| 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) |
|
|
106
107
|
|
|
107
108
|
## License
|
|
108
109
|
|
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',
|
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LocalVaultSizeError = exports.LocalVaultLimitError = exports.LOCAL_SIZE_LIMITS = exports.LOCAL_SOFT_CAPS = void 0;
|
|
7
|
+
exports.listLocalGroups = listLocalGroups;
|
|
8
|
+
exports.saveLocalGroup = saveLocalGroup;
|
|
9
|
+
exports.deleteLocalGroup = deleteLocalGroup;
|
|
10
|
+
exports.renameLocalGroup = renameLocalGroup;
|
|
11
|
+
exports.groupsNeedingPush = groupsNeedingPush;
|
|
12
|
+
exports.attachGroupCloudId = attachGroupCloudId;
|
|
13
|
+
exports.mergeCloudGroups = mergeCloudGroups;
|
|
14
|
+
exports.listLocalTemplates = listLocalTemplates;
|
|
15
|
+
exports.saveLocalTemplate = saveLocalTemplate;
|
|
16
|
+
exports.updateLocalTemplate = updateLocalTemplate;
|
|
17
|
+
exports.deleteLocalTemplate = deleteLocalTemplate;
|
|
18
|
+
exports.templatesNeedingPush = templatesNeedingPush;
|
|
19
|
+
exports.attachTemplateCloudId = attachTemplateCloudId;
|
|
20
|
+
exports.mergeCloudTemplates = mergeCloudTemplates;
|
|
21
|
+
exports.listLocalSnippets = listLocalSnippets;
|
|
22
|
+
exports.saveLocalSnippet = saveLocalSnippet;
|
|
23
|
+
exports.updateLocalSnippet = updateLocalSnippet;
|
|
24
|
+
exports.deleteLocalSnippet = deleteLocalSnippet;
|
|
25
|
+
exports.snippetsNeedingPush = snippetsNeedingPush;
|
|
26
|
+
exports.attachSnippetCloudId = attachSnippetCloudId;
|
|
27
|
+
exports.mergeCloudSnippets = mergeCloudSnippets;
|
|
28
|
+
exports.listLocalEnvSets = listLocalEnvSets;
|
|
29
|
+
exports.saveLocalEnvSet = saveLocalEnvSet;
|
|
30
|
+
exports.deleteLocalEnvSet = deleteLocalEnvSet;
|
|
31
|
+
exports.updateLocalEnvSet = updateLocalEnvSet;
|
|
32
|
+
exports.envSetsNeedingPush = envSetsNeedingPush;
|
|
33
|
+
exports.attachEnvCloudId = attachEnvCloudId;
|
|
34
|
+
exports.mergeCloudEnvSets = mergeCloudEnvSets;
|
|
35
|
+
exports.listLocalAliases = listLocalAliases;
|
|
36
|
+
exports.saveLocalAlias = saveLocalAlias;
|
|
37
|
+
exports.deleteLocalAlias = deleteLocalAlias;
|
|
38
|
+
exports.aliasesNeedingPush = aliasesNeedingPush;
|
|
39
|
+
exports.attachAliasCloudId = attachAliasCloudId;
|
|
40
|
+
exports.localCollectionCounts = localCollectionCounts;
|
|
41
|
+
/**
|
|
42
|
+
* Free local collections under ~/.recall/ (separate files).
|
|
43
|
+
* Soft caps + size limits prevent vault abuse; cloud sync still requires Pro/Team
|
|
44
|
+
* and server-side plan gates.
|
|
45
|
+
*/
|
|
46
|
+
const node_crypto_1 = require("node:crypto");
|
|
47
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
48
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
49
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
50
|
+
const VAULT_DIR = node_path_1.default.join(process.env.RECALL_HOME?.trim() || node_os_1.default.homedir(), '.recall');
|
|
51
|
+
exports.LOCAL_SOFT_CAPS = {
|
|
52
|
+
commands: 500,
|
|
53
|
+
groups: 50,
|
|
54
|
+
templates: 50,
|
|
55
|
+
snippets: 50,
|
|
56
|
+
envSets: 50,
|
|
57
|
+
aliases: 100,
|
|
58
|
+
};
|
|
59
|
+
exports.LOCAL_SIZE_LIMITS = {
|
|
60
|
+
commandChars: 10_000,
|
|
61
|
+
templateChars: 10_000,
|
|
62
|
+
snippetContentChars: 100_000,
|
|
63
|
+
envVarsJsonChars: 50_000,
|
|
64
|
+
nameChars: 200,
|
|
65
|
+
aliasChars: 64,
|
|
66
|
+
};
|
|
67
|
+
class LocalVaultLimitError extends Error {
|
|
68
|
+
kind;
|
|
69
|
+
limit;
|
|
70
|
+
current;
|
|
71
|
+
constructor(kind, limit, current) {
|
|
72
|
+
super(`Local ${kind} limit reached (${current}/${limit}). Delete some or upgrade to Pro to sync a subset.`);
|
|
73
|
+
this.kind = kind;
|
|
74
|
+
this.limit = limit;
|
|
75
|
+
this.current = current;
|
|
76
|
+
this.name = 'LocalVaultLimitError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.LocalVaultLimitError = LocalVaultLimitError;
|
|
80
|
+
class LocalVaultSizeError extends Error {
|
|
81
|
+
constructor(message) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = 'LocalVaultSizeError';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.LocalVaultSizeError = LocalVaultSizeError;
|
|
87
|
+
function nowIso() {
|
|
88
|
+
return new Date().toISOString();
|
|
89
|
+
}
|
|
90
|
+
function ensureDir() {
|
|
91
|
+
node_fs_1.default.mkdirSync(VAULT_DIR, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
function collectionPath(file) {
|
|
94
|
+
return node_path_1.default.join(VAULT_DIR, file);
|
|
95
|
+
}
|
|
96
|
+
function readCollection(file) {
|
|
97
|
+
const p = collectionPath(file);
|
|
98
|
+
try {
|
|
99
|
+
if (!node_fs_1.default.existsSync(p)) {
|
|
100
|
+
return { version: 1, updatedAt: nowIso(), syncedAt: null, items: [] };
|
|
101
|
+
}
|
|
102
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(p, 'utf-8'));
|
|
103
|
+
const items = Array.isArray(parsed.items)
|
|
104
|
+
? parsed.items
|
|
105
|
+
: Array.isArray(parsed.groups)
|
|
106
|
+
? parsed.groups
|
|
107
|
+
: Array.isArray(parsed.templates)
|
|
108
|
+
? parsed.templates
|
|
109
|
+
: Array.isArray(parsed.snippets)
|
|
110
|
+
? parsed.snippets
|
|
111
|
+
: Array.isArray(parsed.envSets)
|
|
112
|
+
? parsed.envSets
|
|
113
|
+
: Array.isArray(parsed.aliases)
|
|
114
|
+
? parsed.aliases
|
|
115
|
+
: [];
|
|
116
|
+
return {
|
|
117
|
+
version: 1,
|
|
118
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : nowIso(),
|
|
119
|
+
syncedAt: typeof parsed.syncedAt === 'string' ? parsed.syncedAt : null,
|
|
120
|
+
items,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return { version: 1, updatedAt: nowIso(), syncedAt: null, items: [] };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function writeCollection(file, key, items, syncedAt) {
|
|
128
|
+
ensureDir();
|
|
129
|
+
const payload = {
|
|
130
|
+
version: 1,
|
|
131
|
+
updatedAt: nowIso(),
|
|
132
|
+
syncedAt: syncedAt ?? null,
|
|
133
|
+
[key]: items,
|
|
134
|
+
items, // dual-write for forward readers
|
|
135
|
+
};
|
|
136
|
+
node_fs_1.default.writeFileSync(collectionPath(file), JSON.stringify(payload, null, 2), { mode: 0o600 });
|
|
137
|
+
}
|
|
138
|
+
function assertUnderCap(kind, current) {
|
|
139
|
+
const limit = exports.LOCAL_SOFT_CAPS[kind];
|
|
140
|
+
if (current >= limit) {
|
|
141
|
+
throw new LocalVaultLimitError(kind, limit, current);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function assertName(name) {
|
|
145
|
+
const t = name.trim();
|
|
146
|
+
if (!t)
|
|
147
|
+
throw new LocalVaultSizeError('Name is required');
|
|
148
|
+
if (t.length > exports.LOCAL_SIZE_LIMITS.nameChars) {
|
|
149
|
+
throw new LocalVaultSizeError(`Name too long (max ${exports.LOCAL_SIZE_LIMITS.nameChars})`);
|
|
150
|
+
}
|
|
151
|
+
return t;
|
|
152
|
+
}
|
|
153
|
+
function listLocalGroups() {
|
|
154
|
+
return readCollection('groups.json').items.sort((a, b) => a.name.localeCompare(b.name));
|
|
155
|
+
}
|
|
156
|
+
function saveLocalGroup(name) {
|
|
157
|
+
const n = assertName(name);
|
|
158
|
+
const file = readCollection('groups.json');
|
|
159
|
+
const existing = file.items.find((g) => g.name.toLowerCase() === n.toLowerCase());
|
|
160
|
+
if (existing) {
|
|
161
|
+
existing.name = n;
|
|
162
|
+
existing.updatedAt = nowIso();
|
|
163
|
+
writeCollection('groups.json', 'groups', file.items, file.syncedAt);
|
|
164
|
+
return existing;
|
|
165
|
+
}
|
|
166
|
+
assertUnderCap('groups', file.items.length);
|
|
167
|
+
const row = {
|
|
168
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
169
|
+
name: n,
|
|
170
|
+
createdAt: nowIso(),
|
|
171
|
+
updatedAt: nowIso(),
|
|
172
|
+
};
|
|
173
|
+
file.items.push(row);
|
|
174
|
+
writeCollection('groups.json', 'groups', file.items, file.syncedAt);
|
|
175
|
+
return row;
|
|
176
|
+
}
|
|
177
|
+
function deleteLocalGroup(id) {
|
|
178
|
+
const file = readCollection('groups.json');
|
|
179
|
+
const idx = file.items.findIndex((g) => g.id === id);
|
|
180
|
+
if (idx < 0)
|
|
181
|
+
return undefined;
|
|
182
|
+
const [removed] = file.items.splice(idx, 1);
|
|
183
|
+
writeCollection('groups.json', 'groups', file.items, file.syncedAt);
|
|
184
|
+
return removed;
|
|
185
|
+
}
|
|
186
|
+
function renameLocalGroup(id, name) {
|
|
187
|
+
const n = assertName(name);
|
|
188
|
+
const file = readCollection('groups.json');
|
|
189
|
+
const row = file.items.find((g) => g.id === id);
|
|
190
|
+
if (!row)
|
|
191
|
+
return undefined;
|
|
192
|
+
row.name = n;
|
|
193
|
+
row.updatedAt = nowIso();
|
|
194
|
+
writeCollection('groups.json', 'groups', file.items, file.syncedAt);
|
|
195
|
+
return row;
|
|
196
|
+
}
|
|
197
|
+
function groupsNeedingPush() {
|
|
198
|
+
return listLocalGroups().filter((g) => !g.cloudId);
|
|
199
|
+
}
|
|
200
|
+
function attachGroupCloudId(localId, cloudId) {
|
|
201
|
+
const file = readCollection('groups.json');
|
|
202
|
+
const row = file.items.find((g) => g.id === localId);
|
|
203
|
+
if (!row)
|
|
204
|
+
return;
|
|
205
|
+
row.cloudId = cloudId;
|
|
206
|
+
row.updatedAt = nowIso();
|
|
207
|
+
writeCollection('groups.json', 'groups', file.items, file.syncedAt);
|
|
208
|
+
}
|
|
209
|
+
function mergeCloudGroups(cloud) {
|
|
210
|
+
const file = readCollection('groups.json');
|
|
211
|
+
const stamp = nowIso();
|
|
212
|
+
let added = 0;
|
|
213
|
+
let updated = 0;
|
|
214
|
+
for (const c of cloud) {
|
|
215
|
+
const byCloud = file.items.find((g) => g.cloudId === c.id);
|
|
216
|
+
const byName = file.items.find((g) => g.name.toLowerCase() === c.name.toLowerCase());
|
|
217
|
+
const target = byCloud ?? byName;
|
|
218
|
+
if (target) {
|
|
219
|
+
target.cloudId = c.id;
|
|
220
|
+
target.name = c.name;
|
|
221
|
+
target.updatedAt = stamp;
|
|
222
|
+
updated++;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
file.items.push({
|
|
226
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
227
|
+
cloudId: c.id,
|
|
228
|
+
name: c.name,
|
|
229
|
+
createdAt: c.createdAt ?? stamp,
|
|
230
|
+
updatedAt: stamp,
|
|
231
|
+
});
|
|
232
|
+
added++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
writeCollection('groups.json', 'groups', file.items, stamp);
|
|
236
|
+
return { added, updated };
|
|
237
|
+
}
|
|
238
|
+
function listLocalTemplates() {
|
|
239
|
+
return readCollection('templates.json').items.sort((a, b) => a.name.localeCompare(b.name));
|
|
240
|
+
}
|
|
241
|
+
function saveLocalTemplate(input) {
|
|
242
|
+
const name = assertName(input.name);
|
|
243
|
+
const template = input.template.trim();
|
|
244
|
+
if (!template)
|
|
245
|
+
throw new LocalVaultSizeError('Template body is required');
|
|
246
|
+
if (template.length > exports.LOCAL_SIZE_LIMITS.templateChars) {
|
|
247
|
+
throw new LocalVaultSizeError(`Template too long (max ${exports.LOCAL_SIZE_LIMITS.templateChars})`);
|
|
248
|
+
}
|
|
249
|
+
const file = readCollection('templates.json');
|
|
250
|
+
const existing = file.items.find((t) => t.name.toLowerCase() === name.toLowerCase());
|
|
251
|
+
if (existing) {
|
|
252
|
+
existing.template = template;
|
|
253
|
+
existing.tags = input.tags?.length ? input.tags : undefined;
|
|
254
|
+
existing.updatedAt = nowIso();
|
|
255
|
+
writeCollection('templates.json', 'templates', file.items, file.syncedAt);
|
|
256
|
+
return existing;
|
|
257
|
+
}
|
|
258
|
+
assertUnderCap('templates', file.items.length);
|
|
259
|
+
const row = {
|
|
260
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
261
|
+
name,
|
|
262
|
+
template,
|
|
263
|
+
tags: input.tags?.length ? input.tags : undefined,
|
|
264
|
+
createdAt: nowIso(),
|
|
265
|
+
updatedAt: nowIso(),
|
|
266
|
+
};
|
|
267
|
+
file.items.push(row);
|
|
268
|
+
writeCollection('templates.json', 'templates', file.items, file.syncedAt);
|
|
269
|
+
return row;
|
|
270
|
+
}
|
|
271
|
+
function updateLocalTemplate(id, patch) {
|
|
272
|
+
const file = readCollection('templates.json');
|
|
273
|
+
const row = file.items.find((t) => t.id === id);
|
|
274
|
+
if (!row)
|
|
275
|
+
return undefined;
|
|
276
|
+
if (patch.name !== undefined)
|
|
277
|
+
row.name = assertName(patch.name);
|
|
278
|
+
if (patch.template !== undefined) {
|
|
279
|
+
const t = patch.template.trim();
|
|
280
|
+
if (t.length > exports.LOCAL_SIZE_LIMITS.templateChars) {
|
|
281
|
+
throw new LocalVaultSizeError(`Template too long (max ${exports.LOCAL_SIZE_LIMITS.templateChars})`);
|
|
282
|
+
}
|
|
283
|
+
row.template = t;
|
|
284
|
+
}
|
|
285
|
+
if (patch.tags !== undefined)
|
|
286
|
+
row.tags = patch.tags.length ? patch.tags : undefined;
|
|
287
|
+
row.updatedAt = nowIso();
|
|
288
|
+
writeCollection('templates.json', 'templates', file.items, file.syncedAt);
|
|
289
|
+
return row;
|
|
290
|
+
}
|
|
291
|
+
function deleteLocalTemplate(id) {
|
|
292
|
+
const file = readCollection('templates.json');
|
|
293
|
+
const idx = file.items.findIndex((t) => t.id === id);
|
|
294
|
+
if (idx < 0)
|
|
295
|
+
return undefined;
|
|
296
|
+
const [removed] = file.items.splice(idx, 1);
|
|
297
|
+
writeCollection('templates.json', 'templates', file.items, file.syncedAt);
|
|
298
|
+
return removed;
|
|
299
|
+
}
|
|
300
|
+
function templatesNeedingPush() {
|
|
301
|
+
return listLocalTemplates().filter((t) => !t.cloudId);
|
|
302
|
+
}
|
|
303
|
+
function attachTemplateCloudId(localId, cloudId) {
|
|
304
|
+
const file = readCollection('templates.json');
|
|
305
|
+
const row = file.items.find((t) => t.id === localId);
|
|
306
|
+
if (!row)
|
|
307
|
+
return;
|
|
308
|
+
row.cloudId = cloudId;
|
|
309
|
+
row.updatedAt = nowIso();
|
|
310
|
+
writeCollection('templates.json', 'templates', file.items, file.syncedAt);
|
|
311
|
+
}
|
|
312
|
+
function mergeCloudTemplates(cloud) {
|
|
313
|
+
const file = readCollection('templates.json');
|
|
314
|
+
const stamp = nowIso();
|
|
315
|
+
let added = 0;
|
|
316
|
+
let updated = 0;
|
|
317
|
+
for (const c of cloud) {
|
|
318
|
+
const byCloud = file.items.find((t) => t.cloudId === c.id);
|
|
319
|
+
const byName = file.items.find((t) => t.name.toLowerCase() === c.name.toLowerCase());
|
|
320
|
+
const target = byCloud ?? byName;
|
|
321
|
+
if (target) {
|
|
322
|
+
target.cloudId = c.id;
|
|
323
|
+
target.name = c.name;
|
|
324
|
+
target.template = c.template;
|
|
325
|
+
target.tags = c.tags?.length ? c.tags : undefined;
|
|
326
|
+
target.updatedAt = stamp;
|
|
327
|
+
updated++;
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
file.items.push({
|
|
331
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
332
|
+
cloudId: c.id,
|
|
333
|
+
name: c.name,
|
|
334
|
+
template: c.template,
|
|
335
|
+
tags: c.tags?.length ? c.tags : undefined,
|
|
336
|
+
createdAt: stamp,
|
|
337
|
+
updatedAt: stamp,
|
|
338
|
+
});
|
|
339
|
+
added++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
writeCollection('templates.json', 'templates', file.items, stamp);
|
|
343
|
+
return { added, updated };
|
|
344
|
+
}
|
|
345
|
+
function listLocalSnippets() {
|
|
346
|
+
return readCollection('snippets.json').items.sort((a, b) => a.name.localeCompare(b.name));
|
|
347
|
+
}
|
|
348
|
+
function saveLocalSnippet(input) {
|
|
349
|
+
const name = assertName(input.name);
|
|
350
|
+
if (input.content.length > exports.LOCAL_SIZE_LIMITS.snippetContentChars) {
|
|
351
|
+
throw new LocalVaultSizeError(`Snippet too large (max ${exports.LOCAL_SIZE_LIMITS.snippetContentChars} chars)`);
|
|
352
|
+
}
|
|
353
|
+
const file = readCollection('snippets.json');
|
|
354
|
+
const existing = file.items.find((s) => s.name.toLowerCase() === name.toLowerCase());
|
|
355
|
+
if (existing) {
|
|
356
|
+
existing.content = input.content;
|
|
357
|
+
existing.tags = input.tags?.length ? input.tags : undefined;
|
|
358
|
+
existing.updatedAt = nowIso();
|
|
359
|
+
writeCollection('snippets.json', 'snippets', file.items, file.syncedAt);
|
|
360
|
+
return existing;
|
|
361
|
+
}
|
|
362
|
+
assertUnderCap('snippets', file.items.length);
|
|
363
|
+
const row = {
|
|
364
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
365
|
+
name,
|
|
366
|
+
content: input.content,
|
|
367
|
+
tags: input.tags?.length ? input.tags : undefined,
|
|
368
|
+
createdAt: nowIso(),
|
|
369
|
+
updatedAt: nowIso(),
|
|
370
|
+
};
|
|
371
|
+
file.items.push(row);
|
|
372
|
+
writeCollection('snippets.json', 'snippets', file.items, file.syncedAt);
|
|
373
|
+
return row;
|
|
374
|
+
}
|
|
375
|
+
function updateLocalSnippet(id, patch) {
|
|
376
|
+
const file = readCollection('snippets.json');
|
|
377
|
+
const row = file.items.find((s) => s.id === id);
|
|
378
|
+
if (!row)
|
|
379
|
+
return undefined;
|
|
380
|
+
if (patch.name !== undefined)
|
|
381
|
+
row.name = assertName(patch.name);
|
|
382
|
+
if (patch.content !== undefined) {
|
|
383
|
+
if (patch.content.length > exports.LOCAL_SIZE_LIMITS.snippetContentChars) {
|
|
384
|
+
throw new LocalVaultSizeError(`Snippet too large (max ${exports.LOCAL_SIZE_LIMITS.snippetContentChars} chars)`);
|
|
385
|
+
}
|
|
386
|
+
row.content = patch.content;
|
|
387
|
+
}
|
|
388
|
+
if (patch.tags !== undefined)
|
|
389
|
+
row.tags = patch.tags.length ? patch.tags : undefined;
|
|
390
|
+
row.updatedAt = nowIso();
|
|
391
|
+
writeCollection('snippets.json', 'snippets', file.items, file.syncedAt);
|
|
392
|
+
return row;
|
|
393
|
+
}
|
|
394
|
+
function deleteLocalSnippet(id) {
|
|
395
|
+
const file = readCollection('snippets.json');
|
|
396
|
+
const idx = file.items.findIndex((s) => s.id === id);
|
|
397
|
+
if (idx < 0)
|
|
398
|
+
return undefined;
|
|
399
|
+
const [removed] = file.items.splice(idx, 1);
|
|
400
|
+
writeCollection('snippets.json', 'snippets', file.items, file.syncedAt);
|
|
401
|
+
return removed;
|
|
402
|
+
}
|
|
403
|
+
function snippetsNeedingPush() {
|
|
404
|
+
return listLocalSnippets().filter((s) => !s.cloudId);
|
|
405
|
+
}
|
|
406
|
+
function attachSnippetCloudId(localId, cloudId) {
|
|
407
|
+
const file = readCollection('snippets.json');
|
|
408
|
+
const row = file.items.find((s) => s.id === localId);
|
|
409
|
+
if (!row)
|
|
410
|
+
return;
|
|
411
|
+
row.cloudId = cloudId;
|
|
412
|
+
row.updatedAt = nowIso();
|
|
413
|
+
writeCollection('snippets.json', 'snippets', file.items, file.syncedAt);
|
|
414
|
+
}
|
|
415
|
+
function mergeCloudSnippets(cloud) {
|
|
416
|
+
const file = readCollection('snippets.json');
|
|
417
|
+
const stamp = nowIso();
|
|
418
|
+
let added = 0;
|
|
419
|
+
let updated = 0;
|
|
420
|
+
for (const c of cloud) {
|
|
421
|
+
const byCloud = file.items.find((s) => s.cloudId === c.id);
|
|
422
|
+
const byName = file.items.find((s) => s.name.toLowerCase() === c.name.toLowerCase());
|
|
423
|
+
const target = byCloud ?? byName;
|
|
424
|
+
if (target) {
|
|
425
|
+
target.cloudId = c.id;
|
|
426
|
+
target.name = c.name;
|
|
427
|
+
target.content = c.content;
|
|
428
|
+
target.tags = c.tags?.length ? c.tags : undefined;
|
|
429
|
+
target.updatedAt = stamp;
|
|
430
|
+
updated++;
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
file.items.push({
|
|
434
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
435
|
+
cloudId: c.id,
|
|
436
|
+
name: c.name,
|
|
437
|
+
content: c.content,
|
|
438
|
+
tags: c.tags?.length ? c.tags : undefined,
|
|
439
|
+
createdAt: stamp,
|
|
440
|
+
updatedAt: stamp,
|
|
441
|
+
});
|
|
442
|
+
added++;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
writeCollection('snippets.json', 'snippets', file.items, stamp);
|
|
446
|
+
return { added, updated };
|
|
447
|
+
}
|
|
448
|
+
function listLocalEnvSets() {
|
|
449
|
+
return readCollection('env.json').items.sort((a, b) => a.name.localeCompare(b.name));
|
|
450
|
+
}
|
|
451
|
+
function saveLocalEnvSet(input) {
|
|
452
|
+
const name = assertName(input.name);
|
|
453
|
+
const json = JSON.stringify(input.vars);
|
|
454
|
+
if (json.length > exports.LOCAL_SIZE_LIMITS.envVarsJsonChars) {
|
|
455
|
+
throw new LocalVaultSizeError(`Env set too large (max ${exports.LOCAL_SIZE_LIMITS.envVarsJsonChars} chars)`);
|
|
456
|
+
}
|
|
457
|
+
// Reject obvious secret-stuffing patterns in keys only (values stay local; sync still Pro-gated)
|
|
458
|
+
for (const k of Object.keys(input.vars)) {
|
|
459
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) {
|
|
460
|
+
throw new LocalVaultSizeError(`Invalid env var name: ${k}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const file = readCollection('env.json');
|
|
464
|
+
const existing = file.items.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
465
|
+
if (existing) {
|
|
466
|
+
existing.vars = input.vars;
|
|
467
|
+
existing.updatedAt = nowIso();
|
|
468
|
+
writeCollection('env.json', 'envSets', file.items, file.syncedAt);
|
|
469
|
+
return existing;
|
|
470
|
+
}
|
|
471
|
+
assertUnderCap('envSets', file.items.length);
|
|
472
|
+
const row = {
|
|
473
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
474
|
+
name,
|
|
475
|
+
vars: input.vars,
|
|
476
|
+
createdAt: nowIso(),
|
|
477
|
+
updatedAt: nowIso(),
|
|
478
|
+
};
|
|
479
|
+
file.items.push(row);
|
|
480
|
+
writeCollection('env.json', 'envSets', file.items, file.syncedAt);
|
|
481
|
+
return row;
|
|
482
|
+
}
|
|
483
|
+
function deleteLocalEnvSet(id) {
|
|
484
|
+
const file = readCollection('env.json');
|
|
485
|
+
const idx = file.items.findIndex((e) => e.id === id);
|
|
486
|
+
if (idx < 0)
|
|
487
|
+
return undefined;
|
|
488
|
+
const [removed] = file.items.splice(idx, 1);
|
|
489
|
+
writeCollection('env.json', 'envSets', file.items, file.syncedAt);
|
|
490
|
+
return removed;
|
|
491
|
+
}
|
|
492
|
+
function updateLocalEnvSet(id, patch) {
|
|
493
|
+
const file = readCollection('env.json');
|
|
494
|
+
const row = file.items.find((e) => e.id === id);
|
|
495
|
+
if (!row)
|
|
496
|
+
return undefined;
|
|
497
|
+
if (patch.name !== undefined)
|
|
498
|
+
row.name = assertName(patch.name);
|
|
499
|
+
if (patch.vars !== undefined) {
|
|
500
|
+
const json = JSON.stringify(patch.vars);
|
|
501
|
+
if (json.length > exports.LOCAL_SIZE_LIMITS.envVarsJsonChars) {
|
|
502
|
+
throw new LocalVaultSizeError(`Env set too large (max ${exports.LOCAL_SIZE_LIMITS.envVarsJsonChars} chars)`);
|
|
503
|
+
}
|
|
504
|
+
row.vars = patch.vars;
|
|
505
|
+
}
|
|
506
|
+
row.updatedAt = nowIso();
|
|
507
|
+
writeCollection('env.json', 'envSets', file.items, file.syncedAt);
|
|
508
|
+
return row;
|
|
509
|
+
}
|
|
510
|
+
function envSetsNeedingPush() {
|
|
511
|
+
return listLocalEnvSets().filter((e) => !e.cloudId);
|
|
512
|
+
}
|
|
513
|
+
function attachEnvCloudId(localId, cloudId) {
|
|
514
|
+
const file = readCollection('env.json');
|
|
515
|
+
const row = file.items.find((e) => e.id === localId);
|
|
516
|
+
if (!row)
|
|
517
|
+
return;
|
|
518
|
+
row.cloudId = cloudId;
|
|
519
|
+
row.updatedAt = nowIso();
|
|
520
|
+
writeCollection('env.json', 'envSets', file.items, file.syncedAt);
|
|
521
|
+
}
|
|
522
|
+
function mergeCloudEnvSets(cloud) {
|
|
523
|
+
const file = readCollection('env.json');
|
|
524
|
+
const stamp = nowIso();
|
|
525
|
+
let added = 0;
|
|
526
|
+
let updated = 0;
|
|
527
|
+
for (const c of cloud) {
|
|
528
|
+
if (!c.vars)
|
|
529
|
+
continue; // list endpoint may omit vars — skip incomplete
|
|
530
|
+
const byCloud = c.id ? file.items.find((e) => e.cloudId === c.id) : undefined;
|
|
531
|
+
const byName = file.items.find((e) => e.name.toLowerCase() === c.name.toLowerCase());
|
|
532
|
+
const target = byCloud ?? byName;
|
|
533
|
+
if (target) {
|
|
534
|
+
if (c.id)
|
|
535
|
+
target.cloudId = c.id;
|
|
536
|
+
target.name = c.name;
|
|
537
|
+
target.vars = c.vars;
|
|
538
|
+
target.updatedAt = stamp;
|
|
539
|
+
updated++;
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
file.items.push({
|
|
543
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
544
|
+
cloudId: c.id,
|
|
545
|
+
name: c.name,
|
|
546
|
+
vars: c.vars,
|
|
547
|
+
createdAt: stamp,
|
|
548
|
+
updatedAt: stamp,
|
|
549
|
+
});
|
|
550
|
+
added++;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
writeCollection('env.json', 'envSets', file.items, stamp);
|
|
554
|
+
return { added, updated };
|
|
555
|
+
}
|
|
556
|
+
function listLocalAliases() {
|
|
557
|
+
return readCollection('aliases.json').items.sort((a, b) => a.alias.localeCompare(b.alias));
|
|
558
|
+
}
|
|
559
|
+
function saveLocalAlias(input) {
|
|
560
|
+
const alias = input.alias.trim();
|
|
561
|
+
if (!alias || /\s/.test(alias)) {
|
|
562
|
+
throw new LocalVaultSizeError('Alias must be a single token without spaces');
|
|
563
|
+
}
|
|
564
|
+
if (alias.length > exports.LOCAL_SIZE_LIMITS.aliasChars) {
|
|
565
|
+
throw new LocalVaultSizeError(`Alias too long (max ${exports.LOCAL_SIZE_LIMITS.aliasChars})`);
|
|
566
|
+
}
|
|
567
|
+
const file = readCollection('aliases.json');
|
|
568
|
+
const existing = file.items.find((a) => a.alias.toLowerCase() === alias.toLowerCase());
|
|
569
|
+
if (existing) {
|
|
570
|
+
existing.localCommandId = input.localCommandId;
|
|
571
|
+
existing.updatedAt = nowIso();
|
|
572
|
+
writeCollection('aliases.json', 'aliases', file.items, file.syncedAt);
|
|
573
|
+
return existing;
|
|
574
|
+
}
|
|
575
|
+
assertUnderCap('aliases', file.items.length);
|
|
576
|
+
const row = {
|
|
577
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
578
|
+
alias,
|
|
579
|
+
localCommandId: input.localCommandId,
|
|
580
|
+
createdAt: nowIso(),
|
|
581
|
+
updatedAt: nowIso(),
|
|
582
|
+
};
|
|
583
|
+
file.items.push(row);
|
|
584
|
+
writeCollection('aliases.json', 'aliases', file.items, file.syncedAt);
|
|
585
|
+
return row;
|
|
586
|
+
}
|
|
587
|
+
function deleteLocalAlias(id) {
|
|
588
|
+
const file = readCollection('aliases.json');
|
|
589
|
+
const idx = file.items.findIndex((a) => a.id === id);
|
|
590
|
+
if (idx < 0)
|
|
591
|
+
return undefined;
|
|
592
|
+
const [removed] = file.items.splice(idx, 1);
|
|
593
|
+
writeCollection('aliases.json', 'aliases', file.items, file.syncedAt);
|
|
594
|
+
return removed;
|
|
595
|
+
}
|
|
596
|
+
function aliasesNeedingPush() {
|
|
597
|
+
return listLocalAliases().filter((a) => !a.cloudId);
|
|
598
|
+
}
|
|
599
|
+
function attachAliasCloudId(localId, cloudId) {
|
|
600
|
+
const file = readCollection('aliases.json');
|
|
601
|
+
const row = file.items.find((a) => a.id === localId);
|
|
602
|
+
if (!row)
|
|
603
|
+
return;
|
|
604
|
+
row.cloudId = cloudId;
|
|
605
|
+
row.updatedAt = nowIso();
|
|
606
|
+
writeCollection('aliases.json', 'aliases', file.items, file.syncedAt);
|
|
607
|
+
}
|
|
608
|
+
function localCollectionCounts() {
|
|
609
|
+
return {
|
|
610
|
+
commands: 0, // filled by caller from commands vault
|
|
611
|
+
groups: listLocalGroups().length,
|
|
612
|
+
templates: listLocalTemplates().length,
|
|
613
|
+
snippets: listLocalSnippets().length,
|
|
614
|
+
envSets: listLocalEnvSets().length,
|
|
615
|
+
aliases: listLocalAliases().length,
|
|
616
|
+
};
|
|
617
|
+
}
|
package/dist/lib/local-vault.js
CHANGED
|
@@ -20,6 +20,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
20
20
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
21
|
const node_os_1 = __importDefault(require("node:os"));
|
|
22
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
23
|
+
const local_collections_1 = require("./local-collections");
|
|
23
24
|
const VAULT_DIR = node_path_1.default.join(process.env.RECALL_HOME?.trim() || node_os_1.default.homedir(), '.recall');
|
|
24
25
|
exports.VAULT_FILE = node_path_1.default.join(VAULT_DIR, 'commands.json');
|
|
25
26
|
function nowIso() {
|
|
@@ -127,13 +128,19 @@ function findLocalByName(name) {
|
|
|
127
128
|
return readVault().commands.find((c) => c.name?.toLowerCase() === needle);
|
|
128
129
|
}
|
|
129
130
|
function saveLocalCommand(input) {
|
|
131
|
+
const command = input.command.trim();
|
|
132
|
+
if (!command)
|
|
133
|
+
throw new local_collections_1.LocalVaultSizeError('Command is required');
|
|
134
|
+
if (command.length > local_collections_1.LOCAL_SIZE_LIMITS.commandChars) {
|
|
135
|
+
throw new local_collections_1.LocalVaultSizeError(`Command too long (max ${local_collections_1.LOCAL_SIZE_LIMITS.commandChars})`);
|
|
136
|
+
}
|
|
130
137
|
const vault = readVault();
|
|
131
138
|
const stamp = nowIso();
|
|
132
139
|
const name = input.name?.trim() || undefined;
|
|
133
140
|
if (name) {
|
|
134
141
|
const existing = vault.commands.find((c) => c.name?.toLowerCase() === name.toLowerCase());
|
|
135
142
|
if (existing) {
|
|
136
|
-
existing.command =
|
|
143
|
+
existing.command = command;
|
|
137
144
|
existing.tags = input.tags?.length ? input.tags : undefined;
|
|
138
145
|
existing.updatedAt = stamp;
|
|
139
146
|
// Local edit invalidates cloud link until next push maps again
|
|
@@ -141,9 +148,12 @@ function saveLocalCommand(input) {
|
|
|
141
148
|
return existing;
|
|
142
149
|
}
|
|
143
150
|
}
|
|
151
|
+
if (vault.commands.length >= local_collections_1.LOCAL_SOFT_CAPS.commands) {
|
|
152
|
+
throw new local_collections_1.LocalVaultLimitError('commands', local_collections_1.LOCAL_SOFT_CAPS.commands, vault.commands.length);
|
|
153
|
+
}
|
|
144
154
|
const saved = {
|
|
145
155
|
id: (0, node_crypto_1.randomUUID)(),
|
|
146
|
-
command
|
|
156
|
+
command,
|
|
147
157
|
name,
|
|
148
158
|
tags: input.tags?.length ? input.tags : undefined,
|
|
149
159
|
createdAt: stamp,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devkitvault/recall",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.11",
|
|
4
4
|
"description": "recall CLI — personal command vault for your terminal (local-first)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"homepage": "https://recall.devkitvault.com",
|
|
21
21
|
"bugs": {
|
|
22
|
-
"url": "https://github.com/
|
|
22
|
+
"url": "https://github.com/devkitvaultorg/recall-cli/issues"
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/
|
|
26
|
+
"url": "git+https://github.com/devkitvaultorg/recall-cli.git"
|
|
27
27
|
},
|
|
28
28
|
"bin": {
|
|
29
29
|
"recall": "dist/index.js",
|