@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 CHANGED
@@ -1,10 +1,13 @@
1
1
  # @devkitvault/recall
2
2
 
3
- **recall** is a CLI command vault for your terminal. Save the commands you keep losing, search them, run them by name, and sync when you want cloud backup.
3
+ I kept losing useful shell commands. So I built a small vault for the ones I actually reuse.
4
4
 
5
- Product site and docs: [recall.devkitvault.com](https://recall.devkitvault.com)
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
- Open source: [github.com/devkitvault/recall-cli](https://github.com/devkitvault/recall-cli)
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 vault)
29
+ ## Quick start (Free — local)
27
30
 
28
- Free is **fully local** — save / list / search / run on your machine with **no account**. Cloud **sync** and **Ask** are Pro/Team.
31
+ No account needed.
29
32
 
30
33
  ```sh
31
- recall save "docker compose up -d" --name docker --tags docker
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 docker
39
+ recall run up
36
40
  ```
37
41
 
38
- Commands live in `~/.recall/commands.json`.
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`) reads the last line from zsh / bash / PowerShell history. Not supported in Windows `cmd.exe`. On bash, if the command is missing: `history -a && recall save --last`.
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
- Local playbooks follow the same idea when available; cloud playbooks need Pro. `recall playbook run` executes steps. Ask never does.
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
- No-args `recall` opens an Ask session (`Recall >`). `/save [name]` stores the last suggestion. `/exit` leaves.
61
+ Ask only prints. You run the command yourself.
60
62
 
61
- ## Auth & sync (Pro / Team)
63
+ No-args `recall` opens an Ask session (`Recall >`). `/save [name]` stores the last suggestion. `/exit` leaves.
62
64
 
63
- Account + Pro/Team syncs your **local** commands to the cloud and unlocks Ask.
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
- ## Prefer a standalone binary?
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 [recall.devkitvault.com/docs/install](https://recall.devkitvault.com/docs/install).
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
- If something still fails, open an issue on [GitHub](https://github.com/devkitvault/recall-cli/issues) or visit [recall.devkitvault.com](https://recall.devkitvault.com).
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 | [recall.devkitvault.com](https://recall.devkitvault.com) |
100
- | Docs | [recall.devkitvault.com/docs](https://recall.devkitvault.com/docs) |
101
- | Dashboard | [recall.devkitvault.com](https://recall.devkitvault.com) |
102
- | Source | [github.com/devkitvault/recall-cli](https://github.com/devkitvault/recall-cli) |
103
- | VS Code | [Marketplace](https://marketplace.visualstudio.com/items?itemName=devkitvault.recall-cmd) |
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
 
@@ -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 ora_1 = __importDefault(require("ora"));
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
11
  const path_1 = __importDefault(require("path"));
12
- const api_1 = require("../lib/api");
13
- const auth_1 = require("../lib/auth");
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
- exports.importCommand = new commander_1.Command('import')
59
- .description('Import commands from a JSON or shell script file')
60
- .argument('<file>', 'Path to the file to import')
61
- .option('-s, --skip-duplicates', 'Skip commands that already exist by name')
62
- .action(async (file, opts) => {
63
- const token = await (0, auth_1.requireAuth)();
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
- console.error(chalk_1.default.red(`\n Failed to parse file: ${err.message}\n`));
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'} to import\n`));
98
- let imported = 0;
99
- let skipped = 0;
100
- let failed = 0;
101
- for (const entry of entries) {
102
- const spinner = (0, ora_1.default)(`Importing: ${chalk_1.default.white(entry.name ?? entry.command.slice(0, 40))}`).start();
103
- try {
104
- await api_1.ApiClient.post('/commands', {
105
- command: entry.command,
106
- name: entry.name,
107
- tags: entry.tags,
108
- }, token);
109
- spinner.succeed(chalk_1.default.green(`Imported: ${entry.name ?? entry.command.slice(0, 40)}`));
110
- imported++;
111
- }
112
- catch (err) {
113
- if (opts.skipDuplicates && err.status === 409) {
114
- spinner.warn(chalk_1.default.yellow(`Skipped: ${entry.name ?? entry.command.slice(0, 40)}`));
115
- skipped++;
116
- }
117
- else {
118
- spinner.fail(chalk_1.default.red(`Failed: ${entry.name ?? entry.command.slice(0, 40)}`));
119
- failed++;
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(` ${chalk_1.default.green(`${imported} imported`)} ${chalk_1.default.yellow(`${skipped} skipped`)} ${chalk_1.default.red(`${failed} failed`)}`);
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
  });
@@ -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 commands → cloud 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(` Local commands: ${chalk_1.default.white(vault.commands.length)}`);
24
- console.log(` Vault file: ${chalk_1.default.dim(local_vault_1.VAULT_FILE)}`);
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: ${chalk_1.default.white(ago < 1 ? 'just now' : `${ago} minutes ago`)}`);
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: ${chalk_1.default.yellow(String(pending))} (recall sync --push)`);
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 { added, updated } = (0, local_vault_1.mergeCloudIntoLocal)(commands);
68
- pulled = added + updated;
69
- pullSpinner.succeed(chalk_1.default.green(`Pulled cloud vault (${added} new, ${updated} updated)`));
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 pending = (0, local_vault_1.localCommandsNeedingPush)();
81
- if (!pending.length) {
82
- console.log(chalk_1.default.dim(' Nothing new to push.'));
83
- }
84
- else {
85
- const pushSpinner = (0, ora_1.default)(`Pushing ${pending.length} local command(s)...`).start();
86
- try {
87
- for (const cmd of pending) {
88
- const saved = await api_1.ApiClient.post('/commands', {
89
- command: cmd.command,
90
- name: cmd.name,
91
- tags: cmd.tags,
92
- }, token);
93
- if (saved?.id)
94
- (0, local_vault_1.attachCloudId)(cmd.id, saved.id);
95
- pushed++;
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
- catch (err) {
100
- pushSpinner.fail(chalk_1.default.red('Push failed'));
101
- if (err instanceof api_1.ApiError && err.status === 403) {
102
- console.log(chalk_1.default.yellow('\n Cloud save requires Pro. Local vault is unchanged.\n'));
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
- process.exit(1);
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();
@@ -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.9';
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',