@any-sync/cli 0.2.0 → 0.2.5

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/cli.js CHANGED
@@ -6,12 +6,15 @@ const path = require('path');
6
6
  const pkg = require('../package.json');
7
7
 
8
8
  const COMMANDS = {
9
+ onboard: 'Interactive setup wizard',
9
10
  pull: 'Pull files from GitHub',
10
11
  push: 'Push local changes to GitHub',
11
12
  status: 'Show sync status',
12
13
  reset: 'Remove config and lockfile',
13
14
  auth: 'Check GitHub authentication',
14
15
  init: 'Create config file (use --preset for defaults)',
16
+ 'update-config': 'Update config mappings (add include patterns)',
17
+ help: 'Show detailed command help',
15
18
  };
16
19
 
17
20
  function usage() {
@@ -19,7 +22,7 @@ function usage() {
19
22
  process.stdout.write('Usage: any-sync <command> [options]\n\n');
20
23
  process.stdout.write('Commands:\n');
21
24
  for (const [cmd, desc] of Object.entries(COMMANDS)) {
22
- process.stdout.write(` ${cmd.padEnd(10)} ${desc}\n`);
25
+ process.stdout.write(` ${cmd.padEnd(16)} ${desc}\n`);
23
26
  }
24
27
  process.stdout.write('\nRun any-sync <command> --help for command-specific usage.\n');
25
28
  }
@@ -46,51 +49,59 @@ if (!COMMANDS[command]) {
46
49
  const lib = require('../lib');
47
50
  const cmdArgs = args.slice(1);
48
51
 
52
+ function resolveConfigAndLock(cmdArgs) {
53
+ const configPath = cmdArgs[0] || lib.findConfig();
54
+ if (!configPath) {
55
+ process.stderr.write(
56
+ 'No config found. Run "any-sync onboard" to set up, or pass a config path.\n',
57
+ );
58
+ process.exit(1);
59
+ }
60
+ const lockfilePath = cmdArgs[1] || path.join(path.dirname(configPath), '.any-sync.lock');
61
+ return { configPath, lockfilePath };
62
+ }
63
+
49
64
  try {
50
65
  switch (command) {
51
66
  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);
67
+ if (cmdArgs.includes('--help')) {
68
+ process.stdout.write('Usage: any-sync pull [config-path] [lockfile-path]\n');
69
+ process.exit(0);
57
70
  }
71
+ const { configPath, lockfilePath } = resolveConfigAndLock(cmdArgs);
58
72
  const result = lib.pull(configPath, lockfilePath);
59
73
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
60
74
  break;
61
75
  }
62
76
 
63
77
  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);
78
+ if (cmdArgs.includes('--help')) {
79
+ process.stdout.write('Usage: any-sync push [config-path] [lockfile-path]\n');
80
+ process.exit(0);
69
81
  }
82
+ const { configPath, lockfilePath } = resolveConfigAndLock(cmdArgs);
70
83
  const result = lib.push(configPath, lockfilePath);
71
84
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
72
85
  break;
73
86
  }
74
87
 
75
88
  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);
89
+ if (cmdArgs.includes('--help')) {
90
+ process.stdout.write('Usage: any-sync status [config-path] [lockfile-path]\n');
91
+ process.exit(0);
81
92
  }
93
+ const { configPath, lockfilePath } = resolveConfigAndLock(cmdArgs);
82
94
  const result = lib.status(configPath, lockfilePath);
83
95
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
84
96
  break;
85
97
  }
86
98
 
87
99
  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);
100
+ if (cmdArgs.includes('--help')) {
101
+ process.stdout.write('Usage: any-sync reset [config-path] [lockfile-path]\n');
102
+ process.exit(0);
93
103
  }
104
+ const { configPath, lockfilePath } = resolveConfigAndLock(cmdArgs);
94
105
  const result = lib.reset(configPath, lockfilePath);
95
106
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
96
107
  break;
@@ -148,6 +159,93 @@ try {
148
159
  process.stdout.write(result + '\n');
149
160
  break;
150
161
  }
162
+
163
+ case 'update-config': {
164
+ if (cmdArgs.includes('--help')) {
165
+ process.stdout.write(
166
+ 'Usage: any-sync update-config <config-path> <mapping-name> --add-include <pattern> [--add-include <pattern> ...]\n',
167
+ );
168
+ process.exit(0);
169
+ }
170
+ const positional = [];
171
+ const addInclude = [];
172
+ for (let i = 0; i < cmdArgs.length; i++) {
173
+ if (cmdArgs[i] === '--add-include') {
174
+ addInclude.push(cmdArgs[++i]);
175
+ } else {
176
+ positional.push(cmdArgs[i]);
177
+ }
178
+ }
179
+ const configPath = positional[0];
180
+ const mappingName = positional[1];
181
+ if (!configPath || !mappingName || addInclude.length === 0) {
182
+ process.stderr.write(
183
+ 'Usage: any-sync update-config <config-path> <mapping-name> --add-include <pattern> [--add-include <pattern> ...]\n',
184
+ );
185
+ process.exit(1);
186
+ }
187
+ const config = lib.loadConfig(configPath);
188
+ const mapping = config.mappings.find(m => m.name === mappingName);
189
+ if (!mapping) {
190
+ process.stderr.write(`Error: Mapping "${mappingName}" not found in config\n`);
191
+ process.exit(1);
192
+ }
193
+ if (!mapping.include) mapping.include = [];
194
+ for (const pattern of addInclude) {
195
+ if (!mapping.include.includes(pattern)) {
196
+ mapping.include.push(pattern);
197
+ }
198
+ }
199
+ lib.saveConfig(configPath, config);
200
+ process.stdout.write(JSON.stringify({ updated: mappingName, include: mapping.include }) + '\n');
201
+ break;
202
+ }
203
+
204
+ case 'help': {
205
+ const subcommand = cmdArgs[0];
206
+ if (!subcommand) {
207
+ usage();
208
+ process.exit(0);
209
+ }
210
+ const helpText = lib.commandHelp(subcommand);
211
+ if (!helpText) {
212
+ process.stderr.write(`Unknown command: ${subcommand}\n`);
213
+ process.stderr.write('Run any-sync --help for available commands.\n');
214
+ process.exit(1);
215
+ }
216
+ process.stdout.write(helpText + '\n');
217
+ break;
218
+ }
219
+
220
+ case 'onboard': {
221
+ if (cmdArgs.includes('--help')) {
222
+ const helpText = lib.commandHelp('onboard');
223
+ process.stdout.write(helpText + '\n');
224
+ process.exit(0);
225
+ }
226
+ const onboardOpts = {};
227
+ onboardOpts.presets = [];
228
+ for (let i = 0; i < cmdArgs.length; i++) {
229
+ if (cmdArgs[i] === '--repo') onboardOpts.repo = cmdArgs[++i];
230
+ else if (cmdArgs[i] === '--preset') onboardOpts.presets.push(cmdArgs[++i]);
231
+ else if (cmdArgs[i] === '--branch') onboardOpts.branch = cmdArgs[++i];
232
+ else if (cmdArgs[i] === '--config') onboardOpts.configPath = cmdArgs[++i];
233
+ else if (cmdArgs[i] === '--no-pull') onboardOpts.pull = false;
234
+ else if (cmdArgs[i] === '--force') onboardOpts.force = true;
235
+ }
236
+ if (onboardOpts.presets.length === 0) delete onboardOpts.presets;
237
+
238
+ lib
239
+ .onboard(onboardOpts)
240
+ .then((result) => {
241
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
242
+ })
243
+ .catch((err) => {
244
+ process.stderr.write('Error: ' + err.message + '\n');
245
+ process.exit(1);
246
+ });
247
+ break;
248
+ }
151
249
  }
152
250
  } catch (err) {
153
251
  process.stderr.write('Error: ' + err.message + '\n');
package/lib/config.js CHANGED
@@ -56,4 +56,13 @@ function parseMapping(m) {
56
56
  };
57
57
  }
58
58
 
59
- module.exports = { loadConfig, findConfig, expandTilde, parseMapping };
59
+ /**
60
+ * Save a config object to disk atomically.
61
+ */
62
+ function saveConfig(configPath, config) {
63
+ const tmpPath = configPath + '.tmp.' + process.pid;
64
+ fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
65
+ fs.renameSync(tmpPath, configPath);
66
+ }
67
+
68
+ module.exports = { loadConfig, findConfig, expandTilde, parseMapping, saveConfig };
package/lib/help.js ADDED
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ const COMMAND_HELP = {
4
+ pull: {
5
+ usage: 'any-sync pull [config-path] [lockfile-path]',
6
+ description:
7
+ 'Download files from GitHub that changed since the last sync.\n' +
8
+ ' Detects conflicts when both local and remote have been modified.\n' +
9
+ ' If no config path is given, searches ~/.any-sync.json then ./.any-sync.json.',
10
+ options: [
11
+ { flag: '[config-path]', desc: 'Path to .any-sync.json (default: auto-detected)' },
12
+ { flag: '[lockfile-path]', desc: 'Path to lockfile (default: alongside config)' },
13
+ ],
14
+ examples: ['any-sync pull', 'any-sync pull ~/.any-sync.json', 'any-sync pull .any-sync.json ./my.lock'],
15
+ },
16
+
17
+ push: {
18
+ usage: 'any-sync push [config-path] [lockfile-path]',
19
+ description:
20
+ 'Upload local file changes to GitHub.\n' +
21
+ ' Creates a new commit on the configured branch for each mapping with changes.\n' +
22
+ ' If no config path is given, searches ~/.any-sync.json then ./.any-sync.json.',
23
+ options: [
24
+ { flag: '[config-path]', desc: 'Path to .any-sync.json (default: auto-detected)' },
25
+ { flag: '[lockfile-path]', desc: 'Path to lockfile (default: alongside config)' },
26
+ ],
27
+ examples: ['any-sync push', 'any-sync push ~/.any-sync.json', 'any-sync push .any-sync.json ./my.lock'],
28
+ },
29
+
30
+ status: {
31
+ usage: 'any-sync status [config-path] [lockfile-path]',
32
+ description:
33
+ 'Show sync status including auth method, config validity,\n' +
34
+ ' tracked files, local changes, and untracked files per mapping.\n' +
35
+ ' If no config path is given, searches ~/.any-sync.json then ./.any-sync.json.',
36
+ options: [
37
+ { flag: '[config-path]', desc: 'Path to .any-sync.json (default: auto-detected)' },
38
+ { flag: '[lockfile-path]', desc: 'Path to lockfile (default: alongside config)' },
39
+ ],
40
+ examples: ['any-sync status', 'any-sync status ~/.any-sync.json'],
41
+ },
42
+
43
+ reset: {
44
+ usage: 'any-sync reset [config-path] [lockfile-path]',
45
+ description:
46
+ 'Remove config and lockfile to start fresh.\n' +
47
+ ' If no config path is given, searches ~/.any-sync.json then ./.any-sync.json.',
48
+ options: [
49
+ { flag: '[config-path]', desc: 'Path to .any-sync.json (default: auto-detected)' },
50
+ { flag: '[lockfile-path]', desc: 'Path to lockfile (default: alongside config)' },
51
+ ],
52
+ examples: ['any-sync reset', 'any-sync reset ~/.any-sync.json'],
53
+ },
54
+
55
+ auth: {
56
+ usage: 'any-sync auth',
57
+ description:
58
+ 'Check GitHub authentication.\n' +
59
+ ' Looks for GITHUB_TOKEN env var first, then tries gh auth token.',
60
+ options: [],
61
+ examples: ['any-sync auth', 'GITHUB_TOKEN=ghp_xxx any-sync auth'],
62
+ },
63
+
64
+ init: {
65
+ usage: 'any-sync init <config-path> <repo> [branch] [--preset claude|openclaw]',
66
+ description:
67
+ 'Create a .any-sync.json config file with preset mappings.\n' +
68
+ ' Skips if config already exists at the given path.',
69
+ options: [
70
+ { flag: '<config-path>', desc: 'Path to write config file (required)' },
71
+ { flag: '<repo>', desc: 'GitHub repo in owner/repo format (required)' },
72
+ { flag: '[branch]', desc: 'Branch to sync (default: main)' },
73
+ { flag: '--preset <name>', desc: 'Use preset mappings: claude, openclaw' },
74
+ ],
75
+ examples: [
76
+ 'any-sync init ~/.any-sync.json myuser/sync-repo --preset claude',
77
+ 'any-sync init .any-sync.json myuser/sync-repo main --preset openclaw',
78
+ ],
79
+ },
80
+
81
+ 'update-config': {
82
+ usage: 'any-sync update-config <config-path> <mapping-name> --add-include <pattern>',
83
+ description:
84
+ 'Add include patterns to an existing mapping.\n' +
85
+ ' Patterns can be specified multiple times.',
86
+ options: [
87
+ { flag: '<config-path>', desc: 'Path to .any-sync.json config file (required)' },
88
+ { flag: '<mapping-name>', desc: 'Name of the mapping to update (required)' },
89
+ { flag: '--add-include <pattern>', desc: 'Glob pattern to add (repeatable)' },
90
+ ],
91
+ examples: [
92
+ 'any-sync update-config ~/.any-sync.json claude-skills --add-include "**/*.md"',
93
+ 'any-sync update-config ~/.any-sync.json claude-config --add-include "rules/**" --add-include "agents/**"',
94
+ ],
95
+ },
96
+
97
+ onboard: {
98
+ usage: 'any-sync onboard [options]',
99
+ description:
100
+ 'Interactive setup wizard. Guides you from zero to syncing:\n' +
101
+ ' checks prerequisites, detects tools, creates config, and pulls files.',
102
+ options: [
103
+ { flag: '--repo <owner/repo>', desc: 'GitHub repo (skip prompt)' },
104
+ { flag: '--preset <name>', desc: 'Tool/preset to enable: claude, openclaw, vscode (repeatable)' },
105
+ { flag: '--branch <name>', desc: 'Branch to sync (default: main)' },
106
+ { flag: '--config <path>', desc: 'Config file path (default: ~/.any-sync.json)' },
107
+ { flag: '--no-pull', desc: 'Skip initial pull' },
108
+ { flag: '--force', desc: 'Overwrite existing config without asking' },
109
+ ],
110
+ examples: [
111
+ 'any-sync onboard',
112
+ 'any-sync onboard --repo myuser/sync-repo --preset claude',
113
+ 'any-sync onboard --repo myuser/sync-repo --preset claude --preset vscode',
114
+ 'any-sync onboard --repo myuser/sync-repo --preset claude --no-pull',
115
+ ],
116
+ },
117
+
118
+ help: {
119
+ usage: 'any-sync help [command]',
120
+ description:
121
+ 'Show detailed help for a command.\n' +
122
+ ' Without arguments, shows the list of all commands.',
123
+ options: [{ flag: '[command]', desc: 'Command to show help for' }],
124
+ examples: ['any-sync help', 'any-sync help pull', 'any-sync help onboard'],
125
+ },
126
+ };
127
+
128
+ /**
129
+ * Return formatted help text for a command, or null if unknown.
130
+ */
131
+ function commandHelp(name) {
132
+ const entry = COMMAND_HELP[name];
133
+ if (!entry) return null;
134
+
135
+ let out = `any-sync ${name} — ${entry.description.split('\n')[0]}\n\n`;
136
+ out += `Usage: ${entry.usage}\n\n`;
137
+ out += ` ${entry.description}\n`;
138
+
139
+ if (entry.options.length > 0) {
140
+ out += '\nOptions:\n';
141
+ const maxFlag = Math.max(...entry.options.map((o) => o.flag.length));
142
+ for (const opt of entry.options) {
143
+ out += ` ${opt.flag.padEnd(maxFlag + 2)} ${opt.desc}\n`;
144
+ }
145
+ }
146
+
147
+ if (entry.examples.length > 0) {
148
+ out += '\nExamples:\n';
149
+ for (const ex of entry.examples) {
150
+ out += ` ${ex}\n`;
151
+ }
152
+ }
153
+
154
+ return out;
155
+ }
156
+
157
+ /**
158
+ * Return the raw help data object for a command, or null if unknown.
159
+ */
160
+ function getCommandHelp(name) {
161
+ return COMMAND_HELP[name] || null;
162
+ }
163
+
164
+ module.exports = { commandHelp, getCommandHelp };
package/lib/index.d.ts CHANGED
@@ -31,6 +31,8 @@ export declare function parseMapping(m: Record<string, unknown>): {
31
31
  exclude: string[];
32
32
  };
33
33
 
34
+ export declare function saveConfig(configPath: string, config: { mappings: Array<Record<string, unknown>> }): void;
35
+
34
36
  export declare function checkAuth(): string;
35
37
 
36
38
  export declare function pull(configPath: string, lockfilePath: string): {
@@ -54,6 +56,7 @@ export declare function status(configPath: string, lockfilePath: string): {
54
56
  lastSync: string | null;
55
57
  tracked: number;
56
58
  changes: Array<{ file: string; type: string }>;
59
+ untracked: Array<{ file: string }>;
57
60
  }>;
58
61
  };
59
62
 
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const { Lockfile, makeKey, hashFile } = require('./lockfile');
4
4
  const { ghApi, ghApiRetry, getAuthToken } = require('./gh');
5
5
  const { globMatch, matchesAny } = require('./glob');
6
- const { loadConfig, findConfig, expandTilde, parseMapping } = require('./config');
6
+ const { loadConfig, findConfig, expandTilde, parseMapping, saveConfig } = require('./config');
7
7
  const { checkAuth } = require('./auth');
8
8
  const { pull } = require('./pull');
9
9
  const { push } = require('./push');
@@ -11,6 +11,8 @@ const { status } = require('./status');
11
11
  const { reset } = require('./reset');
12
12
  const { init, getPresetMappings } = require('./init');
13
13
  const { autoPull, autoPush } = require('./hooks');
14
+ const { onboard, detectTools, mergePresetMappings } = require('./onboard');
15
+ const { commandHelp, getCommandHelp } = require('./help');
14
16
 
15
17
  module.exports = {
16
18
  // Lockfile
@@ -29,6 +31,7 @@ module.exports = {
29
31
  findConfig,
30
32
  expandTilde,
31
33
  parseMapping,
34
+ saveConfig,
32
35
  // Auth
33
36
  checkAuth,
34
37
  // Operations
@@ -41,4 +44,11 @@ module.exports = {
41
44
  getPresetMappings,
42
45
  autoPull,
43
46
  autoPush,
47
+ // Onboard
48
+ onboard,
49
+ detectTools,
50
+ mergePresetMappings,
51
+ // Help
52
+ commandHelp,
53
+ getCommandHelp,
44
54
  };
package/lib/init.js CHANGED
@@ -59,12 +59,11 @@ function getPresetMappings(preset) {
59
59
  destPath: '~/.claude/skills',
60
60
  include: ['**/*.md'],
61
61
  },
62
- { name: 'claude-memory', sourcePath: 'memory', destPath: '~/.claude/memory' },
63
62
  {
64
- name: 'claude-settings',
65
- sourcePath: 'settings',
63
+ name: 'claude-config',
64
+ sourcePath: '.claude',
66
65
  destPath: '~/.claude',
67
- include: ['settings.json'],
66
+ include: ['settings.json', 'CLAUDE.md', 'rules/**/*.md', 'agents/**/*.md', 'memory/**'],
68
67
  },
69
68
  ];
70
69
 
package/lib/onboard.js ADDED
@@ -0,0 +1,322 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const readline = require('readline');
7
+ const { execFileSync } = require('child_process');
8
+ const { getPresetMappings } = require('./init');
9
+ const { findConfig, saveConfig } = require('./config');
10
+ const { getAuthToken } = require('./gh');
11
+ const { pull } = require('./pull');
12
+
13
+ // ── Helpers ──────────────────────────────────────────────────────────────────
14
+
15
+ function isToolInstalled(name) {
16
+ try {
17
+ execFileSync('which', [name], { stdio: 'pipe' });
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ function ask(rl, question, defaultValue) {
25
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
26
+ return new Promise((resolve) => {
27
+ rl.question(`${question}${suffix}: `, (answer) => {
28
+ resolve(answer.trim() || defaultValue || '');
29
+ });
30
+ });
31
+ }
32
+
33
+ function write(output, text) {
34
+ output.write(text);
35
+ }
36
+
37
+ // ── Exported functions ───────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Detect which tools are installed on the system.
41
+ */
42
+ function detectTools() {
43
+ return {
44
+ gh: isToolInstalled('gh'),
45
+ claude: isToolInstalled('claude'),
46
+ openclaw: isToolInstalled('openclaw'),
47
+ code: isToolInstalled('code'),
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Merge mappings from multiple presets into a single array.
53
+ */
54
+ function mergePresetMappings(presetNames) {
55
+ const mappings = [];
56
+ for (const name of presetNames) {
57
+ const preset = getPresetMappings(name);
58
+ if (preset) mappings.push(...preset);
59
+ }
60
+ return mappings;
61
+ }
62
+
63
+ /**
64
+ * Interactive onboard wizard.
65
+ *
66
+ * @param {object} opts
67
+ * @param {string} [opts.repo] - GitHub repo (skip prompt)
68
+ * @param {string} [opts.branch] - Branch (default: 'main')
69
+ * @param {string[]} [opts.presets] - Presets to enable (skip prompt)
70
+ * @param {boolean} [opts.pull] - Run initial pull (default: true)
71
+ * @param {string} [opts.configPath] - Config path (default: ~/.any-sync.json)
72
+ * @param {boolean} [opts.force] - Overwrite existing config
73
+ * @param {NodeJS.ReadableStream} [opts.input] - Input stream (default: stdin)
74
+ * @param {NodeJS.WritableStream} [opts.output] - Output stream (default: stderr)
75
+ */
76
+ async function onboard(opts = {}) {
77
+ const input = opts.input || process.stdin;
78
+ const output = opts.output || process.stderr;
79
+ const configPath = opts.configPath || path.join(os.homedir(), '.any-sync.json');
80
+ const branch = opts.branch || 'main';
81
+ const doPull = opts.pull !== false;
82
+ const interactive = !opts.repo || !opts.presets;
83
+
84
+ // If interactive but not a TTY, bail
85
+ if (interactive && input === process.stdin && !process.stdin.isTTY) {
86
+ throw new Error(
87
+ 'Interactive mode requires a terminal. Use --repo and --preset flags for non-interactive setup.',
88
+ );
89
+ }
90
+
91
+ const rl = interactive
92
+ ? readline.createInterface({ input, output, terminal: input.isTTY !== false })
93
+ : null;
94
+
95
+ try {
96
+ return await _runWizard({ rl, output, configPath, branch, doPull, opts });
97
+ } finally {
98
+ if (rl) rl.close();
99
+ }
100
+ }
101
+
102
+ async function _runWizard({ rl, output, configPath, branch, doPull, opts }) {
103
+ const result = {
104
+ configPath,
105
+ repo: null,
106
+ branch,
107
+ presets: [],
108
+ mappingCount: 0,
109
+ toolsDetected: [],
110
+ pullResult: null,
111
+ pluginInstructions: [],
112
+ };
113
+
114
+ // ── Step 1: Welcome ────────────────────────────────────────────────────────
115
+ write(output, '\nWelcome to Any Sync setup!\n');
116
+ write(output, 'This wizard will configure file sync between your tools and GitHub.\n\n');
117
+
118
+ // ── Step 2: Prerequisites ──────────────────────────────────────────────────
119
+ write(output, 'Checking prerequisites...\n');
120
+ const tools = detectTools();
121
+
122
+ const nodeVersion = process.versions.node;
123
+ const nodeMajor = parseInt(nodeVersion.split('.')[0], 10);
124
+ write(output, ` Node.js: v${nodeVersion} ${nodeMajor >= 18 ? '(OK)' : '(WARNING: v18+ recommended)'}\n`);
125
+ write(output, ` gh CLI: ${tools.gh ? 'installed (OK)' : 'not found'}\n`);
126
+ if (tools.claude) result.toolsDetected.push('claude');
127
+ if (tools.openclaw) result.toolsDetected.push('openclaw');
128
+ if (tools.code) result.toolsDetected.push('code');
129
+ write(output, ` Claude: ${tools.claude ? 'detected' : 'not found'}\n`);
130
+ write(output, ` OpenClaw: ${tools.openclaw ? 'detected' : 'not found'}\n`);
131
+ write(output, ` VS Code: ${tools.code ? 'detected' : 'not found'}\n`);
132
+ write(output, '\n');
133
+
134
+ if (!tools.gh) {
135
+ throw new Error(
136
+ 'gh CLI is required but not installed.\n' +
137
+ 'Install it from https://cli.github.com/ then re-run: any-sync onboard',
138
+ );
139
+ }
140
+
141
+ // ── Step 3: Auth ───────────────────────────────────────────────────────────
142
+ const token = getAuthToken();
143
+ if (!token) {
144
+ throw new Error(
145
+ 'GitHub authentication not found.\n' +
146
+ 'Please authenticate using one of:\n' +
147
+ " 1. Run 'gh auth login'\n" +
148
+ ' 2. Set GITHUB_TOKEN environment variable\n\n' +
149
+ 'Then re-run: any-sync onboard',
150
+ );
151
+ }
152
+ write(output, ' GitHub auth: authenticated (OK)\n\n');
153
+
154
+ // ── Step 4: Existing config ────────────────────────────────────────────────
155
+ const existingConfig = findConfig();
156
+ if (existingConfig && !opts.force) {
157
+ if (rl) {
158
+ const overwrite = await ask(rl, `Existing config found at ${existingConfig}. Overwrite? [y/N]`, 'n');
159
+ if (overwrite.toLowerCase() !== 'y') {
160
+ write(output, 'Setup cancelled. Your existing config is unchanged.\n');
161
+ return result;
162
+ }
163
+ } else {
164
+ throw new Error(
165
+ `Config already exists at ${existingConfig}. Use --force to overwrite.`,
166
+ );
167
+ }
168
+ }
169
+
170
+ // ── Step 5: Repo ───────────────────────────────────────────────────────────
171
+ let repo = opts.repo;
172
+ if (!repo && rl) {
173
+ const repoRegex = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/;
174
+ for (let attempt = 0; attempt < 3; attempt++) {
175
+ repo = await ask(rl, 'GitHub sync repo (owner/repo format)');
176
+ if (repoRegex.test(repo)) break;
177
+ write(output, ' Invalid format. Use owner/repo (e.g., myuser/my-sync-repo)\n');
178
+ if (attempt === 2) throw new Error('Invalid repo format after 3 attempts.');
179
+ repo = null;
180
+ }
181
+ }
182
+ if (!repo) {
183
+ throw new Error('--repo is required in non-interactive mode.');
184
+ }
185
+ result.repo = repo;
186
+
187
+ // ── Step 6: Branch ─────────────────────────────────────────────────────────
188
+ if (rl && !opts.branch) {
189
+ result.branch = await ask(rl, 'Branch to sync', 'main');
190
+ }
191
+
192
+ // ── Step 7: Tool/preset selection ───────────────────────────────────────────
193
+ let presets = opts.presets;
194
+ let installVscode = false;
195
+ if (!presets && rl) {
196
+ write(output, '\nSelect tools to set up (comma-separated):\n');
197
+ const toolOptions = [
198
+ { key: '1', name: 'claude', label: 'skills, memory, settings', detected: tools.claude },
199
+ { key: '2', name: 'openclaw', label: 'workspace skills, memory, config', detected: tools.openclaw },
200
+ { key: '3', name: 'vscode', label: 'VS Code extension for sync', detected: tools.code },
201
+ { key: '4', name: 'custom', label: 'configure manually later', detected: false },
202
+ ];
203
+ const defaults = [];
204
+ for (const opt of toolOptions) {
205
+ const tag = opt.detected ? ' [detected]' : '';
206
+ write(output, ` ${opt.key}. ${opt.name.padEnd(12)} — ${opt.label}${tag}\n`);
207
+ if (opt.detected && opt.name !== 'custom') defaults.push(opt.key);
208
+ }
209
+ const defaultStr = defaults.length > 0 ? defaults.join(',') : '1';
210
+ const choice = await ask(rl, 'Choice', defaultStr);
211
+ const selected = choice.split(',').map((s) => s.trim());
212
+ presets = [];
213
+ for (const s of selected) {
214
+ const opt = toolOptions.find((o) => o.key === s);
215
+ if (!opt) continue;
216
+ if (opt.name === 'vscode') {
217
+ installVscode = true;
218
+ } else if (opt.name !== 'custom') {
219
+ presets.push(opt.name);
220
+ }
221
+ }
222
+ }
223
+ if (!presets) {
224
+ throw new Error('--preset is required in non-interactive mode.');
225
+ }
226
+ if (opts.presets && opts.presets.includes('vscode')) {
227
+ installVscode = true;
228
+ presets = presets.filter((p) => p !== 'vscode');
229
+ }
230
+ result.presets = presets;
231
+
232
+ // ── Step 8: Write config ───────────────────────────────────────────────────
233
+ const mappings = mergePresetMappings(presets);
234
+ const config = {
235
+ mappings: mappings.map((m) => ({
236
+ name: m.name,
237
+ repo,
238
+ branch: result.branch,
239
+ sourcePath: m.sourcePath,
240
+ destPath: m.destPath,
241
+ ...(m.include ? { include: m.include } : {}),
242
+ ...(m.exclude ? { exclude: m.exclude } : {}),
243
+ })),
244
+ };
245
+
246
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
247
+ saveConfig(configPath, config);
248
+ result.mappingCount = config.mappings.length;
249
+ write(output, `\nConfig written to ${configPath}\n`);
250
+
251
+ // ── Step 9: Initial pull ───────────────────────────────────────────────────
252
+ if (doPull && config.mappings.length > 0) {
253
+ write(output, '\nRunning initial pull...\n');
254
+ const lockfilePath = path.join(path.dirname(configPath), '.any-sync.lock');
255
+ try {
256
+ result.pullResult = pull(configPath, lockfilePath);
257
+ const pulled = result.pullResult.pulled || [];
258
+ write(output, ` Pulled ${pulled.length} file(s).\n`);
259
+ } catch (err) {
260
+ write(output, ` Pull failed: ${err.message}\n`);
261
+ write(output, ' Config was saved successfully — you can pull manually later.\n');
262
+ }
263
+ }
264
+
265
+ // ── Step 10: Plugin installation ────────────────────────────────────────────
266
+ write(output, '\nNext steps:\n');
267
+
268
+ if (presets.includes('claude')) {
269
+ const instructions =
270
+ 'Run these commands inside Claude Code:\n' +
271
+ ' /plugin marketplace add imink/any-sync\n' +
272
+ ' /plugin install any-sync@any-sync-marketplace';
273
+ write(output, `\n Claude Code plugin:\n ${instructions.split('\n').join('\n ')}\n`);
274
+ result.pluginInstructions.push({ tool: 'claude', instructions });
275
+ }
276
+
277
+ if (presets.includes('openclaw')) {
278
+ const instructions = 'Run: openclaw plugins install any-sync';
279
+ write(output, `\n OpenClaw plugin:\n ${instructions}\n`);
280
+ result.pluginInstructions.push({ tool: 'openclaw', instructions });
281
+ }
282
+
283
+ if (installVscode) {
284
+ if (tools.code) {
285
+ write(output, '\n Installing VS Code extension...\n');
286
+ try {
287
+ execFileSync('code', ['--install-extension', 'patrickw1029.any-sync'], {
288
+ stdio: 'pipe',
289
+ encoding: 'utf8',
290
+ });
291
+ write(output, ' Any Sync extension installed successfully.\n');
292
+ result.pluginInstructions.push({ tool: 'vscode', installed: true });
293
+ } catch (err) {
294
+ write(output, ` Install failed: ${err.message}\n`);
295
+ write(output, ' Install manually: search "any-sync" in VS Code Extensions.\n');
296
+ result.pluginInstructions.push({ tool: 'vscode', installed: false });
297
+ }
298
+ } else {
299
+ write(output, '\n VS Code not found. Install the extension manually:\n');
300
+ write(output, ' Search "any-sync" in VS Code Extensions, or run:\n');
301
+ write(output, ' code --install-extension patrickw1029.any-sync\n');
302
+ result.pluginInstructions.push({ tool: 'vscode', installed: false });
303
+ }
304
+ }
305
+
306
+ // ── Step 11: Summary ───────────────────────────────────────────────────────
307
+ write(output, '\n---\n');
308
+ write(output, 'Setup complete!\n');
309
+ write(output, ` Config: ${configPath}\n`);
310
+ write(output, ` Repo: ${repo} (branch: ${result.branch})\n`);
311
+ write(output, ` Presets: ${presets.length > 0 ? presets.join(', ') : 'custom (edit config manually)'}\n`);
312
+ write(output, ` Mappings: ${result.mappingCount}\n`);
313
+ if (result.pullResult) {
314
+ const pulled = result.pullResult.pulled || [];
315
+ write(output, ` Pulled: ${pulled.length} file(s)\n`);
316
+ }
317
+ write(output, '\n');
318
+
319
+ return result;
320
+ }
321
+
322
+ module.exports = { onboard, detectTools, mergePresetMappings };
package/lib/status.js CHANGED
@@ -94,17 +94,27 @@ function status(configPath, lockfilePath) {
94
94
  }
95
95
  }
96
96
 
97
- // Check for new files
97
+ // Check for new and untracked files
98
+ const untracked = [];
98
99
  if (fs.existsSync(m.destPath)) {
99
100
  const localFiles = walkDir(m.destPath);
100
101
  for (const relPath of localFiles) {
101
- // Apply include filter
102
- if (m.include.length > 0 && !matchesAny(m.include, relPath)) continue;
103
- // Apply exclude filter
102
+ // Apply exclude filter first — excluded files are never relevant
104
103
  if (m.exclude.length > 0 && matchesAny(m.exclude, relPath)) continue;
105
104
 
106
105
  const lockKey = makeKey(m.name, relPath);
107
- if (!lf.getEntry(lockKey)) {
106
+ const isTracked = !!lf.getEntry(lockKey);
107
+
108
+ if (m.include.length > 0 && !matchesAny(m.include, relPath)) {
109
+ // File doesn't match include patterns — it's untracked (only if not already tracked)
110
+ if (!isTracked) {
111
+ untracked.push({ file: relPath });
112
+ }
113
+ continue;
114
+ }
115
+
116
+ // File matches filters — check if it's new
117
+ if (!isTracked) {
108
118
  changes.push({ file: relPath, type: 'new' });
109
119
  }
110
120
  }
@@ -117,6 +127,7 @@ function status(configPath, lockfilePath) {
117
127
  lastSync,
118
128
  trackedFiles,
119
129
  changes,
130
+ untracked,
120
131
  });
121
132
  }
122
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@any-sync/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.5",
4
4
  "description": "CLI and core library for Any Sync — bidirectional GitHub file sync",
5
5
  "main": "lib/index.js",
6
6
  "exports": {