@magentrix-corp/magentrix-cli 1.0.0

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 (43) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +471 -0
  3. package/actions/autopublish.js +283 -0
  4. package/actions/autopublish.old.js +293 -0
  5. package/actions/autopublish.v2.js +447 -0
  6. package/actions/create.js +329 -0
  7. package/actions/help.js +165 -0
  8. package/actions/main.js +81 -0
  9. package/actions/publish.js +567 -0
  10. package/actions/pull.js +139 -0
  11. package/actions/setup.js +61 -0
  12. package/actions/status.js +17 -0
  13. package/bin/magentrix.js +159 -0
  14. package/package.json +61 -0
  15. package/utils/cacher.js +112 -0
  16. package/utils/cli/checkInstanceUrl.js +29 -0
  17. package/utils/cli/helpers/compare.js +281 -0
  18. package/utils/cli/helpers/ensureApiKey.js +57 -0
  19. package/utils/cli/helpers/ensureCredentials.js +60 -0
  20. package/utils/cli/helpers/ensureInstanceUrl.js +63 -0
  21. package/utils/cli/writeRecords.js +223 -0
  22. package/utils/compare.js +135 -0
  23. package/utils/compress.js +18 -0
  24. package/utils/config.js +451 -0
  25. package/utils/diff.js +49 -0
  26. package/utils/downloadAssets.js +75 -0
  27. package/utils/filetag.js +115 -0
  28. package/utils/hash.js +14 -0
  29. package/utils/magentrix/api/assets.js +145 -0
  30. package/utils/magentrix/api/auth.js +56 -0
  31. package/utils/magentrix/api/createEntity.js +61 -0
  32. package/utils/magentrix/api/deleteEntity.js +55 -0
  33. package/utils/magentrix/api/meqlQuery.js +31 -0
  34. package/utils/magentrix/api/retrieveEntity.js +32 -0
  35. package/utils/magentrix/api/updateEntity.js +66 -0
  36. package/utils/magentrix/fetch.js +154 -0
  37. package/utils/merge.js +22 -0
  38. package/utils/preferences.js +40 -0
  39. package/utils/spinner.js +43 -0
  40. package/utils/template.js +52 -0
  41. package/utils/updateFileBase.js +103 -0
  42. package/vars/config.js +1 -0
  43. package/vars/global.js +33 -0
@@ -0,0 +1,61 @@
1
+ import { ensureApiKey } from "../utils/cli/helpers/ensureApiKey.js";
2
+ import { ensureInstanceUrl } from "../utils/cli/helpers/ensureInstanceUrl.js";
3
+ import Config from "../utils/config.js";
4
+ import { getAccessToken, tryAuthenticate } from "../utils/magentrix/api/auth.js";
5
+ import { ensureVSCodeFileAssociation } from "../utils/preferences.js";
6
+ import { EXPORT_ROOT, HASHED_CWD } from "../vars/global.js";
7
+
8
+ const config = new Config();
9
+
10
+ /**
11
+ * Runs the global setup for the Magentrix CLI.
12
+ *
13
+ * Prompts the user for their API key and instance URL if needed,
14
+ * validates credentials by attempting authentication,
15
+ * and saves them to the global config if successful.
16
+ *
17
+ * @async
18
+ * @param {boolean} [forceNewData=false] - If true, always prompt for new data, even if values exist.
19
+ * @returns {Promise<{apiKey: string, instanceUrl: string}>} The saved API key and instance URL.
20
+ * @throws {Error} If authentication fails with provided credentials.
21
+ */
22
+ export const setup = async () => {
23
+ // Prompt for API key and Instance URL if missing or if forceNewData is true
24
+ const apiKey = await ensureApiKey(true);
25
+ const instanceUrl = await ensureInstanceUrl(true);
26
+
27
+ // Validate credentials by attempting to fetch an access token
28
+ const tokenData = await tryAuthenticate(apiKey, instanceUrl);
29
+
30
+ console.log(); // Blank line for spacing
31
+
32
+ // Save values since authentication succeeded
33
+ config.save('instanceUrl', instanceUrl, { global: true, pathHash: HASHED_CWD });
34
+ console.log('✅ Instance URL saved securely!');
35
+
36
+ config.save('apiKey', apiKey, { global: true, pathHash: HASHED_CWD });
37
+ console.log('✅ API key saved securely!');
38
+
39
+ config.save(
40
+ 'token',
41
+ { value: tokenData.token, validUntil: tokenData.validUntil },
42
+ { global: true, pathHash: HASHED_CWD }
43
+ );
44
+
45
+ // Set up the editor
46
+ await ensureVSCodeFileAssociation('./');
47
+
48
+ console.log(); // Blank line for spacing
49
+
50
+ console.log('🎉 Setup complete.');
51
+
52
+ return {
53
+ apiKey,
54
+ instanceUrl,
55
+ token: {
56
+ value: tokenData.token,
57
+ validUntil: tokenData.validUntil
58
+ }
59
+ }
60
+
61
+ };
@@ -0,0 +1,17 @@
1
+ import { showCurrentConflicts } from "../utils/cli/helpers/compare.js";
2
+ import { ensureValidCredentials } from "../utils/cli/helpers/ensureCredentials.js";
3
+ import { withSpinner } from "../utils/spinner.js";
4
+ import { EXPORT_ROOT } from "../vars/global.js";
5
+
6
+ export const status = async () => {
7
+ // Clear the terminal
8
+ process.stdout.write('\x1Bc');
9
+
10
+ const credentials = await withSpinner('Authenticating...', async () => {
11
+ return await ensureValidCredentials();
12
+ });
13
+
14
+ console.log();
15
+
16
+ await showCurrentConflicts(EXPORT_ROOT, credentials.instanceUrl, credentials.token.value, true)
17
+ };
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Imports
4
+ import { Command } from 'commander';
5
+ import chalk from 'chalk';
6
+ import { VERSION } from '../vars/config.js';
7
+ import { setup } from '../actions/setup.js';
8
+ import { main } from '../actions/main.js';
9
+ import { pull } from '../actions/pull.js';
10
+ import { create } from '../actions/create.js';
11
+ import { autoPublish } from '../actions/autopublish.js';
12
+ import { status } from '../actions/status.js';
13
+ import { cacheDir, recacheFileIdIndex } from '../utils/cacher.js';
14
+ import { EXPORT_ROOT } from '../vars/global.js';
15
+ import { publish } from '../actions/publish.js';
16
+
17
+ // ── Middleware ────────────────────────────────
18
+ async function preMiddleware() {
19
+ await recacheFileIdIndex(EXPORT_ROOT);
20
+ await cacheDir(EXPORT_ROOT);
21
+ }
22
+ async function postMiddleware() {
23
+ await cacheDir(EXPORT_ROOT);
24
+ }
25
+
26
+ const withMiddleware = ({ pre, post }) => (fn) => async (...args) => {
27
+ if (pre) await pre(...args);
28
+ await fn(...args);
29
+ if (post) await post(...args);
30
+ };
31
+
32
+ // ── CLI Setup ────────────────────────────────
33
+ const program = new Command();
34
+ program
35
+ .name('magentrix')
36
+ .description('Manage Magentrix assets and automation')
37
+ .version(VERSION)
38
+ .configureHelp({
39
+ formatHelp: (cmd, helper) => {
40
+ const divider = chalk.gray('━'.repeat(60));
41
+ const titleBar = chalk.bold.bgBlue.white(' Magentrix CLI ');
42
+ const version = chalk.dim(`v${VERSION}`);
43
+
44
+ let help = `\n${divider}\n${titleBar} ${version}\n${divider}\n\n`;
45
+ help += `${chalk.dim('Manage Magentrix assets and automation')}\n\n`;
46
+
47
+ // Usage section
48
+ help += `${chalk.bold.yellow('USAGE')}\n`;
49
+ help += ` ${chalk.cyan('magentrix')} ${chalk.dim('<command> [options]')}\n\n`;
50
+
51
+ // Commands section
52
+ help += `${chalk.bold.yellow('COMMANDS')}\n`;
53
+ const commands = [
54
+ { name: 'setup', desc: 'Configure your Magentrix API key', icon: '⚙️ ' },
55
+ { name: 'pull', desc: 'Pull files from the remote server', icon: '📥 ' },
56
+ { name: 'create', desc: 'Create files locally', icon: '✨ ' },
57
+ { name: 'status', desc: 'Show file conflicts and sync status', icon: '📊 ' },
58
+ { name: 'publish', desc: 'Publish pending changes to the remote server', icon: '📤 ' },
59
+ { name: 'autopublish', desc: 'Watch & sync changes in real time', icon: '🔄 ' }
60
+ ];
61
+
62
+ const maxNameLen = Math.max(...commands.map(c => c.name.length));
63
+ commands.forEach(cmd => {
64
+ const padding = ' '.repeat(maxNameLen - cmd.name.length);
65
+ help += ` ${cmd.icon}${chalk.cyan.bold(cmd.name)}${padding} ${chalk.dim(cmd.desc)}\n`;
66
+ });
67
+
68
+ help += `\n${chalk.bold.yellow('OPTIONS')}\n`;
69
+ help += ` ${chalk.cyan('-V, --version')} ${chalk.dim('Output the version number')}\n`;
70
+ help += ` ${chalk.cyan('-h, --help')} ${chalk.dim('Display this help message')}\n`;
71
+
72
+ help += `\n${chalk.bold.yellow('EXAMPLES')}\n`;
73
+ help += ` ${chalk.dim('# Initial setup')}\n`;
74
+ help += ` ${chalk.cyan('magentrix setup')}\n\n`;
75
+ help += ` ${chalk.dim('# Pull remote files')}\n`;
76
+ help += ` ${chalk.cyan('magentrix pull')}\n\n`;
77
+ help += ` ${chalk.dim('# Auto-sync on file changes')}\n`;
78
+ help += ` ${chalk.cyan('magentrix autopublish')}\n`;
79
+
80
+ help += `\n${divider}\n`;
81
+
82
+ return help;
83
+ }
84
+ });
85
+
86
+ const withDefault = withMiddleware({ pre: preMiddleware, post: postMiddleware });
87
+
88
+ // ── Error Handlers ───────────────────────────
89
+ program.showHelpAfterError(false);
90
+ program.configureOutput({
91
+ outputError: (str, write) => {
92
+ // Custom error message for unknown options
93
+ if (str.includes('unknown option')) {
94
+ const match = str.match(/'([^']+)'/);
95
+ const option = match ? match[1] : str;
96
+
97
+ console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('Unknown option:')} ${chalk.bold(option)}`);
98
+ console.error(`${chalk.yellow('💡 Tip:')} Run ${chalk.cyan('magentrix --help')} to see available options.\n`);
99
+ }
100
+ // Custom error message for unknown commands
101
+ else if (str.includes('unknown command')) {
102
+ const match = str.match(/'([^']+)'/);
103
+ const command = match ? match[1] : str;
104
+
105
+ console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright('Unknown command:')} ${chalk.bold(command)}`);
106
+ console.error(`${chalk.yellow('💡 Tip:')} Run ${chalk.cyan('magentrix --help')} to see available commands.\n`);
107
+ }
108
+ // Generic errors
109
+ else {
110
+ console.error(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright(str.trim())}\n`);
111
+ }
112
+ }
113
+ });
114
+
115
+ // ── Commands ─────────────────────────────────
116
+ program.command('setup').description('Configure your Magentrix API key').action(withDefault(setup));
117
+ program.command('pull').description('Pull files from the remote server').action(withDefault(pull));
118
+ program.command('create').description('Create files locally').action(withDefault(create));
119
+ program.command('status').description('Show file conflicts').action(withDefault(status));
120
+ program.command('autopublish').description('Watch & sync changes in real time').action(withDefault(autoPublish));
121
+ program.command('publish').description('Publish pending changes to the remote server').action(withDefault(publish));
122
+
123
+ // ── Unknown Command Handler ──────────────────
124
+ program.argument('[command]', 'command to run').action((cmd) => {
125
+ const runMain = async () => {
126
+ await preMiddleware();
127
+ await main();
128
+ await postMiddleware();
129
+ };
130
+
131
+ if (cmd) {
132
+ console.log(`\n${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright(`Unknown command:`)} ${chalk.bold(cmd)}\n`);
133
+ console.log(`${chalk.yellow('💡 Tip:')} Run ${chalk.cyan('magentrix --help')} to view available commands.\n`);
134
+ process.exit(1);
135
+ } else {
136
+ runMain().catch(handleFatal);
137
+ }
138
+ });
139
+
140
+ // ── Global Error Handler ─────────────────────
141
+ function handleFatal(err) {
142
+ const divider = chalk.gray('──────────────────────────────────────────────');
143
+ const header = `${chalk.bgRed.white.bold(' FATAL ERROR ')}`;
144
+
145
+ console.error(`\n${divider}\n${header}`);
146
+ console.error(`${chalk.redBright(err?.message || 'An unexpected error occurred.')}\n`);
147
+
148
+ if (process.env.DEBUG === 'true' && err?.stack) {
149
+ console.error(chalk.dim(err.stack));
150
+ console.error();
151
+ } else {
152
+ console.log(`${chalk.yellow('💡 Run with')} ${chalk.cyan('DEBUG=true')} ${chalk.yellow('for full details.')}`);
153
+ }
154
+
155
+ console.log(divider + '\n');
156
+ process.exit(1);
157
+ }
158
+
159
+ program.parseAsync(process.argv).catch(handleFatal);
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@magentrix-corp/magentrix-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool for synchronizing local files with Magentrix cloud platform",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "magentrix": "./bin/magentrix.js"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": [
14
+ "magentrix",
15
+ "cli",
16
+ "sync",
17
+ "cloud",
18
+ "automation",
19
+ "development-tools"
20
+ ],
21
+ "author": "Magentrix Corporation",
22
+ "license": "UNLICENSED",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/Mangoz1x/MagentrixCLI.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/Mangoz1x/MagentrixCLI/issues"
29
+ },
30
+ "homepage": "https://github.com/Mangoz1x/MagentrixCLI#readme",
31
+ "engines": {
32
+ "node": ">=20.0.0"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "files": [
38
+ "bin/",
39
+ "actions/",
40
+ "utils/",
41
+ "vars/",
42
+ "README.md",
43
+ "LICENSE"
44
+ ],
45
+ "dependencies": {
46
+ "@inquirer/prompts": "^7.6.0",
47
+ "chalk": "^5.4.1",
48
+ "chokidar": "^4.0.3",
49
+ "commander": "^14.0.0",
50
+ "diff": "^8.0.2",
51
+ "extract-zip": "^2.0.1",
52
+ "fuzzy": "^0.1.3",
53
+ "inquirer": "^12.7.0",
54
+ "node-diff3": "^3.1.2",
55
+ "ora": "^8.2.0",
56
+ "pako": "^2.1.0",
57
+ "prompts": "^2.4.2",
58
+ "readline-sync": "^1.4.10",
59
+ "uuid": "^11.1.0"
60
+ }
61
+ }
@@ -0,0 +1,112 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import Config from './config.js';
4
+ import { findFileByTag, getFileTag, isPathLinkedToTagByLastKnownPath, setFileTag } from './filetag.js';
5
+ import { compressString } from './compress.js';
6
+ import { sha256 } from './hash.js';
7
+ import { EXPORT_ROOT } from '../vars/global.js';
8
+
9
+ const config = new Config();
10
+
11
+ /**
12
+ * Recursively caches all files in a directory with tagging and content snapshotting
13
+ * @param {string} dir - Directory to cache
14
+ * @returns {Promise<void>}
15
+ */
16
+ export const cacheDir = async (dir) => {
17
+ if (!fs.existsSync(dir)) return;
18
+
19
+ const absDir = path.resolve(dir);
20
+ const files = await walkFiles(absDir);
21
+
22
+ const cache = config.read('cachedFiles', { global: false, filename: 'fileCache.json' }) || {};
23
+
24
+ for (const file of files) {
25
+ const stats = fs.statSync(file);
26
+ if (!stats.isFile()) continue;
27
+
28
+ const checkFileTag = async (retry = true) => {
29
+ // Check file tag
30
+ const tag = await getFileTag(file);
31
+
32
+ if (!tag) {
33
+ // Try to repair if there is a tag linked to that path
34
+ const dirLinkedTag = isPathLinkedToTagByLastKnownPath(file);
35
+ if (dirLinkedTag && retry) {
36
+ await setFileTag(file, dirLinkedTag);
37
+ return await checkFileTag(false);
38
+ }
39
+ return null;
40
+ }
41
+
42
+ return tag;
43
+ }
44
+
45
+ const tag = await checkFileTag();
46
+ if (!tag) {
47
+ // There may not be a tag if the user manually created a file
48
+ // console.warn(`Warning: failed to tag file: ${file}`);
49
+ continue;
50
+ }
51
+
52
+ const content = fs.readFileSync(file, 'utf8');
53
+ const objectId = tag; // Use the tag so we don't get duplicates
54
+
55
+ cache[objectId] = {
56
+ tag,
57
+ lastKnownPath: file,
58
+ contentHash: sha256(content),
59
+ compressedContent: compressString(content),
60
+ size: stats.size,
61
+ mtimeMs: stats.mtimeMs,
62
+ dev: stats.dev,
63
+ ino: stats.ino
64
+ };
65
+ }
66
+
67
+ config.save('cachedFiles', cache, { global: false, filename: 'fileCache.json' });
68
+ };
69
+
70
+ export const recacheFileIdIndex = async (dir) => {
71
+ const files = await walkFiles(dir);
72
+ if (!files || files?.length < 1) return;
73
+
74
+ for (const file of files) {
75
+ const tag = await getFileTag(file);
76
+ if (!tag) continue;
77
+
78
+ // Update the index id cache
79
+ const lastKnownPath = findFileByTag(tag);
80
+ if (lastKnownPath !== file) {
81
+ await setFileTag(file, tag);
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Recursively walks a directory and returns all file paths
88
+ * @param {string} dir
89
+ * @returns {Promise<string[]>}
90
+ */
91
+ export async function walkFiles(dir, settings) {
92
+ const ignore = settings?.ignore || [];
93
+
94
+ if (!fs.existsSync(dir)) return;
95
+ let entries = fs.readdirSync(dir, { withFileTypes: true });
96
+ let paths = [];
97
+
98
+ for (const entry of entries) {
99
+ if (ignore.find(p => entry.path.startsWith(p) || entry.path === p)) {
100
+ continue;
101
+ }
102
+
103
+ const fullPath = path.join(dir, entry.name);
104
+ if (entry.isDirectory()) {
105
+ paths.push(...await walkFiles(fullPath, settings));
106
+ } else if (entry.isFile()) {
107
+ paths.push(fullPath);
108
+ }
109
+ }
110
+
111
+ return paths;
112
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Checks if the provided Magentrix instance URL is reachable by making a GET request.
3
+ * Throws an error if the response status is not 200 (OK), or if there is a network error.
4
+ *
5
+ * @async
6
+ * @param {string} instanceUrl - The https://subdomain.magentrixcloud.com URL to check.
7
+ * @throws {Error} Throws if the instance is unreachable or does not return 200 OK.
8
+ * @returns {Promise<void>} Resolves if the instance is reachable (status 200); otherwise throws.
9
+ */
10
+ export const checkInstanceUrl = async (instanceUrl) => {
11
+ try {
12
+ // Native fetch is available in Node 18+; for older versions, use node-fetch.
13
+ const response = await fetch(instanceUrl, {
14
+ method: "GET",
15
+ // You can add a small timeout here using AbortController if needed.
16
+ });
17
+
18
+ if (response.status !== 200) {
19
+ throw new Error(
20
+ `Instance URL responded with status ${response.status} (${response.statusText}). Expected 200 OK.`
21
+ );
22
+ }
23
+ } catch (err) {
24
+ // Wrap and re-throw to provide a clear error message.
25
+ throw new Error(
26
+ `Failed to reach instance URL "${instanceUrl}": ${err.message || err}`
27
+ );
28
+ }
29
+ };