@any-sync/cli 0.2.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.
package/bin/auth.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { checkAuth } = require('../lib');
5
+
6
+ try {
7
+ const token = checkAuth();
8
+ process.stdout.write(token + '\n');
9
+ } catch (err) {
10
+ process.stderr.write(err.message + '\n');
11
+ process.exit(1);
12
+ }
package/bin/cli.js ADDED
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const pkg = require('../package.json');
7
+
8
+ const COMMANDS = {
9
+ pull: 'Pull files from GitHub',
10
+ push: 'Push local changes to GitHub',
11
+ status: 'Show sync status',
12
+ reset: 'Remove config and lockfile',
13
+ auth: 'Check GitHub authentication',
14
+ init: 'Create config file (use --preset for defaults)',
15
+ };
16
+
17
+ function usage() {
18
+ process.stdout.write(`any-sync v${pkg.version} — bidirectional GitHub file sync\n\n`);
19
+ process.stdout.write('Usage: any-sync <command> [options]\n\n');
20
+ process.stdout.write('Commands:\n');
21
+ for (const [cmd, desc] of Object.entries(COMMANDS)) {
22
+ process.stdout.write(` ${cmd.padEnd(10)} ${desc}\n`);
23
+ }
24
+ process.stdout.write('\nRun any-sync <command> --help for command-specific usage.\n');
25
+ }
26
+
27
+ const args = process.argv.slice(2);
28
+ const command = args[0];
29
+
30
+ if (!command || command === '--help' || command === '-h') {
31
+ usage();
32
+ process.exit(0);
33
+ }
34
+
35
+ if (command === '--version' || command === '-v') {
36
+ process.stdout.write(`${pkg.version}\n`);
37
+ process.exit(0);
38
+ }
39
+
40
+ if (!COMMANDS[command]) {
41
+ process.stderr.write(`Unknown command: ${command}\n`);
42
+ process.stderr.write('Run any-sync --help for available commands.\n');
43
+ process.exit(1);
44
+ }
45
+
46
+ const lib = require('../lib');
47
+ const cmdArgs = args.slice(1);
48
+
49
+ try {
50
+ switch (command) {
51
+ case 'pull': {
52
+ const configPath = cmdArgs[0];
53
+ const lockfilePath = cmdArgs[1] || '.any-sync.lock';
54
+ if (!configPath || cmdArgs.includes('--help')) {
55
+ process.stdout.write('Usage: any-sync pull <config-path> [lockfile-path]\n');
56
+ process.exit(cmdArgs.includes('--help') ? 0 : 1);
57
+ }
58
+ const result = lib.pull(configPath, lockfilePath);
59
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
60
+ break;
61
+ }
62
+
63
+ case 'push': {
64
+ const configPath = cmdArgs[0];
65
+ const lockfilePath = cmdArgs[1] || '.any-sync.lock';
66
+ if (!configPath || cmdArgs.includes('--help')) {
67
+ process.stdout.write('Usage: any-sync push <config-path> [lockfile-path]\n');
68
+ process.exit(cmdArgs.includes('--help') ? 0 : 1);
69
+ }
70
+ const result = lib.push(configPath, lockfilePath);
71
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
72
+ break;
73
+ }
74
+
75
+ case 'status': {
76
+ const configPath = cmdArgs[0];
77
+ const lockfilePath = cmdArgs[1] || '.any-sync.lock';
78
+ if (!configPath || cmdArgs.includes('--help')) {
79
+ process.stdout.write('Usage: any-sync status <config-path> [lockfile-path]\n');
80
+ process.exit(cmdArgs.includes('--help') ? 0 : 1);
81
+ }
82
+ const result = lib.status(configPath, lockfilePath);
83
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
84
+ break;
85
+ }
86
+
87
+ case 'reset': {
88
+ const configPath = cmdArgs[0];
89
+ const lockfilePath = cmdArgs[1] || '.any-sync.lock';
90
+ if (!configPath || cmdArgs.includes('--help')) {
91
+ process.stdout.write('Usage: any-sync reset <config-path> [lockfile-path]\n');
92
+ process.exit(cmdArgs.includes('--help') ? 0 : 1);
93
+ }
94
+ const result = lib.reset(configPath, lockfilePath);
95
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
96
+ break;
97
+ }
98
+
99
+ case 'auth': {
100
+ if (cmdArgs.includes('--help')) {
101
+ process.stdout.write('Usage: any-sync auth\n');
102
+ process.exit(0);
103
+ }
104
+ const token = lib.checkAuth();
105
+ process.stdout.write(token + '\n');
106
+ break;
107
+ }
108
+
109
+ case 'init': {
110
+ if (cmdArgs.includes('--help')) {
111
+ process.stdout.write(
112
+ 'Usage: any-sync init <config-path> <repo> [branch] [--preset claude|openclaw]\n',
113
+ );
114
+ process.exit(0);
115
+ }
116
+ // Parse --preset flag
117
+ const presetIdx = cmdArgs.indexOf('--preset');
118
+ let preset = null;
119
+ const positional = [];
120
+ for (let i = 0; i < cmdArgs.length; i++) {
121
+ if (cmdArgs[i] === '--preset') {
122
+ preset = cmdArgs[++i];
123
+ } else {
124
+ positional.push(cmdArgs[i]);
125
+ }
126
+ }
127
+ const configPath = positional[0];
128
+ const repo = positional[1];
129
+ const branch = positional[2] || 'main';
130
+ if (!configPath || !repo) {
131
+ process.stderr.write(
132
+ 'Usage: any-sync init <config-path> <repo> [branch] [--preset claude|openclaw]\n',
133
+ );
134
+ process.exit(1);
135
+ }
136
+ let mappings;
137
+ if (preset) {
138
+ mappings = lib.getPresetMappings(preset);
139
+ if (!mappings) {
140
+ process.stderr.write(`Unknown preset: ${preset}. Available: claude, openclaw\n`);
141
+ process.exit(1);
142
+ }
143
+ } else {
144
+ process.stderr.write('Error: --preset is required. Available: claude, openclaw\n');
145
+ process.exit(1);
146
+ }
147
+ const result = lib.init(configPath, repo, branch, mappings);
148
+ process.stdout.write(result + '\n');
149
+ break;
150
+ }
151
+ }
152
+ } catch (err) {
153
+ process.stderr.write('Error: ' + err.message + '\n');
154
+ process.exit(1);
155
+ }
package/bin/pull.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { pull } = require('../lib');
5
+
6
+ const configPath = process.argv[2];
7
+ const lockfilePath = process.argv[3] || '.any-sync.lock';
8
+
9
+ if (!configPath) {
10
+ process.stderr.write('Usage: pull.js <config-path> [lockfile-path]\n');
11
+ process.exit(1);
12
+ }
13
+
14
+ try {
15
+ const result = pull(configPath, lockfilePath);
16
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
17
+ } catch (err) {
18
+ process.stderr.write('Error: ' + err.message + '\n');
19
+ process.exit(1);
20
+ }
package/bin/push.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { push } = require('../lib');
5
+
6
+ const configPath = process.argv[2];
7
+ const lockfilePath = process.argv[3] || '.any-sync.lock';
8
+
9
+ if (!configPath) {
10
+ process.stderr.write('Usage: push.js <config-path> [lockfile-path]\n');
11
+ process.exit(1);
12
+ }
13
+
14
+ try {
15
+ const result = push(configPath, lockfilePath);
16
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
17
+ } catch (err) {
18
+ process.stderr.write('Error: ' + err.message + '\n');
19
+ process.exit(1);
20
+ }
package/bin/reset.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { reset } = require('../lib');
5
+
6
+ const configPath = process.argv[2];
7
+ const lockfilePath = process.argv[3] || '.any-sync.lock';
8
+
9
+ if (!configPath) {
10
+ process.stderr.write('Usage: reset.js <config-path> [lockfile-path]\n');
11
+ process.exit(1);
12
+ }
13
+
14
+ try {
15
+ const result = reset(configPath, lockfilePath);
16
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
17
+ } catch (err) {
18
+ process.stderr.write('Error: ' + err.message + '\n');
19
+ process.exit(1);
20
+ }
package/bin/status.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { status } = require('../lib');
5
+
6
+ const configPath = process.argv[2];
7
+ const lockfilePath = process.argv[3] || '.any-sync.lock';
8
+
9
+ if (!configPath) {
10
+ process.stderr.write('Usage: status.js <config-path> [lockfile-path]\n');
11
+ process.exit(1);
12
+ }
13
+
14
+ try {
15
+ const result = status(configPath, lockfilePath);
16
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
17
+ } catch (err) {
18
+ process.stderr.write('Error: ' + err.message + '\n');
19
+ process.exit(1);
20
+ }
package/lib/auth.js ADDED
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+
5
+ /**
6
+ * Check GitHub authentication.
7
+ * Returns the auth token string.
8
+ * Throws if no auth available.
9
+ */
10
+ function checkAuth() {
11
+ // 1. Check GITHUB_TOKEN env var
12
+ if (process.env.GITHUB_TOKEN) {
13
+ return process.env.GITHUB_TOKEN;
14
+ }
15
+
16
+ // 2. Try gh CLI auth
17
+ try {
18
+ const token = execFileSync('gh', ['auth', 'token'], {
19
+ encoding: 'utf8',
20
+ stdio: ['pipe', 'pipe', 'pipe'],
21
+ }).trim();
22
+ if (token) return token;
23
+ } catch {
24
+ // Fall through
25
+ }
26
+
27
+ // 3. No auth available
28
+ throw new Error(
29
+ 'No GitHub authentication found.\n\n' +
30
+ 'Set up authentication using one of:\n' +
31
+ ' 1. Set GITHUB_TOKEN environment variable\n' +
32
+ " 2. Run 'gh auth login' to authenticate with GitHub CLI"
33
+ );
34
+ }
35
+
36
+ module.exports = { checkAuth };
package/lib/config.js ADDED
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ /**
8
+ * Load and validate a config file.
9
+ * Returns the parsed config object.
10
+ */
11
+ function loadConfig(configPath) {
12
+ if (!fs.existsSync(configPath)) {
13
+ throw new Error('Config file not found: ' + configPath);
14
+ }
15
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
16
+ if (!Array.isArray(config.mappings)) {
17
+ throw new Error('Invalid config: missing "mappings" array');
18
+ }
19
+ return config;
20
+ }
21
+
22
+ /**
23
+ * Find config file. Checks $HOME/.any-sync.json first, then cwd.
24
+ * Returns the path or null.
25
+ */
26
+ function findConfig() {
27
+ const home = path.join(os.homedir(), '.any-sync.json');
28
+ if (fs.existsSync(home)) return home;
29
+ const local = path.resolve('.any-sync.json');
30
+ if (fs.existsSync(local)) return local;
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Expand ~ to home directory in a path.
36
+ */
37
+ function expandTilde(p) {
38
+ if (p.startsWith('~/') || p === '~') {
39
+ return path.join(os.homedir(), p.slice(1));
40
+ }
41
+ return p;
42
+ }
43
+
44
+ /**
45
+ * Parse a mapping entry, applying defaults and expanding paths.
46
+ */
47
+ function parseMapping(m) {
48
+ return {
49
+ name: m.name,
50
+ repo: m.repo,
51
+ branch: m.branch || 'main',
52
+ sourcePath: (m.sourcePath || '').replace(/^\/+|\/+$/g, ''),
53
+ destPath: expandTilde(m.destPath),
54
+ include: m.include || [],
55
+ exclude: m.exclude || [],
56
+ };
57
+ }
58
+
59
+ module.exports = { loadConfig, findConfig, expandTilde, parseMapping };
package/lib/gh.js ADDED
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+
5
+ /**
6
+ * Call gh api with the given arguments.
7
+ * For POST/PATCH with a body, pass the body string as opts.input.
8
+ */
9
+ function ghApi(args, opts = {}) {
10
+ const options = { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 };
11
+ if (opts.input) {
12
+ options.input = opts.input;
13
+ }
14
+ return execFileSync('gh', ['api', ...args], options).trim();
15
+ }
16
+
17
+ /**
18
+ * Retry gh api on 5xx/network errors with exponential backoff.
19
+ */
20
+ function ghApiRetry(args, opts = {}) {
21
+ const maxAttempts = opts.maxAttempts || 3;
22
+ let backoff = 1000;
23
+
24
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
25
+ try {
26
+ return ghApi(args, opts);
27
+ } catch (err) {
28
+ const msg = (err.stderr || err.message || '').toString();
29
+ const isRetryable = /50[0234]|connect|timeout|network/i.test(msg);
30
+ if (!isRetryable || attempt === maxAttempts - 1) {
31
+ throw err;
32
+ }
33
+ // Synchronous sleep via Atomics
34
+ const buf = new SharedArrayBuffer(4);
35
+ Atomics.wait(new Int32Array(buf), 0, 0, backoff);
36
+ backoff *= 2;
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Get GitHub auth token from GITHUB_TOKEN env or gh auth token.
43
+ */
44
+ function getAuthToken() {
45
+ if (process.env.GITHUB_TOKEN) {
46
+ return process.env.GITHUB_TOKEN;
47
+ }
48
+ try {
49
+ return execFileSync('gh', ['auth', 'token'], { encoding: 'utf8' }).trim();
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ module.exports = { ghApi, ghApiRetry, getAuthToken };
package/lib/glob.js ADDED
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Match a file path against a glob pattern.
5
+ * Supports: * (single segment), ** (multi-segment), ? (single char)
6
+ */
7
+ function globMatch(pattern, filePath) {
8
+ const re = globToRegExp(pattern);
9
+ return re.test(filePath);
10
+ }
11
+
12
+ /**
13
+ * Convert a glob pattern to a RegExp.
14
+ */
15
+ function globToRegExp(pattern) {
16
+ let re = '';
17
+ let i = 0;
18
+ while (i < pattern.length) {
19
+ const c = pattern[i];
20
+ if (c === '*') {
21
+ if (pattern[i + 1] === '*') {
22
+ // ** — match zero or more path segments
23
+ i += 2;
24
+ if (pattern[i] === '/') {
25
+ i++; // consume trailing slash in **/
26
+ re += '(?:.+/)?';
27
+ } else {
28
+ re += '.*';
29
+ }
30
+ } else {
31
+ // * — match within a single path segment
32
+ re += '[^/]*';
33
+ i++;
34
+ }
35
+ } else if (c === '?') {
36
+ re += '[^/]';
37
+ i++;
38
+ } else if (c === '.') {
39
+ re += '\\.';
40
+ i++;
41
+ } else if (c === '(' || c === ')' || c === '{' || c === '}' || c === '+' || c === '^' || c === '$' || c === '|' || c === '\\') {
42
+ re += '\\' + c;
43
+ i++;
44
+ } else {
45
+ re += c;
46
+ i++;
47
+ }
48
+ }
49
+ return new RegExp('^' + re + '$');
50
+ }
51
+
52
+ /**
53
+ * Check if a file path matches any pattern in the list.
54
+ */
55
+ function matchesAny(patterns, filePath) {
56
+ return patterns.some(p => globMatch(p, filePath));
57
+ }
58
+
59
+ module.exports = { globMatch, matchesAny };
package/lib/hooks.js ADDED
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const { findConfig } = require('./config');
4
+ const { getAuthToken } = require('./gh');
5
+ const { pull } = require('./pull');
6
+ const { push } = require('./push');
7
+ const { status } = require('./status');
8
+
9
+ /**
10
+ * Auto-pull if config and auth are available.
11
+ * Returns the pull result, or null if skipped.
12
+ */
13
+ function autoPull(lockfilePath) {
14
+ lockfilePath = lockfilePath || '.any-sync.lock';
15
+
16
+ const configPath = findConfig();
17
+ if (!configPath) return null;
18
+
19
+ const token = getAuthToken();
20
+ if (!token) return null;
21
+
22
+ return pull(configPath, lockfilePath);
23
+ }
24
+
25
+ /**
26
+ * Auto-push if config and auth are available and there are changes.
27
+ * Returns the push result, or null if skipped.
28
+ */
29
+ function autoPush(lockfilePath) {
30
+ lockfilePath = lockfilePath || '.any-sync.lock';
31
+
32
+ const configPath = findConfig();
33
+ if (!configPath) return null;
34
+
35
+ const token = getAuthToken();
36
+ if (!token) return null;
37
+
38
+ const statusResult = status(configPath, lockfilePath);
39
+ const hasChanges = (statusResult.mappings || []).some(m => (m.changes || []).length > 0);
40
+ if (!hasChanges) return null;
41
+
42
+ return push(configPath, lockfilePath);
43
+ }
44
+
45
+ module.exports = { autoPull, autoPush };
package/lib/index.d.ts ADDED
@@ -0,0 +1,96 @@
1
+ export declare class Lockfile {
2
+ static load(filePath: string): Lockfile;
3
+ save(): void;
4
+ getEntry(key: string): { remoteSha: string; localHash: string; syncedAt: string } | null;
5
+ setEntry(key: string, remoteSha: string, localHash: string): void;
6
+ getEntriesForMapping(name: string): Record<string, { remoteSha: string; localHash: string; syncedAt: string }>;
7
+ setLastSync(name: string): void;
8
+ getLastSync(name: string): string | null;
9
+ }
10
+
11
+ export declare function makeKey(mapping: string, relpath: string): string;
12
+ export declare function hashFile(filePath: string): string;
13
+
14
+ export declare function ghApi(args: string[], opts?: { input?: string }): string;
15
+ export declare function ghApiRetry(args: string[], opts?: { maxAttempts?: number; input?: string }): string;
16
+ export declare function getAuthToken(): string | null;
17
+
18
+ export declare function globMatch(pattern: string, filePath: string): boolean;
19
+ export declare function matchesAny(patterns: string[], filePath: string): boolean;
20
+
21
+ export declare function loadConfig(configPath: string): { mappings: Array<Record<string, unknown>> };
22
+ export declare function findConfig(): string | null;
23
+ export declare function expandTilde(p: string): string;
24
+ export declare function parseMapping(m: Record<string, unknown>): {
25
+ name: string;
26
+ repo: string;
27
+ branch: string;
28
+ sourcePath: string;
29
+ destPath: string;
30
+ include: string[];
31
+ exclude: string[];
32
+ };
33
+
34
+ export declare function checkAuth(): string;
35
+
36
+ export declare function pull(configPath: string, lockfilePath: string): {
37
+ pulled: string[];
38
+ conflicts: string[];
39
+ skipped: number;
40
+ };
41
+
42
+ export declare function push(configPath: string, lockfilePath: string): {
43
+ pushed: string[];
44
+ branch: string;
45
+ };
46
+
47
+ export declare function status(configPath: string, lockfilePath: string): {
48
+ auth: { method: string; user: string | null };
49
+ config: { path: string; valid: boolean };
50
+ mappings: Array<{
51
+ name: string;
52
+ repo: string;
53
+ branch: string;
54
+ lastSync: string | null;
55
+ tracked: number;
56
+ changes: Array<{ file: string; type: string }>;
57
+ }>;
58
+ };
59
+
60
+ export declare function reset(configPath: string, lockfilePath: string): {
61
+ deletedConfig: boolean;
62
+ configPath: string;
63
+ deletedLockfile: boolean;
64
+ lockfilePath: string;
65
+ };
66
+
67
+ export declare function init(
68
+ configPath: string,
69
+ repo: string,
70
+ branch: string,
71
+ mappings: Array<{
72
+ name: string;
73
+ sourcePath: string;
74
+ destPath: string;
75
+ include?: string[];
76
+ exclude?: string[];
77
+ }>,
78
+ ): string;
79
+
80
+ export declare function getPresetMappings(
81
+ preset: string,
82
+ ): Array<{
83
+ name: string;
84
+ sourcePath: string;
85
+ destPath: string;
86
+ include?: string[];
87
+ exclude?: string[];
88
+ }> | null;
89
+
90
+ export declare function autoPull(
91
+ lockfilePath?: string,
92
+ ): { pulled: string[]; conflicts: string[]; skipped: number } | null;
93
+
94
+ export declare function autoPush(
95
+ lockfilePath?: string,
96
+ ): { pushed: string[]; branch: string } | null;
package/lib/index.js ADDED
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const { Lockfile, makeKey, hashFile } = require('./lockfile');
4
+ const { ghApi, ghApiRetry, getAuthToken } = require('./gh');
5
+ const { globMatch, matchesAny } = require('./glob');
6
+ const { loadConfig, findConfig, expandTilde, parseMapping } = require('./config');
7
+ const { checkAuth } = require('./auth');
8
+ const { pull } = require('./pull');
9
+ const { push } = require('./push');
10
+ const { status } = require('./status');
11
+ const { reset } = require('./reset');
12
+ const { init, getPresetMappings } = require('./init');
13
+ const { autoPull, autoPush } = require('./hooks');
14
+
15
+ module.exports = {
16
+ // Lockfile
17
+ Lockfile,
18
+ makeKey,
19
+ hashFile,
20
+ // GitHub
21
+ ghApi,
22
+ ghApiRetry,
23
+ getAuthToken,
24
+ // Glob
25
+ globMatch,
26
+ matchesAny,
27
+ // Config
28
+ loadConfig,
29
+ findConfig,
30
+ expandTilde,
31
+ parseMapping,
32
+ // Auth
33
+ checkAuth,
34
+ // Operations
35
+ pull,
36
+ push,
37
+ status,
38
+ reset,
39
+ init,
40
+ // Presets & hooks
41
+ getPresetMappings,
42
+ autoPull,
43
+ autoPush,
44
+ };