@devkitvault/recall 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +11 -22
  2. package/dist/commands/alias.js +84 -0
  3. package/dist/commands/approve.js +108 -0
  4. package/dist/commands/audit.js +58 -0
  5. package/dist/commands/auth/forgot.js +33 -0
  6. package/dist/commands/auth/index.js +16 -0
  7. package/dist/commands/auth/login.js +151 -0
  8. package/dist/commands/auth/logout.js +15 -0
  9. package/dist/commands/auth/register.js +84 -0
  10. package/dist/commands/auth/reset.js +43 -0
  11. package/dist/commands/completion.js +158 -0
  12. package/dist/commands/config.js +89 -0
  13. package/dist/commands/delete.js +58 -0
  14. package/dist/commands/doctor.js +106 -0
  15. package/dist/commands/env.js +135 -0
  16. package/dist/commands/export.js +68 -0
  17. package/dist/commands/feedback.js +25 -0
  18. package/dist/commands/group.js +206 -0
  19. package/dist/commands/history.js +41 -0
  20. package/dist/commands/import.js +126 -0
  21. package/dist/commands/list.js +69 -0
  22. package/dist/commands/org.js +408 -0
  23. package/dist/commands/pin.js +30 -0
  24. package/dist/commands/run.js +55 -0
  25. package/dist/commands/save.js +73 -0
  26. package/dist/commands/search.js +82 -0
  27. package/dist/commands/share.js +88 -0
  28. package/dist/commands/snippet.js +147 -0
  29. package/dist/commands/support.js +96 -0
  30. package/dist/commands/sync.js +80 -0
  31. package/dist/commands/template.js +177 -0
  32. package/dist/commands/update.js +66 -0
  33. package/dist/commands/upgrade.js +122 -0
  34. package/dist/commands/whoami.js +70 -0
  35. package/dist/index.js +71 -0
  36. package/dist/lib/api.js +51 -0
  37. package/dist/lib/auth.js +88 -0
  38. package/dist/lib/config.js +37 -0
  39. package/package.json +12 -7
  40. package/bin/recall.js +0 -169
@@ -0,0 +1,122 @@
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.upgradeCommand = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const commander_1 = require("commander");
9
+ const inquirer_1 = __importDefault(require("inquirer"));
10
+ const open_1 = __importDefault(require("open"));
11
+ const ora_1 = __importDefault(require("ora"));
12
+ const api_1 = require("../lib/api");
13
+ const auth_1 = require("../lib/auth");
14
+ const PLANS = {
15
+ free: {
16
+ label: 'Free',
17
+ price: '$0/month',
18
+ features: [
19
+ 'Unlimited command saves',
20
+ 'Search and filter',
21
+ 'Export as JSON or shell script',
22
+ 'Cross-machine sync',
23
+ ],
24
+ },
25
+ pro: {
26
+ label: 'Pro',
27
+ price: '$9/month',
28
+ features: [
29
+ 'Everything in Free',
30
+ 'Command groups',
31
+ 'Share commands via link',
32
+ 'Priority support',
33
+ ],
34
+ },
35
+ team: {
36
+ label: 'Team',
37
+ price: '$19/month per seat',
38
+ features: [
39
+ 'Everything in Pro',
40
+ 'Org-level vaults',
41
+ 'Role-based access',
42
+ 'SSO / SAML',
43
+ 'Audit log',
44
+ ],
45
+ },
46
+ };
47
+ function printPlan(key, current) {
48
+ const plan = PLANS[key];
49
+ const prefix = current ? chalk_1.default.green('▶ ') : ' ';
50
+ const label = current
51
+ ? chalk_1.default.green.bold(`${plan.label} (current)`)
52
+ : chalk_1.default.white.bold(plan.label);
53
+ console.log(`${prefix}${label} — ${chalk_1.default.dim(plan.price)}`);
54
+ for (const f of plan.features) {
55
+ console.log(` ${chalk_1.default.dim('·')} ${chalk_1.default.dim(f)}`);
56
+ }
57
+ console.log();
58
+ }
59
+ exports.upgradeCommand = new commander_1.Command('upgrade')
60
+ .description('Upgrade your recall plan')
61
+ .action(async () => {
62
+ const token = await (0, auth_1.requireAuth)();
63
+ const spinner = (0, ora_1.default)('Fetching account info...').start();
64
+ let currentPlan = 'free';
65
+ try {
66
+ const { user } = await api_1.ApiClient.get('/auth/me', token);
67
+ currentPlan = user.plan ?? 'free';
68
+ spinner.stop();
69
+ }
70
+ catch {
71
+ spinner.stop();
72
+ }
73
+ console.log();
74
+ console.log(chalk_1.default.bold(' recall plans\n'));
75
+ printPlan('free', currentPlan === 'free');
76
+ printPlan('pro', currentPlan === 'pro');
77
+ printPlan('team', currentPlan === 'team');
78
+ if (currentPlan === 'team') {
79
+ console.log(chalk_1.default.green(' You are on the highest plan. Thank you!\n'));
80
+ return;
81
+ }
82
+ const upgradeTo = currentPlan === 'free'
83
+ ? [
84
+ { name: 'Upgrade to Pro — $9/month', value: 'pro' },
85
+ { name: 'Upgrade to Team — $19/month per seat', value: 'team' },
86
+ { name: 'Maybe later', value: 'exit' },
87
+ ]
88
+ : [
89
+ { name: 'Upgrade to Team — $19/month per seat', value: 'team' },
90
+ { name: 'Maybe later', value: 'exit' },
91
+ ];
92
+ const { choice } = await inquirer_1.default.prompt([
93
+ {
94
+ type: 'list',
95
+ name: 'choice',
96
+ message: 'Which plan would you like?',
97
+ choices: upgradeTo,
98
+ },
99
+ ]);
100
+ if (choice === 'exit') {
101
+ console.log(chalk_1.default.dim('\n No changes made.\n'));
102
+ return;
103
+ }
104
+ const { confirm } = await inquirer_1.default.prompt([
105
+ {
106
+ type: 'confirm',
107
+ name: 'confirm',
108
+ message: `Open billing page to upgrade to ${PLANS[choice].label}?`,
109
+ default: true,
110
+ },
111
+ ]);
112
+ if (!confirm) {
113
+ console.log(chalk_1.default.dim('\n No changes made.\n'));
114
+ return;
115
+ }
116
+ const billingUrl = `https://devkitvault.com/recall/upgrade?plan=${choice}`;
117
+ await (0, open_1.default)(billingUrl);
118
+ console.log();
119
+ console.log(chalk_1.default.green(' Billing page opened in your browser.'));
120
+ console.log(chalk_1.default.dim(` ${billingUrl}`));
121
+ console.log(chalk_1.default.dim('\n After payment your plan will update automatically.\n'));
122
+ });
@@ -0,0 +1,70 @@
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.whoamiCommand = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const commander_1 = require("commander");
9
+ const ora_1 = __importDefault(require("ora"));
10
+ const api_1 = require("../lib/api");
11
+ const auth_1 = require("../lib/auth");
12
+ const PLAN_LABELS = {
13
+ free: chalk_1.default.dim('Free'),
14
+ pro: chalk_1.default.cyan('Pro'),
15
+ team: chalk_1.default.magenta('Team'),
16
+ };
17
+ const PLAN_FEATURES = {
18
+ free: [
19
+ 'Unlimited command saves',
20
+ 'Search and filter',
21
+ 'Export as JSON or shell script',
22
+ 'Cross-machine sync',
23
+ ],
24
+ pro: [
25
+ 'Everything in Free',
26
+ 'Command groups',
27
+ 'Share commands via link',
28
+ ],
29
+ team: [
30
+ 'Everything in Pro',
31
+ 'Org-level vaults',
32
+ 'Role-based access',
33
+ 'SSO / SAML',
34
+ 'Audit log',
35
+ ],
36
+ };
37
+ exports.whoamiCommand = new commander_1.Command('whoami')
38
+ .description('Show current logged in user and plan info')
39
+ .action(async () => {
40
+ const token = await (0, auth_1.requireAuth)();
41
+ const spinner = (0, ora_1.default)('Fetching account info...').start();
42
+ try {
43
+ const { user, stats } = await api_1.ApiClient.get('/auth/me', token);
44
+ spinner.stop();
45
+ const plan = user.plan ?? 'free';
46
+ const label = PLAN_LABELS[plan] ?? plan;
47
+ const features = PLAN_FEATURES[plan] ?? [];
48
+ console.log();
49
+ console.log(` ${chalk_1.default.bold(user.username)}`);
50
+ console.log(` ${chalk_1.default.dim(user.email)}`);
51
+ console.log();
52
+ console.log(` Plan: ${label}`);
53
+ console.log(` Commands: ${chalk_1.default.white(stats.commandCount)}`);
54
+ console.log(` Member since: ${chalk_1.default.dim(new Date(user.createdAt).toLocaleDateString())}`);
55
+ console.log();
56
+ console.log(chalk_1.default.dim(' Plan includes:'));
57
+ for (const feature of features) {
58
+ console.log(chalk_1.default.dim(` · ${feature}`));
59
+ }
60
+ if (plan === 'free') {
61
+ console.log();
62
+ console.log(chalk_1.default.dim(' Upgrade to Pro for groups and sharing:'));
63
+ console.log(chalk_1.default.dim(' https://devkitvault.com/recall/upgrade'));
64
+ }
65
+ console.log();
66
+ }
67
+ catch {
68
+ spinner.fail(chalk_1.default.red('Failed to fetch account info'));
69
+ }
70
+ });
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const commander_1 = require("commander");
9
+ const alias_1 = require("./commands/alias");
10
+ const approve_1 = require("./commands/approve");
11
+ const audit_1 = require("./commands/audit");
12
+ const index_1 = require("./commands/auth/index");
13
+ const completion_1 = require("./commands/completion");
14
+ const config_1 = require("./commands/config");
15
+ const delete_1 = require("./commands/delete");
16
+ const doctor_1 = require("./commands/doctor");
17
+ const env_1 = require("./commands/env");
18
+ const export_1 = require("./commands/export");
19
+ const feedback_1 = require("./commands/feedback");
20
+ const group_1 = require("./commands/group");
21
+ const history_1 = require("./commands/history");
22
+ const import_1 = require("./commands/import");
23
+ const list_1 = require("./commands/list");
24
+ const org_1 = require("./commands/org");
25
+ const pin_1 = require("./commands/pin");
26
+ const run_1 = require("./commands/run");
27
+ const save_1 = require("./commands/save");
28
+ const search_1 = require("./commands/search");
29
+ const share_1 = require("./commands/share");
30
+ const snippet_1 = require("./commands/snippet");
31
+ const support_1 = require("./commands/support");
32
+ const sync_1 = require("./commands/sync");
33
+ const template_1 = require("./commands/template");
34
+ const update_1 = require("./commands/update");
35
+ const upgrade_1 = require("./commands/upgrade");
36
+ const whoami_1 = require("./commands/whoami");
37
+ const config_2 = require("./lib/config");
38
+ const program = new commander_1.Command();
39
+ program
40
+ .name('recall')
41
+ .description(chalk_1.default.cyan('Save and retrieve your CLI commands from anywhere'))
42
+ .version(config_2.APP_VERSION);
43
+ program.addCommand(index_1.authCommand);
44
+ program.addCommand(save_1.saveCommand);
45
+ program.addCommand(list_1.listCommand);
46
+ program.addCommand(run_1.runCommand);
47
+ program.addCommand(delete_1.deleteCommand);
48
+ program.addCommand(search_1.searchCommand);
49
+ program.addCommand(update_1.updateCommand);
50
+ program.addCommand(export_1.exportCommand);
51
+ program.addCommand(import_1.importCommand);
52
+ program.addCommand(whoami_1.whoamiCommand);
53
+ program.addCommand(upgrade_1.upgradeCommand);
54
+ program.addCommand(sync_1.syncCommand);
55
+ program.addCommand(group_1.groupCommand);
56
+ program.addCommand(org_1.orgCommand);
57
+ program.addCommand(config_1.configCommand);
58
+ program.addCommand(pin_1.pinCommand);
59
+ program.addCommand(alias_1.aliasCommand);
60
+ program.addCommand(history_1.historyCommand);
61
+ program.addCommand(share_1.shareCommand);
62
+ program.addCommand(snippet_1.snippetCommand);
63
+ program.addCommand(env_1.envCommand);
64
+ program.addCommand(audit_1.auditCommand);
65
+ program.addCommand(approve_1.approveCommand);
66
+ program.addCommand(template_1.templateCommand);
67
+ program.addCommand(doctor_1.doctorCommand);
68
+ program.addCommand(completion_1.completionCommand);
69
+ program.addCommand(feedback_1.feedbackCommand);
70
+ program.addCommand(support_1.supportCommand);
71
+ program.parse();
@@ -0,0 +1,51 @@
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.ApiClient = exports.ApiError = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const config_1 = require("./config");
9
+ class ApiError extends Error {
10
+ status;
11
+ body;
12
+ constructor(message, status, body) {
13
+ super(message);
14
+ this.status = status;
15
+ this.body = body;
16
+ }
17
+ }
18
+ exports.ApiError = ApiError;
19
+ async function request(method, path, body, token) {
20
+ const { apiUrl } = (0, config_1.getConfig)();
21
+ if (!apiUrl.startsWith('https') && !apiUrl.includes('127.0.0.1') && !apiUrl.includes('localhost')) {
22
+ console.warn(chalk_1.default.yellow(' Warning: connecting over HTTP. Use HTTPS in production.'));
23
+ }
24
+ const headers = {
25
+ 'Content-Type': 'application/json',
26
+ };
27
+ if (token)
28
+ headers['Authorization'] = `Bearer ${token}`;
29
+ const res = await fetch(`${apiUrl}${path}`, {
30
+ method,
31
+ headers,
32
+ body: body ? JSON.stringify(body) : undefined,
33
+ });
34
+ const data = await res.json().catch(() => ({}));
35
+ if (!res.ok) {
36
+ if (res.status === 401) {
37
+ const message = data.message === 'Invalid credentials'
38
+ ? 'Invalid credentials'
39
+ : 'Session expired. Run: recall auth login';
40
+ throw new ApiError(message, 401, data);
41
+ }
42
+ throw new ApiError(data.message ?? 'Request failed', res.status, data);
43
+ }
44
+ return data;
45
+ }
46
+ exports.ApiClient = {
47
+ get: (path, token) => request('GET', path, undefined, token),
48
+ post: (path, body, token) => request('POST', path, body, token),
49
+ patch: (path, body, token) => request('PATCH', path, body, token),
50
+ delete: (path, token) => request('DELETE', path, undefined, token),
51
+ };
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.getToken = getToken;
40
+ exports.setToken = setToken;
41
+ exports.deleteToken = deleteToken;
42
+ exports.requireAuth = requireAuth;
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const TOKEN_FILE = require('path').join(require('os').homedir(), '.recall', 'token');
45
+ async function getToken() {
46
+ try {
47
+ const keytar = await Promise.resolve().then(() => __importStar(require('keytar')));
48
+ return keytar.getPassword('recall', 'token');
49
+ }
50
+ catch {
51
+ // fallback to file if keytar not available
52
+ const fs = require('fs');
53
+ if (fs.existsSync(TOKEN_FILE))
54
+ return fs.readFileSync(TOKEN_FILE, 'utf-8').trim();
55
+ return null;
56
+ }
57
+ }
58
+ async function setToken(token) {
59
+ try {
60
+ const keytar = await Promise.resolve().then(() => __importStar(require('keytar')));
61
+ await keytar.setPassword('recall', 'token', token);
62
+ }
63
+ catch {
64
+ const fs = require('fs');
65
+ const path = require('path');
66
+ fs.mkdirSync(path.dirname(TOKEN_FILE), { recursive: true });
67
+ fs.writeFileSync(TOKEN_FILE, token, { mode: 0o600 });
68
+ }
69
+ }
70
+ async function deleteToken() {
71
+ try {
72
+ const keytar = await Promise.resolve().then(() => __importStar(require('keytar')));
73
+ await keytar.deletePassword('recall', 'token');
74
+ }
75
+ catch {
76
+ const fs = require('fs');
77
+ if (fs.existsSync(TOKEN_FILE))
78
+ fs.unlinkSync(TOKEN_FILE);
79
+ }
80
+ }
81
+ async function requireAuth() {
82
+ const token = await getToken();
83
+ if (!token) {
84
+ console.error(chalk_1.default.red('\n Not logged in. Run: recall auth login\n'));
85
+ process.exit(1);
86
+ }
87
+ return token;
88
+ }
@@ -0,0 +1,37 @@
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.ENVIRONMENTS = exports.APP_VERSION = void 0;
7
+ exports.getConfig = getConfig;
8
+ exports.saveConfig = saveConfig;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.recall');
13
+ const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
14
+ exports.APP_VERSION = "1.0.0";
15
+ exports.ENVIRONMENTS = {
16
+ production: 'https://api.devkitvault.com',
17
+ local: 'http://127.0.0.1:3001',
18
+ };
19
+ const DEFAULTS = {
20
+ apiUrl: exports.ENVIRONMENTS.production,
21
+ env: 'production',
22
+ };
23
+ function getConfig() {
24
+ if (!fs_1.default.existsSync(CONFIG_FILE))
25
+ return DEFAULTS;
26
+ try {
27
+ return { ...DEFAULTS, ...JSON.parse(fs_1.default.readFileSync(CONFIG_FILE, 'utf-8')) };
28
+ }
29
+ catch {
30
+ return DEFAULTS;
31
+ }
32
+ }
33
+ function saveConfig(data) {
34
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
35
+ const current = getConfig();
36
+ fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...data }, null, 2));
37
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devkitvault/recall",
3
- "version": "1.0.0",
4
- "description": "recall CLI installer — cloud-synced shell command vault (downloads native binary from GitHub releases)",
3
+ "version": "1.0.2",
4
+ "description": "recall CLI — cloud-synced command vault (JavaScript build for npm; no bundled native binary)",
5
5
  "keywords": [
6
6
  "cli",
7
7
  "shell",
@@ -27,10 +27,10 @@
27
27
  "directory": "packages/npm-cli"
28
28
  },
29
29
  "bin": {
30
- "recall": "bin/recall.js"
30
+ "recall": "dist/index.js"
31
31
  },
32
32
  "files": [
33
- "bin",
33
+ "dist",
34
34
  "README.md",
35
35
  "LICENSE"
36
36
  ],
@@ -41,9 +41,14 @@
41
41
  "access": "public"
42
42
  },
43
43
  "scripts": {
44
- "prepublishOnly": "node --check bin/recall.js"
44
+ "prepublishOnly": "node scripts/prepublish.cjs && node --check dist/index.js"
45
45
  },
46
- "recall": {
47
- "releaseTag": "v1.0.0"
46
+ "dependencies": {
47
+ "chalk": "^4.1.2",
48
+ "cli-table3": "^0.6.3",
49
+ "commander": "^12.0.0",
50
+ "inquirer": "^8.2.7",
51
+ "open": "^8.4.2",
52
+ "ora": "^5.4.1"
48
53
  }
49
54
  }
package/bin/recall.js DELETED
@@ -1,169 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * @devkitvault/recall — thin installer for the native recall CLI.
4
- * First run downloads the matching GitHub release binary into ~/.local/bin, then execs it.
5
- * @see https://github.com/devkitvault/recall/releases
6
- */
7
- 'use strict'
8
-
9
- const fs = require('fs')
10
- const path = require('path')
11
- const os = require('os')
12
- const https = require('https')
13
- const http = require('http')
14
- const { spawnSync } = require('child_process')
15
-
16
- const BINARY_MAP = {
17
- 'darwin-arm64': 'recall-macos-arm64',
18
- 'darwin-x64': 'recall-macos-x64',
19
- 'linux-x64': 'recall-linux-x64',
20
- 'win32-x64': 'recall-windows-x64.exe',
21
- }
22
-
23
- const USER_AGENT = 'devkitvault-recall-npm/1.0 (+https://www.npmjs.com/package/@devkitvault/recall)'
24
-
25
- function platformKey() {
26
- return `${process.platform}-${process.arch}`
27
- }
28
-
29
- function binaryAssetName() {
30
- const key = platformKey()
31
- const name = BINARY_MAP[key]
32
- if (!name) {
33
- console.error(
34
- `recall: no prebuilt binary for "${key}".\n` +
35
- `Supported: ${Object.keys(BINARY_MAP).join(', ')}\n` +
36
- 'Install from https://devkitvault.com or build from source (packages/cli).',
37
- )
38
- process.exit(1)
39
- }
40
- return name
41
- }
42
-
43
- function readPkg() {
44
- return require(path.join(__dirname, '..', 'package.json'))
45
- }
46
-
47
- function releaseTag(pkg) {
48
- if (process.env.RECALL_RELEASE_TAG) {
49
- const t = process.env.RECALL_RELEASE_TAG.trim()
50
- return t.startsWith('v') ? t : `v${t}`
51
- }
52
- const t = pkg.recall && pkg.recall.releaseTag
53
- if (typeof t === 'string' && t.length) return t.startsWith('v') ? t : `v${t}`
54
- return `v${pkg.version}`
55
- }
56
-
57
- function binPaths() {
58
- const binDir = path.join(os.homedir(), '.local', 'bin')
59
- const binName = process.platform === 'win32' ? 'recall.exe' : 'recall'
60
- return { binDir, binPath: path.join(binDir, binName) }
61
- }
62
-
63
- function httpGet(url, redirectsLeft) {
64
- return new Promise((resolve, reject) => {
65
- if (redirectsLeft <= 0) return reject(new Error('Too many redirects'))
66
- const lib = url.startsWith('https:') ? https : http
67
- const req = lib.get(
68
- url,
69
- {
70
- headers: { 'User-Agent': USER_AGENT },
71
- },
72
- (res) => {
73
- const loc = res.headers.location
74
- if (loc && res.statusCode >= 300 && res.statusCode < 400) {
75
- res.resume()
76
- const next = new URL(loc, url).href
77
- return httpGet(next, redirectsLeft - 1).then(resolve).catch(reject)
78
- }
79
- if (res.statusCode !== 200) {
80
- res.resume()
81
- return reject(new Error(`HTTP ${res.statusCode} GET ${url}`))
82
- }
83
- resolve(res)
84
- },
85
- )
86
- req.on('error', reject)
87
- })
88
- }
89
-
90
- function streamToFile(readable, destPath) {
91
- return fs.promises.mkdir(path.dirname(destPath), { recursive: true }).then(() => {
92
- const tmp = `${destPath}.${process.pid}.${Date.now()}.part`
93
- return new Promise((resolve, reject) => {
94
- const w = fs.createWriteStream(tmp, { mode: 0o644 })
95
- readable.pipe(w)
96
- w.on('finish', () => w.close((err) => (err ? reject(err) : resolve(tmp))))
97
- w.on('error', reject)
98
- readable.on('error', reject)
99
- }).then(async (tmpPath) => {
100
- try {
101
- await fs.promises.rename(tmpPath, destPath)
102
- } catch (e) {
103
- try {
104
- await fs.promises.unlink(tmpPath)
105
- } catch { /* ignore */ }
106
- throw e
107
- }
108
- })
109
- })
110
- }
111
-
112
- async function ensureBinary(binPath, tag, assetName) {
113
- const url = `https://github.com/devkitvault/recall/releases/download/${tag}/${assetName}`
114
- process.stderr.write(`recall: downloading ${assetName} (${tag}) …\n`)
115
- const res = await httpGet(url, 16)
116
- await streamToFile(res, binPath)
117
- if (process.platform !== 'win32') {
118
- await fs.promises.chmod(binPath, 0o755)
119
- }
120
- }
121
-
122
- async function main() {
123
- const pkg = readPkg()
124
- const tag = releaseTag(pkg)
125
- const assetName = binaryAssetName()
126
- const { binPath } = binPaths()
127
-
128
- const reinstall =
129
- process.env.RECALL_REINSTALL === '1' ||
130
- process.env.FORCE_RECALL_REINSTALL === '1'
131
-
132
- if (reinstall) {
133
- try {
134
- await fs.promises.unlink(binPath)
135
- } catch { /* missing is fine */ }
136
- }
137
-
138
- let exists = false
139
- try {
140
- const st = await fs.promises.stat(binPath)
141
- exists = st.isFile() && st.size > 0
142
- } catch {
143
- exists = false
144
- }
145
-
146
- if (!exists) {
147
- try {
148
- await ensureBinary(binPath, tag, assetName)
149
- } catch (err) {
150
- const msg = err instanceof Error ? err.message : String(err)
151
- console.error(`recall: install failed: ${msg}`)
152
- console.error(`Expected release assets at: https://github.com/devkitvault/recall/releases/tag/${tag}`)
153
- console.error('Tip: set RECALL_RELEASE_TAG=vX.Y.Z if your npm version differs from the CLI release tag.')
154
- process.exit(1)
155
- }
156
- }
157
-
158
- const result = spawnSync(binPath, process.argv.slice(2), { stdio: 'inherit' })
159
- if (result.error) {
160
- console.error(result.error.message || result.error)
161
- process.exit(1)
162
- }
163
- process.exit(result.status === null ? 1 : result.status)
164
- }
165
-
166
- main().catch((e) => {
167
- console.error(e instanceof Error ? e.message : e)
168
- process.exit(1)
169
- })