ccsini 0.1.52 → 0.1.54

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 (2) hide show
  1. package/dist/index.js +2218 -2180
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28020,7 +28020,7 @@ var {
28020
28020
  } = import__.default;
28021
28021
 
28022
28022
  // src/version.ts
28023
- var VERSION = "0.1.52";
28023
+ var VERSION = "0.1.54";
28024
28024
 
28025
28025
  // src/commands/init.ts
28026
28026
  init_source();
@@ -29755,6 +29755,7 @@ async function uninstallLinux() {
29755
29755
  }
29756
29756
 
29757
29757
  // src/commands/init.ts
29758
+ import { spawn as spawn2 } from "child_process";
29758
29759
  function registerInitCommand(program2) {
29759
29760
  program2.command("init").description("Connect this device to your ccsini account").option("--token <token>", "Setup token from dashboard").action(async (opts) => {
29760
29761
  const configDir = getConfigDir();
@@ -29933,12 +29934,25 @@ function registerInitCommand(program2) {
29933
29934
  try {
29934
29935
  await installHeartbeatScheduler();
29935
29936
  } catch {}
29937
+ saveSpinner.text = "Starting background daemon...";
29938
+ let daemonStarted = false;
29939
+ try {
29940
+ const isWin = platform2() === "win32";
29941
+ const child = spawn2("ccsini", ["daemon", "_run"], {
29942
+ detached: true,
29943
+ stdio: "ignore",
29944
+ ...isWin ? { shell: true } : {}
29945
+ });
29946
+ child.unref();
29947
+ daemonStarted = true;
29948
+ } catch {}
29936
29949
  saveSpinner.succeed("Setup complete!");
29937
29950
  console.log(source_default.green(`
29938
29951
  \u2713 Encryption keys generated`));
29939
29952
  console.log(source_default.green(" \u2713 Device registered"));
29940
29953
  console.log(source_default.green(" \u2713 Claude Code hooks installed"));
29941
29954
  console.log(source_default.green(" \u2713 Heartbeat scheduler installed"));
29955
+ console.log(source_default.green(daemonStarted ? " \u2713 Background daemon started" : " \u2717 Daemon not started (run 'ccsini daemon start')"));
29942
29956
  console.log(source_default.green(" \u2713 Auto-sync is active"));
29943
29957
  console.log(source_default.dim(`
29944
29958
  Just use 'claude' as normal. Sync happens automatically.
@@ -29962,1065 +29976,283 @@ function detectPlatform() {
29962
29976
  init_auth();
29963
29977
  init_auth();
29964
29978
  init_src();
29979
+ import { readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
29980
+ import { join as join12 } from "path";
29981
+ import { platform as platform4 } from "os";
29982
+ import { spawn as spawn3 } from "child_process";
29983
+
29984
+ // src/daemon/runner.ts
29965
29985
  import { readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
29966
- import { join as join9 } from "path";
29967
- async function getMasterKey(configDir) {
29968
- const cachedKeyPath = join9(configDir, ".cached-key");
29969
- try {
29970
- const cached = await readFile7(cachedKeyPath);
29971
- return new Uint8Array(cached);
29972
- } catch {
29973
- throw new Error("No cached key. Run 'ccsini unlock' to enter your encryption password.");
29986
+ import { join as join11 } from "path";
29987
+ import { platform as platform3 } from "os";
29988
+ import { appendFileSync } from "fs";
29989
+ init_auth();
29990
+
29991
+ // ../../node_modules/.bun/chokidar@5.0.0/node_modules/chokidar/index.js
29992
+ import { EventEmitter } from "events";
29993
+ import { stat as statcb, Stats } from "fs";
29994
+ import { readdir as readdir3, stat as stat4 } from "fs/promises";
29995
+ import * as sp2 from "path";
29996
+
29997
+ // ../../node_modules/.bun/readdirp@5.0.0/node_modules/readdirp/index.js
29998
+ import { lstat, readdir as readdir2, realpath, stat as stat2 } from "fs/promises";
29999
+ import { join as pjoin, relative as prelative, resolve as presolve, sep as psep } from "path";
30000
+ import { Readable } from "stream";
30001
+ var EntryTypes = {
30002
+ FILE_TYPE: "files",
30003
+ DIR_TYPE: "directories",
30004
+ FILE_DIR_TYPE: "files_directories",
30005
+ EVERYTHING_TYPE: "all"
30006
+ };
30007
+ var defaultOptions = {
30008
+ root: ".",
30009
+ fileFilter: (_entryInfo) => true,
30010
+ directoryFilter: (_entryInfo) => true,
30011
+ type: EntryTypes.FILE_TYPE,
30012
+ lstat: false,
30013
+ depth: 2147483648,
30014
+ alwaysStat: false,
30015
+ highWaterMark: 4096
30016
+ };
30017
+ Object.freeze(defaultOptions);
30018
+ var RECURSIVE_ERROR_CODE = "READDIRP_RECURSIVE_ERROR";
30019
+ var NORMAL_FLOW_ERRORS = new Set(["ENOENT", "EPERM", "EACCES", "ELOOP", RECURSIVE_ERROR_CODE]);
30020
+ var ALL_TYPES = [
30021
+ EntryTypes.DIR_TYPE,
30022
+ EntryTypes.EVERYTHING_TYPE,
30023
+ EntryTypes.FILE_DIR_TYPE,
30024
+ EntryTypes.FILE_TYPE
30025
+ ];
30026
+ var DIR_TYPES = new Set([
30027
+ EntryTypes.DIR_TYPE,
30028
+ EntryTypes.EVERYTHING_TYPE,
30029
+ EntryTypes.FILE_DIR_TYPE
30030
+ ]);
30031
+ var FILE_TYPES = new Set([
30032
+ EntryTypes.EVERYTHING_TYPE,
30033
+ EntryTypes.FILE_DIR_TYPE,
30034
+ EntryTypes.FILE_TYPE
30035
+ ]);
30036
+ var isNormalFlowError = (error) => NORMAL_FLOW_ERRORS.has(error.code);
30037
+ var wantBigintFsStats = process.platform === "win32";
30038
+ var emptyFn = (_entryInfo) => true;
30039
+ var normalizeFilter = (filter2) => {
30040
+ if (filter2 === undefined)
30041
+ return emptyFn;
30042
+ if (typeof filter2 === "function")
30043
+ return filter2;
30044
+ if (typeof filter2 === "string") {
30045
+ const fl = filter2.trim();
30046
+ return (entry) => entry.basename === fl;
29974
30047
  }
29975
- }
29976
- async function getAuthenticatedClient(configDir) {
29977
- const config = await loadKeys(configDir);
29978
- const privateKey = await importPrivateKey(config.devicePrivateKey);
29979
- const jwt = await createDeviceJWT(privateKey, config.deviceId);
29980
- return new CcsiniClient(config.apiUrl, jwt);
29981
- }
29982
- function registerAutoCommands(program2) {
29983
- program2.command("auto-pull").description("Auto-pull changes (called by Claude Code hooks)").action(async () => {
29984
- const configDir = getConfigDir();
29985
- if (!await configExists(configDir))
29986
- return;
29987
- try {
29988
- const masterKey = await getMasterKey(configDir);
29989
- const client = await getAuthenticatedClient(configDir);
29990
- const config = await loadKeys(configDir);
29991
- const storedSession = await loadSessionConfig(configDir);
29992
- const sessionOptions = {
29993
- enabled: storedSession.enabled,
29994
- maxPerProject: storedSession.maxPerProject,
29995
- maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
29996
- };
29997
- const result = await pullSync(client, masterKey, config.deviceName, configDir, undefined, sessionOptions);
29998
- if (result.filesChanged > 0) {
29999
- console.log(`[ccsini] Pulled ${result.filesChanged} files (${result.durationMs}ms)`);
30000
- }
30001
- if (result.conflicts.length > 0) {
30002
- console.log(`[ccsini] \u26A0 ${result.conflicts.length} conflict(s) \u2014 check .conflict files in ~/.claude/`);
30003
- }
30004
- } catch {}
30005
- });
30006
- program2.command("auto-push").description("Auto-push changes (called by Claude Code hooks)").action(async () => {
30007
- const configDir = getConfigDir();
30008
- if (!await configExists(configDir))
30048
+ if (Array.isArray(filter2)) {
30049
+ const trItems = filter2.map((item) => item.trim());
30050
+ return (entry) => trItems.some((f) => entry.basename === f);
30051
+ }
30052
+ return emptyFn;
30053
+ };
30054
+
30055
+ class ReaddirpStream extends Readable {
30056
+ parents;
30057
+ reading;
30058
+ parent;
30059
+ _stat;
30060
+ _maxDepth;
30061
+ _wantsDir;
30062
+ _wantsFile;
30063
+ _wantsEverything;
30064
+ _root;
30065
+ _isDirent;
30066
+ _statsProp;
30067
+ _rdOptions;
30068
+ _fileFilter;
30069
+ _directoryFilter;
30070
+ constructor(options = {}) {
30071
+ super({
30072
+ objectMode: true,
30073
+ autoDestroy: true,
30074
+ highWaterMark: options.highWaterMark
30075
+ });
30076
+ const opts = { ...defaultOptions, ...options };
30077
+ const { root, type } = opts;
30078
+ this._fileFilter = normalizeFilter(opts.fileFilter);
30079
+ this._directoryFilter = normalizeFilter(opts.directoryFilter);
30080
+ const statMethod = opts.lstat ? lstat : stat2;
30081
+ if (wantBigintFsStats) {
30082
+ this._stat = (path2) => statMethod(path2, { bigint: true });
30083
+ } else {
30084
+ this._stat = statMethod;
30085
+ }
30086
+ this._maxDepth = opts.depth != null && Number.isSafeInteger(opts.depth) ? opts.depth : defaultOptions.depth;
30087
+ this._wantsDir = type ? DIR_TYPES.has(type) : false;
30088
+ this._wantsFile = type ? FILE_TYPES.has(type) : false;
30089
+ this._wantsEverything = type === EntryTypes.EVERYTHING_TYPE;
30090
+ this._root = presolve(root);
30091
+ this._isDirent = !opts.alwaysStat;
30092
+ this._statsProp = this._isDirent ? "dirent" : "stats";
30093
+ this._rdOptions = { encoding: "utf8", withFileTypes: this._isDirent };
30094
+ this.parents = [this._exploreDir(root, 1)];
30095
+ this.reading = false;
30096
+ this.parent = undefined;
30097
+ }
30098
+ async _read(batch) {
30099
+ if (this.reading)
30009
30100
  return;
30101
+ this.reading = true;
30010
30102
  try {
30011
- const masterKey = await getMasterKey(configDir);
30012
- const client = await getAuthenticatedClient(configDir);
30013
- const config = await loadKeys(configDir);
30014
- const storedSession = await loadSessionConfig(configDir);
30015
- const sessionOptions = {
30016
- enabled: storedSession.enabled,
30017
- maxPerProject: storedSession.maxPerProject,
30018
- maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
30019
- };
30020
- const result = await pushSync(client, masterKey, config.deviceName, configDir, undefined, sessionOptions, { skipConflictBackup: true });
30021
- if (result.filesChanged > 0) {
30022
- let msg = `[ccsini] Pushed ${result.filesChanged} files (${result.durationMs}ms)`;
30023
- if (result.cacheStats && result.cacheStats.hits > 0) {
30024
- msg += ` [cache: ${result.cacheStats.hitRate.toFixed(0)}% hits]`;
30103
+ while (!this.destroyed && batch > 0) {
30104
+ const par = this.parent;
30105
+ const fil = par && par.files;
30106
+ if (fil && fil.length > 0) {
30107
+ const { path: path2, depth } = par;
30108
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path2));
30109
+ const awaited = await Promise.all(slice);
30110
+ for (const entry of awaited) {
30111
+ if (!entry)
30112
+ continue;
30113
+ if (this.destroyed)
30114
+ return;
30115
+ const entryType = await this._getEntryType(entry);
30116
+ if (entryType === "directory" && this._directoryFilter(entry)) {
30117
+ if (depth <= this._maxDepth) {
30118
+ this.parents.push(this._exploreDir(entry.fullPath, depth + 1));
30119
+ }
30120
+ if (this._wantsDir) {
30121
+ this.push(entry);
30122
+ batch--;
30123
+ }
30124
+ } else if ((entryType === "file" || this._includeAsFile(entry)) && this._fileFilter(entry)) {
30125
+ if (this._wantsFile) {
30126
+ this.push(entry);
30127
+ batch--;
30128
+ }
30129
+ }
30130
+ }
30131
+ } else {
30132
+ const parent = this.parents.pop();
30133
+ if (!parent) {
30134
+ this.push(null);
30135
+ break;
30136
+ }
30137
+ this.parent = await parent;
30138
+ if (this.destroyed)
30139
+ return;
30025
30140
  }
30026
- console.log(msg);
30027
- }
30028
- if (result.conflicts.length > 0) {
30029
- console.log(`[ccsini] \u26A0 ${result.conflicts.length} conflict(s) \u2014 check .conflict files in ~/.claude/`);
30030
30141
  }
30031
- } catch {}
30032
- });
30033
- program2.command("unlock").description("Enter encryption password to enable auto-sync").action(async () => {
30034
- const configDir = getConfigDir();
30035
- if (!await configExists(configDir)) {
30036
- console.error("Not initialized. Run 'ccsini init' first.");
30037
- process.exit(1);
30142
+ } catch (error) {
30143
+ this.destroy(error);
30144
+ } finally {
30145
+ this.reading = false;
30038
30146
  }
30039
- const inquirer2 = await Promise.resolve().then(() => (init_dist16(), exports_dist));
30040
- const { password } = await inquirer2.default.prompt([
30041
- {
30042
- type: "password",
30043
- name: "password",
30044
- message: "Encryption password:",
30045
- mask: "*"
30046
- }
30047
- ]);
30048
- const config = await loadKeys(configDir);
30049
- const masterKey = await deriveKeyFromPassphrase(password, config.salt);
30050
- await writeFile8(join9(configDir, ".cached-key"), masterKey, {
30051
- mode: 384
30052
- });
30053
- console.log("Unlocked. Auto-sync is now active.");
30054
- });
30055
- program2.command("heartbeat").description("Send heartbeat to keep device online (runs via scheduled task)").action(async () => {
30056
- const configDir = getConfigDir();
30057
- if (!await configExists(configDir))
30058
- return;
30059
- try {
30060
- const client = await getAuthenticatedClient(configDir);
30061
- await client.heartbeat();
30062
- } catch {}
30063
- });
30064
- program2.command("lock").description("Remove cached encryption key").action(async () => {
30065
- const configDir = getConfigDir();
30066
- const { rm } = await import("fs/promises");
30147
+ }
30148
+ async _exploreDir(path2, depth) {
30149
+ let files;
30067
30150
  try {
30068
- await rm(join9(configDir, ".cached-key"));
30069
- console.log("Locked. Auto-sync paused until next unlock.");
30070
- } catch {
30071
- console.log("Already locked.");
30151
+ files = await readdir2(path2, this._rdOptions);
30152
+ } catch (error) {
30153
+ this._onError(error);
30072
30154
  }
30073
- });
30074
- }
30075
-
30076
- // src/commands/doctor.ts
30077
- init_source();
30078
- import { access as access2 } from "fs/promises";
30079
- import { join as join10 } from "path";
30080
- function registerDoctorCommand(program2) {
30081
- program2.command("doctor").description("Diagnose ccsini setup and connectivity").action(async () => {
30082
- const configDir = getConfigDir();
30083
- const claudeDir = getClaudeDir();
30084
- let allGood = true;
30085
- console.log(source_default.bold(`
30086
- ccsini doctor
30087
- `));
30155
+ return { files, depth, path: path2 };
30156
+ }
30157
+ async _formatEntry(dirent, path2) {
30158
+ let entry;
30159
+ const basename = this._isDirent ? dirent.name : dirent;
30088
30160
  try {
30089
- await access2(claudeDir);
30090
- console.log(source_default.green(" \u2713 Claude Code directory found"));
30091
- } catch {
30092
- console.log(source_default.red(" \u2717 Claude Code directory not found"));
30093
- allGood = false;
30094
- }
30095
- if (await configExists(configDir)) {
30096
- console.log(source_default.green(" \u2713 ccsini initialized"));
30097
- } else {
30098
- console.log(source_default.red(" \u2717 ccsini not initialized (run 'ccsini init')"));
30099
- allGood = false;
30161
+ const fullPath = presolve(pjoin(path2, basename));
30162
+ entry = { path: prelative(this._root, fullPath), fullPath, basename };
30163
+ entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
30164
+ } catch (err) {
30165
+ this._onError(err);
30100
30166
  return;
30101
30167
  }
30102
- const config = await loadKeys(configDir);
30103
- console.log(source_default.green(` \u2713 Device: ${config.deviceName} (${config.deviceId.slice(0, 8)}...)`));
30104
- if (await hooksInstalled(claudeDir)) {
30105
- console.log(source_default.green(" \u2713 Hooks installed"));
30106
- const { healthy, issues } = await checkHooksHealth(claudeDir);
30107
- if (healthy) {
30108
- console.log(source_default.green(" \u2713 Hooks format valid"));
30109
- } else {
30110
- for (const issue of issues) {
30111
- console.log(source_default.red(` \u2717 ${issue}`));
30112
- }
30113
- console.log(source_default.yellow(" Run 'ccsini hooks fix' to repair"));
30114
- allGood = false;
30115
- }
30168
+ return entry;
30169
+ }
30170
+ _onError(err) {
30171
+ if (isNormalFlowError(err) && !this.destroyed) {
30172
+ this.emit("warn", err);
30116
30173
  } else {
30117
- console.log(source_default.yellow(" \u26A0 Hooks not installed (run 'ccsini init')"));
30118
- allGood = false;
30119
- }
30120
- try {
30121
- await access2(join10(configDir, ".cached-key"));
30122
- console.log(source_default.green(" \u2713 Auto-sync unlocked"));
30123
- } catch {
30124
- console.log(source_default.yellow(" \u26A0 Auto-sync locked (run 'ccsini unlock')"));
30125
- }
30126
- try {
30127
- const start = Date.now();
30128
- const res = await fetch(`${config.apiUrl}/health`);
30129
- const latency = Date.now() - start;
30130
- if (res.ok) {
30131
- console.log(source_default.green(` \u2713 Server reachable (${latency}ms)`));
30132
- } else {
30133
- console.log(source_default.red(" \u2717 Server returned error"));
30134
- allGood = false;
30135
- }
30136
- } catch {
30137
- console.log(source_default.red(" \u2717 Cannot reach server"));
30138
- allGood = false;
30174
+ this.destroy(err);
30139
30175
  }
30140
- console.log(allGood ? source_default.green(`
30141
- Everything looks good!
30142
- `) : source_default.yellow(`
30143
- Some issues found. See above.
30144
- `));
30145
- });
30146
- }
30147
-
30148
- // src/commands/self.ts
30149
- import { execSync as execSync2 } from "child_process";
30150
- function getLatestVersion() {
30151
- try {
30152
- const result = execSync2("npm view ccsini version", { encoding: "utf-8" }).trim();
30153
- return result;
30154
- } catch {
30155
- return null;
30156
30176
  }
30157
- }
30158
- function registerSelfCommands(program2) {
30159
- program2.command("update").description("Update ccsini to the latest version").action(async () => {
30160
- console.log(`Current version: ${VERSION}`);
30161
- const latest = getLatestVersion();
30162
- if (!latest) {
30163
- console.error("Failed to check latest version. Check your internet connection.");
30164
- process.exit(1);
30165
- }
30166
- if (latest === VERSION) {
30167
- console.log(`Already on the latest version (${VERSION})`);
30168
- return;
30177
+ async _getEntryType(entry) {
30178
+ if (!entry && this._statsProp in entry) {
30179
+ return "";
30169
30180
  }
30170
- console.log(`New version available: ${latest}`);
30171
- console.log(`Updating...
30172
- `);
30173
- let updated = false;
30174
- try {
30175
- execSync2(`bun add -g ccsini@${latest}`, { stdio: "inherit" });
30176
- updated = true;
30177
- } catch {
30178
- console.log(`Bun failed (registry cache), trying npm...
30179
- `);
30181
+ const stats = entry[this._statsProp];
30182
+ if (stats.isFile())
30183
+ return "file";
30184
+ if (stats.isDirectory())
30185
+ return "directory";
30186
+ if (stats && stats.isSymbolicLink()) {
30187
+ const full = entry.fullPath;
30180
30188
  try {
30181
- execSync2(`npm install -g ccsini@${latest}`, { stdio: "inherit" });
30182
- updated = true;
30183
- } catch {}
30184
- }
30185
- if (!updated) {
30186
- console.error(`
30187
- Update failed. Try manually:`);
30188
- console.error(` npm install -g ccsini@${latest}`);
30189
- process.exit(1);
30190
- }
30191
- console.log(`
30192
- Updated to v${latest}`);
30193
- try {
30194
- const { fixed } = await fixHooks(getClaudeDir());
30195
- if (fixed > 0) {
30196
- console.log(`Repaired ${fixed} hook issue${fixed > 1 ? "s" : ""} in Claude Code settings.`);
30189
+ const entryRealPath = await realpath(full);
30190
+ const entryRealPathStats = await lstat(entryRealPath);
30191
+ if (entryRealPathStats.isFile()) {
30192
+ return "file";
30193
+ }
30194
+ if (entryRealPathStats.isDirectory()) {
30195
+ const len = entryRealPath.length;
30196
+ if (full.startsWith(entryRealPath) && full.substr(len, 1) === psep) {
30197
+ const recursiveError = new Error(`Circular symlink detected: "${full}" points to "${entryRealPath}"`);
30198
+ recursiveError.code = RECURSIVE_ERROR_CODE;
30199
+ return this._onError(recursiveError);
30200
+ }
30201
+ return "directory";
30202
+ }
30203
+ } catch (error) {
30204
+ this._onError(error);
30205
+ return "";
30197
30206
  }
30198
- } catch {}
30199
- console.log("Restart your terminal to use the new version.");
30200
- });
30201
- program2.command("uninstall").description("Uninstall ccsini from this machine").action(async () => {
30202
- console.log(`Uninstalling ccsini...
30203
- `);
30204
- try {
30205
- await uninstallHeartbeatScheduler();
30206
- } catch {}
30207
- let removed = false;
30208
- try {
30209
- execSync2("bun remove -g ccsini", { stdio: "inherit" });
30210
- removed = true;
30211
- } catch {}
30212
- try {
30213
- execSync2("npm uninstall -g ccsini", { stdio: "inherit" });
30214
- removed = true;
30215
- } catch {}
30216
- if (removed) {
30217
- console.log(`
30218
- ccsini has been uninstalled. Bye!`);
30219
- } else {
30220
- console.error(`
30221
- Uninstall failed. Try manually:`);
30222
- console.error(" npm uninstall -g ccsini");
30223
- process.exit(1);
30224
30207
  }
30225
- });
30226
- }
30227
-
30228
- // src/commands/sync.ts
30229
- init_auth();
30230
- init_src();
30231
- import { readFile as readFile8 } from "fs/promises";
30232
- import { join as join11 } from "path";
30233
- async function getMasterKey2(configDir) {
30234
- const cachedKeyPath = join11(configDir, ".cached-key");
30235
- try {
30236
- const cached = await readFile8(cachedKeyPath);
30237
- return new Uint8Array(cached);
30238
- } catch {
30239
- const inquirer2 = await Promise.resolve().then(() => (init_dist16(), exports_dist));
30240
- const { password } = await inquirer2.default.prompt([
30241
- {
30242
- type: "password",
30243
- name: "password",
30244
- message: "Encryption password:",
30245
- mask: "*"
30246
- }
30247
- ]);
30248
- const config = await loadKeys(configDir);
30249
- const masterKey = await deriveKeyFromPassphrase(password, config.salt);
30250
- const { writeFile: writeFile9 } = await import("fs/promises");
30251
- await writeFile9(cachedKeyPath, masterKey, { mode: 384 });
30252
- return masterKey;
30208
+ }
30209
+ _includeAsFile(entry) {
30210
+ const stats = entry && entry[this._statsProp];
30211
+ return stats && this._wantsEverything && !stats.isDirectory();
30253
30212
  }
30254
30213
  }
30255
- async function getAuthenticatedClient2(configDir) {
30256
- const config = await loadKeys(configDir);
30257
- const privateKey = await importPrivateKey(config.devicePrivateKey);
30258
- const jwt = await createDeviceJWT(privateKey, config.deviceId);
30259
- return new CcsiniClient(config.apiUrl, jwt);
30260
- }
30261
- function formatBytes(bytes) {
30262
- if (bytes < 1024)
30263
- return `${bytes} B`;
30264
- if (bytes < 1048576)
30265
- return `${(bytes / 1024).toFixed(1)} KB`;
30266
- return `${(bytes / 1048576).toFixed(1)} MB`;
30267
- }
30268
- function registerSyncCommands(program2) {
30269
- const syncCmd = program2.command("sync").description("Sync management commands");
30270
- syncCmd.command("push").description("Push local changes to cloud").option("--with-sessions", "Include session files in sync").option("--no-sessions", "Exclude session files from sync").action(async (opts) => {
30271
- const configDir = getConfigDir();
30272
- if (!await configExists(configDir)) {
30273
- console.error("Not initialized. Run 'ccsini init' first.");
30274
- process.exit(1);
30275
- }
30276
- const ora2 = (await Promise.resolve().then(() => (init_ora(), exports_ora))).default;
30277
- const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
30278
- try {
30279
- const masterKey = await getMasterKey2(configDir);
30280
- const client = await getAuthenticatedClient2(configDir);
30281
- const config = await loadKeys(configDir);
30282
- const storedSession = await loadSessionConfig(configDir);
30283
- const sessionOptions = {
30284
- enabled: opts.withSessions ? true : opts.sessions === false ? false : storedSession.enabled,
30285
- maxPerProject: storedSession.maxPerProject,
30286
- maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
30287
- };
30288
- const spinner = ora2("Scanning files...").start();
30289
- const result = await pushSync(client, masterKey, config.deviceName, configDir, (msg) => {
30290
- spinner.text = msg;
30291
- }, sessionOptions);
30292
- if (result.errors.length > 0) {
30293
- spinner.warn("Push completed with errors");
30294
- for (const err of result.errors) {
30295
- console.error(chalk2.red(` ${err}`));
30296
- }
30297
- } else {
30298
- spinner.succeed("Push complete");
30299
- }
30300
- console.log(chalk2.dim(` ${result.filesChanged} files changed, ${formatBytes(result.bytesTransferred)} transferred in ${result.durationMs}ms`));
30301
- if (sessionOptions.enabled) {
30302
- console.log(chalk2.dim(" Sessions: enabled"));
30303
- }
30304
- if (result.conflicts.length > 0) {
30305
- console.log(chalk2.yellow(` \u26A0 ${result.conflicts.length} conflict(s) \u2014 check .conflict files in ~/.claude/`));
30306
- }
30307
- } catch (e) {
30308
- console.error(`Push failed: ${e.message}`);
30309
- process.exit(1);
30310
- }
30311
- });
30312
- syncCmd.command("pull").description("Pull changes from cloud").option("--with-sessions", "Include session files in sync").option("--no-sessions", "Exclude session files from sync").action(async (opts) => {
30313
- const configDir = getConfigDir();
30314
- if (!await configExists(configDir)) {
30315
- console.error("Not initialized. Run 'ccsini init' first.");
30316
- process.exit(1);
30317
- }
30318
- const ora2 = (await Promise.resolve().then(() => (init_ora(), exports_ora))).default;
30319
- const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
30320
- try {
30321
- const masterKey = await getMasterKey2(configDir);
30322
- const client = await getAuthenticatedClient2(configDir);
30323
- const config = await loadKeys(configDir);
30324
- const storedSession = await loadSessionConfig(configDir);
30325
- const sessionOptions = {
30326
- enabled: opts.withSessions ? true : opts.sessions === false ? false : storedSession.enabled,
30327
- maxPerProject: storedSession.maxPerProject,
30328
- maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
30329
- };
30330
- const spinner = ora2("Pulling from cloud...").start();
30331
- const result = await pullSync(client, masterKey, config.deviceName, configDir, (msg) => {
30332
- spinner.text = msg;
30333
- }, sessionOptions);
30334
- if (result.errors.length > 0) {
30335
- spinner.warn("Pull completed with errors");
30336
- for (const err of result.errors) {
30337
- console.error(chalk2.red(` ${err}`));
30338
- }
30339
- } else {
30340
- spinner.succeed("Pull complete");
30341
- }
30342
- console.log(chalk2.dim(` ${result.filesChanged} files changed, ${formatBytes(result.bytesTransferred)} transferred in ${result.durationMs}ms`));
30343
- if (sessionOptions.enabled) {
30344
- console.log(chalk2.dim(" Sessions: enabled"));
30345
- }
30346
- if (result.conflicts.length > 0) {
30347
- console.log(chalk2.yellow(` \u26A0 ${result.conflicts.length} conflict(s) \u2014 check .conflict files in ~/.claude/`));
30348
- }
30349
- } catch (e) {
30350
- console.error(`Pull failed: ${e.message}`);
30351
- process.exit(1);
30352
- }
30353
- });
30354
- syncCmd.command("cleanup").description("Remove orphaned blobs from cloud storage").option("--dry-run", "Show what would be deleted without deleting").action(async (opts) => {
30355
- const configDir = getConfigDir();
30356
- if (!await configExists(configDir)) {
30357
- console.error("Not initialized. Run 'ccsini init' first.");
30358
- process.exit(1);
30359
- }
30360
- const ora2 = (await Promise.resolve().then(() => (init_ora(), exports_ora))).default;
30361
- const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
30362
- try {
30363
- const masterKey = await getMasterKey2(configDir);
30364
- const client = await getAuthenticatedClient2(configDir);
30365
- const spinner = ora2("Fetching remote manifest...").start();
30366
- const remoteManifestEnc = await client.getManifest();
30367
- if (!remoteManifestEnc) {
30368
- spinner.fail("No remote manifest found. Nothing to clean up.");
30369
- return;
30370
- }
30371
- let remoteManifest;
30372
- try {
30373
- const decrypted = decryptFile(masterKey, "__manifest__", remoteManifestEnc);
30374
- remoteManifest = JSON.parse(new TextDecoder().decode(decrypted));
30375
- } catch {
30376
- spinner.fail("Failed to decrypt remote manifest");
30377
- process.exit(1);
30378
- }
30379
- const referencedHashes = new Set;
30380
- for (const entry of Object.values(remoteManifest.files)) {
30381
- referencedHashes.add(entry.hash);
30382
- }
30383
- spinner.text = "Listing remote blobs...";
30384
- const { hashes: serverHashes, totalSize } = await client.listBlobs();
30385
- const orphans = serverHashes.filter((h) => !referencedHashes.has(h));
30386
- if (orphans.length === 0) {
30387
- spinner.succeed("No orphaned blobs found");
30388
- console.log(chalk2.dim(` ${serverHashes.length} blobs on server, all referenced by manifest`));
30389
- return;
30390
- }
30391
- spinner.stop();
30392
- console.log(`Found ${chalk2.yellow(orphans.length)} orphaned blobs out of ${serverHashes.length} total`);
30393
- if (opts.dryRun) {
30394
- console.log(chalk2.dim(" Dry run \u2014 no blobs deleted"));
30395
- for (const hash of orphans.slice(0, 10)) {
30396
- console.log(chalk2.dim(` ${hash}`));
30397
- }
30398
- if (orphans.length > 10) {
30399
- console.log(chalk2.dim(` ... and ${orphans.length - 10} more`));
30400
- }
30401
- return;
30402
- }
30403
- const inquirer2 = await Promise.resolve().then(() => (init_dist16(), exports_dist));
30404
- const { confirm } = await inquirer2.default.prompt([
30405
- {
30406
- type: "confirm",
30407
- name: "confirm",
30408
- message: `Delete ${orphans.length} orphaned blobs?`,
30409
- default: false
30410
- }
30411
- ]);
30412
- if (!confirm) {
30413
- console.log("Cancelled.");
30414
- return;
30415
- }
30416
- const deleteSpinner = ora2("Deleting orphaned blobs...").start();
30417
- const batchSize = 500;
30418
- let deleted = 0;
30419
- for (let i = 0;i < orphans.length; i += batchSize) {
30420
- const batch = orphans.slice(i, i + batchSize);
30421
- const result = await client.deleteBlobs(batch);
30422
- deleted += result.deleted;
30423
- deleteSpinner.text = `Deleting orphaned blobs... ${deleted}/${orphans.length}`;
30424
- }
30425
- deleteSpinner.succeed(`Deleted ${deleted} orphaned blobs`);
30426
- } catch (e) {
30427
- console.error(`Cleanup failed: ${e.message}`);
30428
- process.exit(1);
30429
- }
30430
- });
30431
- syncCmd.command("status").description("Show sync status").action(async () => {
30432
- const configDir = getConfigDir();
30433
- if (!await configExists(configDir)) {
30434
- console.error("Not initialized. Run 'ccsini init' first.");
30435
- process.exit(1);
30436
- }
30437
- const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
30438
- const ora2 = (await Promise.resolve().then(() => (init_ora(), exports_ora))).default;
30439
- try {
30440
- const masterKey = await getMasterKey2(configDir);
30441
- const client = await getAuthenticatedClient2(configDir);
30442
- const config = await loadKeys(configDir);
30443
- const spinner = ora2("Checking status...").start();
30444
- const localManifest = await loadManifest(configDir);
30445
- const claudeDir = getClaudeDir();
30446
- const { manifest: currentManifest } = await generateManifest(claudeDir, config.deviceName, (msg) => {
30447
- spinner.text = msg;
30448
- });
30449
- let remoteManifest = null;
30450
- const remoteManifestEnc = await client.getManifest();
30451
- if (remoteManifestEnc) {
30452
- try {
30453
- const decrypted = decryptFile(masterKey, "__manifest__", remoteManifestEnc);
30454
- remoteManifest = JSON.parse(new TextDecoder().decode(decrypted));
30455
- } catch {
30456
- spinner.fail("Failed to decrypt remote manifest");
30457
- console.error("Wrong encryption password?");
30458
- process.exit(1);
30459
- }
30460
- }
30461
- spinner.stop();
30462
- const diffs = diffManifests(currentManifest, remoteManifest);
30463
- const pendingPush = diffs.filter((d) => d.action === "push" || d.action === "merge").length;
30464
- const pendingPull = diffs.filter((d) => d.action === "pull" || d.action === "merge").length;
30465
- console.log(chalk2.bold("Sync Status"));
30466
- console.log(` Device: ${config.deviceName}`);
30467
- console.log(` Pending push: ${pendingPush === 0 ? chalk2.green("0") : chalk2.yellow(pendingPush)} files`);
30468
- console.log(` Pending pull: ${pendingPull === 0 ? chalk2.green("0") : chalk2.yellow(pendingPull)} files`);
30469
- if (localManifest) {
30470
- const lastSync = new Date(localManifest.timestamp);
30471
- console.log(` Last sync: ${lastSync.toLocaleString()}`);
30472
- console.log(` Last device: ${localManifest.device}`);
30473
- } else {
30474
- console.log(` Last sync: ${chalk2.dim("never")}`);
30475
- }
30476
- if (!remoteManifestEnc) {
30477
- console.log(chalk2.dim(`
30478
- No remote data yet. Run 'ccsini sync push' first.`));
30479
- }
30480
- } catch (e) {
30481
- console.error(`Status check failed: ${e.message}`);
30482
- process.exit(1);
30483
- }
30484
- });
30485
- }
30486
-
30487
- // src/commands/hooks.ts
30488
- init_source();
30489
- function registerHooksCommands(program2) {
30490
- const hooksCmd = program2.command("hooks").description("Manage Claude Code hooks");
30491
- hooksCmd.command("fix").description("Repair broken hook entries in Claude Code settings").action(async () => {
30492
- const claudeDir = getClaudeDir();
30493
- console.log(source_default.bold(`
30494
- Checking hooks...
30495
- `));
30496
- const { fixed, details } = await fixHooks(claudeDir);
30497
- if (fixed === 0) {
30498
- console.log(source_default.green(` Hooks are already healthy. Nothing to fix.
30499
- `));
30500
- return;
30501
- }
30502
- for (const detail of details) {
30503
- console.log(source_default.yellow(` Fixed: ${detail}`));
30504
- }
30505
- console.log(source_default.green(`
30506
- Repaired ${fixed} hook issue${fixed > 1 ? "s" : ""}.
30507
- `));
30508
- });
30214
+ function readdirp(root, options = {}) {
30215
+ let type = options.entryType || options.type;
30216
+ if (type === "both")
30217
+ type = EntryTypes.FILE_DIR_TYPE;
30218
+ if (type)
30219
+ options.type = type;
30220
+ if (!root) {
30221
+ throw new Error("readdirp: root argument is required. Usage: readdirp(root, options)");
30222
+ } else if (typeof root !== "string") {
30223
+ throw new TypeError("readdirp: root argument must be a string. Usage: readdirp(root, options)");
30224
+ } else if (type && !ALL_TYPES.includes(type)) {
30225
+ throw new Error(`readdirp: Invalid type passed. Use one of ${ALL_TYPES.join(", ")}`);
30226
+ }
30227
+ options.root = root;
30228
+ return new ReaddirpStream(options);
30509
30229
  }
30510
30230
 
30511
- // src/commands/reset.ts
30512
- init_auth();
30513
- import { rm } from "fs/promises";
30514
- function registerResetCommand(program2) {
30515
- program2.command("reset").description("Wipe all server data and local config (full account reset)").action(async () => {
30516
- const configDir = getConfigDir();
30517
- if (!await configExists(configDir)) {
30518
- console.error("Not initialized. Nothing to reset.");
30519
- process.exit(1);
30520
- }
30521
- const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
30522
- const inquirer2 = await Promise.resolve().then(() => (init_dist16(), exports_dist));
30523
- console.log(chalk2.red.bold(`
30524
- WARNING: This will permanently delete:`));
30525
- console.log(chalk2.red(" - All encrypted files on the server"));
30526
- console.log(chalk2.red(" - Your encryption salt"));
30527
- console.log(chalk2.red(` - Your local config (~/.ccsini/)
30528
- `));
30529
- const { confirm } = await inquirer2.default.prompt([
30530
- {
30531
- type: "confirm",
30532
- name: "confirm",
30533
- message: "Are you sure you want to wipe everything?",
30534
- default: false
30535
- }
30536
- ]);
30537
- if (!confirm) {
30538
- console.log("Cancelled.");
30539
- return;
30540
- }
30541
- const ora2 = (await Promise.resolve().then(() => (init_ora(), exports_ora))).default;
30542
- try {
30543
- const config = await loadKeys(configDir);
30544
- const privateKey = await importPrivateKey(config.devicePrivateKey);
30545
- const jwt = await createDeviceJWT(privateKey, config.deviceId);
30546
- const client = new CcsiniClient(config.apiUrl, jwt);
30547
- const spinner = ora2("Wiping server data...").start();
30548
- try {
30549
- const { blobsDeleted } = await client.resetAll();
30550
- spinner.succeed(`Server data wiped (${blobsDeleted} blob${blobsDeleted !== 1 ? "s" : ""} deleted)`);
30551
- } catch {
30552
- spinner.warn("Could not wipe server data (device may have been removed from dashboard)");
30553
- }
30554
- const localSpinner = ora2("Removing local config...").start();
30555
- await rm(configDir, { recursive: true, force: true });
30556
- localSpinner.succeed("Local config removed");
30557
- console.log(chalk2.green(`
30558
- Account fully reset. Run 'ccsini init --token <token>' to start fresh.`));
30559
- } catch (e) {
30560
- console.error(`Reset failed: ${e.message}`);
30561
- process.exit(1);
30562
- }
30563
- });
30564
- }
30565
-
30566
- // src/commands/sessions.ts
30567
- init_src();
30568
- function formatBytes2(bytes) {
30569
- if (bytes < 1024)
30570
- return `${bytes} B`;
30571
- if (bytes < 1024 * 1024)
30572
- return `${(bytes / 1024).toFixed(1)} KB`;
30573
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
30574
- }
30575
- function registerSessionsCommand(program2) {
30576
- const sessionsCmd = program2.command("sessions").description("Configure session sync (continue conversations across devices)");
30577
- sessionsCmd.command("enable").description("Enable session sync").option("-n, --per-project <count>", "Max sessions per project", String(SESSION_SYNC_DEFAULTS.maxPerProject)).action(async (opts) => {
30578
- const configDir = getConfigDir();
30579
- if (!await configExists(configDir)) {
30580
- console.error("Not initialized. Run 'ccsini init' first.");
30581
- process.exit(1);
30582
- }
30583
- const maxPerProject = parseInt(opts.perProject, 10);
30584
- if (isNaN(maxPerProject) || maxPerProject < 1) {
30585
- console.error("--per-project must be a positive integer");
30586
- process.exit(1);
30587
- }
30588
- await saveSessionConfig(configDir, {
30589
- enabled: true,
30590
- maxPerProject
30591
- });
30592
- console.log(`Session sync enabled (${maxPerProject} sessions per project)`);
30593
- });
30594
- sessionsCmd.command("disable").description("Disable session sync").action(async () => {
30595
- const configDir = getConfigDir();
30596
- if (!await configExists(configDir)) {
30597
- console.error("Not initialized. Run 'ccsini init' first.");
30598
- process.exit(1);
30599
- }
30600
- const current = await loadSessionConfig(configDir);
30601
- await saveSessionConfig(configDir, {
30602
- enabled: false,
30603
- maxPerProject: current.maxPerProject
30604
- });
30605
- console.log("Session sync disabled");
30606
- });
30607
- sessionsCmd.command("status").description("Show session sync status and preview").action(async () => {
30608
- const configDir = getConfigDir();
30609
- if (!await configExists(configDir)) {
30610
- console.error("Not initialized. Run 'ccsini init' first.");
30611
- process.exit(1);
30612
- }
30613
- const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
30614
- const ora2 = (await Promise.resolve().then(() => (init_ora(), exports_ora))).default;
30615
- const sessionConfig = await loadSessionConfig(configDir);
30616
- console.log(chalk2.bold("Session Sync"));
30617
- console.log(` Status: ${sessionConfig.enabled ? chalk2.green("enabled") : chalk2.yellow("disabled")}`);
30618
- console.log(` Per project: ${sessionConfig.maxPerProject}`);
30619
- console.log(` Max file size: ${formatBytes2(SESSION_SYNC_DEFAULTS.maxFileSize)}`);
30620
- const spinner = ora2("Scanning session files...").start();
30621
- try {
30622
- const claudeDir = getClaudeDir();
30623
- const enabledOpts = {
30624
- enabled: true,
30625
- maxPerProject: sessionConfig.maxPerProject,
30626
- maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
30627
- };
30628
- const withSessionsResult = await scanClaudeDir(claudeDir, undefined, enabledOpts);
30629
- const sessionFiles = withSessionsResult.files.filter((f) => isSessionFile(f.relativePath));
30630
- const disabledOpts = {
30631
- enabled: false,
30632
- maxPerProject: sessionConfig.maxPerProject,
30633
- maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
30634
- };
30635
- const withoutSessionsResult = await scanClaudeDir(claudeDir, undefined, disabledOpts);
30636
- const withoutSessions = withoutSessionsResult.files;
30637
- spinner.stop();
30638
- const totalSessionSize = sessionFiles.reduce((sum, f) => sum + f.size, 0);
30639
- const projects = new Set(sessionFiles.map((f) => f.relativePath.split("/").slice(0, 2).join("/")));
30640
- console.log(chalk2.bold(`
30641
- Preview`));
30642
- console.log(` Projects: ${projects.size}`);
30643
- console.log(` Sessions: ${sessionFiles.length} files`);
30644
- console.log(` Session size: ${formatBytes2(totalSessionSize)}`);
30645
- console.log(` Other files: ${withoutSessions.length}`);
30646
- if (!sessionConfig.enabled) {
30647
- console.log(chalk2.dim(`
30648
- Run 'ccsini sessions enable' to start syncing sessions`));
30649
- }
30650
- } catch (e) {
30651
- spinner.fail(`Scan failed: ${e.message}`);
30652
- }
30653
- });
30654
- }
30655
-
30656
- // src/commands/conflicts.ts
30657
- import { readdir as readdir2, readFile as readFile9, unlink as unlink2, writeFile as writeFile9 } from "fs/promises";
30658
- import { join as join12, relative as relative2 } from "path";
30659
- async function findConflictFiles(dir, base) {
30660
- base = base ?? dir;
30661
- const results = [];
30662
- const entries = await readdir2(dir, { withFileTypes: true });
30663
- for (const entry of entries) {
30664
- const full = join12(dir, entry.name);
30665
- if (entry.isDirectory()) {
30666
- results.push(...await findConflictFiles(full, base));
30667
- } else if (entry.name.endsWith(".conflict")) {
30668
- results.push(relative2(base, full).split("\\").join("/"));
30669
- }
30670
- }
30671
- return results;
30672
- }
30673
- function registerConflictsCommand(program2) {
30674
- program2.command("conflicts").description("List and resolve sync conflicts").option("--keep <side>", "Resolve all: 'local' keeps .conflict, 'remote' discards .conflict").action(async (opts) => {
30675
- const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
30676
- const claudeDir = getClaudeDir();
30677
- const conflictFiles = await findConflictFiles(claudeDir);
30678
- if (conflictFiles.length === 0) {
30679
- console.log(chalk2.green("No conflicts found."));
30680
- return;
30681
- }
30682
- console.log(chalk2.bold(`
30683
- ${conflictFiles.length} conflict(s) found:
30684
- `));
30685
- for (const file of conflictFiles) {
30686
- const original = file.replace(/\.conflict$/, "");
30687
- console.log(` ${chalk2.yellow("\u26A0")} ${original}`);
30688
- console.log(chalk2.dim(` conflict: ~/.claude/${file}`));
30689
- }
30690
- console.log();
30691
- if (opts.keep === "remote") {
30692
- for (const file of conflictFiles) {
30693
- await unlink2(join12(claudeDir, file));
30694
- }
30695
- console.log(chalk2.green(`Resolved ${conflictFiles.length} conflict(s) \u2014 kept remote versions.`));
30696
- return;
30697
- }
30698
- if (opts.keep === "local") {
30699
- for (const file of conflictFiles) {
30700
- const conflictPath = join12(claudeDir, file);
30701
- const originalPath = conflictPath.replace(/\.conflict$/, "");
30702
- const conflictContent = await readFile9(conflictPath);
30703
- await writeFile9(originalPath, conflictContent);
30704
- await unlink2(conflictPath);
30705
- }
30706
- console.log(chalk2.green(`Resolved ${conflictFiles.length} conflict(s) \u2014 kept local versions.`));
30707
- return;
30708
- }
30709
- const inquirer2 = await Promise.resolve().then(() => (init_dist16(), exports_dist));
30710
- for (const file of conflictFiles) {
30711
- const original = file.replace(/\.conflict$/, "");
30712
- const conflictPath = join12(claudeDir, file);
30713
- const originalPath = join12(claudeDir, original);
30714
- const { action } = await inquirer2.default.prompt([
30715
- {
30716
- type: "list",
30717
- name: "action",
30718
- message: `${original}:`,
30719
- choices: [
30720
- { name: "Keep remote (current file)", value: "remote" },
30721
- { name: "Keep local (restore from .conflict)", value: "local" },
30722
- { name: "Skip", value: "skip" }
30723
- ]
30724
- }
30725
- ]);
30726
- if (action === "remote") {
30727
- await unlink2(conflictPath);
30728
- console.log(chalk2.dim(` Kept remote \u2014 deleted ${file}`));
30729
- } else if (action === "local") {
30730
- const conflictContent = await readFile9(conflictPath);
30731
- await writeFile9(originalPath, conflictContent);
30732
- await unlink2(conflictPath);
30733
- console.log(chalk2.dim(` Restored local version`));
30734
- }
30735
- }
30736
- const remaining = await findConflictFiles(claudeDir);
30737
- if (remaining.length === 0) {
30738
- console.log(chalk2.green(`
30739
- All conflicts resolved.`));
30740
- } else {
30741
- console.log(chalk2.yellow(`
30742
- ${remaining.length} conflict(s) remaining.`));
30743
- }
30744
- });
30745
- }
30746
-
30747
- // src/commands/daemon.ts
30748
- import { spawn as spawn2 } from "child_process";
30749
- import { readFile as readFile11, rm as rm2 } from "fs/promises";
30750
- import { platform as platform4 } from "os";
30751
-
30752
- // src/daemon/runner.ts
30753
- import { readFile as readFile10, writeFile as writeFile10 } from "fs/promises";
30754
- import { join as join15 } from "path";
30755
- import { platform as platform3 } from "os";
30756
- import { appendFileSync } from "fs";
30757
- init_auth();
30758
-
30759
- // ../../node_modules/.bun/chokidar@5.0.0/node_modules/chokidar/index.js
30760
- import { EventEmitter } from "events";
30761
- import { stat as statcb, Stats } from "fs";
30762
- import { readdir as readdir4, stat as stat4 } from "fs/promises";
30763
- import * as sp2 from "path";
30764
-
30765
- // ../../node_modules/.bun/readdirp@5.0.0/node_modules/readdirp/index.js
30766
- import { lstat, readdir as readdir3, realpath, stat as stat2 } from "fs/promises";
30767
- import { join as pjoin, relative as prelative, resolve as presolve, sep as psep } from "path";
30768
- import { Readable } from "stream";
30769
- var EntryTypes = {
30770
- FILE_TYPE: "files",
30771
- DIR_TYPE: "directories",
30772
- FILE_DIR_TYPE: "files_directories",
30773
- EVERYTHING_TYPE: "all"
30774
- };
30775
- var defaultOptions = {
30776
- root: ".",
30777
- fileFilter: (_entryInfo) => true,
30778
- directoryFilter: (_entryInfo) => true,
30779
- type: EntryTypes.FILE_TYPE,
30780
- lstat: false,
30781
- depth: 2147483648,
30782
- alwaysStat: false,
30783
- highWaterMark: 4096
30784
- };
30785
- Object.freeze(defaultOptions);
30786
- var RECURSIVE_ERROR_CODE = "READDIRP_RECURSIVE_ERROR";
30787
- var NORMAL_FLOW_ERRORS = new Set(["ENOENT", "EPERM", "EACCES", "ELOOP", RECURSIVE_ERROR_CODE]);
30788
- var ALL_TYPES = [
30789
- EntryTypes.DIR_TYPE,
30790
- EntryTypes.EVERYTHING_TYPE,
30791
- EntryTypes.FILE_DIR_TYPE,
30792
- EntryTypes.FILE_TYPE
30793
- ];
30794
- var DIR_TYPES = new Set([
30795
- EntryTypes.DIR_TYPE,
30796
- EntryTypes.EVERYTHING_TYPE,
30797
- EntryTypes.FILE_DIR_TYPE
30798
- ]);
30799
- var FILE_TYPES = new Set([
30800
- EntryTypes.EVERYTHING_TYPE,
30801
- EntryTypes.FILE_DIR_TYPE,
30802
- EntryTypes.FILE_TYPE
30803
- ]);
30804
- var isNormalFlowError = (error) => NORMAL_FLOW_ERRORS.has(error.code);
30805
- var wantBigintFsStats = process.platform === "win32";
30806
- var emptyFn = (_entryInfo) => true;
30807
- var normalizeFilter = (filter2) => {
30808
- if (filter2 === undefined)
30809
- return emptyFn;
30810
- if (typeof filter2 === "function")
30811
- return filter2;
30812
- if (typeof filter2 === "string") {
30813
- const fl = filter2.trim();
30814
- return (entry) => entry.basename === fl;
30815
- }
30816
- if (Array.isArray(filter2)) {
30817
- const trItems = filter2.map((item) => item.trim());
30818
- return (entry) => trItems.some((f) => entry.basename === f);
30819
- }
30820
- return emptyFn;
30821
- };
30822
-
30823
- class ReaddirpStream extends Readable {
30824
- parents;
30825
- reading;
30826
- parent;
30827
- _stat;
30828
- _maxDepth;
30829
- _wantsDir;
30830
- _wantsFile;
30831
- _wantsEverything;
30832
- _root;
30833
- _isDirent;
30834
- _statsProp;
30835
- _rdOptions;
30836
- _fileFilter;
30837
- _directoryFilter;
30838
- constructor(options = {}) {
30839
- super({
30840
- objectMode: true,
30841
- autoDestroy: true,
30842
- highWaterMark: options.highWaterMark
30843
- });
30844
- const opts = { ...defaultOptions, ...options };
30845
- const { root, type } = opts;
30846
- this._fileFilter = normalizeFilter(opts.fileFilter);
30847
- this._directoryFilter = normalizeFilter(opts.directoryFilter);
30848
- const statMethod = opts.lstat ? lstat : stat2;
30849
- if (wantBigintFsStats) {
30850
- this._stat = (path2) => statMethod(path2, { bigint: true });
30851
- } else {
30852
- this._stat = statMethod;
30853
- }
30854
- this._maxDepth = opts.depth != null && Number.isSafeInteger(opts.depth) ? opts.depth : defaultOptions.depth;
30855
- this._wantsDir = type ? DIR_TYPES.has(type) : false;
30856
- this._wantsFile = type ? FILE_TYPES.has(type) : false;
30857
- this._wantsEverything = type === EntryTypes.EVERYTHING_TYPE;
30858
- this._root = presolve(root);
30859
- this._isDirent = !opts.alwaysStat;
30860
- this._statsProp = this._isDirent ? "dirent" : "stats";
30861
- this._rdOptions = { encoding: "utf8", withFileTypes: this._isDirent };
30862
- this.parents = [this._exploreDir(root, 1)];
30863
- this.reading = false;
30864
- this.parent = undefined;
30865
- }
30866
- async _read(batch) {
30867
- if (this.reading)
30868
- return;
30869
- this.reading = true;
30870
- try {
30871
- while (!this.destroyed && batch > 0) {
30872
- const par = this.parent;
30873
- const fil = par && par.files;
30874
- if (fil && fil.length > 0) {
30875
- const { path: path2, depth } = par;
30876
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path2));
30877
- const awaited = await Promise.all(slice);
30878
- for (const entry of awaited) {
30879
- if (!entry)
30880
- continue;
30881
- if (this.destroyed)
30882
- return;
30883
- const entryType = await this._getEntryType(entry);
30884
- if (entryType === "directory" && this._directoryFilter(entry)) {
30885
- if (depth <= this._maxDepth) {
30886
- this.parents.push(this._exploreDir(entry.fullPath, depth + 1));
30887
- }
30888
- if (this._wantsDir) {
30889
- this.push(entry);
30890
- batch--;
30891
- }
30892
- } else if ((entryType === "file" || this._includeAsFile(entry)) && this._fileFilter(entry)) {
30893
- if (this._wantsFile) {
30894
- this.push(entry);
30895
- batch--;
30896
- }
30897
- }
30898
- }
30899
- } else {
30900
- const parent = this.parents.pop();
30901
- if (!parent) {
30902
- this.push(null);
30903
- break;
30904
- }
30905
- this.parent = await parent;
30906
- if (this.destroyed)
30907
- return;
30908
- }
30909
- }
30910
- } catch (error) {
30911
- this.destroy(error);
30912
- } finally {
30913
- this.reading = false;
30914
- }
30915
- }
30916
- async _exploreDir(path2, depth) {
30917
- let files;
30918
- try {
30919
- files = await readdir3(path2, this._rdOptions);
30920
- } catch (error) {
30921
- this._onError(error);
30922
- }
30923
- return { files, depth, path: path2 };
30924
- }
30925
- async _formatEntry(dirent, path2) {
30926
- let entry;
30927
- const basename = this._isDirent ? dirent.name : dirent;
30928
- try {
30929
- const fullPath = presolve(pjoin(path2, basename));
30930
- entry = { path: prelative(this._root, fullPath), fullPath, basename };
30931
- entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
30932
- } catch (err) {
30933
- this._onError(err);
30934
- return;
30935
- }
30936
- return entry;
30937
- }
30938
- _onError(err) {
30939
- if (isNormalFlowError(err) && !this.destroyed) {
30940
- this.emit("warn", err);
30941
- } else {
30942
- this.destroy(err);
30943
- }
30944
- }
30945
- async _getEntryType(entry) {
30946
- if (!entry && this._statsProp in entry) {
30947
- return "";
30948
- }
30949
- const stats = entry[this._statsProp];
30950
- if (stats.isFile())
30951
- return "file";
30952
- if (stats.isDirectory())
30953
- return "directory";
30954
- if (stats && stats.isSymbolicLink()) {
30955
- const full = entry.fullPath;
30956
- try {
30957
- const entryRealPath = await realpath(full);
30958
- const entryRealPathStats = await lstat(entryRealPath);
30959
- if (entryRealPathStats.isFile()) {
30960
- return "file";
30961
- }
30962
- if (entryRealPathStats.isDirectory()) {
30963
- const len = entryRealPath.length;
30964
- if (full.startsWith(entryRealPath) && full.substr(len, 1) === psep) {
30965
- const recursiveError = new Error(`Circular symlink detected: "${full}" points to "${entryRealPath}"`);
30966
- recursiveError.code = RECURSIVE_ERROR_CODE;
30967
- return this._onError(recursiveError);
30968
- }
30969
- return "directory";
30970
- }
30971
- } catch (error) {
30972
- this._onError(error);
30973
- return "";
30974
- }
30975
- }
30976
- }
30977
- _includeAsFile(entry) {
30978
- const stats = entry && entry[this._statsProp];
30979
- return stats && this._wantsEverything && !stats.isDirectory();
30980
- }
30981
- }
30982
- function readdirp(root, options = {}) {
30983
- let type = options.entryType || options.type;
30984
- if (type === "both")
30985
- type = EntryTypes.FILE_DIR_TYPE;
30986
- if (type)
30987
- options.type = type;
30988
- if (!root) {
30989
- throw new Error("readdirp: root argument is required. Usage: readdirp(root, options)");
30990
- } else if (typeof root !== "string") {
30991
- throw new TypeError("readdirp: root argument must be a string. Usage: readdirp(root, options)");
30992
- } else if (type && !ALL_TYPES.includes(type)) {
30993
- throw new Error(`readdirp: Invalid type passed. Use one of ${ALL_TYPES.join(", ")}`);
30994
- }
30995
- options.root = root;
30996
- return new ReaddirpStream(options);
30997
- }
30998
-
30999
- // ../../node_modules/.bun/chokidar@5.0.0/node_modules/chokidar/handler.js
31000
- import { watch as fs_watch, unwatchFile, watchFile } from "fs";
31001
- import { realpath as fsrealpath, lstat as lstat2, open, stat as stat3 } from "fs/promises";
31002
- import { type as osType } from "os";
31003
- import * as sp from "path";
31004
- var STR_DATA = "data";
31005
- var STR_END = "end";
31006
- var STR_CLOSE = "close";
31007
- var EMPTY_FN = () => {};
31008
- var pl = process.platform;
31009
- var isWindows = pl === "win32";
31010
- var isMacos = pl === "darwin";
31011
- var isLinux = pl === "linux";
31012
- var isFreeBSD = pl === "freebsd";
31013
- var isIBMi = osType() === "OS400";
31014
- var EVENTS = {
31015
- ALL: "all",
31016
- READY: "ready",
31017
- ADD: "add",
31018
- CHANGE: "change",
31019
- ADD_DIR: "addDir",
31020
- UNLINK: "unlink",
31021
- UNLINK_DIR: "unlinkDir",
31022
- RAW: "raw",
31023
- ERROR: "error"
30231
+ // ../../node_modules/.bun/chokidar@5.0.0/node_modules/chokidar/handler.js
30232
+ import { watch as fs_watch, unwatchFile, watchFile } from "fs";
30233
+ import { realpath as fsrealpath, lstat as lstat2, open, stat as stat3 } from "fs/promises";
30234
+ import { type as osType } from "os";
30235
+ import * as sp from "path";
30236
+ var STR_DATA = "data";
30237
+ var STR_END = "end";
30238
+ var STR_CLOSE = "close";
30239
+ var EMPTY_FN = () => {};
30240
+ var pl = process.platform;
30241
+ var isWindows = pl === "win32";
30242
+ var isMacos = pl === "darwin";
30243
+ var isLinux = pl === "linux";
30244
+ var isFreeBSD = pl === "freebsd";
30245
+ var isIBMi = osType() === "OS400";
30246
+ var EVENTS = {
30247
+ ALL: "all",
30248
+ READY: "ready",
30249
+ ADD: "add",
30250
+ CHANGE: "change",
30251
+ ADD_DIR: "addDir",
30252
+ UNLINK: "unlink",
30253
+ UNLINK_DIR: "unlinkDir",
30254
+ RAW: "raw",
30255
+ ERROR: "error"
31024
30256
  };
31025
30257
  var EV = EVENTS;
31026
30258
  var THROTTLE_MODE_WATCH = "watch";
@@ -31300,1260 +30532,2066 @@ var foreach = (val, fn) => {
31300
30532
  fn(val);
31301
30533
  }
31302
30534
  };
31303
- var addAndConvert = (main2, prop, item) => {
31304
- let container = main2[prop];
31305
- if (!(container instanceof Set)) {
31306
- main2[prop] = container = new Set([container]);
30535
+ var addAndConvert = (main2, prop, item) => {
30536
+ let container = main2[prop];
30537
+ if (!(container instanceof Set)) {
30538
+ main2[prop] = container = new Set([container]);
30539
+ }
30540
+ container.add(item);
30541
+ };
30542
+ var clearItem = (cont) => (key) => {
30543
+ const set = cont[key];
30544
+ if (set instanceof Set) {
30545
+ set.clear();
30546
+ } else {
30547
+ delete cont[key];
30548
+ }
30549
+ };
30550
+ var delFromSet = (main2, prop, item) => {
30551
+ const container = main2[prop];
30552
+ if (container instanceof Set) {
30553
+ container.delete(item);
30554
+ } else if (container === item) {
30555
+ delete main2[prop];
30556
+ }
30557
+ };
30558
+ var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
30559
+ var FsWatchInstances = new Map;
30560
+ function createFsWatchInstance(path2, options, listener, errHandler, emitRaw) {
30561
+ const handleEvent = (rawEvent, evPath) => {
30562
+ listener(path2);
30563
+ emitRaw(rawEvent, evPath, { watchedPath: path2 });
30564
+ if (evPath && path2 !== evPath) {
30565
+ fsWatchBroadcast(sp.resolve(path2, evPath), KEY_LISTENERS, sp.join(path2, evPath));
30566
+ }
30567
+ };
30568
+ try {
30569
+ return fs_watch(path2, {
30570
+ persistent: options.persistent
30571
+ }, handleEvent);
30572
+ } catch (error) {
30573
+ errHandler(error);
30574
+ return;
30575
+ }
30576
+ }
30577
+ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
30578
+ const cont = FsWatchInstances.get(fullPath);
30579
+ if (!cont)
30580
+ return;
30581
+ foreach(cont[listenerType], (listener) => {
30582
+ listener(val1, val2, val3);
30583
+ });
30584
+ };
30585
+ var setFsWatchListener = (path2, fullPath, options, handlers) => {
30586
+ const { listener, errHandler, rawEmitter } = handlers;
30587
+ let cont = FsWatchInstances.get(fullPath);
30588
+ let watcher;
30589
+ if (!options.persistent) {
30590
+ watcher = createFsWatchInstance(path2, options, listener, errHandler, rawEmitter);
30591
+ if (!watcher)
30592
+ return;
30593
+ return watcher.close.bind(watcher);
30594
+ }
30595
+ if (cont) {
30596
+ addAndConvert(cont, KEY_LISTENERS, listener);
30597
+ addAndConvert(cont, KEY_ERR, errHandler);
30598
+ addAndConvert(cont, KEY_RAW, rawEmitter);
30599
+ } else {
30600
+ watcher = createFsWatchInstance(path2, options, fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS), errHandler, fsWatchBroadcast.bind(null, fullPath, KEY_RAW));
30601
+ if (!watcher)
30602
+ return;
30603
+ watcher.on(EV.ERROR, async (error) => {
30604
+ const broadcastErr = fsWatchBroadcast.bind(null, fullPath, KEY_ERR);
30605
+ if (cont)
30606
+ cont.watcherUnusable = true;
30607
+ if (isWindows && error.code === "EPERM") {
30608
+ try {
30609
+ const fd = await open(path2, "r");
30610
+ await fd.close();
30611
+ broadcastErr(error);
30612
+ } catch (err) {}
30613
+ } else {
30614
+ broadcastErr(error);
30615
+ }
30616
+ });
30617
+ cont = {
30618
+ listeners: listener,
30619
+ errHandlers: errHandler,
30620
+ rawEmitters: rawEmitter,
30621
+ watcher
30622
+ };
30623
+ FsWatchInstances.set(fullPath, cont);
30624
+ }
30625
+ return () => {
30626
+ delFromSet(cont, KEY_LISTENERS, listener);
30627
+ delFromSet(cont, KEY_ERR, errHandler);
30628
+ delFromSet(cont, KEY_RAW, rawEmitter);
30629
+ if (isEmptySet(cont.listeners)) {
30630
+ cont.watcher.close();
30631
+ FsWatchInstances.delete(fullPath);
30632
+ HANDLER_KEYS.forEach(clearItem(cont));
30633
+ cont.watcher = undefined;
30634
+ Object.freeze(cont);
30635
+ }
30636
+ };
30637
+ };
30638
+ var FsWatchFileInstances = new Map;
30639
+ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
30640
+ const { listener, rawEmitter } = handlers;
30641
+ let cont = FsWatchFileInstances.get(fullPath);
30642
+ const copts = cont && cont.options;
30643
+ if (copts && (copts.persistent < options.persistent || copts.interval > options.interval)) {
30644
+ unwatchFile(fullPath);
30645
+ cont = undefined;
30646
+ }
30647
+ if (cont) {
30648
+ addAndConvert(cont, KEY_LISTENERS, listener);
30649
+ addAndConvert(cont, KEY_RAW, rawEmitter);
30650
+ } else {
30651
+ cont = {
30652
+ listeners: listener,
30653
+ rawEmitters: rawEmitter,
30654
+ options,
30655
+ watcher: watchFile(fullPath, options, (curr, prev) => {
30656
+ foreach(cont.rawEmitters, (rawEmitter2) => {
30657
+ rawEmitter2(EV.CHANGE, fullPath, { curr, prev });
30658
+ });
30659
+ const currmtime = curr.mtimeMs;
30660
+ if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
30661
+ foreach(cont.listeners, (listener2) => listener2(path2, curr));
30662
+ }
30663
+ })
30664
+ };
30665
+ FsWatchFileInstances.set(fullPath, cont);
30666
+ }
30667
+ return () => {
30668
+ delFromSet(cont, KEY_LISTENERS, listener);
30669
+ delFromSet(cont, KEY_RAW, rawEmitter);
30670
+ if (isEmptySet(cont.listeners)) {
30671
+ FsWatchFileInstances.delete(fullPath);
30672
+ unwatchFile(fullPath);
30673
+ cont.options = cont.watcher = undefined;
30674
+ Object.freeze(cont);
30675
+ }
30676
+ };
30677
+ };
30678
+
30679
+ class NodeFsHandler {
30680
+ fsw;
30681
+ _boundHandleError;
30682
+ constructor(fsW) {
30683
+ this.fsw = fsW;
30684
+ this._boundHandleError = (error) => fsW._handleError(error);
30685
+ }
30686
+ _watchWithNodeFs(path2, listener) {
30687
+ const opts = this.fsw.options;
30688
+ const directory = sp.dirname(path2);
30689
+ const basename2 = sp.basename(path2);
30690
+ const parent = this.fsw._getWatchedDir(directory);
30691
+ parent.add(basename2);
30692
+ const absolutePath = sp.resolve(path2);
30693
+ const options = {
30694
+ persistent: opts.persistent
30695
+ };
30696
+ if (!listener)
30697
+ listener = EMPTY_FN;
30698
+ let closer;
30699
+ if (opts.usePolling) {
30700
+ const enableBin = opts.interval !== opts.binaryInterval;
30701
+ options.interval = enableBin && isBinaryPath(basename2) ? opts.binaryInterval : opts.interval;
30702
+ closer = setFsWatchFileListener(path2, absolutePath, options, {
30703
+ listener,
30704
+ rawEmitter: this.fsw._emitRaw
30705
+ });
30706
+ } else {
30707
+ closer = setFsWatchListener(path2, absolutePath, options, {
30708
+ listener,
30709
+ errHandler: this._boundHandleError,
30710
+ rawEmitter: this.fsw._emitRaw
30711
+ });
30712
+ }
30713
+ return closer;
30714
+ }
30715
+ _handleFile(file, stats, initialAdd) {
30716
+ if (this.fsw.closed) {
30717
+ return;
30718
+ }
30719
+ const dirname3 = sp.dirname(file);
30720
+ const basename2 = sp.basename(file);
30721
+ const parent = this.fsw._getWatchedDir(dirname3);
30722
+ let prevStats = stats;
30723
+ if (parent.has(basename2))
30724
+ return;
30725
+ const listener = async (path2, newStats) => {
30726
+ if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
30727
+ return;
30728
+ if (!newStats || newStats.mtimeMs === 0) {
30729
+ try {
30730
+ const newStats2 = await stat3(file);
30731
+ if (this.fsw.closed)
30732
+ return;
30733
+ const at = newStats2.atimeMs;
30734
+ const mt = newStats2.mtimeMs;
30735
+ if (!at || at <= mt || mt !== prevStats.mtimeMs) {
30736
+ this.fsw._emit(EV.CHANGE, file, newStats2);
30737
+ }
30738
+ if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
30739
+ this.fsw._closeFile(path2);
30740
+ prevStats = newStats2;
30741
+ const closer2 = this._watchWithNodeFs(file, listener);
30742
+ if (closer2)
30743
+ this.fsw._addPathCloser(path2, closer2);
30744
+ } else {
30745
+ prevStats = newStats2;
30746
+ }
30747
+ } catch (error) {
30748
+ this.fsw._remove(dirname3, basename2);
30749
+ }
30750
+ } else if (parent.has(basename2)) {
30751
+ const at = newStats.atimeMs;
30752
+ const mt = newStats.mtimeMs;
30753
+ if (!at || at <= mt || mt !== prevStats.mtimeMs) {
30754
+ this.fsw._emit(EV.CHANGE, file, newStats);
30755
+ }
30756
+ prevStats = newStats;
30757
+ }
30758
+ };
30759
+ const closer = this._watchWithNodeFs(file, listener);
30760
+ if (!(initialAdd && this.fsw.options.ignoreInitial) && this.fsw._isntIgnored(file)) {
30761
+ if (!this.fsw._throttle(EV.ADD, file, 0))
30762
+ return;
30763
+ this.fsw._emit(EV.ADD, file, stats);
30764
+ }
30765
+ return closer;
30766
+ }
30767
+ async _handleSymlink(entry, directory, path2, item) {
30768
+ if (this.fsw.closed) {
30769
+ return;
30770
+ }
30771
+ const full = entry.fullPath;
30772
+ const dir = this.fsw._getWatchedDir(directory);
30773
+ if (!this.fsw.options.followSymlinks) {
30774
+ this.fsw._incrReadyCount();
30775
+ let linkPath;
30776
+ try {
30777
+ linkPath = await fsrealpath(path2);
30778
+ } catch (e) {
30779
+ this.fsw._emitReady();
30780
+ return true;
30781
+ }
30782
+ if (this.fsw.closed)
30783
+ return;
30784
+ if (dir.has(item)) {
30785
+ if (this.fsw._symlinkPaths.get(full) !== linkPath) {
30786
+ this.fsw._symlinkPaths.set(full, linkPath);
30787
+ this.fsw._emit(EV.CHANGE, path2, entry.stats);
30788
+ }
30789
+ } else {
30790
+ dir.add(item);
30791
+ this.fsw._symlinkPaths.set(full, linkPath);
30792
+ this.fsw._emit(EV.ADD, path2, entry.stats);
30793
+ }
30794
+ this.fsw._emitReady();
30795
+ return true;
30796
+ }
30797
+ if (this.fsw._symlinkPaths.has(full)) {
30798
+ return true;
30799
+ }
30800
+ this.fsw._symlinkPaths.set(full, true);
30801
+ }
30802
+ _handleRead(directory, initialAdd, wh, target, dir, depth, throttler) {
30803
+ directory = sp.join(directory, "");
30804
+ const throttleKey = target ? `${directory}:${target}` : directory;
30805
+ throttler = this.fsw._throttle("readdir", throttleKey, 1000);
30806
+ if (!throttler)
30807
+ return;
30808
+ const previous = this.fsw._getWatchedDir(wh.path);
30809
+ const current = new Set;
30810
+ let stream = this.fsw._readdirp(directory, {
30811
+ fileFilter: (entry) => wh.filterPath(entry),
30812
+ directoryFilter: (entry) => wh.filterDir(entry)
30813
+ });
30814
+ if (!stream)
30815
+ return;
30816
+ stream.on(STR_DATA, async (entry) => {
30817
+ if (this.fsw.closed) {
30818
+ stream = undefined;
30819
+ return;
30820
+ }
30821
+ const item = entry.path;
30822
+ let path2 = sp.join(directory, item);
30823
+ current.add(item);
30824
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path2, item)) {
30825
+ return;
30826
+ }
30827
+ if (this.fsw.closed) {
30828
+ stream = undefined;
30829
+ return;
30830
+ }
30831
+ if (item === target || !target && !previous.has(item)) {
30832
+ this.fsw._incrReadyCount();
30833
+ path2 = sp.join(dir, sp.relative(dir, path2));
30834
+ this._addToNodeFs(path2, initialAdd, wh, depth + 1);
30835
+ }
30836
+ }).on(EV.ERROR, this._boundHandleError);
30837
+ return new Promise((resolve2, reject) => {
30838
+ if (!stream)
30839
+ return reject();
30840
+ stream.once(STR_END, () => {
30841
+ if (this.fsw.closed) {
30842
+ stream = undefined;
30843
+ return;
30844
+ }
30845
+ const wasThrottled = throttler ? throttler.clear() : false;
30846
+ resolve2(undefined);
30847
+ previous.getChildren().filter((item) => {
30848
+ return item !== directory && !current.has(item);
30849
+ }).forEach((item) => {
30850
+ this.fsw._remove(directory, item);
30851
+ });
30852
+ stream = undefined;
30853
+ if (wasThrottled)
30854
+ this._handleRead(directory, false, wh, target, dir, depth, throttler);
30855
+ });
30856
+ });
30857
+ }
30858
+ async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath2) {
30859
+ const parentDir = this.fsw._getWatchedDir(sp.dirname(dir));
30860
+ const tracked = parentDir.has(sp.basename(dir));
30861
+ if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) {
30862
+ this.fsw._emit(EV.ADD_DIR, dir, stats);
30863
+ }
30864
+ parentDir.add(sp.basename(dir));
30865
+ this.fsw._getWatchedDir(dir);
30866
+ let throttler;
30867
+ let closer;
30868
+ const oDepth = this.fsw.options.depth;
30869
+ if ((oDepth == null || depth <= oDepth) && !this.fsw._symlinkPaths.has(realpath2)) {
30870
+ if (!target) {
30871
+ await this._handleRead(dir, initialAdd, wh, target, dir, depth, throttler);
30872
+ if (this.fsw.closed)
30873
+ return;
30874
+ }
30875
+ closer = this._watchWithNodeFs(dir, (dirPath, stats2) => {
30876
+ if (stats2 && stats2.mtimeMs === 0)
30877
+ return;
30878
+ this._handleRead(dirPath, false, wh, target, dir, depth, throttler);
30879
+ });
30880
+ }
30881
+ return closer;
30882
+ }
30883
+ async _addToNodeFs(path2, initialAdd, priorWh, depth, target) {
30884
+ const ready = this.fsw._emitReady;
30885
+ if (this.fsw._isIgnored(path2) || this.fsw.closed) {
30886
+ ready();
30887
+ return false;
30888
+ }
30889
+ const wh = this.fsw._getWatchHelpers(path2);
30890
+ if (priorWh) {
30891
+ wh.filterPath = (entry) => priorWh.filterPath(entry);
30892
+ wh.filterDir = (entry) => priorWh.filterDir(entry);
30893
+ }
30894
+ try {
30895
+ const stats = await statMethods[wh.statMethod](wh.watchPath);
30896
+ if (this.fsw.closed)
30897
+ return;
30898
+ if (this.fsw._isIgnored(wh.watchPath, stats)) {
30899
+ ready();
30900
+ return false;
30901
+ }
30902
+ const follow = this.fsw.options.followSymlinks;
30903
+ let closer;
30904
+ if (stats.isDirectory()) {
30905
+ const absPath = sp.resolve(path2);
30906
+ const targetPath = follow ? await fsrealpath(path2) : path2;
30907
+ if (this.fsw.closed)
30908
+ return;
30909
+ closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
30910
+ if (this.fsw.closed)
30911
+ return;
30912
+ if (absPath !== targetPath && targetPath !== undefined) {
30913
+ this.fsw._symlinkPaths.set(absPath, targetPath);
30914
+ }
30915
+ } else if (stats.isSymbolicLink()) {
30916
+ const targetPath = follow ? await fsrealpath(path2) : path2;
30917
+ if (this.fsw.closed)
30918
+ return;
30919
+ const parent = sp.dirname(wh.watchPath);
30920
+ this.fsw._getWatchedDir(parent).add(wh.watchPath);
30921
+ this.fsw._emit(EV.ADD, wh.watchPath, stats);
30922
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path2, wh, targetPath);
30923
+ if (this.fsw.closed)
30924
+ return;
30925
+ if (targetPath !== undefined) {
30926
+ this.fsw._symlinkPaths.set(sp.resolve(path2), targetPath);
30927
+ }
30928
+ } else {
30929
+ closer = this._handleFile(wh.watchPath, stats, initialAdd);
30930
+ }
30931
+ ready();
30932
+ if (closer)
30933
+ this.fsw._addPathCloser(path2, closer);
30934
+ return false;
30935
+ } catch (error) {
30936
+ if (this.fsw._handleError(error)) {
30937
+ ready();
30938
+ return path2;
30939
+ }
30940
+ }
30941
+ }
30942
+ }
30943
+
30944
+ // ../../node_modules/.bun/chokidar@5.0.0/node_modules/chokidar/index.js
30945
+ /*! chokidar - MIT License (c) 2012 Paul Miller (paulmillr.com) */
30946
+ var SLASH = "/";
30947
+ var SLASH_SLASH = "//";
30948
+ var ONE_DOT = ".";
30949
+ var TWO_DOTS = "..";
30950
+ var STRING_TYPE = "string";
30951
+ var BACK_SLASH_RE = /\\/g;
30952
+ var DOUBLE_SLASH_RE = /\/\//g;
30953
+ var DOT_RE = /\..*\.(sw[px])$|~$|\.subl.*\.tmp/;
30954
+ var REPLACER_RE = /^\.[/\\]/;
30955
+ function arrify(item) {
30956
+ return Array.isArray(item) ? item : [item];
30957
+ }
30958
+ var isMatcherObject = (matcher) => typeof matcher === "object" && matcher !== null && !(matcher instanceof RegExp);
30959
+ function createPattern(matcher) {
30960
+ if (typeof matcher === "function")
30961
+ return matcher;
30962
+ if (typeof matcher === "string")
30963
+ return (string) => matcher === string;
30964
+ if (matcher instanceof RegExp)
30965
+ return (string) => matcher.test(string);
30966
+ if (typeof matcher === "object" && matcher !== null) {
30967
+ return (string) => {
30968
+ if (matcher.path === string)
30969
+ return true;
30970
+ if (matcher.recursive) {
30971
+ const relative4 = sp2.relative(matcher.path, string);
30972
+ if (!relative4) {
30973
+ return false;
30974
+ }
30975
+ return !relative4.startsWith("..") && !sp2.isAbsolute(relative4);
30976
+ }
30977
+ return false;
30978
+ };
30979
+ }
30980
+ return () => false;
30981
+ }
30982
+ function normalizePath(path2) {
30983
+ if (typeof path2 !== "string")
30984
+ throw new Error("string expected");
30985
+ path2 = sp2.normalize(path2);
30986
+ path2 = path2.replace(/\\/g, "/");
30987
+ let prepend = false;
30988
+ if (path2.startsWith("//"))
30989
+ prepend = true;
30990
+ path2 = path2.replace(DOUBLE_SLASH_RE, "/");
30991
+ if (prepend)
30992
+ path2 = "/" + path2;
30993
+ return path2;
30994
+ }
30995
+ function matchPatterns(patterns, testString, stats) {
30996
+ const path2 = normalizePath(testString);
30997
+ for (let index = 0;index < patterns.length; index++) {
30998
+ const pattern = patterns[index];
30999
+ if (pattern(path2, stats)) {
31000
+ return true;
31001
+ }
31002
+ }
31003
+ return false;
31004
+ }
31005
+ function anymatch(matchers, testString) {
31006
+ if (matchers == null) {
31007
+ throw new TypeError("anymatch: specify first argument");
31008
+ }
31009
+ const matchersArray = arrify(matchers);
31010
+ const patterns = matchersArray.map((matcher) => createPattern(matcher));
31011
+ if (testString == null) {
31012
+ return (testString2, stats) => {
31013
+ return matchPatterns(patterns, testString2, stats);
31014
+ };
31015
+ }
31016
+ return matchPatterns(patterns, testString);
31017
+ }
31018
+ var unifyPaths = (paths_) => {
31019
+ const paths = arrify(paths_).flat();
31020
+ if (!paths.every((p) => typeof p === STRING_TYPE)) {
31021
+ throw new TypeError(`Non-string provided as watch path: ${paths}`);
31022
+ }
31023
+ return paths.map(normalizePathToUnix);
31024
+ };
31025
+ var toUnix = (string) => {
31026
+ let str = string.replace(BACK_SLASH_RE, SLASH);
31027
+ let prepend = false;
31028
+ if (str.startsWith(SLASH_SLASH)) {
31029
+ prepend = true;
31307
31030
  }
31308
- container.add(item);
31031
+ str = str.replace(DOUBLE_SLASH_RE, SLASH);
31032
+ if (prepend) {
31033
+ str = SLASH + str;
31034
+ }
31035
+ return str;
31309
31036
  };
31310
- var clearItem = (cont) => (key) => {
31311
- const set = cont[key];
31312
- if (set instanceof Set) {
31313
- set.clear();
31037
+ var normalizePathToUnix = (path2) => toUnix(sp2.normalize(toUnix(path2)));
31038
+ var normalizeIgnored = (cwd = "") => (path2) => {
31039
+ if (typeof path2 === "string") {
31040
+ return normalizePathToUnix(sp2.isAbsolute(path2) ? path2 : sp2.join(cwd, path2));
31314
31041
  } else {
31315
- delete cont[key];
31042
+ return path2;
31316
31043
  }
31317
31044
  };
31318
- var delFromSet = (main2, prop, item) => {
31319
- const container = main2[prop];
31320
- if (container instanceof Set) {
31321
- container.delete(item);
31322
- } else if (container === item) {
31323
- delete main2[prop];
31045
+ var getAbsolutePath = (path2, cwd) => {
31046
+ if (sp2.isAbsolute(path2)) {
31047
+ return path2;
31324
31048
  }
31049
+ return sp2.join(cwd, path2);
31325
31050
  };
31326
- var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
31327
- var FsWatchInstances = new Map;
31328
- function createFsWatchInstance(path2, options, listener, errHandler, emitRaw) {
31329
- const handleEvent = (rawEvent, evPath) => {
31330
- listener(path2);
31331
- emitRaw(rawEvent, evPath, { watchedPath: path2 });
31332
- if (evPath && path2 !== evPath) {
31333
- fsWatchBroadcast(sp.resolve(path2, evPath), KEY_LISTENERS, sp.join(path2, evPath));
31334
- }
31335
- };
31336
- try {
31337
- return fs_watch(path2, {
31338
- persistent: options.persistent
31339
- }, handleEvent);
31340
- } catch (error) {
31341
- errHandler(error);
31342
- return;
31051
+ var EMPTY_SET = Object.freeze(new Set);
31052
+
31053
+ class DirEntry {
31054
+ path;
31055
+ _removeWatcher;
31056
+ items;
31057
+ constructor(dir, removeWatcher) {
31058
+ this.path = dir;
31059
+ this._removeWatcher = removeWatcher;
31060
+ this.items = new Set;
31343
31061
  }
31344
- }
31345
- var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
31346
- const cont = FsWatchInstances.get(fullPath);
31347
- if (!cont)
31348
- return;
31349
- foreach(cont[listenerType], (listener) => {
31350
- listener(val1, val2, val3);
31351
- });
31352
- };
31353
- var setFsWatchListener = (path2, fullPath, options, handlers) => {
31354
- const { listener, errHandler, rawEmitter } = handlers;
31355
- let cont = FsWatchInstances.get(fullPath);
31356
- let watcher;
31357
- if (!options.persistent) {
31358
- watcher = createFsWatchInstance(path2, options, listener, errHandler, rawEmitter);
31359
- if (!watcher)
31062
+ add(item) {
31063
+ const { items } = this;
31064
+ if (!items)
31360
31065
  return;
31361
- return watcher.close.bind(watcher);
31066
+ if (item !== ONE_DOT && item !== TWO_DOTS)
31067
+ items.add(item);
31362
31068
  }
31363
- if (cont) {
31364
- addAndConvert(cont, KEY_LISTENERS, listener);
31365
- addAndConvert(cont, KEY_ERR, errHandler);
31366
- addAndConvert(cont, KEY_RAW, rawEmitter);
31367
- } else {
31368
- watcher = createFsWatchInstance(path2, options, fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS), errHandler, fsWatchBroadcast.bind(null, fullPath, KEY_RAW));
31369
- if (!watcher)
31069
+ async remove(item) {
31070
+ const { items } = this;
31071
+ if (!items)
31370
31072
  return;
31371
- watcher.on(EV.ERROR, async (error) => {
31372
- const broadcastErr = fsWatchBroadcast.bind(null, fullPath, KEY_ERR);
31373
- if (cont)
31374
- cont.watcherUnusable = true;
31375
- if (isWindows && error.code === "EPERM") {
31376
- try {
31377
- const fd = await open(path2, "r");
31378
- await fd.close();
31379
- broadcastErr(error);
31380
- } catch (err) {}
31381
- } else {
31382
- broadcastErr(error);
31073
+ items.delete(item);
31074
+ if (items.size > 0)
31075
+ return;
31076
+ const dir = this.path;
31077
+ try {
31078
+ await readdir3(dir);
31079
+ } catch (err) {
31080
+ if (this._removeWatcher) {
31081
+ this._removeWatcher(sp2.dirname(dir), sp2.basename(dir));
31082
+ }
31083
+ }
31084
+ }
31085
+ has(item) {
31086
+ const { items } = this;
31087
+ if (!items)
31088
+ return;
31089
+ return items.has(item);
31090
+ }
31091
+ getChildren() {
31092
+ const { items } = this;
31093
+ if (!items)
31094
+ return [];
31095
+ return [...items.values()];
31096
+ }
31097
+ dispose() {
31098
+ this.items.clear();
31099
+ this.path = "";
31100
+ this._removeWatcher = EMPTY_FN;
31101
+ this.items = EMPTY_SET;
31102
+ Object.freeze(this);
31103
+ }
31104
+ }
31105
+ var STAT_METHOD_F = "stat";
31106
+ var STAT_METHOD_L = "lstat";
31107
+
31108
+ class WatchHelper {
31109
+ fsw;
31110
+ path;
31111
+ watchPath;
31112
+ fullWatchPath;
31113
+ dirParts;
31114
+ followSymlinks;
31115
+ statMethod;
31116
+ constructor(path2, follow, fsw) {
31117
+ this.fsw = fsw;
31118
+ const watchPath = path2;
31119
+ this.path = path2 = path2.replace(REPLACER_RE, "");
31120
+ this.watchPath = watchPath;
31121
+ this.fullWatchPath = sp2.resolve(watchPath);
31122
+ this.dirParts = [];
31123
+ this.dirParts.forEach((parts) => {
31124
+ if (parts.length > 1)
31125
+ parts.pop();
31126
+ });
31127
+ this.followSymlinks = follow;
31128
+ this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L;
31129
+ }
31130
+ entryPath(entry) {
31131
+ return sp2.join(this.watchPath, sp2.relative(this.watchPath, entry.fullPath));
31132
+ }
31133
+ filterPath(entry) {
31134
+ const { stats } = entry;
31135
+ if (stats && stats.isSymbolicLink())
31136
+ return this.filterDir(entry);
31137
+ const resolvedPath = this.entryPath(entry);
31138
+ return this.fsw._isntIgnored(resolvedPath, stats) && this.fsw._hasReadPermissions(stats);
31139
+ }
31140
+ filterDir(entry) {
31141
+ return this.fsw._isntIgnored(this.entryPath(entry), entry.stats);
31142
+ }
31143
+ }
31144
+
31145
+ class FSWatcher extends EventEmitter {
31146
+ closed;
31147
+ options;
31148
+ _closers;
31149
+ _ignoredPaths;
31150
+ _throttled;
31151
+ _streams;
31152
+ _symlinkPaths;
31153
+ _watched;
31154
+ _pendingWrites;
31155
+ _pendingUnlinks;
31156
+ _readyCount;
31157
+ _emitReady;
31158
+ _closePromise;
31159
+ _userIgnored;
31160
+ _readyEmitted;
31161
+ _emitRaw;
31162
+ _boundRemove;
31163
+ _nodeFsHandler;
31164
+ constructor(_opts = {}) {
31165
+ super();
31166
+ this.closed = false;
31167
+ this._closers = new Map;
31168
+ this._ignoredPaths = new Set;
31169
+ this._throttled = new Map;
31170
+ this._streams = new Set;
31171
+ this._symlinkPaths = new Map;
31172
+ this._watched = new Map;
31173
+ this._pendingWrites = new Map;
31174
+ this._pendingUnlinks = new Map;
31175
+ this._readyCount = 0;
31176
+ this._readyEmitted = false;
31177
+ const awf = _opts.awaitWriteFinish;
31178
+ const DEF_AWF = { stabilityThreshold: 2000, pollInterval: 100 };
31179
+ const opts = {
31180
+ persistent: true,
31181
+ ignoreInitial: false,
31182
+ ignorePermissionErrors: false,
31183
+ interval: 100,
31184
+ binaryInterval: 300,
31185
+ followSymlinks: true,
31186
+ usePolling: false,
31187
+ atomic: true,
31188
+ ..._opts,
31189
+ ignored: _opts.ignored ? arrify(_opts.ignored) : arrify([]),
31190
+ awaitWriteFinish: awf === true ? DEF_AWF : typeof awf === "object" ? { ...DEF_AWF, ...awf } : false
31191
+ };
31192
+ if (isIBMi)
31193
+ opts.usePolling = true;
31194
+ if (opts.atomic === undefined)
31195
+ opts.atomic = !opts.usePolling;
31196
+ const envPoll = process.env.CHOKIDAR_USEPOLLING;
31197
+ if (envPoll !== undefined) {
31198
+ const envLower = envPoll.toLowerCase();
31199
+ if (envLower === "false" || envLower === "0")
31200
+ opts.usePolling = false;
31201
+ else if (envLower === "true" || envLower === "1")
31202
+ opts.usePolling = true;
31203
+ else
31204
+ opts.usePolling = !!envLower;
31205
+ }
31206
+ const envInterval = process.env.CHOKIDAR_INTERVAL;
31207
+ if (envInterval)
31208
+ opts.interval = Number.parseInt(envInterval, 10);
31209
+ let readyCalls = 0;
31210
+ this._emitReady = () => {
31211
+ readyCalls++;
31212
+ if (readyCalls >= this._readyCount) {
31213
+ this._emitReady = EMPTY_FN;
31214
+ this._readyEmitted = true;
31215
+ process.nextTick(() => this.emit(EVENTS.READY));
31383
31216
  }
31384
- });
31385
- cont = {
31386
- listeners: listener,
31387
- errHandlers: errHandler,
31388
- rawEmitters: rawEmitter,
31389
- watcher
31390
31217
  };
31391
- FsWatchInstances.set(fullPath, cont);
31218
+ this._emitRaw = (...args) => this.emit(EVENTS.RAW, ...args);
31219
+ this._boundRemove = this._remove.bind(this);
31220
+ this.options = opts;
31221
+ this._nodeFsHandler = new NodeFsHandler(this);
31222
+ Object.freeze(opts);
31392
31223
  }
31393
- return () => {
31394
- delFromSet(cont, KEY_LISTENERS, listener);
31395
- delFromSet(cont, KEY_ERR, errHandler);
31396
- delFromSet(cont, KEY_RAW, rawEmitter);
31397
- if (isEmptySet(cont.listeners)) {
31398
- cont.watcher.close();
31399
- FsWatchInstances.delete(fullPath);
31400
- HANDLER_KEYS.forEach(clearItem(cont));
31401
- cont.watcher = undefined;
31402
- Object.freeze(cont);
31224
+ _addIgnoredPath(matcher) {
31225
+ if (isMatcherObject(matcher)) {
31226
+ for (const ignored of this._ignoredPaths) {
31227
+ if (isMatcherObject(ignored) && ignored.path === matcher.path && ignored.recursive === matcher.recursive) {
31228
+ return;
31229
+ }
31230
+ }
31403
31231
  }
31404
- };
31405
- };
31406
- var FsWatchFileInstances = new Map;
31407
- var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
31408
- const { listener, rawEmitter } = handlers;
31409
- let cont = FsWatchFileInstances.get(fullPath);
31410
- const copts = cont && cont.options;
31411
- if (copts && (copts.persistent < options.persistent || copts.interval > options.interval)) {
31412
- unwatchFile(fullPath);
31413
- cont = undefined;
31232
+ this._ignoredPaths.add(matcher);
31414
31233
  }
31415
- if (cont) {
31416
- addAndConvert(cont, KEY_LISTENERS, listener);
31417
- addAndConvert(cont, KEY_RAW, rawEmitter);
31418
- } else {
31419
- cont = {
31420
- listeners: listener,
31421
- rawEmitters: rawEmitter,
31422
- options,
31423
- watcher: watchFile(fullPath, options, (curr, prev) => {
31424
- foreach(cont.rawEmitters, (rawEmitter2) => {
31425
- rawEmitter2(EV.CHANGE, fullPath, { curr, prev });
31426
- });
31427
- const currmtime = curr.mtimeMs;
31428
- if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
31429
- foreach(cont.listeners, (listener2) => listener2(path2, curr));
31234
+ _removeIgnoredPath(matcher) {
31235
+ this._ignoredPaths.delete(matcher);
31236
+ if (typeof matcher === "string") {
31237
+ for (const ignored of this._ignoredPaths) {
31238
+ if (isMatcherObject(ignored) && ignored.path === matcher) {
31239
+ this._ignoredPaths.delete(ignored);
31430
31240
  }
31431
- })
31432
- };
31433
- FsWatchFileInstances.set(fullPath, cont);
31434
- }
31435
- return () => {
31436
- delFromSet(cont, KEY_LISTENERS, listener);
31437
- delFromSet(cont, KEY_RAW, rawEmitter);
31438
- if (isEmptySet(cont.listeners)) {
31439
- FsWatchFileInstances.delete(fullPath);
31440
- unwatchFile(fullPath);
31441
- cont.options = cont.watcher = undefined;
31442
- Object.freeze(cont);
31241
+ }
31443
31242
  }
31444
- };
31445
- };
31446
-
31447
- class NodeFsHandler {
31448
- fsw;
31449
- _boundHandleError;
31450
- constructor(fsW) {
31451
- this.fsw = fsW;
31452
- this._boundHandleError = (error) => fsW._handleError(error);
31453
31243
  }
31454
- _watchWithNodeFs(path2, listener) {
31455
- const opts = this.fsw.options;
31456
- const directory = sp.dirname(path2);
31457
- const basename2 = sp.basename(path2);
31458
- const parent = this.fsw._getWatchedDir(directory);
31459
- parent.add(basename2);
31460
- const absolutePath = sp.resolve(path2);
31461
- const options = {
31462
- persistent: opts.persistent
31463
- };
31464
- if (!listener)
31465
- listener = EMPTY_FN;
31466
- let closer;
31467
- if (opts.usePolling) {
31468
- const enableBin = opts.interval !== opts.binaryInterval;
31469
- options.interval = enableBin && isBinaryPath(basename2) ? opts.binaryInterval : opts.interval;
31470
- closer = setFsWatchFileListener(path2, absolutePath, options, {
31471
- listener,
31472
- rawEmitter: this.fsw._emitRaw
31244
+ add(paths_, _origAdd, _internal) {
31245
+ const { cwd } = this.options;
31246
+ this.closed = false;
31247
+ this._closePromise = undefined;
31248
+ let paths = unifyPaths(paths_);
31249
+ if (cwd) {
31250
+ paths = paths.map((path2) => {
31251
+ const absPath = getAbsolutePath(path2, cwd);
31252
+ return absPath;
31473
31253
  });
31474
- } else {
31475
- closer = setFsWatchListener(path2, absolutePath, options, {
31476
- listener,
31477
- errHandler: this._boundHandleError,
31478
- rawEmitter: this.fsw._emitRaw
31254
+ }
31255
+ paths.forEach((path2) => {
31256
+ this._removeIgnoredPath(path2);
31257
+ });
31258
+ this._userIgnored = undefined;
31259
+ if (!this._readyCount)
31260
+ this._readyCount = 0;
31261
+ this._readyCount += paths.length;
31262
+ Promise.all(paths.map(async (path2) => {
31263
+ const res = await this._nodeFsHandler._addToNodeFs(path2, !_internal, undefined, 0, _origAdd);
31264
+ if (res)
31265
+ this._emitReady();
31266
+ return res;
31267
+ })).then((results) => {
31268
+ if (this.closed)
31269
+ return;
31270
+ results.forEach((item) => {
31271
+ if (item)
31272
+ this.add(sp2.dirname(item), sp2.basename(_origAdd || item));
31479
31273
  });
31274
+ });
31275
+ return this;
31276
+ }
31277
+ unwatch(paths_) {
31278
+ if (this.closed)
31279
+ return this;
31280
+ const paths = unifyPaths(paths_);
31281
+ const { cwd } = this.options;
31282
+ paths.forEach((path2) => {
31283
+ if (!sp2.isAbsolute(path2) && !this._closers.has(path2)) {
31284
+ if (cwd)
31285
+ path2 = sp2.join(cwd, path2);
31286
+ path2 = sp2.resolve(path2);
31287
+ }
31288
+ this._closePath(path2);
31289
+ this._addIgnoredPath(path2);
31290
+ if (this._watched.has(path2)) {
31291
+ this._addIgnoredPath({
31292
+ path: path2,
31293
+ recursive: true
31294
+ });
31295
+ }
31296
+ this._userIgnored = undefined;
31297
+ });
31298
+ return this;
31299
+ }
31300
+ close() {
31301
+ if (this._closePromise) {
31302
+ return this._closePromise;
31480
31303
  }
31481
- return closer;
31304
+ this.closed = true;
31305
+ this.removeAllListeners();
31306
+ const closers = [];
31307
+ this._closers.forEach((closerList) => closerList.forEach((closer) => {
31308
+ const promise = closer();
31309
+ if (promise instanceof Promise)
31310
+ closers.push(promise);
31311
+ }));
31312
+ this._streams.forEach((stream) => stream.destroy());
31313
+ this._userIgnored = undefined;
31314
+ this._readyCount = 0;
31315
+ this._readyEmitted = false;
31316
+ this._watched.forEach((dirent) => dirent.dispose());
31317
+ this._closers.clear();
31318
+ this._watched.clear();
31319
+ this._streams.clear();
31320
+ this._symlinkPaths.clear();
31321
+ this._throttled.clear();
31322
+ this._closePromise = closers.length ? Promise.all(closers).then(() => {
31323
+ return;
31324
+ }) : Promise.resolve();
31325
+ return this._closePromise;
31482
31326
  }
31483
- _handleFile(file, stats, initialAdd) {
31484
- if (this.fsw.closed) {
31327
+ getWatched() {
31328
+ const watchList = {};
31329
+ this._watched.forEach((entry, dir) => {
31330
+ const key = this.options.cwd ? sp2.relative(this.options.cwd, dir) : dir;
31331
+ const index = key || ONE_DOT;
31332
+ watchList[index] = entry.getChildren().sort();
31333
+ });
31334
+ return watchList;
31335
+ }
31336
+ emitWithAll(event, args) {
31337
+ this.emit(event, ...args);
31338
+ if (event !== EVENTS.ERROR)
31339
+ this.emit(EVENTS.ALL, event, ...args);
31340
+ }
31341
+ async _emit(event, path2, stats) {
31342
+ if (this.closed)
31485
31343
  return;
31344
+ const opts = this.options;
31345
+ if (isWindows)
31346
+ path2 = sp2.normalize(path2);
31347
+ if (opts.cwd)
31348
+ path2 = sp2.relative(opts.cwd, path2);
31349
+ const args = [path2];
31350
+ if (stats != null)
31351
+ args.push(stats);
31352
+ const awf = opts.awaitWriteFinish;
31353
+ let pw;
31354
+ if (awf && (pw = this._pendingWrites.get(path2))) {
31355
+ pw.lastChange = new Date;
31356
+ return this;
31486
31357
  }
31487
- const dirname3 = sp.dirname(file);
31488
- const basename2 = sp.basename(file);
31489
- const parent = this.fsw._getWatchedDir(dirname3);
31490
- let prevStats = stats;
31491
- if (parent.has(basename2))
31492
- return;
31493
- const listener = async (path2, newStats) => {
31494
- if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
31495
- return;
31496
- if (!newStats || newStats.mtimeMs === 0) {
31497
- try {
31498
- const newStats2 = await stat3(file);
31499
- if (this.fsw.closed)
31500
- return;
31501
- const at = newStats2.atimeMs;
31502
- const mt = newStats2.mtimeMs;
31503
- if (!at || at <= mt || mt !== prevStats.mtimeMs) {
31504
- this.fsw._emit(EV.CHANGE, file, newStats2);
31505
- }
31506
- if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
31507
- this.fsw._closeFile(path2);
31508
- prevStats = newStats2;
31509
- const closer2 = this._watchWithNodeFs(file, listener);
31510
- if (closer2)
31511
- this.fsw._addPathCloser(path2, closer2);
31358
+ if (opts.atomic) {
31359
+ if (event === EVENTS.UNLINK) {
31360
+ this._pendingUnlinks.set(path2, [event, ...args]);
31361
+ setTimeout(() => {
31362
+ this._pendingUnlinks.forEach((entry, path3) => {
31363
+ this.emit(...entry);
31364
+ this.emit(EVENTS.ALL, ...entry);
31365
+ this._pendingUnlinks.delete(path3);
31366
+ });
31367
+ }, typeof opts.atomic === "number" ? opts.atomic : 100);
31368
+ return this;
31369
+ }
31370
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path2)) {
31371
+ event = EVENTS.CHANGE;
31372
+ this._pendingUnlinks.delete(path2);
31373
+ }
31374
+ }
31375
+ if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
31376
+ const awfEmit = (err, stats2) => {
31377
+ if (err) {
31378
+ event = EVENTS.ERROR;
31379
+ args[0] = err;
31380
+ this.emitWithAll(event, args);
31381
+ } else if (stats2) {
31382
+ if (args.length > 1) {
31383
+ args[1] = stats2;
31512
31384
  } else {
31513
- prevStats = newStats2;
31385
+ args.push(stats2);
31514
31386
  }
31515
- } catch (error) {
31516
- this.fsw._remove(dirname3, basename2);
31517
- }
31518
- } else if (parent.has(basename2)) {
31519
- const at = newStats.atimeMs;
31520
- const mt = newStats.mtimeMs;
31521
- if (!at || at <= mt || mt !== prevStats.mtimeMs) {
31522
- this.fsw._emit(EV.CHANGE, file, newStats);
31387
+ this.emitWithAll(event, args);
31523
31388
  }
31524
- prevStats = newStats;
31525
- }
31526
- };
31527
- const closer = this._watchWithNodeFs(file, listener);
31528
- if (!(initialAdd && this.fsw.options.ignoreInitial) && this.fsw._isntIgnored(file)) {
31529
- if (!this.fsw._throttle(EV.ADD, file, 0))
31389
+ };
31390
+ this._awaitWriteFinish(path2, awf.stabilityThreshold, event, awfEmit);
31391
+ return this;
31392
+ }
31393
+ if (event === EVENTS.CHANGE) {
31394
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path2, 50);
31395
+ if (isThrottled)
31396
+ return this;
31397
+ }
31398
+ if (opts.alwaysStat && stats === undefined && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
31399
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path2) : path2;
31400
+ let stats2;
31401
+ try {
31402
+ stats2 = await stat4(fullPath);
31403
+ } catch (err) {}
31404
+ if (!stats2 || this.closed)
31530
31405
  return;
31531
- this.fsw._emit(EV.ADD, file, stats);
31406
+ args.push(stats2);
31532
31407
  }
31533
- return closer;
31408
+ this.emitWithAll(event, args);
31409
+ return this;
31534
31410
  }
31535
- async _handleSymlink(entry, directory, path2, item) {
31536
- if (this.fsw.closed) {
31411
+ _handleError(error) {
31412
+ const code = error && error.code;
31413
+ if (error && code !== "ENOENT" && code !== "ENOTDIR" && (!this.options.ignorePermissionErrors || code !== "EPERM" && code !== "EACCES")) {
31414
+ this.emit(EVENTS.ERROR, error);
31415
+ }
31416
+ return error || this.closed;
31417
+ }
31418
+ _throttle(actionType, path2, timeout) {
31419
+ if (!this._throttled.has(actionType)) {
31420
+ this._throttled.set(actionType, new Map);
31421
+ }
31422
+ const action = this._throttled.get(actionType);
31423
+ if (!action)
31424
+ throw new Error("invalid throttle");
31425
+ const actionPath = action.get(path2);
31426
+ if (actionPath) {
31427
+ actionPath.count++;
31428
+ return false;
31429
+ }
31430
+ let timeoutObject;
31431
+ const clear = () => {
31432
+ const item = action.get(path2);
31433
+ const count = item ? item.count : 0;
31434
+ action.delete(path2);
31435
+ clearTimeout(timeoutObject);
31436
+ if (item)
31437
+ clearTimeout(item.timeoutObject);
31438
+ return count;
31439
+ };
31440
+ timeoutObject = setTimeout(clear, timeout);
31441
+ const thr = { timeoutObject, clear, count: 0 };
31442
+ action.set(path2, thr);
31443
+ return thr;
31444
+ }
31445
+ _incrReadyCount() {
31446
+ return this._readyCount++;
31447
+ }
31448
+ _awaitWriteFinish(path2, threshold, event, awfEmit) {
31449
+ const awf = this.options.awaitWriteFinish;
31450
+ if (typeof awf !== "object")
31537
31451
  return;
31452
+ const pollInterval = awf.pollInterval;
31453
+ let timeoutHandler;
31454
+ let fullPath = path2;
31455
+ if (this.options.cwd && !sp2.isAbsolute(path2)) {
31456
+ fullPath = sp2.join(this.options.cwd, path2);
31538
31457
  }
31539
- const full = entry.fullPath;
31540
- const dir = this.fsw._getWatchedDir(directory);
31541
- if (!this.fsw.options.followSymlinks) {
31542
- this.fsw._incrReadyCount();
31543
- let linkPath;
31544
- try {
31545
- linkPath = await fsrealpath(path2);
31546
- } catch (e) {
31547
- this.fsw._emitReady();
31548
- return true;
31549
- }
31550
- if (this.fsw.closed)
31551
- return;
31552
- if (dir.has(item)) {
31553
- if (this.fsw._symlinkPaths.get(full) !== linkPath) {
31554
- this.fsw._symlinkPaths.set(full, linkPath);
31555
- this.fsw._emit(EV.CHANGE, path2, entry.stats);
31458
+ const now = new Date;
31459
+ const writes = this._pendingWrites;
31460
+ function awaitWriteFinishFn(prevStat) {
31461
+ statcb(fullPath, (err, curStat) => {
31462
+ if (err || !writes.has(path2)) {
31463
+ if (err && err.code !== "ENOENT")
31464
+ awfEmit(err);
31465
+ return;
31556
31466
  }
31557
- } else {
31558
- dir.add(item);
31559
- this.fsw._symlinkPaths.set(full, linkPath);
31560
- this.fsw._emit(EV.ADD, path2, entry.stats);
31561
- }
31562
- this.fsw._emitReady();
31467
+ const now2 = Number(new Date);
31468
+ if (prevStat && curStat.size !== prevStat.size) {
31469
+ writes.get(path2).lastChange = now2;
31470
+ }
31471
+ const pw = writes.get(path2);
31472
+ const df = now2 - pw.lastChange;
31473
+ if (df >= threshold) {
31474
+ writes.delete(path2);
31475
+ awfEmit(undefined, curStat);
31476
+ } else {
31477
+ timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
31478
+ }
31479
+ });
31480
+ }
31481
+ if (!writes.has(path2)) {
31482
+ writes.set(path2, {
31483
+ lastChange: now,
31484
+ cancelWait: () => {
31485
+ writes.delete(path2);
31486
+ clearTimeout(timeoutHandler);
31487
+ return event;
31488
+ }
31489
+ });
31490
+ timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval);
31491
+ }
31492
+ }
31493
+ _isIgnored(path2, stats) {
31494
+ if (this.options.atomic && DOT_RE.test(path2))
31563
31495
  return true;
31496
+ if (!this._userIgnored) {
31497
+ const { cwd } = this.options;
31498
+ const ign = this.options.ignored;
31499
+ const ignored = (ign || []).map(normalizeIgnored(cwd));
31500
+ const ignoredPaths = [...this._ignoredPaths];
31501
+ const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
31502
+ this._userIgnored = anymatch(list, undefined);
31564
31503
  }
31565
- if (this.fsw._symlinkPaths.has(full)) {
31504
+ return this._userIgnored(path2, stats);
31505
+ }
31506
+ _isntIgnored(path2, stat5) {
31507
+ return !this._isIgnored(path2, stat5);
31508
+ }
31509
+ _getWatchHelpers(path2) {
31510
+ return new WatchHelper(path2, this.options.followSymlinks, this);
31511
+ }
31512
+ _getWatchedDir(directory) {
31513
+ const dir = sp2.resolve(directory);
31514
+ if (!this._watched.has(dir))
31515
+ this._watched.set(dir, new DirEntry(dir, this._boundRemove));
31516
+ return this._watched.get(dir);
31517
+ }
31518
+ _hasReadPermissions(stats) {
31519
+ if (this.options.ignorePermissionErrors)
31566
31520
  return true;
31521
+ return Boolean(Number(stats.mode) & 256);
31522
+ }
31523
+ _remove(directory, item, isDirectory) {
31524
+ const path2 = sp2.join(directory, item);
31525
+ const fullPath = sp2.resolve(path2);
31526
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path2) || this._watched.has(fullPath);
31527
+ if (!this._throttle("remove", path2, 100))
31528
+ return;
31529
+ if (!isDirectory && this._watched.size === 1) {
31530
+ this.add(directory, item, true);
31567
31531
  }
31568
- this.fsw._symlinkPaths.set(full, true);
31532
+ const wp = this._getWatchedDir(path2);
31533
+ const nestedDirectoryChildren = wp.getChildren();
31534
+ nestedDirectoryChildren.forEach((nested) => this._remove(path2, nested));
31535
+ const parent = this._getWatchedDir(directory);
31536
+ const wasTracked = parent.has(item);
31537
+ parent.remove(item);
31538
+ if (this._symlinkPaths.has(fullPath)) {
31539
+ this._symlinkPaths.delete(fullPath);
31540
+ }
31541
+ let relPath = path2;
31542
+ if (this.options.cwd)
31543
+ relPath = sp2.relative(this.options.cwd, path2);
31544
+ if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
31545
+ const event = this._pendingWrites.get(relPath).cancelWait();
31546
+ if (event === EVENTS.ADD)
31547
+ return;
31548
+ }
31549
+ this._watched.delete(path2);
31550
+ this._watched.delete(fullPath);
31551
+ const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
31552
+ if (wasTracked && !this._isIgnored(path2))
31553
+ this._emit(eventName, path2);
31554
+ this._closePath(path2);
31569
31555
  }
31570
- _handleRead(directory, initialAdd, wh, target, dir, depth, throttler) {
31571
- directory = sp.join(directory, "");
31572
- const throttleKey = target ? `${directory}:${target}` : directory;
31573
- throttler = this.fsw._throttle("readdir", throttleKey, 1000);
31574
- if (!throttler)
31556
+ _closePath(path2) {
31557
+ this._closeFile(path2);
31558
+ const dir = sp2.dirname(path2);
31559
+ this._getWatchedDir(dir).remove(sp2.basename(path2));
31560
+ }
31561
+ _closeFile(path2) {
31562
+ const closers = this._closers.get(path2);
31563
+ if (!closers)
31575
31564
  return;
31576
- const previous = this.fsw._getWatchedDir(wh.path);
31577
- const current = new Set;
31578
- let stream = this.fsw._readdirp(directory, {
31579
- fileFilter: (entry) => wh.filterPath(entry),
31580
- directoryFilter: (entry) => wh.filterDir(entry)
31581
- });
31582
- if (!stream)
31565
+ closers.forEach((closer) => closer());
31566
+ this._closers.delete(path2);
31567
+ }
31568
+ _addPathCloser(path2, closer) {
31569
+ if (!closer)
31570
+ return;
31571
+ let list = this._closers.get(path2);
31572
+ if (!list) {
31573
+ list = [];
31574
+ this._closers.set(path2, list);
31575
+ }
31576
+ list.push(closer);
31577
+ }
31578
+ _readdirp(root, opts) {
31579
+ if (this.closed)
31583
31580
  return;
31584
- stream.on(STR_DATA, async (entry) => {
31585
- if (this.fsw.closed) {
31586
- stream = undefined;
31587
- return;
31588
- }
31589
- const item = entry.path;
31590
- let path2 = sp.join(directory, item);
31591
- current.add(item);
31592
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path2, item)) {
31593
- return;
31594
- }
31595
- if (this.fsw.closed) {
31581
+ const options = { type: EVENTS.ALL, alwaysStat: true, lstat: true, ...opts, depth: 0 };
31582
+ let stream = readdirp(root, options);
31583
+ this._streams.add(stream);
31584
+ stream.once(STR_CLOSE, () => {
31585
+ stream = undefined;
31586
+ });
31587
+ stream.once(STR_END, () => {
31588
+ if (stream) {
31589
+ this._streams.delete(stream);
31596
31590
  stream = undefined;
31597
- return;
31598
- }
31599
- if (item === target || !target && !previous.has(item)) {
31600
- this.fsw._incrReadyCount();
31601
- path2 = sp.join(dir, sp.relative(dir, path2));
31602
- this._addToNodeFs(path2, initialAdd, wh, depth + 1);
31603
31591
  }
31604
- }).on(EV.ERROR, this._boundHandleError);
31605
- return new Promise((resolve2, reject) => {
31606
- if (!stream)
31607
- return reject();
31608
- stream.once(STR_END, () => {
31609
- if (this.fsw.closed) {
31610
- stream = undefined;
31611
- return;
31612
- }
31613
- const wasThrottled = throttler ? throttler.clear() : false;
31614
- resolve2(undefined);
31615
- previous.getChildren().filter((item) => {
31616
- return item !== directory && !current.has(item);
31617
- }).forEach((item) => {
31618
- this.fsw._remove(directory, item);
31619
- });
31620
- stream = undefined;
31621
- if (wasThrottled)
31622
- this._handleRead(directory, false, wh, target, dir, depth, throttler);
31623
- });
31624
31592
  });
31593
+ return stream;
31625
31594
  }
31626
- async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath2) {
31627
- const parentDir = this.fsw._getWatchedDir(sp.dirname(dir));
31628
- const tracked = parentDir.has(sp.basename(dir));
31629
- if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) {
31630
- this.fsw._emit(EV.ADD_DIR, dir, stats);
31631
- }
31632
- parentDir.add(sp.basename(dir));
31633
- this.fsw._getWatchedDir(dir);
31634
- let throttler;
31635
- let closer;
31636
- const oDepth = this.fsw.options.depth;
31637
- if ((oDepth == null || depth <= oDepth) && !this.fsw._symlinkPaths.has(realpath2)) {
31638
- if (!target) {
31639
- await this._handleRead(dir, initialAdd, wh, target, dir, depth, throttler);
31640
- if (this.fsw.closed)
31641
- return;
31642
- }
31643
- closer = this._watchWithNodeFs(dir, (dirPath, stats2) => {
31644
- if (stats2 && stats2.mtimeMs === 0)
31645
- return;
31646
- this._handleRead(dirPath, false, wh, target, dir, depth, throttler);
31647
- });
31648
- }
31649
- return closer;
31650
- }
31651
- async _addToNodeFs(path2, initialAdd, priorWh, depth, target) {
31652
- const ready = this.fsw._emitReady;
31653
- if (this.fsw._isIgnored(path2) || this.fsw.closed) {
31654
- ready();
31655
- return false;
31656
- }
31657
- const wh = this.fsw._getWatchHelpers(path2);
31658
- if (priorWh) {
31659
- wh.filterPath = (entry) => priorWh.filterPath(entry);
31660
- wh.filterDir = (entry) => priorWh.filterDir(entry);
31595
+ }
31596
+ function watch(paths, options = {}) {
31597
+ const watcher = new FSWatcher(options);
31598
+ watcher.add(paths);
31599
+ return watcher;
31600
+ }
31601
+
31602
+ // src/daemon/watcher.ts
31603
+ init_src();
31604
+ function startWatcher(onSync) {
31605
+ const claudeDir = getClaudeDir();
31606
+ let debounceTimer = null;
31607
+ let syncing = false;
31608
+ const watcher = watch(claudeDir, {
31609
+ ignored: NEVER_SYNC_PATTERNS.map((p) => `**/${p}`),
31610
+ persistent: true,
31611
+ ignoreInitial: true,
31612
+ awaitWriteFinish: {
31613
+ stabilityThreshold: 2000,
31614
+ pollInterval: 500
31661
31615
  }
31662
- try {
31663
- const stats = await statMethods[wh.statMethod](wh.watchPath);
31664
- if (this.fsw.closed)
31616
+ });
31617
+ const triggerSync = () => {
31618
+ if (debounceTimer)
31619
+ clearTimeout(debounceTimer);
31620
+ debounceTimer = setTimeout(async () => {
31621
+ if (syncing)
31665
31622
  return;
31666
- if (this.fsw._isIgnored(wh.watchPath, stats)) {
31667
- ready();
31668
- return false;
31669
- }
31670
- const follow = this.fsw.options.followSymlinks;
31671
- let closer;
31672
- if (stats.isDirectory()) {
31673
- const absPath = sp.resolve(path2);
31674
- const targetPath = follow ? await fsrealpath(path2) : path2;
31675
- if (this.fsw.closed)
31676
- return;
31677
- closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
31678
- if (this.fsw.closed)
31679
- return;
31680
- if (absPath !== targetPath && targetPath !== undefined) {
31681
- this.fsw._symlinkPaths.set(absPath, targetPath);
31682
- }
31683
- } else if (stats.isSymbolicLink()) {
31684
- const targetPath = follow ? await fsrealpath(path2) : path2;
31685
- if (this.fsw.closed)
31686
- return;
31687
- const parent = sp.dirname(wh.watchPath);
31688
- this.fsw._getWatchedDir(parent).add(wh.watchPath);
31689
- this.fsw._emit(EV.ADD, wh.watchPath, stats);
31690
- closer = await this._handleDir(parent, stats, initialAdd, depth, path2, wh, targetPath);
31691
- if (this.fsw.closed)
31692
- return;
31693
- if (targetPath !== undefined) {
31694
- this.fsw._symlinkPaths.set(sp.resolve(path2), targetPath);
31695
- }
31696
- } else {
31697
- closer = this._handleFile(wh.watchPath, stats, initialAdd);
31698
- }
31699
- ready();
31700
- if (closer)
31701
- this.fsw._addPathCloser(path2, closer);
31702
- return false;
31703
- } catch (error) {
31704
- if (this.fsw._handleError(error)) {
31705
- ready();
31706
- return path2;
31623
+ syncing = true;
31624
+ try {
31625
+ await onSync();
31626
+ } catch {} finally {
31627
+ syncing = false;
31707
31628
  }
31629
+ }, DAEMON_DEBOUNCE_MS);
31630
+ };
31631
+ watcher.on("change", triggerSync);
31632
+ watcher.on("add", triggerSync);
31633
+ watcher.on("unlink", triggerSync);
31634
+ return {
31635
+ stop: () => {
31636
+ if (debounceTimer)
31637
+ clearTimeout(debounceTimer);
31638
+ watcher.close();
31708
31639
  }
31709
- }
31640
+ };
31710
31641
  }
31711
31642
 
31712
- // ../../node_modules/.bun/chokidar@5.0.0/node_modules/chokidar/index.js
31713
- /*! chokidar - MIT License (c) 2012 Paul Miller (paulmillr.com) */
31714
- var SLASH = "/";
31715
- var SLASH_SLASH = "//";
31716
- var ONE_DOT = ".";
31717
- var TWO_DOTS = "..";
31718
- var STRING_TYPE = "string";
31719
- var BACK_SLASH_RE = /\\/g;
31720
- var DOUBLE_SLASH_RE = /\/\//g;
31721
- var DOT_RE = /\..*\.(sw[px])$|~$|\.subl.*\.tmp/;
31722
- var REPLACER_RE = /^\.[/\\]/;
31723
- function arrify(item) {
31724
- return Array.isArray(item) ? item : [item];
31643
+ // src/daemon/runner.ts
31644
+ init_src();
31645
+ init_src();
31646
+ var HEARTBEAT_INTERVAL_MS = 3 * 60 * 1000;
31647
+ function getStatusPath(configDir) {
31648
+ return join11(configDir, "daemon.status.json");
31725
31649
  }
31726
- var isMatcherObject = (matcher) => typeof matcher === "object" && matcher !== null && !(matcher instanceof RegExp);
31727
- function createPattern(matcher) {
31728
- if (typeof matcher === "function")
31729
- return matcher;
31730
- if (typeof matcher === "string")
31731
- return (string) => matcher === string;
31732
- if (matcher instanceof RegExp)
31733
- return (string) => matcher.test(string);
31734
- if (typeof matcher === "object" && matcher !== null) {
31735
- return (string) => {
31736
- if (matcher.path === string)
31737
- return true;
31738
- if (matcher.recursive) {
31739
- const relative5 = sp2.relative(matcher.path, string);
31740
- if (!relative5) {
31741
- return false;
31742
- }
31743
- return !relative5.startsWith("..") && !sp2.isAbsolute(relative5);
31744
- }
31745
- return false;
31746
- };
31747
- }
31748
- return () => false;
31650
+ function getLogPath(configDir) {
31651
+ return join11(configDir, "daemon.log");
31749
31652
  }
31750
- function normalizePath(path2) {
31751
- if (typeof path2 !== "string")
31752
- throw new Error("string expected");
31753
- path2 = sp2.normalize(path2);
31754
- path2 = path2.replace(/\\/g, "/");
31755
- let prepend = false;
31756
- if (path2.startsWith("//"))
31757
- prepend = true;
31758
- path2 = path2.replace(DOUBLE_SLASH_RE, "/");
31759
- if (prepend)
31760
- path2 = "/" + path2;
31761
- return path2;
31653
+ function getPidPath(configDir) {
31654
+ return join11(configDir, "daemon.pid");
31762
31655
  }
31763
- function matchPatterns(patterns, testString, stats) {
31764
- const path2 = normalizePath(testString);
31765
- for (let index = 0;index < patterns.length; index++) {
31766
- const pattern = patterns[index];
31767
- if (pattern(path2, stats)) {
31768
- return true;
31769
- }
31770
- }
31771
- return false;
31656
+ function log(configDir, message2) {
31657
+ const ts = new Date().toISOString();
31658
+ const line = `[${ts}] ${message2}
31659
+ `;
31660
+ try {
31661
+ appendFileSync(getLogPath(configDir), line);
31662
+ } catch {}
31663
+ }
31664
+ async function getMasterKey(configDir) {
31665
+ const cachedKeyPath = join11(configDir, ".cached-key");
31666
+ const cached = await readFile7(cachedKeyPath);
31667
+ return new Uint8Array(cached);
31668
+ }
31669
+ async function getAuthenticatedClient(configDir) {
31670
+ const config = await loadKeys(configDir);
31671
+ const privateKey = await importPrivateKey(config.devicePrivateKey);
31672
+ const jwt = await createDeviceJWT(privateKey, config.deviceId);
31673
+ return new CcsiniClient(config.apiUrl, jwt);
31674
+ }
31675
+ async function getSessionOptions(configDir) {
31676
+ const storedSession = await loadSessionConfig(configDir);
31677
+ return {
31678
+ enabled: storedSession.enabled,
31679
+ maxPerProject: storedSession.maxPerProject,
31680
+ maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
31681
+ };
31772
31682
  }
31773
- function anymatch(matchers, testString) {
31774
- if (matchers == null) {
31775
- throw new TypeError("anymatch: specify first argument");
31776
- }
31777
- const matchersArray = arrify(matchers);
31778
- const patterns = matchersArray.map((matcher) => createPattern(matcher));
31779
- if (testString == null) {
31780
- return (testString2, stats) => {
31781
- return matchPatterns(patterns, testString2, stats);
31782
- };
31783
- }
31784
- return matchPatterns(patterns, testString);
31683
+ async function writeStatus(configDir, status) {
31684
+ await writeFile8(getStatusPath(configDir), JSON.stringify(status, null, 2));
31785
31685
  }
31786
- var unifyPaths = (paths_) => {
31787
- const paths = arrify(paths_).flat();
31788
- if (!paths.every((p) => typeof p === STRING_TYPE)) {
31789
- throw new TypeError(`Non-string provided as watch path: ${paths}`);
31790
- }
31791
- return paths.map(normalizePathToUnix);
31792
- };
31793
- var toUnix = (string) => {
31794
- let str = string.replace(BACK_SLASH_RE, SLASH);
31795
- let prepend = false;
31796
- if (str.startsWith(SLASH_SLASH)) {
31797
- prepend = true;
31798
- }
31799
- str = str.replace(DOUBLE_SLASH_RE, SLASH);
31800
- if (prepend) {
31801
- str = SLASH + str;
31802
- }
31803
- return str;
31804
- };
31805
- var normalizePathToUnix = (path2) => toUnix(sp2.normalize(toUnix(path2)));
31806
- var normalizeIgnored = (cwd = "") => (path2) => {
31807
- if (typeof path2 === "string") {
31808
- return normalizePathToUnix(sp2.isAbsolute(path2) ? path2 : sp2.join(cwd, path2));
31809
- } else {
31810
- return path2;
31811
- }
31812
- };
31813
- var getAbsolutePath = (path2, cwd) => {
31814
- if (sp2.isAbsolute(path2)) {
31815
- return path2;
31816
- }
31817
- return sp2.join(cwd, path2);
31818
- };
31819
- var EMPTY_SET = Object.freeze(new Set);
31820
-
31821
- class DirEntry {
31822
- path;
31823
- _removeWatcher;
31824
- items;
31825
- constructor(dir, removeWatcher) {
31826
- this.path = dir;
31827
- this._removeWatcher = removeWatcher;
31828
- this.items = new Set;
31686
+ async function runDaemon() {
31687
+ const configDir = getConfigDir();
31688
+ if (!await configExists(configDir)) {
31689
+ console.error("Not initialized. Run 'ccsini init' first.");
31690
+ process.exit(1);
31829
31691
  }
31830
- add(item) {
31831
- const { items } = this;
31832
- if (!items)
31833
- return;
31834
- if (item !== ONE_DOT && item !== TWO_DOTS)
31835
- items.add(item);
31692
+ try {
31693
+ await getMasterKey(configDir);
31694
+ } catch {
31695
+ console.error("No cached key. Run 'ccsini unlock' first.");
31696
+ process.exit(1);
31836
31697
  }
31837
- async remove(item) {
31838
- const { items } = this;
31839
- if (!items)
31840
- return;
31841
- items.delete(item);
31842
- if (items.size > 0)
31843
- return;
31844
- const dir = this.path;
31698
+ const pid = process.pid;
31699
+ await writeFile8(getPidPath(configDir), String(pid));
31700
+ const status = {
31701
+ pid,
31702
+ startedAt: Date.now(),
31703
+ lastPushAt: null,
31704
+ lastPullAt: null,
31705
+ lastError: null,
31706
+ pushCount: 0,
31707
+ pullCount: 0
31708
+ };
31709
+ await writeStatus(configDir, status);
31710
+ log(configDir, `Daemon started (PID ${pid})`);
31711
+ async function doPush() {
31845
31712
  try {
31846
- await readdir4(dir);
31847
- } catch (err) {
31848
- if (this._removeWatcher) {
31849
- this._removeWatcher(sp2.dirname(dir), sp2.basename(dir));
31713
+ const masterKey = await getMasterKey(configDir);
31714
+ const client = await getAuthenticatedClient(configDir);
31715
+ const config = await loadKeys(configDir);
31716
+ const sessionOptions = await getSessionOptions(configDir);
31717
+ const result = await pushSync(client, masterKey, config.deviceName, configDir, undefined, sessionOptions, { skipConflictBackup: true });
31718
+ status.lastPushAt = Date.now();
31719
+ status.pushCount++;
31720
+ status.lastError = null;
31721
+ await writeStatus(configDir, status);
31722
+ if (result.filesChanged > 0) {
31723
+ log(configDir, `Push: ${result.filesChanged} files (${result.durationMs}ms)`);
31724
+ }
31725
+ if (result.conflicts.length > 0) {
31726
+ log(configDir, `Push: ${result.conflicts.length} conflict(s)`);
31850
31727
  }
31728
+ } catch (e) {
31729
+ status.lastError = e.message;
31730
+ await writeStatus(configDir, status).catch(() => {});
31731
+ log(configDir, `Push error: ${e.message}`);
31851
31732
  }
31852
31733
  }
31853
- has(item) {
31854
- const { items } = this;
31855
- if (!items)
31856
- return;
31857
- return items.has(item);
31858
- }
31859
- getChildren() {
31860
- const { items } = this;
31861
- if (!items)
31862
- return [];
31863
- return [...items.values()];
31864
- }
31865
- dispose() {
31866
- this.items.clear();
31867
- this.path = "";
31868
- this._removeWatcher = EMPTY_FN;
31869
- this.items = EMPTY_SET;
31870
- Object.freeze(this);
31871
- }
31872
- }
31873
- var STAT_METHOD_F = "stat";
31874
- var STAT_METHOD_L = "lstat";
31875
-
31876
- class WatchHelper {
31877
- fsw;
31878
- path;
31879
- watchPath;
31880
- fullWatchPath;
31881
- dirParts;
31882
- followSymlinks;
31883
- statMethod;
31884
- constructor(path2, follow, fsw) {
31885
- this.fsw = fsw;
31886
- const watchPath = path2;
31887
- this.path = path2 = path2.replace(REPLACER_RE, "");
31888
- this.watchPath = watchPath;
31889
- this.fullWatchPath = sp2.resolve(watchPath);
31890
- this.dirParts = [];
31891
- this.dirParts.forEach((parts) => {
31892
- if (parts.length > 1)
31893
- parts.pop();
31894
- });
31895
- this.followSymlinks = follow;
31896
- this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L;
31897
- }
31898
- entryPath(entry) {
31899
- return sp2.join(this.watchPath, sp2.relative(this.watchPath, entry.fullPath));
31900
- }
31901
- filterPath(entry) {
31902
- const { stats } = entry;
31903
- if (stats && stats.isSymbolicLink())
31904
- return this.filterDir(entry);
31905
- const resolvedPath = this.entryPath(entry);
31906
- return this.fsw._isntIgnored(resolvedPath, stats) && this.fsw._hasReadPermissions(stats);
31907
- }
31908
- filterDir(entry) {
31909
- return this.fsw._isntIgnored(this.entryPath(entry), entry.stats);
31910
- }
31911
- }
31912
-
31913
- class FSWatcher extends EventEmitter {
31914
- closed;
31915
- options;
31916
- _closers;
31917
- _ignoredPaths;
31918
- _throttled;
31919
- _streams;
31920
- _symlinkPaths;
31921
- _watched;
31922
- _pendingWrites;
31923
- _pendingUnlinks;
31924
- _readyCount;
31925
- _emitReady;
31926
- _closePromise;
31927
- _userIgnored;
31928
- _readyEmitted;
31929
- _emitRaw;
31930
- _boundRemove;
31931
- _nodeFsHandler;
31932
- constructor(_opts = {}) {
31933
- super();
31934
- this.closed = false;
31935
- this._closers = new Map;
31936
- this._ignoredPaths = new Set;
31937
- this._throttled = new Map;
31938
- this._streams = new Set;
31939
- this._symlinkPaths = new Map;
31940
- this._watched = new Map;
31941
- this._pendingWrites = new Map;
31942
- this._pendingUnlinks = new Map;
31943
- this._readyCount = 0;
31944
- this._readyEmitted = false;
31945
- const awf = _opts.awaitWriteFinish;
31946
- const DEF_AWF = { stabilityThreshold: 2000, pollInterval: 100 };
31947
- const opts = {
31948
- persistent: true,
31949
- ignoreInitial: false,
31950
- ignorePermissionErrors: false,
31951
- interval: 100,
31952
- binaryInterval: 300,
31953
- followSymlinks: true,
31954
- usePolling: false,
31955
- atomic: true,
31956
- ..._opts,
31957
- ignored: _opts.ignored ? arrify(_opts.ignored) : arrify([]),
31958
- awaitWriteFinish: awf === true ? DEF_AWF : typeof awf === "object" ? { ...DEF_AWF, ...awf } : false
31959
- };
31960
- if (isIBMi)
31961
- opts.usePolling = true;
31962
- if (opts.atomic === undefined)
31963
- opts.atomic = !opts.usePolling;
31964
- const envPoll = process.env.CHOKIDAR_USEPOLLING;
31965
- if (envPoll !== undefined) {
31966
- const envLower = envPoll.toLowerCase();
31967
- if (envLower === "false" || envLower === "0")
31968
- opts.usePolling = false;
31969
- else if (envLower === "true" || envLower === "1")
31970
- opts.usePolling = true;
31971
- else
31972
- opts.usePolling = !!envLower;
31973
- }
31974
- const envInterval = process.env.CHOKIDAR_INTERVAL;
31975
- if (envInterval)
31976
- opts.interval = Number.parseInt(envInterval, 10);
31977
- let readyCalls = 0;
31978
- this._emitReady = () => {
31979
- readyCalls++;
31980
- if (readyCalls >= this._readyCount) {
31981
- this._emitReady = EMPTY_FN;
31982
- this._readyEmitted = true;
31983
- process.nextTick(() => this.emit(EVENTS.READY));
31734
+ async function doPull() {
31735
+ try {
31736
+ const masterKey = await getMasterKey(configDir);
31737
+ const client = await getAuthenticatedClient(configDir);
31738
+ const config = await loadKeys(configDir);
31739
+ const sessionOptions = await getSessionOptions(configDir);
31740
+ const result = await pullSync(client, masterKey, config.deviceName, configDir, undefined, sessionOptions);
31741
+ status.lastPullAt = Date.now();
31742
+ status.pullCount++;
31743
+ status.lastError = null;
31744
+ await writeStatus(configDir, status);
31745
+ if (result.filesChanged > 0) {
31746
+ log(configDir, `Pull: ${result.filesChanged} files (${result.durationMs}ms)`);
31984
31747
  }
31985
- };
31986
- this._emitRaw = (...args) => this.emit(EVENTS.RAW, ...args);
31987
- this._boundRemove = this._remove.bind(this);
31988
- this.options = opts;
31989
- this._nodeFsHandler = new NodeFsHandler(this);
31990
- Object.freeze(opts);
31991
- }
31992
- _addIgnoredPath(matcher) {
31993
- if (isMatcherObject(matcher)) {
31994
- for (const ignored of this._ignoredPaths) {
31995
- if (isMatcherObject(ignored) && ignored.path === matcher.path && ignored.recursive === matcher.recursive) {
31996
- return;
31997
- }
31748
+ if (result.conflicts.length > 0) {
31749
+ log(configDir, `Pull: ${result.conflicts.length} conflict(s)`);
31998
31750
  }
31751
+ } catch (e) {
31752
+ status.lastError = e.message;
31753
+ await writeStatus(configDir, status).catch(() => {});
31754
+ log(configDir, `Pull error: ${e.message}`);
31999
31755
  }
32000
- this._ignoredPaths.add(matcher);
32001
31756
  }
32002
- _removeIgnoredPath(matcher) {
32003
- this._ignoredPaths.delete(matcher);
32004
- if (typeof matcher === "string") {
32005
- for (const ignored of this._ignoredPaths) {
32006
- if (isMatcherObject(ignored) && ignored.path === matcher) {
32007
- this._ignoredPaths.delete(ignored);
32008
- }
32009
- }
31757
+ async function doHeartbeat() {
31758
+ try {
31759
+ const client = await getAuthenticatedClient(configDir);
31760
+ await client.heartbeat("daemon");
31761
+ } catch (e) {
31762
+ log(configDir, `Heartbeat error: ${e.message}`);
32010
31763
  }
32011
31764
  }
32012
- add(paths_, _origAdd, _internal) {
32013
- const { cwd } = this.options;
32014
- this.closed = false;
32015
- this._closePromise = undefined;
32016
- let paths = unifyPaths(paths_);
32017
- if (cwd) {
32018
- paths = paths.map((path2) => {
32019
- const absPath = getAbsolutePath(path2, cwd);
32020
- return absPath;
32021
- });
31765
+ const watcher = startWatcher(doPush);
31766
+ const pullInterval = setInterval(doPull, DAEMON_INTERVAL_MS);
31767
+ const heartbeatInterval = setInterval(doHeartbeat, HEARTBEAT_INTERVAL_MS);
31768
+ await doPull();
31769
+ await doHeartbeat();
31770
+ const shutdown = async () => {
31771
+ log(configDir, "Daemon shutting down...");
31772
+ watcher.stop();
31773
+ clearInterval(pullInterval);
31774
+ clearInterval(heartbeatInterval);
31775
+ const { rm } = await import("fs/promises");
31776
+ await rm(getPidPath(configDir)).catch(() => {});
31777
+ await rm(getStatusPath(configDir)).catch(() => {});
31778
+ log(configDir, "Daemon stopped");
31779
+ process.exit(0);
31780
+ };
31781
+ process.on("SIGTERM", shutdown);
31782
+ process.on("SIGINT", shutdown);
31783
+ if (platform3() === "win32") {
31784
+ process.on("SIGBREAK", shutdown);
31785
+ }
31786
+ }
31787
+
31788
+ // src/commands/auto.ts
31789
+ async function getMasterKey2(configDir) {
31790
+ const cachedKeyPath = join12(configDir, ".cached-key");
31791
+ try {
31792
+ const cached = await readFile8(cachedKeyPath);
31793
+ return new Uint8Array(cached);
31794
+ } catch {
31795
+ throw new Error("No cached key. Run 'ccsini unlock' to enter your encryption password.");
31796
+ }
31797
+ }
31798
+ async function getAuthenticatedClient2(configDir) {
31799
+ const config = await loadKeys(configDir);
31800
+ const privateKey = await importPrivateKey(config.devicePrivateKey);
31801
+ const jwt = await createDeviceJWT(privateKey, config.deviceId);
31802
+ return new CcsiniClient(config.apiUrl, jwt);
31803
+ }
31804
+ async function ensureDaemon(configDir) {
31805
+ try {
31806
+ const raw = await readFile8(getPidPath(configDir), "utf-8");
31807
+ const pid = parseInt(raw.trim(), 10);
31808
+ if (!isNaN(pid)) {
31809
+ try {
31810
+ process.kill(pid, 0);
31811
+ return;
31812
+ } catch {}
32022
31813
  }
32023
- paths.forEach((path2) => {
32024
- this._removeIgnoredPath(path2);
31814
+ } catch {}
31815
+ try {
31816
+ const isWin = platform4() === "win32";
31817
+ const child = spawn3("ccsini", ["daemon", "_run"], {
31818
+ detached: true,
31819
+ stdio: "ignore",
31820
+ ...isWin ? { shell: true } : {}
32025
31821
  });
32026
- this._userIgnored = undefined;
32027
- if (!this._readyCount)
32028
- this._readyCount = 0;
32029
- this._readyCount += paths.length;
32030
- Promise.all(paths.map(async (path2) => {
32031
- const res = await this._nodeFsHandler._addToNodeFs(path2, !_internal, undefined, 0, _origAdd);
32032
- if (res)
32033
- this._emitReady();
32034
- return res;
32035
- })).then((results) => {
32036
- if (this.closed)
32037
- return;
32038
- results.forEach((item) => {
32039
- if (item)
32040
- this.add(sp2.dirname(item), sp2.basename(_origAdd || item));
32041
- });
31822
+ child.unref();
31823
+ } catch {}
31824
+ }
31825
+ function registerAutoCommands(program2) {
31826
+ program2.command("auto-pull").description("Auto-pull changes (called by Claude Code hooks)").action(async () => {
31827
+ const configDir = getConfigDir();
31828
+ if (!await configExists(configDir))
31829
+ return;
31830
+ await ensureDaemon(configDir);
31831
+ try {
31832
+ const masterKey = await getMasterKey2(configDir);
31833
+ const client = await getAuthenticatedClient2(configDir);
31834
+ const config = await loadKeys(configDir);
31835
+ const storedSession = await loadSessionConfig(configDir);
31836
+ const sessionOptions = {
31837
+ enabled: storedSession.enabled,
31838
+ maxPerProject: storedSession.maxPerProject,
31839
+ maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
31840
+ };
31841
+ const result = await pullSync(client, masterKey, config.deviceName, configDir, undefined, sessionOptions);
31842
+ if (result.filesChanged > 0) {
31843
+ console.log(`[ccsini] Pulled ${result.filesChanged} files (${result.durationMs}ms)`);
31844
+ }
31845
+ if (result.conflicts.length > 0) {
31846
+ console.log(`[ccsini] \u26A0 ${result.conflicts.length} conflict(s) \u2014 check .conflict files in ~/.claude/`);
31847
+ }
31848
+ } catch {}
31849
+ });
31850
+ program2.command("auto-push").description("Auto-push changes (called by Claude Code hooks)").action(async () => {
31851
+ const configDir = getConfigDir();
31852
+ if (!await configExists(configDir))
31853
+ return;
31854
+ try {
31855
+ const masterKey = await getMasterKey2(configDir);
31856
+ const client = await getAuthenticatedClient2(configDir);
31857
+ const config = await loadKeys(configDir);
31858
+ const storedSession = await loadSessionConfig(configDir);
31859
+ const sessionOptions = {
31860
+ enabled: storedSession.enabled,
31861
+ maxPerProject: storedSession.maxPerProject,
31862
+ maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
31863
+ };
31864
+ const result = await pushSync(client, masterKey, config.deviceName, configDir, undefined, sessionOptions, { skipConflictBackup: true });
31865
+ if (result.filesChanged > 0) {
31866
+ let msg = `[ccsini] Pushed ${result.filesChanged} files (${result.durationMs}ms)`;
31867
+ if (result.cacheStats && result.cacheStats.hits > 0) {
31868
+ msg += ` [cache: ${result.cacheStats.hitRate.toFixed(0)}% hits]`;
31869
+ }
31870
+ console.log(msg);
31871
+ }
31872
+ if (result.conflicts.length > 0) {
31873
+ console.log(`[ccsini] \u26A0 ${result.conflicts.length} conflict(s) \u2014 check .conflict files in ~/.claude/`);
31874
+ }
31875
+ } catch {}
31876
+ });
31877
+ program2.command("unlock").description("Enter encryption password to enable auto-sync").action(async () => {
31878
+ const configDir = getConfigDir();
31879
+ if (!await configExists(configDir)) {
31880
+ console.error("Not initialized. Run 'ccsini init' first.");
31881
+ process.exit(1);
31882
+ }
31883
+ const inquirer2 = await Promise.resolve().then(() => (init_dist16(), exports_dist));
31884
+ const { password } = await inquirer2.default.prompt([
31885
+ {
31886
+ type: "password",
31887
+ name: "password",
31888
+ message: "Encryption password:",
31889
+ mask: "*"
31890
+ }
31891
+ ]);
31892
+ const config = await loadKeys(configDir);
31893
+ const masterKey = await deriveKeyFromPassphrase(password, config.salt);
31894
+ await writeFile9(join12(configDir, ".cached-key"), masterKey, {
31895
+ mode: 384
32042
31896
  });
32043
- return this;
32044
- }
32045
- unwatch(paths_) {
32046
- if (this.closed)
32047
- return this;
32048
- const paths = unifyPaths(paths_);
32049
- const { cwd } = this.options;
32050
- paths.forEach((path2) => {
32051
- if (!sp2.isAbsolute(path2) && !this._closers.has(path2)) {
32052
- if (cwd)
32053
- path2 = sp2.join(cwd, path2);
32054
- path2 = sp2.resolve(path2);
31897
+ console.log("Unlocked. Auto-sync is now active.");
31898
+ });
31899
+ program2.command("heartbeat").description("Send heartbeat to keep device online (runs via scheduled task)").action(async () => {
31900
+ const configDir = getConfigDir();
31901
+ if (!await configExists(configDir))
31902
+ return;
31903
+ try {
31904
+ const client = await getAuthenticatedClient2(configDir);
31905
+ await client.heartbeat();
31906
+ } catch {}
31907
+ });
31908
+ program2.command("lock").description("Remove cached encryption key").action(async () => {
31909
+ const configDir = getConfigDir();
31910
+ const { rm } = await import("fs/promises");
31911
+ try {
31912
+ await rm(join12(configDir, ".cached-key"));
31913
+ console.log("Locked. Auto-sync paused until next unlock.");
31914
+ } catch {
31915
+ console.log("Already locked.");
31916
+ }
31917
+ });
31918
+ }
31919
+
31920
+ // src/commands/doctor.ts
31921
+ init_source();
31922
+ import { access as access2 } from "fs/promises";
31923
+ import { join as join13 } from "path";
31924
+ function registerDoctorCommand(program2) {
31925
+ program2.command("doctor").description("Diagnose ccsini setup and connectivity").action(async () => {
31926
+ const configDir = getConfigDir();
31927
+ const claudeDir = getClaudeDir();
31928
+ let allGood = true;
31929
+ console.log(source_default.bold(`
31930
+ ccsini doctor
31931
+ `));
31932
+ try {
31933
+ await access2(claudeDir);
31934
+ console.log(source_default.green(" \u2713 Claude Code directory found"));
31935
+ } catch {
31936
+ console.log(source_default.red(" \u2717 Claude Code directory not found"));
31937
+ allGood = false;
31938
+ }
31939
+ if (await configExists(configDir)) {
31940
+ console.log(source_default.green(" \u2713 ccsini initialized"));
31941
+ } else {
31942
+ console.log(source_default.red(" \u2717 ccsini not initialized (run 'ccsini init')"));
31943
+ allGood = false;
31944
+ return;
31945
+ }
31946
+ const config = await loadKeys(configDir);
31947
+ console.log(source_default.green(` \u2713 Device: ${config.deviceName} (${config.deviceId.slice(0, 8)}...)`));
31948
+ if (await hooksInstalled(claudeDir)) {
31949
+ console.log(source_default.green(" \u2713 Hooks installed"));
31950
+ const { healthy, issues } = await checkHooksHealth(claudeDir);
31951
+ if (healthy) {
31952
+ console.log(source_default.green(" \u2713 Hooks format valid"));
31953
+ } else {
31954
+ for (const issue of issues) {
31955
+ console.log(source_default.red(` \u2717 ${issue}`));
31956
+ }
31957
+ console.log(source_default.yellow(" Run 'ccsini hooks fix' to repair"));
31958
+ allGood = false;
32055
31959
  }
32056
- this._closePath(path2);
32057
- this._addIgnoredPath(path2);
32058
- if (this._watched.has(path2)) {
32059
- this._addIgnoredPath({
32060
- path: path2,
32061
- recursive: true
32062
- });
31960
+ } else {
31961
+ console.log(source_default.yellow(" \u26A0 Hooks not installed (run 'ccsini init')"));
31962
+ allGood = false;
31963
+ }
31964
+ try {
31965
+ await access2(join13(configDir, ".cached-key"));
31966
+ console.log(source_default.green(" \u2713 Auto-sync unlocked"));
31967
+ } catch {
31968
+ console.log(source_default.yellow(" \u26A0 Auto-sync locked (run 'ccsini unlock')"));
31969
+ }
31970
+ try {
31971
+ const start = Date.now();
31972
+ const res = await fetch(`${config.apiUrl}/health`);
31973
+ const latency = Date.now() - start;
31974
+ if (res.ok) {
31975
+ console.log(source_default.green(` \u2713 Server reachable (${latency}ms)`));
31976
+ } else {
31977
+ console.log(source_default.red(" \u2717 Server returned error"));
31978
+ allGood = false;
32063
31979
  }
32064
- this._userIgnored = undefined;
32065
- });
32066
- return this;
32067
- }
32068
- close() {
32069
- if (this._closePromise) {
32070
- return this._closePromise;
31980
+ } catch {
31981
+ console.log(source_default.red(" \u2717 Cannot reach server"));
31982
+ allGood = false;
32071
31983
  }
32072
- this.closed = true;
32073
- this.removeAllListeners();
32074
- const closers = [];
32075
- this._closers.forEach((closerList) => closerList.forEach((closer) => {
32076
- const promise = closer();
32077
- if (promise instanceof Promise)
32078
- closers.push(promise);
32079
- }));
32080
- this._streams.forEach((stream) => stream.destroy());
32081
- this._userIgnored = undefined;
32082
- this._readyCount = 0;
32083
- this._readyEmitted = false;
32084
- this._watched.forEach((dirent) => dirent.dispose());
32085
- this._closers.clear();
32086
- this._watched.clear();
32087
- this._streams.clear();
32088
- this._symlinkPaths.clear();
32089
- this._throttled.clear();
32090
- this._closePromise = closers.length ? Promise.all(closers).then(() => {
32091
- return;
32092
- }) : Promise.resolve();
32093
- return this._closePromise;
32094
- }
32095
- getWatched() {
32096
- const watchList = {};
32097
- this._watched.forEach((entry, dir) => {
32098
- const key = this.options.cwd ? sp2.relative(this.options.cwd, dir) : dir;
32099
- const index = key || ONE_DOT;
32100
- watchList[index] = entry.getChildren().sort();
32101
- });
32102
- return watchList;
32103
- }
32104
- emitWithAll(event, args) {
32105
- this.emit(event, ...args);
32106
- if (event !== EVENTS.ERROR)
32107
- this.emit(EVENTS.ALL, event, ...args);
31984
+ console.log(allGood ? source_default.green(`
31985
+ Everything looks good!
31986
+ `) : source_default.yellow(`
31987
+ Some issues found. See above.
31988
+ `));
31989
+ });
31990
+ }
31991
+
31992
+ // src/commands/self.ts
31993
+ import { execSync as execSync2 } from "child_process";
31994
+ function getLatestVersion() {
31995
+ try {
31996
+ const result = execSync2("npm view ccsini version", { encoding: "utf-8" }).trim();
31997
+ return result;
31998
+ } catch {
31999
+ return null;
32108
32000
  }
32109
- async _emit(event, path2, stats) {
32110
- if (this.closed)
32001
+ }
32002
+ function registerSelfCommands(program2) {
32003
+ program2.command("update").description("Update ccsini to the latest version").action(async () => {
32004
+ console.log(`Current version: ${VERSION}`);
32005
+ const latest = getLatestVersion();
32006
+ if (!latest) {
32007
+ console.error("Failed to check latest version. Check your internet connection.");
32008
+ process.exit(1);
32009
+ }
32010
+ if (latest === VERSION) {
32011
+ console.log(`Already on the latest version (${VERSION})`);
32111
32012
  return;
32112
- const opts = this.options;
32113
- if (isWindows)
32114
- path2 = sp2.normalize(path2);
32115
- if (opts.cwd)
32116
- path2 = sp2.relative(opts.cwd, path2);
32117
- const args = [path2];
32118
- if (stats != null)
32119
- args.push(stats);
32120
- const awf = opts.awaitWriteFinish;
32121
- let pw;
32122
- if (awf && (pw = this._pendingWrites.get(path2))) {
32123
- pw.lastChange = new Date;
32124
- return this;
32125
32013
  }
32126
- if (opts.atomic) {
32127
- if (event === EVENTS.UNLINK) {
32128
- this._pendingUnlinks.set(path2, [event, ...args]);
32129
- setTimeout(() => {
32130
- this._pendingUnlinks.forEach((entry, path3) => {
32131
- this.emit(...entry);
32132
- this.emit(EVENTS.ALL, ...entry);
32133
- this._pendingUnlinks.delete(path3);
32134
- });
32135
- }, typeof opts.atomic === "number" ? opts.atomic : 100);
32136
- return this;
32014
+ console.log(`New version available: ${latest}`);
32015
+ console.log(`Updating...
32016
+ `);
32017
+ let updated = false;
32018
+ try {
32019
+ execSync2(`bun add -g ccsini@${latest}`, { stdio: "inherit" });
32020
+ updated = true;
32021
+ } catch {
32022
+ console.log(`Bun failed (registry cache), trying npm...
32023
+ `);
32024
+ try {
32025
+ execSync2(`npm install -g ccsini@${latest}`, { stdio: "inherit" });
32026
+ updated = true;
32027
+ } catch {}
32028
+ }
32029
+ if (!updated) {
32030
+ console.error(`
32031
+ Update failed. Try manually:`);
32032
+ console.error(` npm install -g ccsini@${latest}`);
32033
+ process.exit(1);
32034
+ }
32035
+ console.log(`
32036
+ Updated to v${latest}`);
32037
+ try {
32038
+ const { fixed } = await fixHooks(getClaudeDir());
32039
+ if (fixed > 0) {
32040
+ console.log(`Repaired ${fixed} hook issue${fixed > 1 ? "s" : ""} in Claude Code settings.`);
32137
32041
  }
32138
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path2)) {
32139
- event = EVENTS.CHANGE;
32140
- this._pendingUnlinks.delete(path2);
32042
+ } catch {}
32043
+ console.log("Restart your terminal to use the new version.");
32044
+ });
32045
+ program2.command("uninstall").description("Uninstall ccsini from this machine").action(async () => {
32046
+ console.log(`Uninstalling ccsini...
32047
+ `);
32048
+ try {
32049
+ await uninstallHeartbeatScheduler();
32050
+ } catch {}
32051
+ let removed = false;
32052
+ try {
32053
+ execSync2("bun remove -g ccsini", { stdio: "inherit" });
32054
+ removed = true;
32055
+ } catch {}
32056
+ try {
32057
+ execSync2("npm uninstall -g ccsini", { stdio: "inherit" });
32058
+ removed = true;
32059
+ } catch {}
32060
+ if (removed) {
32061
+ console.log(`
32062
+ ccsini has been uninstalled. Bye!`);
32063
+ } else {
32064
+ console.error(`
32065
+ Uninstall failed. Try manually:`);
32066
+ console.error(" npm uninstall -g ccsini");
32067
+ process.exit(1);
32068
+ }
32069
+ });
32070
+ }
32071
+
32072
+ // src/commands/sync.ts
32073
+ init_auth();
32074
+ init_src();
32075
+ import { readFile as readFile9 } from "fs/promises";
32076
+ import { join as join14 } from "path";
32077
+ async function getMasterKey3(configDir) {
32078
+ const cachedKeyPath = join14(configDir, ".cached-key");
32079
+ try {
32080
+ const cached = await readFile9(cachedKeyPath);
32081
+ return new Uint8Array(cached);
32082
+ } catch {
32083
+ const inquirer2 = await Promise.resolve().then(() => (init_dist16(), exports_dist));
32084
+ const { password } = await inquirer2.default.prompt([
32085
+ {
32086
+ type: "password",
32087
+ name: "password",
32088
+ message: "Encryption password:",
32089
+ mask: "*"
32141
32090
  }
32091
+ ]);
32092
+ const config = await loadKeys(configDir);
32093
+ const masterKey = await deriveKeyFromPassphrase(password, config.salt);
32094
+ const { writeFile: writeFile10 } = await import("fs/promises");
32095
+ await writeFile10(cachedKeyPath, masterKey, { mode: 384 });
32096
+ return masterKey;
32097
+ }
32098
+ }
32099
+ async function getAuthenticatedClient3(configDir) {
32100
+ const config = await loadKeys(configDir);
32101
+ const privateKey = await importPrivateKey(config.devicePrivateKey);
32102
+ const jwt = await createDeviceJWT(privateKey, config.deviceId);
32103
+ return new CcsiniClient(config.apiUrl, jwt);
32104
+ }
32105
+ function formatBytes(bytes) {
32106
+ if (bytes < 1024)
32107
+ return `${bytes} B`;
32108
+ if (bytes < 1048576)
32109
+ return `${(bytes / 1024).toFixed(1)} KB`;
32110
+ return `${(bytes / 1048576).toFixed(1)} MB`;
32111
+ }
32112
+ function registerSyncCommands(program2) {
32113
+ const syncCmd = program2.command("sync").description("Sync management commands");
32114
+ syncCmd.command("push").description("Push local changes to cloud").option("--with-sessions", "Include session files in sync").option("--no-sessions", "Exclude session files from sync").action(async (opts) => {
32115
+ const configDir = getConfigDir();
32116
+ if (!await configExists(configDir)) {
32117
+ console.error("Not initialized. Run 'ccsini init' first.");
32118
+ process.exit(1);
32142
32119
  }
32143
- if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
32144
- const awfEmit = (err, stats2) => {
32145
- if (err) {
32146
- event = EVENTS.ERROR;
32147
- args[0] = err;
32148
- this.emitWithAll(event, args);
32149
- } else if (stats2) {
32150
- if (args.length > 1) {
32151
- args[1] = stats2;
32152
- } else {
32153
- args.push(stats2);
32154
- }
32155
- this.emitWithAll(event, args);
32120
+ const ora2 = (await Promise.resolve().then(() => (init_ora(), exports_ora))).default;
32121
+ const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
32122
+ try {
32123
+ const masterKey = await getMasterKey3(configDir);
32124
+ const client = await getAuthenticatedClient3(configDir);
32125
+ const config = await loadKeys(configDir);
32126
+ const storedSession = await loadSessionConfig(configDir);
32127
+ const sessionOptions = {
32128
+ enabled: opts.withSessions ? true : opts.sessions === false ? false : storedSession.enabled,
32129
+ maxPerProject: storedSession.maxPerProject,
32130
+ maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
32131
+ };
32132
+ const spinner = ora2("Scanning files...").start();
32133
+ const result = await pushSync(client, masterKey, config.deviceName, configDir, (msg) => {
32134
+ spinner.text = msg;
32135
+ }, sessionOptions);
32136
+ if (result.errors.length > 0) {
32137
+ spinner.warn("Push completed with errors");
32138
+ for (const err of result.errors) {
32139
+ console.error(chalk2.red(` ${err}`));
32156
32140
  }
32141
+ } else {
32142
+ spinner.succeed("Push complete");
32143
+ }
32144
+ console.log(chalk2.dim(` ${result.filesChanged} files changed, ${formatBytes(result.bytesTransferred)} transferred in ${result.durationMs}ms`));
32145
+ if (sessionOptions.enabled) {
32146
+ console.log(chalk2.dim(" Sessions: enabled"));
32147
+ }
32148
+ if (result.conflicts.length > 0) {
32149
+ console.log(chalk2.yellow(` \u26A0 ${result.conflicts.length} conflict(s) \u2014 check .conflict files in ~/.claude/`));
32150
+ }
32151
+ } catch (e) {
32152
+ console.error(`Push failed: ${e.message}`);
32153
+ process.exit(1);
32154
+ }
32155
+ });
32156
+ syncCmd.command("pull").description("Pull changes from cloud").option("--with-sessions", "Include session files in sync").option("--no-sessions", "Exclude session files from sync").action(async (opts) => {
32157
+ const configDir = getConfigDir();
32158
+ if (!await configExists(configDir)) {
32159
+ console.error("Not initialized. Run 'ccsini init' first.");
32160
+ process.exit(1);
32161
+ }
32162
+ const ora2 = (await Promise.resolve().then(() => (init_ora(), exports_ora))).default;
32163
+ const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
32164
+ try {
32165
+ const masterKey = await getMasterKey3(configDir);
32166
+ const client = await getAuthenticatedClient3(configDir);
32167
+ const config = await loadKeys(configDir);
32168
+ const storedSession = await loadSessionConfig(configDir);
32169
+ const sessionOptions = {
32170
+ enabled: opts.withSessions ? true : opts.sessions === false ? false : storedSession.enabled,
32171
+ maxPerProject: storedSession.maxPerProject,
32172
+ maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
32157
32173
  };
32158
- this._awaitWriteFinish(path2, awf.stabilityThreshold, event, awfEmit);
32159
- return this;
32174
+ const spinner = ora2("Pulling from cloud...").start();
32175
+ const result = await pullSync(client, masterKey, config.deviceName, configDir, (msg) => {
32176
+ spinner.text = msg;
32177
+ }, sessionOptions);
32178
+ if (result.errors.length > 0) {
32179
+ spinner.warn("Pull completed with errors");
32180
+ for (const err of result.errors) {
32181
+ console.error(chalk2.red(` ${err}`));
32182
+ }
32183
+ } else {
32184
+ spinner.succeed("Pull complete");
32185
+ }
32186
+ console.log(chalk2.dim(` ${result.filesChanged} files changed, ${formatBytes(result.bytesTransferred)} transferred in ${result.durationMs}ms`));
32187
+ if (sessionOptions.enabled) {
32188
+ console.log(chalk2.dim(" Sessions: enabled"));
32189
+ }
32190
+ if (result.conflicts.length > 0) {
32191
+ console.log(chalk2.yellow(` \u26A0 ${result.conflicts.length} conflict(s) \u2014 check .conflict files in ~/.claude/`));
32192
+ }
32193
+ } catch (e) {
32194
+ console.error(`Pull failed: ${e.message}`);
32195
+ process.exit(1);
32160
32196
  }
32161
- if (event === EVENTS.CHANGE) {
32162
- const isThrottled = !this._throttle(EVENTS.CHANGE, path2, 50);
32163
- if (isThrottled)
32164
- return this;
32197
+ });
32198
+ syncCmd.command("cleanup").description("Remove orphaned blobs from cloud storage").option("--dry-run", "Show what would be deleted without deleting").action(async (opts) => {
32199
+ const configDir = getConfigDir();
32200
+ if (!await configExists(configDir)) {
32201
+ console.error("Not initialized. Run 'ccsini init' first.");
32202
+ process.exit(1);
32165
32203
  }
32166
- if (opts.alwaysStat && stats === undefined && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
32167
- const fullPath = opts.cwd ? sp2.join(opts.cwd, path2) : path2;
32168
- let stats2;
32204
+ const ora2 = (await Promise.resolve().then(() => (init_ora(), exports_ora))).default;
32205
+ const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
32206
+ try {
32207
+ const masterKey = await getMasterKey3(configDir);
32208
+ const client = await getAuthenticatedClient3(configDir);
32209
+ const spinner = ora2("Fetching remote manifest...").start();
32210
+ const remoteManifestEnc = await client.getManifest();
32211
+ if (!remoteManifestEnc) {
32212
+ spinner.fail("No remote manifest found. Nothing to clean up.");
32213
+ return;
32214
+ }
32215
+ let remoteManifest;
32169
32216
  try {
32170
- stats2 = await stat4(fullPath);
32171
- } catch (err) {}
32172
- if (!stats2 || this.closed)
32217
+ const decrypted = decryptFile(masterKey, "__manifest__", remoteManifestEnc);
32218
+ remoteManifest = JSON.parse(new TextDecoder().decode(decrypted));
32219
+ } catch {
32220
+ spinner.fail("Failed to decrypt remote manifest");
32221
+ process.exit(1);
32222
+ }
32223
+ const referencedHashes = new Set;
32224
+ for (const entry of Object.values(remoteManifest.files)) {
32225
+ referencedHashes.add(entry.hash);
32226
+ }
32227
+ spinner.text = "Listing remote blobs...";
32228
+ const { hashes: serverHashes, totalSize } = await client.listBlobs();
32229
+ const orphans = serverHashes.filter((h) => !referencedHashes.has(h));
32230
+ if (orphans.length === 0) {
32231
+ spinner.succeed("No orphaned blobs found");
32232
+ console.log(chalk2.dim(` ${serverHashes.length} blobs on server, all referenced by manifest`));
32173
32233
  return;
32174
- args.push(stats2);
32175
- }
32176
- this.emitWithAll(event, args);
32177
- return this;
32178
- }
32179
- _handleError(error) {
32180
- const code = error && error.code;
32181
- if (error && code !== "ENOENT" && code !== "ENOTDIR" && (!this.options.ignorePermissionErrors || code !== "EPERM" && code !== "EACCES")) {
32182
- this.emit(EVENTS.ERROR, error);
32183
- }
32184
- return error || this.closed;
32185
- }
32186
- _throttle(actionType, path2, timeout) {
32187
- if (!this._throttled.has(actionType)) {
32188
- this._throttled.set(actionType, new Map);
32189
- }
32190
- const action = this._throttled.get(actionType);
32191
- if (!action)
32192
- throw new Error("invalid throttle");
32193
- const actionPath = action.get(path2);
32194
- if (actionPath) {
32195
- actionPath.count++;
32196
- return false;
32197
- }
32198
- let timeoutObject;
32199
- const clear = () => {
32200
- const item = action.get(path2);
32201
- const count = item ? item.count : 0;
32202
- action.delete(path2);
32203
- clearTimeout(timeoutObject);
32204
- if (item)
32205
- clearTimeout(item.timeoutObject);
32206
- return count;
32207
- };
32208
- timeoutObject = setTimeout(clear, timeout);
32209
- const thr = { timeoutObject, clear, count: 0 };
32210
- action.set(path2, thr);
32211
- return thr;
32212
- }
32213
- _incrReadyCount() {
32214
- return this._readyCount++;
32215
- }
32216
- _awaitWriteFinish(path2, threshold, event, awfEmit) {
32217
- const awf = this.options.awaitWriteFinish;
32218
- if (typeof awf !== "object")
32219
- return;
32220
- const pollInterval = awf.pollInterval;
32221
- let timeoutHandler;
32222
- let fullPath = path2;
32223
- if (this.options.cwd && !sp2.isAbsolute(path2)) {
32224
- fullPath = sp2.join(this.options.cwd, path2);
32225
- }
32226
- const now = new Date;
32227
- const writes = this._pendingWrites;
32228
- function awaitWriteFinishFn(prevStat) {
32229
- statcb(fullPath, (err, curStat) => {
32230
- if (err || !writes.has(path2)) {
32231
- if (err && err.code !== "ENOENT")
32232
- awfEmit(err);
32233
- return;
32234
+ }
32235
+ spinner.stop();
32236
+ console.log(`Found ${chalk2.yellow(orphans.length)} orphaned blobs out of ${serverHashes.length} total`);
32237
+ if (opts.dryRun) {
32238
+ console.log(chalk2.dim(" Dry run \u2014 no blobs deleted"));
32239
+ for (const hash of orphans.slice(0, 10)) {
32240
+ console.log(chalk2.dim(` ${hash}`));
32234
32241
  }
32235
- const now2 = Number(new Date);
32236
- if (prevStat && curStat.size !== prevStat.size) {
32237
- writes.get(path2).lastChange = now2;
32242
+ if (orphans.length > 10) {
32243
+ console.log(chalk2.dim(` ... and ${orphans.length - 10} more`));
32238
32244
  }
32239
- const pw = writes.get(path2);
32240
- const df = now2 - pw.lastChange;
32241
- if (df >= threshold) {
32242
- writes.delete(path2);
32243
- awfEmit(undefined, curStat);
32244
- } else {
32245
- timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
32245
+ return;
32246
+ }
32247
+ const inquirer2 = await Promise.resolve().then(() => (init_dist16(), exports_dist));
32248
+ const { confirm } = await inquirer2.default.prompt([
32249
+ {
32250
+ type: "confirm",
32251
+ name: "confirm",
32252
+ message: `Delete ${orphans.length} orphaned blobs?`,
32253
+ default: false
32246
32254
  }
32255
+ ]);
32256
+ if (!confirm) {
32257
+ console.log("Cancelled.");
32258
+ return;
32259
+ }
32260
+ const deleteSpinner = ora2("Deleting orphaned blobs...").start();
32261
+ const batchSize = 500;
32262
+ let deleted = 0;
32263
+ for (let i = 0;i < orphans.length; i += batchSize) {
32264
+ const batch = orphans.slice(i, i + batchSize);
32265
+ const result = await client.deleteBlobs(batch);
32266
+ deleted += result.deleted;
32267
+ deleteSpinner.text = `Deleting orphaned blobs... ${deleted}/${orphans.length}`;
32268
+ }
32269
+ deleteSpinner.succeed(`Deleted ${deleted} orphaned blobs`);
32270
+ } catch (e) {
32271
+ console.error(`Cleanup failed: ${e.message}`);
32272
+ process.exit(1);
32273
+ }
32274
+ });
32275
+ syncCmd.command("status").description("Show sync status").action(async () => {
32276
+ const configDir = getConfigDir();
32277
+ if (!await configExists(configDir)) {
32278
+ console.error("Not initialized. Run 'ccsini init' first.");
32279
+ process.exit(1);
32280
+ }
32281
+ const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
32282
+ const ora2 = (await Promise.resolve().then(() => (init_ora(), exports_ora))).default;
32283
+ try {
32284
+ const masterKey = await getMasterKey3(configDir);
32285
+ const client = await getAuthenticatedClient3(configDir);
32286
+ const config = await loadKeys(configDir);
32287
+ const spinner = ora2("Checking status...").start();
32288
+ const localManifest = await loadManifest(configDir);
32289
+ const claudeDir = getClaudeDir();
32290
+ const { manifest: currentManifest } = await generateManifest(claudeDir, config.deviceName, (msg) => {
32291
+ spinner.text = msg;
32247
32292
  });
32293
+ let remoteManifest = null;
32294
+ const remoteManifestEnc = await client.getManifest();
32295
+ if (remoteManifestEnc) {
32296
+ try {
32297
+ const decrypted = decryptFile(masterKey, "__manifest__", remoteManifestEnc);
32298
+ remoteManifest = JSON.parse(new TextDecoder().decode(decrypted));
32299
+ } catch {
32300
+ spinner.fail("Failed to decrypt remote manifest");
32301
+ console.error("Wrong encryption password?");
32302
+ process.exit(1);
32303
+ }
32304
+ }
32305
+ spinner.stop();
32306
+ const diffs = diffManifests(currentManifest, remoteManifest);
32307
+ const pendingPush = diffs.filter((d) => d.action === "push" || d.action === "merge").length;
32308
+ const pendingPull = diffs.filter((d) => d.action === "pull" || d.action === "merge").length;
32309
+ console.log(chalk2.bold("Sync Status"));
32310
+ console.log(` Device: ${config.deviceName}`);
32311
+ console.log(` Pending push: ${pendingPush === 0 ? chalk2.green("0") : chalk2.yellow(pendingPush)} files`);
32312
+ console.log(` Pending pull: ${pendingPull === 0 ? chalk2.green("0") : chalk2.yellow(pendingPull)} files`);
32313
+ if (localManifest) {
32314
+ const lastSync = new Date(localManifest.timestamp);
32315
+ console.log(` Last sync: ${lastSync.toLocaleString()}`);
32316
+ console.log(` Last device: ${localManifest.device}`);
32317
+ } else {
32318
+ console.log(` Last sync: ${chalk2.dim("never")}`);
32319
+ }
32320
+ if (!remoteManifestEnc) {
32321
+ console.log(chalk2.dim(`
32322
+ No remote data yet. Run 'ccsini sync push' first.`));
32323
+ }
32324
+ } catch (e) {
32325
+ console.error(`Status check failed: ${e.message}`);
32326
+ process.exit(1);
32248
32327
  }
32249
- if (!writes.has(path2)) {
32250
- writes.set(path2, {
32251
- lastChange: now,
32252
- cancelWait: () => {
32253
- writes.delete(path2);
32254
- clearTimeout(timeoutHandler);
32255
- return event;
32256
- }
32257
- });
32258
- timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval);
32328
+ });
32329
+ }
32330
+
32331
+ // src/commands/hooks.ts
32332
+ init_source();
32333
+ function registerHooksCommands(program2) {
32334
+ const hooksCmd = program2.command("hooks").description("Manage Claude Code hooks");
32335
+ hooksCmd.command("fix").description("Repair broken hook entries in Claude Code settings").action(async () => {
32336
+ const claudeDir = getClaudeDir();
32337
+ console.log(source_default.bold(`
32338
+ Checking hooks...
32339
+ `));
32340
+ const { fixed, details } = await fixHooks(claudeDir);
32341
+ if (fixed === 0) {
32342
+ console.log(source_default.green(` Hooks are already healthy. Nothing to fix.
32343
+ `));
32344
+ return;
32259
32345
  }
32260
- }
32261
- _isIgnored(path2, stats) {
32262
- if (this.options.atomic && DOT_RE.test(path2))
32263
- return true;
32264
- if (!this._userIgnored) {
32265
- const { cwd } = this.options;
32266
- const ign = this.options.ignored;
32267
- const ignored = (ign || []).map(normalizeIgnored(cwd));
32268
- const ignoredPaths = [...this._ignoredPaths];
32269
- const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
32270
- this._userIgnored = anymatch(list, undefined);
32346
+ for (const detail of details) {
32347
+ console.log(source_default.yellow(` Fixed: ${detail}`));
32271
32348
  }
32272
- return this._userIgnored(path2, stats);
32273
- }
32274
- _isntIgnored(path2, stat5) {
32275
- return !this._isIgnored(path2, stat5);
32276
- }
32277
- _getWatchHelpers(path2) {
32278
- return new WatchHelper(path2, this.options.followSymlinks, this);
32279
- }
32280
- _getWatchedDir(directory) {
32281
- const dir = sp2.resolve(directory);
32282
- if (!this._watched.has(dir))
32283
- this._watched.set(dir, new DirEntry(dir, this._boundRemove));
32284
- return this._watched.get(dir);
32285
- }
32286
- _hasReadPermissions(stats) {
32287
- if (this.options.ignorePermissionErrors)
32288
- return true;
32289
- return Boolean(Number(stats.mode) & 256);
32290
- }
32291
- _remove(directory, item, isDirectory) {
32292
- const path2 = sp2.join(directory, item);
32293
- const fullPath = sp2.resolve(path2);
32294
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path2) || this._watched.has(fullPath);
32295
- if (!this._throttle("remove", path2, 100))
32349
+ console.log(source_default.green(`
32350
+ Repaired ${fixed} hook issue${fixed > 1 ? "s" : ""}.
32351
+ `));
32352
+ });
32353
+ }
32354
+
32355
+ // src/commands/reset.ts
32356
+ init_auth();
32357
+ import { rm } from "fs/promises";
32358
+ function registerResetCommand(program2) {
32359
+ program2.command("reset").description("Wipe all server data and local config (full account reset)").action(async () => {
32360
+ const configDir = getConfigDir();
32361
+ if (!await configExists(configDir)) {
32362
+ console.error("Not initialized. Nothing to reset.");
32363
+ process.exit(1);
32364
+ }
32365
+ const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
32366
+ const inquirer2 = await Promise.resolve().then(() => (init_dist16(), exports_dist));
32367
+ console.log(chalk2.red.bold(`
32368
+ WARNING: This will permanently delete:`));
32369
+ console.log(chalk2.red(" - All encrypted files on the server"));
32370
+ console.log(chalk2.red(" - Your encryption salt"));
32371
+ console.log(chalk2.red(` - Your local config (~/.ccsini/)
32372
+ `));
32373
+ const { confirm } = await inquirer2.default.prompt([
32374
+ {
32375
+ type: "confirm",
32376
+ name: "confirm",
32377
+ message: "Are you sure you want to wipe everything?",
32378
+ default: false
32379
+ }
32380
+ ]);
32381
+ if (!confirm) {
32382
+ console.log("Cancelled.");
32296
32383
  return;
32297
- if (!isDirectory && this._watched.size === 1) {
32298
- this.add(directory, item, true);
32299
32384
  }
32300
- const wp = this._getWatchedDir(path2);
32301
- const nestedDirectoryChildren = wp.getChildren();
32302
- nestedDirectoryChildren.forEach((nested) => this._remove(path2, nested));
32303
- const parent = this._getWatchedDir(directory);
32304
- const wasTracked = parent.has(item);
32305
- parent.remove(item);
32306
- if (this._symlinkPaths.has(fullPath)) {
32307
- this._symlinkPaths.delete(fullPath);
32385
+ const ora2 = (await Promise.resolve().then(() => (init_ora(), exports_ora))).default;
32386
+ try {
32387
+ const config = await loadKeys(configDir);
32388
+ const privateKey = await importPrivateKey(config.devicePrivateKey);
32389
+ const jwt = await createDeviceJWT(privateKey, config.deviceId);
32390
+ const client = new CcsiniClient(config.apiUrl, jwt);
32391
+ const spinner = ora2("Wiping server data...").start();
32392
+ try {
32393
+ const { blobsDeleted } = await client.resetAll();
32394
+ spinner.succeed(`Server data wiped (${blobsDeleted} blob${blobsDeleted !== 1 ? "s" : ""} deleted)`);
32395
+ } catch {
32396
+ spinner.warn("Could not wipe server data (device may have been removed from dashboard)");
32397
+ }
32398
+ const localSpinner = ora2("Removing local config...").start();
32399
+ await rm(configDir, { recursive: true, force: true });
32400
+ localSpinner.succeed("Local config removed");
32401
+ console.log(chalk2.green(`
32402
+ Account fully reset. Run 'ccsini init --token <token>' to start fresh.`));
32403
+ } catch (e) {
32404
+ console.error(`Reset failed: ${e.message}`);
32405
+ process.exit(1);
32308
32406
  }
32309
- let relPath = path2;
32310
- if (this.options.cwd)
32311
- relPath = sp2.relative(this.options.cwd, path2);
32312
- if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
32313
- const event = this._pendingWrites.get(relPath).cancelWait();
32314
- if (event === EVENTS.ADD)
32315
- return;
32407
+ });
32408
+ }
32409
+
32410
+ // src/commands/sessions.ts
32411
+ init_src();
32412
+ function formatBytes2(bytes) {
32413
+ if (bytes < 1024)
32414
+ return `${bytes} B`;
32415
+ if (bytes < 1024 * 1024)
32416
+ return `${(bytes / 1024).toFixed(1)} KB`;
32417
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
32418
+ }
32419
+ function registerSessionsCommand(program2) {
32420
+ const sessionsCmd = program2.command("sessions").description("Configure session sync (continue conversations across devices)");
32421
+ sessionsCmd.command("enable").description("Enable session sync").option("-n, --per-project <count>", "Max sessions per project", String(SESSION_SYNC_DEFAULTS.maxPerProject)).action(async (opts) => {
32422
+ const configDir = getConfigDir();
32423
+ if (!await configExists(configDir)) {
32424
+ console.error("Not initialized. Run 'ccsini init' first.");
32425
+ process.exit(1);
32316
32426
  }
32317
- this._watched.delete(path2);
32318
- this._watched.delete(fullPath);
32319
- const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
32320
- if (wasTracked && !this._isIgnored(path2))
32321
- this._emit(eventName, path2);
32322
- this._closePath(path2);
32323
- }
32324
- _closePath(path2) {
32325
- this._closeFile(path2);
32326
- const dir = sp2.dirname(path2);
32327
- this._getWatchedDir(dir).remove(sp2.basename(path2));
32328
- }
32329
- _closeFile(path2) {
32330
- const closers = this._closers.get(path2);
32331
- if (!closers)
32332
- return;
32333
- closers.forEach((closer) => closer());
32334
- this._closers.delete(path2);
32335
- }
32336
- _addPathCloser(path2, closer) {
32337
- if (!closer)
32338
- return;
32339
- let list = this._closers.get(path2);
32340
- if (!list) {
32341
- list = [];
32342
- this._closers.set(path2, list);
32427
+ const maxPerProject = parseInt(opts.perProject, 10);
32428
+ if (isNaN(maxPerProject) || maxPerProject < 1) {
32429
+ console.error("--per-project must be a positive integer");
32430
+ process.exit(1);
32343
32431
  }
32344
- list.push(closer);
32345
- }
32346
- _readdirp(root, opts) {
32347
- if (this.closed)
32348
- return;
32349
- const options = { type: EVENTS.ALL, alwaysStat: true, lstat: true, ...opts, depth: 0 };
32350
- let stream = readdirp(root, options);
32351
- this._streams.add(stream);
32352
- stream.once(STR_CLOSE, () => {
32353
- stream = undefined;
32432
+ await saveSessionConfig(configDir, {
32433
+ enabled: true,
32434
+ maxPerProject
32354
32435
  });
32355
- stream.once(STR_END, () => {
32356
- if (stream) {
32357
- this._streams.delete(stream);
32358
- stream = undefined;
32436
+ console.log(`Session sync enabled (${maxPerProject} sessions per project)`);
32437
+ });
32438
+ sessionsCmd.command("disable").description("Disable session sync").action(async () => {
32439
+ const configDir = getConfigDir();
32440
+ if (!await configExists(configDir)) {
32441
+ console.error("Not initialized. Run 'ccsini init' first.");
32442
+ process.exit(1);
32443
+ }
32444
+ const current = await loadSessionConfig(configDir);
32445
+ await saveSessionConfig(configDir, {
32446
+ enabled: false,
32447
+ maxPerProject: current.maxPerProject
32448
+ });
32449
+ console.log("Session sync disabled");
32450
+ });
32451
+ sessionsCmd.command("status").description("Show session sync status and preview").action(async () => {
32452
+ const configDir = getConfigDir();
32453
+ if (!await configExists(configDir)) {
32454
+ console.error("Not initialized. Run 'ccsini init' first.");
32455
+ process.exit(1);
32456
+ }
32457
+ const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
32458
+ const ora2 = (await Promise.resolve().then(() => (init_ora(), exports_ora))).default;
32459
+ const sessionConfig = await loadSessionConfig(configDir);
32460
+ console.log(chalk2.bold("Session Sync"));
32461
+ console.log(` Status: ${sessionConfig.enabled ? chalk2.green("enabled") : chalk2.yellow("disabled")}`);
32462
+ console.log(` Per project: ${sessionConfig.maxPerProject}`);
32463
+ console.log(` Max file size: ${formatBytes2(SESSION_SYNC_DEFAULTS.maxFileSize)}`);
32464
+ const spinner = ora2("Scanning session files...").start();
32465
+ try {
32466
+ const claudeDir = getClaudeDir();
32467
+ const enabledOpts = {
32468
+ enabled: true,
32469
+ maxPerProject: sessionConfig.maxPerProject,
32470
+ maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
32471
+ };
32472
+ const withSessionsResult = await scanClaudeDir(claudeDir, undefined, enabledOpts);
32473
+ const sessionFiles = withSessionsResult.files.filter((f) => isSessionFile(f.relativePath));
32474
+ const disabledOpts = {
32475
+ enabled: false,
32476
+ maxPerProject: sessionConfig.maxPerProject,
32477
+ maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
32478
+ };
32479
+ const withoutSessionsResult = await scanClaudeDir(claudeDir, undefined, disabledOpts);
32480
+ const withoutSessions = withoutSessionsResult.files;
32481
+ spinner.stop();
32482
+ const totalSessionSize = sessionFiles.reduce((sum, f) => sum + f.size, 0);
32483
+ const projects = new Set(sessionFiles.map((f) => f.relativePath.split("/").slice(0, 2).join("/")));
32484
+ console.log(chalk2.bold(`
32485
+ Preview`));
32486
+ console.log(` Projects: ${projects.size}`);
32487
+ console.log(` Sessions: ${sessionFiles.length} files`);
32488
+ console.log(` Session size: ${formatBytes2(totalSessionSize)}`);
32489
+ console.log(` Other files: ${withoutSessions.length}`);
32490
+ if (!sessionConfig.enabled) {
32491
+ console.log(chalk2.dim(`
32492
+ Run 'ccsini sessions enable' to start syncing sessions`));
32359
32493
  }
32360
- });
32361
- return stream;
32362
- }
32363
- }
32364
- function watch(paths, options = {}) {
32365
- const watcher = new FSWatcher(options);
32366
- watcher.add(paths);
32367
- return watcher;
32368
- }
32369
-
32370
- // src/daemon/watcher.ts
32371
- init_src();
32372
- function startWatcher(onSync) {
32373
- const claudeDir = getClaudeDir();
32374
- let debounceTimer = null;
32375
- let syncing = false;
32376
- const watcher = watch(claudeDir, {
32377
- ignored: NEVER_SYNC_PATTERNS.map((p) => `**/${p}`),
32378
- persistent: true,
32379
- ignoreInitial: true,
32380
- awaitWriteFinish: {
32381
- stabilityThreshold: 2000,
32382
- pollInterval: 500
32494
+ } catch (e) {
32495
+ spinner.fail(`Scan failed: ${e.message}`);
32383
32496
  }
32384
32497
  });
32385
- const triggerSync = () => {
32386
- if (debounceTimer)
32387
- clearTimeout(debounceTimer);
32388
- debounceTimer = setTimeout(async () => {
32389
- if (syncing)
32390
- return;
32391
- syncing = true;
32392
- try {
32393
- await onSync();
32394
- } catch {} finally {
32395
- syncing = false;
32396
- }
32397
- }, DAEMON_DEBOUNCE_MS);
32398
- };
32399
- watcher.on("change", triggerSync);
32400
- watcher.on("add", triggerSync);
32401
- watcher.on("unlink", triggerSync);
32402
- return {
32403
- stop: () => {
32404
- if (debounceTimer)
32405
- clearTimeout(debounceTimer);
32406
- watcher.close();
32407
- }
32408
- };
32409
32498
  }
32410
32499
 
32411
- // src/daemon/runner.ts
32412
- init_src();
32413
- init_src();
32414
- var HEARTBEAT_INTERVAL_MS = 3 * 60 * 1000;
32415
- function getStatusPath(configDir) {
32416
- return join15(configDir, "daemon.status.json");
32417
- }
32418
- function getLogPath(configDir) {
32419
- return join15(configDir, "daemon.log");
32420
- }
32421
- function getPidPath(configDir) {
32422
- return join15(configDir, "daemon.pid");
32423
- }
32424
- function log(configDir, message2) {
32425
- const ts = new Date().toISOString();
32426
- const line = `[${ts}] ${message2}
32427
- `;
32428
- try {
32429
- appendFileSync(getLogPath(configDir), line);
32430
- } catch {}
32431
- }
32432
- async function getMasterKey3(configDir) {
32433
- const cachedKeyPath = join15(configDir, ".cached-key");
32434
- const cached = await readFile10(cachedKeyPath);
32435
- return new Uint8Array(cached);
32436
- }
32437
- async function getAuthenticatedClient3(configDir) {
32438
- const config = await loadKeys(configDir);
32439
- const privateKey = await importPrivateKey(config.devicePrivateKey);
32440
- const jwt = await createDeviceJWT(privateKey, config.deviceId);
32441
- return new CcsiniClient(config.apiUrl, jwt);
32442
- }
32443
- async function getSessionOptions(configDir) {
32444
- const storedSession = await loadSessionConfig(configDir);
32445
- return {
32446
- enabled: storedSession.enabled,
32447
- maxPerProject: storedSession.maxPerProject,
32448
- maxFileSize: SESSION_SYNC_DEFAULTS.maxFileSize
32449
- };
32450
- }
32451
- async function writeStatus(configDir, status) {
32452
- await writeFile10(getStatusPath(configDir), JSON.stringify(status, null, 2));
32453
- }
32454
- async function runDaemon() {
32455
- const configDir = getConfigDir();
32456
- if (!await configExists(configDir)) {
32457
- console.error("Not initialized. Run 'ccsini init' first.");
32458
- process.exit(1);
32459
- }
32460
- try {
32461
- await getMasterKey3(configDir);
32462
- } catch {
32463
- console.error("No cached key. Run 'ccsini unlock' first.");
32464
- process.exit(1);
32500
+ // src/commands/conflicts.ts
32501
+ import { readdir as readdir4, readFile as readFile10, unlink as unlink2, writeFile as writeFile10 } from "fs/promises";
32502
+ import { join as join15, relative as relative4 } from "path";
32503
+ async function findConflictFiles(dir, base) {
32504
+ base = base ?? dir;
32505
+ const results = [];
32506
+ const entries = await readdir4(dir, { withFileTypes: true });
32507
+ for (const entry of entries) {
32508
+ const full = join15(dir, entry.name);
32509
+ if (entry.isDirectory()) {
32510
+ results.push(...await findConflictFiles(full, base));
32511
+ } else if (entry.name.endsWith(".conflict")) {
32512
+ results.push(relative4(base, full).split("\\").join("/"));
32513
+ }
32465
32514
  }
32466
- const pid = process.pid;
32467
- await writeFile10(getPidPath(configDir), String(pid));
32468
- const status = {
32469
- pid,
32470
- startedAt: Date.now(),
32471
- lastPushAt: null,
32472
- lastPullAt: null,
32473
- lastError: null,
32474
- pushCount: 0,
32475
- pullCount: 0
32476
- };
32477
- await writeStatus(configDir, status);
32478
- log(configDir, `Daemon started (PID ${pid})`);
32479
- async function doPush() {
32480
- try {
32481
- const masterKey = await getMasterKey3(configDir);
32482
- const client = await getAuthenticatedClient3(configDir);
32483
- const config = await loadKeys(configDir);
32484
- const sessionOptions = await getSessionOptions(configDir);
32485
- const result = await pushSync(client, masterKey, config.deviceName, configDir, undefined, sessionOptions, { skipConflictBackup: true });
32486
- status.lastPushAt = Date.now();
32487
- status.pushCount++;
32488
- status.lastError = null;
32489
- await writeStatus(configDir, status);
32490
- if (result.filesChanged > 0) {
32491
- log(configDir, `Push: ${result.filesChanged} files (${result.durationMs}ms)`);
32492
- }
32493
- if (result.conflicts.length > 0) {
32494
- log(configDir, `Push: ${result.conflicts.length} conflict(s)`);
32515
+ return results;
32516
+ }
32517
+ function registerConflictsCommand(program2) {
32518
+ program2.command("conflicts").description("List and resolve sync conflicts").option("--keep <side>", "Resolve all: 'local' keeps .conflict, 'remote' discards .conflict").action(async (opts) => {
32519
+ const chalk2 = (await Promise.resolve().then(() => (init_source(), exports_source))).default;
32520
+ const claudeDir = getClaudeDir();
32521
+ const conflictFiles = await findConflictFiles(claudeDir);
32522
+ if (conflictFiles.length === 0) {
32523
+ console.log(chalk2.green("No conflicts found."));
32524
+ return;
32525
+ }
32526
+ console.log(chalk2.bold(`
32527
+ ${conflictFiles.length} conflict(s) found:
32528
+ `));
32529
+ for (const file of conflictFiles) {
32530
+ const original = file.replace(/\.conflict$/, "");
32531
+ console.log(` ${chalk2.yellow("\u26A0")} ${original}`);
32532
+ console.log(chalk2.dim(` conflict: ~/.claude/${file}`));
32533
+ }
32534
+ console.log();
32535
+ if (opts.keep === "remote") {
32536
+ for (const file of conflictFiles) {
32537
+ await unlink2(join15(claudeDir, file));
32495
32538
  }
32496
- } catch (e) {
32497
- status.lastError = e.message;
32498
- await writeStatus(configDir, status).catch(() => {});
32499
- log(configDir, `Push error: ${e.message}`);
32539
+ console.log(chalk2.green(`Resolved ${conflictFiles.length} conflict(s) \u2014 kept remote versions.`));
32540
+ return;
32500
32541
  }
32501
- }
32502
- async function doPull() {
32503
- try {
32504
- const masterKey = await getMasterKey3(configDir);
32505
- const client = await getAuthenticatedClient3(configDir);
32506
- const config = await loadKeys(configDir);
32507
- const sessionOptions = await getSessionOptions(configDir);
32508
- const result = await pullSync(client, masterKey, config.deviceName, configDir, undefined, sessionOptions);
32509
- status.lastPullAt = Date.now();
32510
- status.pullCount++;
32511
- status.lastError = null;
32512
- await writeStatus(configDir, status);
32513
- if (result.filesChanged > 0) {
32514
- log(configDir, `Pull: ${result.filesChanged} files (${result.durationMs}ms)`);
32542
+ if (opts.keep === "local") {
32543
+ for (const file of conflictFiles) {
32544
+ const conflictPath = join15(claudeDir, file);
32545
+ const originalPath = conflictPath.replace(/\.conflict$/, "");
32546
+ const conflictContent = await readFile10(conflictPath);
32547
+ await writeFile10(originalPath, conflictContent);
32548
+ await unlink2(conflictPath);
32515
32549
  }
32516
- if (result.conflicts.length > 0) {
32517
- log(configDir, `Pull: ${result.conflicts.length} conflict(s)`);
32550
+ console.log(chalk2.green(`Resolved ${conflictFiles.length} conflict(s) \u2014 kept local versions.`));
32551
+ return;
32552
+ }
32553
+ const inquirer2 = await Promise.resolve().then(() => (init_dist16(), exports_dist));
32554
+ for (const file of conflictFiles) {
32555
+ const original = file.replace(/\.conflict$/, "");
32556
+ const conflictPath = join15(claudeDir, file);
32557
+ const originalPath = join15(claudeDir, original);
32558
+ const { action } = await inquirer2.default.prompt([
32559
+ {
32560
+ type: "list",
32561
+ name: "action",
32562
+ message: `${original}:`,
32563
+ choices: [
32564
+ { name: "Keep remote (current file)", value: "remote" },
32565
+ { name: "Keep local (restore from .conflict)", value: "local" },
32566
+ { name: "Skip", value: "skip" }
32567
+ ]
32568
+ }
32569
+ ]);
32570
+ if (action === "remote") {
32571
+ await unlink2(conflictPath);
32572
+ console.log(chalk2.dim(` Kept remote \u2014 deleted ${file}`));
32573
+ } else if (action === "local") {
32574
+ const conflictContent = await readFile10(conflictPath);
32575
+ await writeFile10(originalPath, conflictContent);
32576
+ await unlink2(conflictPath);
32577
+ console.log(chalk2.dim(` Restored local version`));
32518
32578
  }
32519
- } catch (e) {
32520
- status.lastError = e.message;
32521
- await writeStatus(configDir, status).catch(() => {});
32522
- log(configDir, `Pull error: ${e.message}`);
32523
32579
  }
32524
- }
32525
- async function doHeartbeat() {
32526
- try {
32527
- const client = await getAuthenticatedClient3(configDir);
32528
- await client.heartbeat("daemon");
32529
- } catch (e) {
32530
- log(configDir, `Heartbeat error: ${e.message}`);
32580
+ const remaining = await findConflictFiles(claudeDir);
32581
+ if (remaining.length === 0) {
32582
+ console.log(chalk2.green(`
32583
+ All conflicts resolved.`));
32584
+ } else {
32585
+ console.log(chalk2.yellow(`
32586
+ ${remaining.length} conflict(s) remaining.`));
32531
32587
  }
32532
- }
32533
- const watcher = startWatcher(doPush);
32534
- const pullInterval = setInterval(doPull, DAEMON_INTERVAL_MS);
32535
- const heartbeatInterval = setInterval(doHeartbeat, HEARTBEAT_INTERVAL_MS);
32536
- await doPull();
32537
- await doHeartbeat();
32538
- const shutdown = async () => {
32539
- log(configDir, "Daemon shutting down...");
32540
- watcher.stop();
32541
- clearInterval(pullInterval);
32542
- clearInterval(heartbeatInterval);
32543
- const { rm: rm2 } = await import("fs/promises");
32544
- await rm2(getPidPath(configDir)).catch(() => {});
32545
- await rm2(getStatusPath(configDir)).catch(() => {});
32546
- log(configDir, "Daemon stopped");
32547
- process.exit(0);
32548
- };
32549
- process.on("SIGTERM", shutdown);
32550
- process.on("SIGINT", shutdown);
32551
- if (platform3() === "win32") {
32552
- process.on("SIGBREAK", shutdown);
32553
- }
32588
+ });
32554
32589
  }
32555
32590
 
32556
32591
  // src/commands/daemon.ts
32592
+ import { spawn as spawn4 } from "child_process";
32593
+ import { readFile as readFile11, rm as rm2 } from "fs/promises";
32594
+ import { platform as platform5 } from "os";
32557
32595
  function isProcessRunning(pid) {
32558
32596
  try {
32559
32597
  process.kill(pid, 0);
@@ -32593,8 +32631,8 @@ function registerDaemonCommands(program2) {
32593
32631
  await rm2(getPidPath(configDir)).catch(() => {});
32594
32632
  await rm2(getStatusPath(configDir)).catch(() => {});
32595
32633
  }
32596
- const isWin = platform4() === "win32";
32597
- const child = spawn2("ccsini", ["daemon", "_run"], {
32634
+ const isWin = platform5() === "win32";
32635
+ const child = spawn4("ccsini", ["daemon", "_run"], {
32598
32636
  detached: true,
32599
32637
  stdio: "ignore",
32600
32638
  ...isWin ? { shell: true } : {}
@@ -32621,7 +32659,7 @@ function registerDaemonCommands(program2) {
32621
32659
  console.log("Daemon is not running (cleaned up stale PID file).");
32622
32660
  return;
32623
32661
  }
32624
- const isWin = platform4() === "win32";
32662
+ const isWin = platform5() === "win32";
32625
32663
  if (isWin) {
32626
32664
  const { execSync: execSync3 } = await import("child_process");
32627
32665
  try {