@devkitvault/recall 1.2.7 → 1.2.9
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 +34 -39
- package/dist/commands/ask.js +15 -2
- package/dist/commands/delete.js +9 -25
- package/dist/commands/list.js +33 -52
- package/dist/commands/pin.js +10 -16
- package/dist/commands/run.js +23 -37
- package/dist/commands/save.js +49 -52
- package/dist/commands/search.js +31 -46
- package/dist/commands/sync.js +92 -54
- package/dist/commands/upgrade.js +5 -6
- package/dist/commands/whoami.js +7 -8
- package/dist/lib/config.js +1 -1
- package/dist/lib/local-vault.js +251 -0
- package/dist/lib/shell-history.js +145 -0
- package/package.json +4 -5
package/dist/commands/search.js
CHANGED
|
@@ -7,9 +7,7 @@ exports.searchCommand = void 0;
|
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
9
9
|
const commander_1 = require("commander");
|
|
10
|
-
const
|
|
11
|
-
const api_1 = require("../lib/api");
|
|
12
|
-
const auth_1 = require("../lib/auth");
|
|
10
|
+
const local_vault_1 = require("../lib/local-vault");
|
|
13
11
|
function highlight(text, query) {
|
|
14
12
|
if (!query)
|
|
15
13
|
return text;
|
|
@@ -19,10 +17,8 @@ function highlight(text, query) {
|
|
|
19
17
|
function fuzzyMatch(text, query) {
|
|
20
18
|
const t = text.toLowerCase();
|
|
21
19
|
const q = query.toLowerCase();
|
|
22
|
-
// exact substring match
|
|
23
20
|
if (t.includes(q))
|
|
24
21
|
return true;
|
|
25
|
-
// fuzzy — all chars of query appear in order in text
|
|
26
22
|
let i = 0;
|
|
27
23
|
for (const ch of t) {
|
|
28
24
|
if (ch === q[i])
|
|
@@ -33,50 +29,39 @@ function fuzzyMatch(text, query) {
|
|
|
33
29
|
return false;
|
|
34
30
|
}
|
|
35
31
|
exports.searchCommand = new commander_1.Command('search')
|
|
36
|
-
.description('Search your
|
|
32
|
+
.description('Search your local vault')
|
|
37
33
|
.argument('<query>', 'Search query')
|
|
38
34
|
.option('-t, --tag <tag>', 'Filter by tag')
|
|
39
35
|
.action(async (query, opts) => {
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const results = commands.filter((cmd) => {
|
|
50
|
-
return (fuzzyMatch(cmd.command, query) ||
|
|
51
|
-
(cmd.name && fuzzyMatch(cmd.name, query)) ||
|
|
52
|
-
(cmd.tags && cmd.tags.some((t) => fuzzyMatch(t, query))));
|
|
53
|
-
});
|
|
54
|
-
if (!results.length) {
|
|
55
|
-
console.log(chalk_1.default.dim(`\n No commands found matching "${query}".\n`));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
console.log(chalk_1.default.dim(`\n ${results.length} result${results.length === 1 ? '' : 's'} for "${chalk_1.default.white(query)}"\n`));
|
|
59
|
-
const table = new cli_table3_1.default({
|
|
60
|
-
head: [chalk_1.default.cyan('Name'), chalk_1.default.cyan('Command'), chalk_1.default.cyan('Tags')],
|
|
61
|
-
style: { head: [], border: ['grey'] },
|
|
62
|
-
colWidths: [20, 46, 18],
|
|
63
|
-
wordWrap: true,
|
|
64
|
-
});
|
|
65
|
-
for (const cmd of results) {
|
|
66
|
-
const name = cmd.name ?? '—';
|
|
67
|
-
const command = cmd.command.length > 44
|
|
68
|
-
? cmd.command.slice(0, 44) + '…'
|
|
69
|
-
: cmd.command;
|
|
70
|
-
const tags = (cmd.tags ?? []).join(', ') || '—';
|
|
71
|
-
table.push([
|
|
72
|
-
highlight(name, query),
|
|
73
|
-
highlight(command, query),
|
|
74
|
-
highlight(tags, query),
|
|
75
|
-
]);
|
|
76
|
-
}
|
|
77
|
-
console.log(table.toString());
|
|
36
|
+
const commands = (0, local_vault_1.listLocalCommands)({ tag: opts.tag });
|
|
37
|
+
const results = commands.filter((cmd) => {
|
|
38
|
+
return (fuzzyMatch(cmd.command, query) ||
|
|
39
|
+
(cmd.name ? fuzzyMatch(cmd.name, query) : false) ||
|
|
40
|
+
(cmd.tags ?? []).some((t) => fuzzyMatch(t, query)));
|
|
41
|
+
});
|
|
42
|
+
if (!results.length) {
|
|
43
|
+
console.log(chalk_1.default.dim(`\n No local commands matching "${query}".\n`));
|
|
44
|
+
return;
|
|
78
45
|
}
|
|
79
|
-
|
|
80
|
-
|
|
46
|
+
console.log(chalk_1.default.dim(`\n ${results.length} result${results.length === 1 ? '' : 's'} for "${chalk_1.default.white(query)}"\n`));
|
|
47
|
+
const table = new cli_table3_1.default({
|
|
48
|
+
head: [chalk_1.default.cyan('Name'), chalk_1.default.cyan('Command'), chalk_1.default.cyan('Tags')],
|
|
49
|
+
style: { head: [], border: ['grey'] },
|
|
50
|
+
colWidths: [20, 46, 18],
|
|
51
|
+
wordWrap: true,
|
|
52
|
+
});
|
|
53
|
+
for (const cmd of results) {
|
|
54
|
+
const name = cmd.name ?? '—';
|
|
55
|
+
const command = cmd.command.length > 44
|
|
56
|
+
? cmd.command.slice(0, 44) + '…'
|
|
57
|
+
: cmd.command;
|
|
58
|
+
const tags = (cmd.tags ?? []).join(', ') || '—';
|
|
59
|
+
table.push([
|
|
60
|
+
highlight(name, query),
|
|
61
|
+
highlight(command, query),
|
|
62
|
+
highlight(tags, query),
|
|
63
|
+
]);
|
|
81
64
|
}
|
|
65
|
+
console.log(table.toString());
|
|
66
|
+
console.log();
|
|
82
67
|
});
|
package/dist/commands/sync.js
CHANGED
|
@@ -7,74 +7,112 @@ exports.syncCommand = void 0;
|
|
|
7
7
|
exports.getCachedCommands = getCachedCommands;
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
9
|
const commander_1 = require("commander");
|
|
10
|
-
const fs_1 = __importDefault(require("fs"));
|
|
11
10
|
const ora_1 = __importDefault(require("ora"));
|
|
12
|
-
const os_1 = __importDefault(require("os"));
|
|
13
|
-
const path_1 = __importDefault(require("path"));
|
|
14
11
|
const api_1 = require("../lib/api");
|
|
15
12
|
const auth_1 = require("../lib/auth");
|
|
16
|
-
const
|
|
17
|
-
const CACHE_FILE = path_1.default.join(CACHE_DIR, 'commands.json');
|
|
18
|
-
function writeCache(commands) {
|
|
19
|
-
fs_1.default.mkdirSync(CACHE_DIR, { recursive: true });
|
|
20
|
-
fs_1.default.writeFileSync(CACHE_FILE, JSON.stringify({
|
|
21
|
-
syncedAt: new Date().toISOString(),
|
|
22
|
-
commands,
|
|
23
|
-
}, null, 2));
|
|
24
|
-
}
|
|
25
|
-
function readCache() {
|
|
26
|
-
try {
|
|
27
|
-
if (!fs_1.default.existsSync(CACHE_FILE))
|
|
28
|
-
return null;
|
|
29
|
-
return JSON.parse(fs_1.default.readFileSync(CACHE_FILE, 'utf-8'));
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function getCachedCommands() {
|
|
36
|
-
const cache = readCache();
|
|
37
|
-
return cache?.commands ?? [];
|
|
38
|
-
}
|
|
13
|
+
const local_vault_1 = require("../lib/local-vault");
|
|
39
14
|
exports.syncCommand = new commander_1.Command('sync')
|
|
40
|
-
.description('Sync
|
|
41
|
-
.option('-s, --status', 'Show last sync status
|
|
15
|
+
.description('Sync local vault ↔ cloud (Pro/Team)')
|
|
16
|
+
.option('-s, --status', 'Show local vault + last sync status')
|
|
17
|
+
.option('--pull', 'Pull cloud → local only')
|
|
18
|
+
.option('--push', 'Push local-only commands → cloud only')
|
|
42
19
|
.action(async (opts) => {
|
|
43
|
-
|
|
20
|
+
const vault = (0, local_vault_1.readVault)();
|
|
44
21
|
if (opts.status) {
|
|
45
|
-
const cache = readCache();
|
|
46
|
-
if (!cache) {
|
|
47
|
-
console.log(chalk_1.default.dim('\n Never synced. Run: recall sync\n'));
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
const ago = Math.round((Date.now() - new Date(cache.syncedAt).getTime()) / 1000 / 60);
|
|
51
22
|
console.log();
|
|
52
|
-
console.log(`
|
|
53
|
-
console.log(`
|
|
54
|
-
|
|
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)}`);
|
|
25
|
+
if (!vault.syncedAt) {
|
|
26
|
+
console.log(chalk_1.default.dim(' Never synced to cloud.'));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
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`)}`);
|
|
31
|
+
}
|
|
32
|
+
const pending = (0, local_vault_1.localCommandsNeedingPush)().length;
|
|
33
|
+
if (pending) {
|
|
34
|
+
console.log(` Not on cloud: ${chalk_1.default.yellow(String(pending))} (recall sync --push)`);
|
|
35
|
+
}
|
|
55
36
|
console.log();
|
|
56
37
|
return;
|
|
57
38
|
}
|
|
39
|
+
const doPull = opts.pull || (!opts.pull && !opts.push);
|
|
40
|
+
const doPush = opts.push || (!opts.pull && !opts.push);
|
|
58
41
|
const token = await (0, auth_1.requireAuth)();
|
|
59
|
-
const spinner = (0, ora_1.default)('
|
|
42
|
+
const spinner = (0, ora_1.default)('Checking plan...').start();
|
|
60
43
|
try {
|
|
61
|
-
const {
|
|
62
|
-
|
|
63
|
-
spinner.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.log();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
44
|
+
const { user } = await api_1.ApiClient.get('/auth/me', token);
|
|
45
|
+
const plan = (user.plan ?? 'free');
|
|
46
|
+
spinner.stop();
|
|
47
|
+
if (plan !== 'pro' && plan !== 'team') {
|
|
48
|
+
console.log(chalk_1.default.yellow('\n Cloud sync requires Pro or Team.'));
|
|
49
|
+
console.log(chalk_1.default.dim(' Local vault still works without sync.'));
|
|
50
|
+
console.log(chalk_1.default.dim(' Upgrade: recall upgrade\n'));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
spinner.fail(chalk_1.default.red('Could not verify plan'));
|
|
56
|
+
if (err instanceof api_1.ApiError && err.status === 401) {
|
|
57
|
+
console.log(chalk_1.default.red('\n Session expired. Run: recall auth login\n'));
|
|
58
|
+
}
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
let pulled = 0;
|
|
62
|
+
let pushed = 0;
|
|
63
|
+
if (doPull) {
|
|
64
|
+
const pullSpinner = (0, ora_1.default)('Pulling from cloud...').start();
|
|
65
|
+
try {
|
|
66
|
+
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)`));
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
pullSpinner.fail(chalk_1.default.red('Pull failed'));
|
|
73
|
+
if (err instanceof api_1.ApiError && err.status === 403) {
|
|
74
|
+
console.log(chalk_1.default.yellow('\n Cloud vault requires Pro.\n'));
|
|
75
|
+
}
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
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`));
|
|
98
|
+
}
|
|
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'));
|
|
103
|
+
}
|
|
104
|
+
process.exit(1);
|
|
72
105
|
}
|
|
73
106
|
}
|
|
74
|
-
console.log();
|
|
75
107
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(` ${chalk_1.default.dim('Local vault:')} ${chalk_1.default.dim(local_vault_1.VAULT_FILE)}`);
|
|
110
|
+
if (doPull || doPush) {
|
|
111
|
+
console.log(chalk_1.default.dim(` pull≈${pulled} · push=${pushed}`));
|
|
79
112
|
}
|
|
113
|
+
console.log();
|
|
80
114
|
});
|
|
115
|
+
/** @deprecated kept for any imports — use readVault */
|
|
116
|
+
function getCachedCommands() {
|
|
117
|
+
return (0, local_vault_1.readVault)().commands;
|
|
118
|
+
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -16,18 +16,17 @@ const PLANS = {
|
|
|
16
16
|
label: 'Free',
|
|
17
17
|
price: '$0/month',
|
|
18
18
|
features: [
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'Export as JSON or shell script',
|
|
24
|
-
'Cross-machine sync',
|
|
19
|
+
'Fully local vault (no account)',
|
|
20
|
+
'Save, list, search, run on your machine',
|
|
21
|
+
'No cloud sync',
|
|
22
|
+
'No AI Ask (Pro)',
|
|
25
23
|
],
|
|
26
24
|
},
|
|
27
25
|
pro: {
|
|
28
26
|
label: 'Pro',
|
|
29
27
|
price: '$6/month',
|
|
30
28
|
features: [
|
|
29
|
+
'Sync local ↔ cloud',
|
|
31
30
|
'500 AI asks / month',
|
|
32
31
|
'Unlimited playbooks',
|
|
33
32
|
'Unlimited commands',
|
package/dist/commands/whoami.js
CHANGED
|
@@ -16,13 +16,13 @@ const PLAN_LABELS = {
|
|
|
16
16
|
};
|
|
17
17
|
const PLAN_FEATURES = {
|
|
18
18
|
free: [
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'Cross-machine sync',
|
|
19
|
+
'Fully local vault (no cloud)',
|
|
20
|
+
'Save, list, search, run offline',
|
|
21
|
+
'No Ask / no sync',
|
|
23
22
|
],
|
|
24
23
|
pro: [
|
|
25
|
-
'
|
|
24
|
+
'Sync local ↔ cloud',
|
|
25
|
+
'Ask (NL, print-only)',
|
|
26
26
|
'Command groups',
|
|
27
27
|
'Share commands via link',
|
|
28
28
|
],
|
|
@@ -30,7 +30,6 @@ const PLAN_FEATURES = {
|
|
|
30
30
|
'Everything in Pro',
|
|
31
31
|
'Org-level vaults',
|
|
32
32
|
'Role-based access',
|
|
33
|
-
'SSO / SAML',
|
|
34
33
|
'Audit log',
|
|
35
34
|
],
|
|
36
35
|
};
|
|
@@ -59,8 +58,8 @@ exports.whoamiCommand = new commander_1.Command('whoami')
|
|
|
59
58
|
}
|
|
60
59
|
if (plan === 'free') {
|
|
61
60
|
console.log();
|
|
62
|
-
console.log(chalk_1.default.dim(' Upgrade to Pro for
|
|
63
|
-
console.log(chalk_1.default.dim(' https://devkitvault.com/
|
|
61
|
+
console.log(chalk_1.default.dim(' Upgrade to Pro for sync + Ask:'));
|
|
62
|
+
console.log(chalk_1.default.dim(' https://recall.devkitvault.com/dashboard/upgrade'));
|
|
64
63
|
}
|
|
65
64
|
console.log();
|
|
66
65
|
}
|
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 =
|
|
14
|
+
exports.APP_VERSION = '1.2.9';
|
|
15
15
|
exports.ENVIRONMENTS = {
|
|
16
16
|
production: 'https://api.devkitvault.com',
|
|
17
17
|
local: 'http://127.0.0.1:3001',
|
|
@@ -0,0 +1,251 @@
|
|
|
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.VAULT_FILE = void 0;
|
|
7
|
+
exports.readVault = readVault;
|
|
8
|
+
exports.writeVault = writeVault;
|
|
9
|
+
exports.listLocalCommands = listLocalCommands;
|
|
10
|
+
exports.findLocalByName = findLocalByName;
|
|
11
|
+
exports.saveLocalCommand = saveLocalCommand;
|
|
12
|
+
exports.deleteLocalByName = deleteLocalByName;
|
|
13
|
+
exports.setLocalPinned = setLocalPinned;
|
|
14
|
+
exports.markVaultSynced = markVaultSynced;
|
|
15
|
+
exports.replaceLocalFromCloud = replaceLocalFromCloud;
|
|
16
|
+
exports.mergeCloudIntoLocal = mergeCloudIntoLocal;
|
|
17
|
+
exports.localCommandsNeedingPush = localCommandsNeedingPush;
|
|
18
|
+
exports.attachCloudId = attachCloudId;
|
|
19
|
+
const node_crypto_1 = require("node:crypto");
|
|
20
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
22
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
23
|
+
const VAULT_DIR = node_path_1.default.join(process.env.RECALL_HOME?.trim() || node_os_1.default.homedir(), '.recall');
|
|
24
|
+
exports.VAULT_FILE = node_path_1.default.join(VAULT_DIR, 'commands.json');
|
|
25
|
+
function nowIso() {
|
|
26
|
+
return new Date().toISOString();
|
|
27
|
+
}
|
|
28
|
+
function ensureDir() {
|
|
29
|
+
node_fs_1.default.mkdirSync(VAULT_DIR, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
function normalizeCommand(raw, legacySyncCache) {
|
|
32
|
+
const command = String(raw.command ?? '');
|
|
33
|
+
const name = typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : undefined;
|
|
34
|
+
const tags = Array.isArray(raw.tags)
|
|
35
|
+
? raw.tags.filter((t) => typeof t === 'string' && t.trim().length > 0)
|
|
36
|
+
: undefined;
|
|
37
|
+
const pinned = Boolean(raw.pinned);
|
|
38
|
+
const createdAt = typeof raw.createdAt === 'string' ? raw.createdAt : nowIso();
|
|
39
|
+
const updatedAt = typeof raw.updatedAt === 'string' ? raw.updatedAt : createdAt;
|
|
40
|
+
let cloudId;
|
|
41
|
+
if (typeof raw.cloudId === 'string' && raw.cloudId) {
|
|
42
|
+
cloudId = raw.cloudId;
|
|
43
|
+
}
|
|
44
|
+
else if (legacySyncCache && typeof raw.id === 'string' && raw.id) {
|
|
45
|
+
// Old cache used API ids as `id`
|
|
46
|
+
cloudId = raw.id;
|
|
47
|
+
}
|
|
48
|
+
const id = !legacySyncCache && typeof raw.id === 'string' && raw.id
|
|
49
|
+
? raw.id
|
|
50
|
+
: (0, node_crypto_1.randomUUID)();
|
|
51
|
+
return {
|
|
52
|
+
id,
|
|
53
|
+
command,
|
|
54
|
+
name,
|
|
55
|
+
tags: tags?.length ? tags : undefined,
|
|
56
|
+
pinned: pinned || undefined,
|
|
57
|
+
createdAt,
|
|
58
|
+
updatedAt,
|
|
59
|
+
cloudId,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function emptyVault() {
|
|
63
|
+
return {
|
|
64
|
+
version: 1,
|
|
65
|
+
updatedAt: nowIso(),
|
|
66
|
+
syncedAt: null,
|
|
67
|
+
commands: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function readVault() {
|
|
71
|
+
try {
|
|
72
|
+
if (!node_fs_1.default.existsSync(exports.VAULT_FILE))
|
|
73
|
+
return emptyVault();
|
|
74
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(exports.VAULT_FILE, 'utf-8'));
|
|
75
|
+
const rawCommands = Array.isArray(parsed.commands) ? parsed.commands : [];
|
|
76
|
+
const legacySyncCache = parsed.version !== 1;
|
|
77
|
+
const commands = rawCommands
|
|
78
|
+
.filter((c) => c !== null && typeof c === 'object')
|
|
79
|
+
.map((c) => normalizeCommand(c, legacySyncCache))
|
|
80
|
+
.filter((c) => c.command.trim().length > 0);
|
|
81
|
+
return {
|
|
82
|
+
version: 1,
|
|
83
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : nowIso(),
|
|
84
|
+
syncedAt: typeof parsed.syncedAt === 'string' ? parsed.syncedAt : null,
|
|
85
|
+
commands,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return emptyVault();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function writeVault(vault) {
|
|
93
|
+
ensureDir();
|
|
94
|
+
const payload = {
|
|
95
|
+
version: 1,
|
|
96
|
+
updatedAt: nowIso(),
|
|
97
|
+
syncedAt: vault.syncedAt ?? null,
|
|
98
|
+
commands: vault.commands,
|
|
99
|
+
};
|
|
100
|
+
node_fs_1.default.writeFileSync(exports.VAULT_FILE, JSON.stringify(payload, null, 2), { mode: 0o600 });
|
|
101
|
+
}
|
|
102
|
+
function listLocalCommands(filters) {
|
|
103
|
+
let rows = readVault().commands;
|
|
104
|
+
if (filters?.tag) {
|
|
105
|
+
const tag = filters.tag.toLowerCase();
|
|
106
|
+
rows = rows.filter((c) => (c.tags ?? []).some((t) => t.toLowerCase() === tag));
|
|
107
|
+
}
|
|
108
|
+
if (filters?.search) {
|
|
109
|
+
const q = filters.search.toLowerCase();
|
|
110
|
+
rows = rows.filter((c) => c.command.toLowerCase().includes(q) ||
|
|
111
|
+
(c.name?.toLowerCase().includes(q) ?? false) ||
|
|
112
|
+
(c.tags ?? []).some((t) => t.toLowerCase().includes(q)));
|
|
113
|
+
}
|
|
114
|
+
if (filters?.pinned) {
|
|
115
|
+
rows = rows.filter((c) => c.pinned);
|
|
116
|
+
}
|
|
117
|
+
return rows.sort((a, b) => {
|
|
118
|
+
if (a.pinned && !b.pinned)
|
|
119
|
+
return -1;
|
|
120
|
+
if (!a.pinned && b.pinned)
|
|
121
|
+
return 1;
|
|
122
|
+
return b.updatedAt.localeCompare(a.updatedAt);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function findLocalByName(name) {
|
|
126
|
+
const needle = name.toLowerCase();
|
|
127
|
+
return readVault().commands.find((c) => c.name?.toLowerCase() === needle);
|
|
128
|
+
}
|
|
129
|
+
function saveLocalCommand(input) {
|
|
130
|
+
const vault = readVault();
|
|
131
|
+
const stamp = nowIso();
|
|
132
|
+
const name = input.name?.trim() || undefined;
|
|
133
|
+
if (name) {
|
|
134
|
+
const existing = vault.commands.find((c) => c.name?.toLowerCase() === name.toLowerCase());
|
|
135
|
+
if (existing) {
|
|
136
|
+
existing.command = input.command;
|
|
137
|
+
existing.tags = input.tags?.length ? input.tags : undefined;
|
|
138
|
+
existing.updatedAt = stamp;
|
|
139
|
+
// Local edit invalidates cloud link until next push maps again
|
|
140
|
+
writeVault(vault);
|
|
141
|
+
return existing;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const saved = {
|
|
145
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
146
|
+
command: input.command,
|
|
147
|
+
name,
|
|
148
|
+
tags: input.tags?.length ? input.tags : undefined,
|
|
149
|
+
createdAt: stamp,
|
|
150
|
+
updatedAt: stamp,
|
|
151
|
+
};
|
|
152
|
+
vault.commands.push(saved);
|
|
153
|
+
writeVault(vault);
|
|
154
|
+
return saved;
|
|
155
|
+
}
|
|
156
|
+
function deleteLocalByName(name) {
|
|
157
|
+
const vault = readVault();
|
|
158
|
+
const idx = vault.commands.findIndex((c) => c.name?.toLowerCase() === name.toLowerCase());
|
|
159
|
+
if (idx < 0)
|
|
160
|
+
return undefined;
|
|
161
|
+
const [removed] = vault.commands.splice(idx, 1);
|
|
162
|
+
writeVault(vault);
|
|
163
|
+
return removed;
|
|
164
|
+
}
|
|
165
|
+
function setLocalPinned(name, pinned) {
|
|
166
|
+
const vault = readVault();
|
|
167
|
+
const cmd = vault.commands.find((c) => c.name?.toLowerCase() === name.toLowerCase());
|
|
168
|
+
if (!cmd)
|
|
169
|
+
return undefined;
|
|
170
|
+
cmd.pinned = pinned || undefined;
|
|
171
|
+
cmd.updatedAt = nowIso();
|
|
172
|
+
writeVault(vault);
|
|
173
|
+
return cmd;
|
|
174
|
+
}
|
|
175
|
+
function markVaultSynced(at = nowIso()) {
|
|
176
|
+
const vault = readVault();
|
|
177
|
+
vault.syncedAt = at;
|
|
178
|
+
writeVault(vault);
|
|
179
|
+
}
|
|
180
|
+
function replaceLocalFromCloud(cloudCommands) {
|
|
181
|
+
const stamp = nowIso();
|
|
182
|
+
const mapped = cloudCommands.map((c) => ({
|
|
183
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
184
|
+
cloudId: c.id,
|
|
185
|
+
command: c.command,
|
|
186
|
+
name: c.name ?? undefined,
|
|
187
|
+
tags: c.tags?.length ? c.tags : undefined,
|
|
188
|
+
pinned: c.pinned || undefined,
|
|
189
|
+
createdAt: c.createdAt ?? stamp,
|
|
190
|
+
updatedAt: c.updatedAt ?? stamp,
|
|
191
|
+
}));
|
|
192
|
+
writeVault({
|
|
193
|
+
version: 1,
|
|
194
|
+
updatedAt: stamp,
|
|
195
|
+
syncedAt: stamp,
|
|
196
|
+
commands: mapped,
|
|
197
|
+
});
|
|
198
|
+
return mapped;
|
|
199
|
+
}
|
|
200
|
+
/** Merge cloud into local by name (cloud wins on conflict); keep local-only rows. */
|
|
201
|
+
function mergeCloudIntoLocal(cloudCommands) {
|
|
202
|
+
const vault = readVault();
|
|
203
|
+
const stamp = nowIso();
|
|
204
|
+
let added = 0;
|
|
205
|
+
let updated = 0;
|
|
206
|
+
for (const cloud of cloudCommands) {
|
|
207
|
+
const name = cloud.name?.trim();
|
|
208
|
+
const byCloudId = vault.commands.find((c) => c.cloudId === cloud.id);
|
|
209
|
+
const byName = name
|
|
210
|
+
? vault.commands.find((c) => c.name?.toLowerCase() === name.toLowerCase())
|
|
211
|
+
: undefined;
|
|
212
|
+
const target = byCloudId ?? byName;
|
|
213
|
+
if (target) {
|
|
214
|
+
target.cloudId = cloud.id;
|
|
215
|
+
target.command = cloud.command;
|
|
216
|
+
target.name = name || target.name;
|
|
217
|
+
target.tags = cloud.tags?.length ? cloud.tags : undefined;
|
|
218
|
+
target.pinned = cloud.pinned || undefined;
|
|
219
|
+
target.updatedAt = stamp;
|
|
220
|
+
updated++;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
vault.commands.push({
|
|
224
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
225
|
+
cloudId: cloud.id,
|
|
226
|
+
command: cloud.command,
|
|
227
|
+
name: name || undefined,
|
|
228
|
+
tags: cloud.tags?.length ? cloud.tags : undefined,
|
|
229
|
+
pinned: cloud.pinned || undefined,
|
|
230
|
+
createdAt: cloud.createdAt ?? stamp,
|
|
231
|
+
updatedAt: stamp,
|
|
232
|
+
});
|
|
233
|
+
added++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
vault.syncedAt = stamp;
|
|
237
|
+
writeVault(vault);
|
|
238
|
+
return { local: vault.commands, added, updated };
|
|
239
|
+
}
|
|
240
|
+
function localCommandsNeedingPush() {
|
|
241
|
+
return readVault().commands.filter((c) => !c.cloudId);
|
|
242
|
+
}
|
|
243
|
+
function attachCloudId(localId, cloudId) {
|
|
244
|
+
const vault = readVault();
|
|
245
|
+
const cmd = vault.commands.find((c) => c.id === localId);
|
|
246
|
+
if (!cmd)
|
|
247
|
+
return;
|
|
248
|
+
cmd.cloudId = cloudId;
|
|
249
|
+
cmd.updatedAt = nowIso();
|
|
250
|
+
writeVault(vault);
|
|
251
|
+
}
|