hsync-cli 1.1.4 → 1.1.6

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 (3) hide show
  1. package/dist/bin.js +2442 -2140
  2. package/dist/web/index.html +246 -130
  3. package/package.json +1 -1
@@ -208,45 +208,45 @@
208
208
  align-items: center;
209
209
  }
210
210
 
211
- /* Cards */
211
+ /* Profile list */
212
212
  .grid-layout {
213
- display: grid;
214
- grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
215
- gap: 20px;
213
+ display: flex;
214
+ flex-direction: column;
215
+ gap: 8px;
216
216
  }
217
217
 
218
218
  .card {
219
219
  background: var(--surface);
220
220
  border: 1px solid var(--surface-border);
221
221
  border-radius: var(--radius);
222
- padding: 20px;
223
- backdrop-filter: blur(12px);
224
- -webkit-backdrop-filter: blur(12px);
222
+ padding: 12px 16px;
225
223
  display: flex;
226
- flex-direction: column;
227
- justify-content: space-between;
228
- transition: transform 0.2s ease, border-color 0.2s ease;
229
- position: relative;
230
- overflow: hidden;
224
+ align-items: center;
225
+ gap: 16px;
226
+ transition: border-color 0.2s ease;
231
227
  }
232
228
 
233
229
  .card:hover {
234
- transform: translateY(-2px);
235
230
  border-color: rgba(255, 255, 255, 0.12);
236
231
  }
237
232
 
238
- .card-header {
233
+ .card-name {
239
234
  display: flex;
240
- justify-content: space-between;
235
+ flex-direction: column;
241
236
  align-items: flex-start;
242
- margin-bottom: 16px;
237
+ gap: 6px;
238
+ width: 140px;
239
+ flex-shrink: 0;
243
240
  }
244
241
 
245
242
  .profile-title {
246
- font-size: 1rem;
243
+ font-size: 0.95rem;
247
244
  font-weight: 700;
248
245
  color: #fff;
249
- letter-spacing: -0.01em;
246
+ overflow: hidden;
247
+ text-overflow: ellipsis;
248
+ white-space: nowrap;
249
+ max-width: 100%;
250
250
  }
251
251
 
252
252
  .status-badge {
@@ -296,16 +296,26 @@
296
296
  }
297
297
 
298
298
  .card-body {
299
+ flex: 1;
300
+ min-width: 0;
299
301
  display: flex;
300
302
  flex-direction: column;
301
- gap: 12px;
302
- margin-bottom: 20px;
303
+ gap: 4px;
303
304
  }
304
305
 
305
- .path-group {
306
- display: flex;
307
- flex-direction: column;
308
- gap: 4px;
306
+ .path-value {
307
+ font-family: var(--font-mono);
308
+ font-size: 0.75rem;
309
+ color: #e2e8f0;
310
+ overflow: hidden;
311
+ text-overflow: ellipsis;
312
+ white-space: nowrap;
313
+ }
314
+
315
+ .path-value .path-label {
316
+ display: inline-block;
317
+ width: 52px;
318
+ font-family: inherit;
309
319
  }
310
320
 
311
321
  .path-label {
@@ -316,53 +326,24 @@
316
326
  letter-spacing: 0.05em;
317
327
  }
318
328
 
319
- .path-value {
320
- font-family: var(--font-mono);
321
- font-size: 0.75rem;
322
- background: rgba(0, 0, 0, 0.25);
323
- padding: 6px 10px;
324
- border-radius: 8px;
325
- border: 1px solid rgba(255, 255, 255, 0.03);
326
- overflow-x: auto;
327
- white-space: nowrap;
328
- color: #e2e8f0;
329
- }
330
-
331
329
  .meta-row {
332
- display: grid;
333
- grid-template-columns: repeat(3, 1fr);
334
- gap: 8px;
335
- margin-top: 4px;
336
- }
337
-
338
- .meta-item {
339
- display: flex;
340
- flex-direction: column;
341
- gap: 2px;
342
- background: rgba(255, 255, 255, 0.02);
343
- padding: 6px 8px;
344
- border-radius: 8px;
345
- border: 1px solid rgba(255, 255, 255, 0.02);
346
- text-align: center;
330
+ font-size: 0.7rem;
331
+ color: var(--text-muted);
347
332
  }
348
333
 
349
- .meta-val {
350
- font-size: 0.7rem;
351
- font-weight: 700;
334
+ .meta-row b {
352
335
  color: #fff;
353
336
  }
354
337
 
355
338
  .card-actions {
356
339
  display: flex;
357
- justify-content: space-between;
358
- align-items: center;
359
- border-top: 1px solid var(--surface-border);
360
- padding-top: 14px;
340
+ gap: 8px;
341
+ flex-shrink: 0;
361
342
  }
362
343
 
363
- .card-actions-left {
364
- display: flex;
365
- gap: 8px;
344
+ @media (max-width: 720px) {
345
+ .card { flex-wrap: wrap; }
346
+ .card-name { width: 100%; flex-direction: row; justify-content: space-between; }
366
347
  }
367
348
 
368
349
  /* Toggle Switch */
@@ -639,6 +620,35 @@
639
620
  line-height: 1.6;
640
621
  }
641
622
 
623
+ .health-dot {
624
+ width: 8px;
625
+ height: 8px;
626
+ border-radius: 50%;
627
+ background: var(--text-muted);
628
+ flex-shrink: 0;
629
+ }
630
+ .health-dot.ok { background: var(--success); }
631
+ .health-dot.warn { background: var(--warning); }
632
+ .health-dot.bad { background: var(--danger); }
633
+
634
+ .link-btn {
635
+ background: none;
636
+ border: none;
637
+ color: var(--accent);
638
+ font: inherit;
639
+ cursor: pointer;
640
+ }
641
+
642
+ .error-banner {
643
+ background: var(--danger-glow);
644
+ border: 1px solid rgba(244, 63, 94, 0.3);
645
+ color: var(--danger);
646
+ border-radius: 12px;
647
+ padding: 10px 14px;
648
+ font-size: 0.75rem;
649
+ margin-bottom: 16px;
650
+ }
651
+
642
652
  /* Autostart Card */
643
653
  .settings-card {
644
654
  background: var(--surface);
@@ -708,8 +718,10 @@
708
718
  <div class="brand-title">
709
719
  <h1>hsync Control Dashboard</h1>
710
720
  <div class="brand-server">
721
+ <span class="health-dot" id="healthDot" title="Checking server..."></span>
711
722
  <span>Server:</span>
712
723
  <span class="url" id="serverUrlText">Loading...</span>
724
+ <button class="link-btn" onclick="changeServer()">Change</button>
713
725
  </div>
714
726
  </div>
715
727
  </div>
@@ -736,6 +748,8 @@
736
748
  </div>
737
749
  </header>
738
750
 
751
+ <div class="error-banner" id="healthError" style="display: none;"></div>
752
+
739
753
  <div class="dashboard-grid">
740
754
  <!-- Profiles list -->
741
755
  <div class="profiles-section">
@@ -787,6 +801,20 @@
787
801
  </div>
788
802
  </div>
789
803
 
804
+ <div class="settings-card" style="flex-wrap: wrap; gap: 12px;">
805
+ <div class="settings-info">
806
+ <div class="settings-title">Account</div>
807
+ <div class="settings-desc" id="accountDesc">Checking login...</div>
808
+ </div>
809
+ <form id="loginForm" style="display: none; flex-wrap: wrap; gap: 8px; align-items: center;" onsubmit="event.preventDefault(); doLogin();">
810
+ <input class="form-input" id="loginEmail" type="email" placeholder="Email" autocomplete="username" required style="width: 190px;">
811
+ <input class="form-input" id="loginPassword" type="password" placeholder="Password" autocomplete="current-password" required style="width: 150px;">
812
+ <button class="btn btn-primary" type="submit">Login</button>
813
+ <button class="btn btn-secondary" type="button" onclick="doApiKeyLogin()">Use API Key</button>
814
+ </form>
815
+ <button class="btn btn-secondary" id="logoutBtn" style="display: none;" onclick="doLogout()">Logout</button>
816
+ </div>
817
+
790
818
  <!-- Logs -->
791
819
  <div class="terminal-section">
792
820
  <div class="console-header">
@@ -797,9 +825,14 @@
797
825
  </svg>
798
826
  <span>Sync Console Logs</span>
799
827
  </div>
800
- <button class="btn btn-secondary btn-icon" onclick="clearLogs()" title="Clear Log Feed" style="padding: 6px 12px; font-size: 0.7rem;">
801
- Clear Feed
802
- </button>
828
+ <div style="display: flex; gap: 8px;">
829
+ <button class="btn btn-danger btn-icon" onclick="deleteLogFile()" title="Delete all lines from ~/.hsync/daemon.log to free disk space" style="padding: 6px 12px; font-size: 0.7rem;">
830
+ Delete Log File
831
+ </button>
832
+ <button class="btn btn-secondary btn-icon" onclick="clearLogs()" title="Clear Log Feed" style="padding: 6px 12px; font-size: 0.7rem;">
833
+ Clear Feed
834
+ </button>
835
+ </div>
803
836
  </div>
804
837
  <div class="console-window" id="consoleLogs">
805
838
  <div class="console-empty">
@@ -871,11 +904,27 @@
871
904
  </div>
872
905
  </div>
873
906
 
874
- <div class="checkbox-group" onclick="toggleCheckboxValue(event)">
875
- <input type="checkbox" id="jobConflictCopy" checked>
876
- <label class="checkbox-label" for="jobConflictCopy">Create copy of conflicted files</label>
907
+ <div class="form-row">
908
+ <div class="form-group">
909
+ <label class="form-label" for="jobInclude">Include Globs (one per line)</label>
910
+ <textarea class="form-input" id="jobInclude" rows="3" placeholder="src/**&#10;*.md"></textarea>
911
+ </div>
912
+ <div class="form-group">
913
+ <label class="form-label" for="jobExclude">Exclude Globs (one per line)</label>
914
+ <textarea class="form-input" id="jobExclude" rows="3" placeholder="**/*.log&#10;dist/**"></textarea>
915
+ </div>
877
916
  </div>
878
917
 
918
+ <label class="checkbox-group">
919
+ <input type="checkbox" id="jobConflictCopy" checked>
920
+ <span class="checkbox-label">Create copy of conflicted files</span>
921
+ </label>
922
+
923
+ <label class="checkbox-group">
924
+ <input type="checkbox" id="jobGitignore" checked>
925
+ <span class="checkbox-label">Respect .gitignore files</span>
926
+ </label>
927
+
879
928
  <div class="modal-footer">
880
929
  <button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
881
930
  <button type="submit" class="btn btn-primary" id="saveBtn">Register Profile</button>
@@ -901,8 +950,82 @@
901
950
  // Setup periodic polling
902
951
  pollIntervalId = setInterval(pollServiceState, 3000);
903
952
  logPollIntervalId = setInterval(pollLogs, 3000);
953
+ pollHealth();
954
+ setInterval(pollHealth, 15000);
904
955
  });
905
956
 
957
+ async function pollHealth() {
958
+ let health;
959
+ try {
960
+ health = await (await fetch('/api/local/status')).json();
961
+ } catch {
962
+ return;
963
+ }
964
+ const ok = health.serverUp && health.loggedIn;
965
+ document.getElementById('serverUrlText').textContent = health.serverUrl;
966
+ const dot = document.getElementById('healthDot');
967
+ dot.className = `health-dot ${ok ? 'ok' : health.serverUp ? 'warn' : 'bad'}`;
968
+ dot.title = ok ? `Server up, logged in as ${health.email}` : health.error;
969
+
970
+ const errorEl = document.getElementById('healthError');
971
+ errorEl.textContent = health.error || '';
972
+ errorEl.style.display = health.error ? 'block' : 'none';
973
+
974
+ document.getElementById('accountDesc').textContent = health.loggedIn
975
+ ? `Signed in as ${health.email} (${health.role})`
976
+ : health.serverUp ? 'Not logged in. Sync jobs cannot run until you log in.' : 'Server unreachable.';
977
+ document.getElementById('loginForm').style.display = health.loggedIn ? 'none' : 'flex';
978
+ document.getElementById('logoutBtn').style.display = health.loggedIn ? 'inline-flex' : 'none';
979
+ }
980
+
981
+ async function postLocal(url, body) {
982
+ const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
983
+ if (!res.ok) throw new Error((await res.json()).error || `Request failed (${res.status})`);
984
+ }
985
+
986
+ async function changeServer() {
987
+ const url = prompt('Server URL', document.getElementById('serverUrlText').textContent);
988
+ if (!url) return;
989
+ try {
990
+ await postLocal('/api/local/server', { url: url.trim() });
991
+ } catch (err) {
992
+ alert(err.message);
993
+ }
994
+ await pollHealth();
995
+ }
996
+
997
+ async function doLogin() {
998
+ const passwordEl = document.getElementById('loginPassword');
999
+ try {
1000
+ await postLocal('/api/local/login', { email: document.getElementById('loginEmail').value.trim(), password: passwordEl.value });
1001
+ passwordEl.value = '';
1002
+ } catch (err) {
1003
+ alert(`Login failed: ${err.message}`);
1004
+ }
1005
+ await pollHealth();
1006
+ }
1007
+
1008
+ async function doApiKeyLogin() {
1009
+ const apiKey = prompt('Paste an API key (create one in the web app under Settings)');
1010
+ if (!apiKey) return;
1011
+ try {
1012
+ await postLocal('/api/local/login', { apiKey: apiKey.trim() });
1013
+ } catch (err) {
1014
+ alert(`Login failed: ${err.message}`);
1015
+ }
1016
+ await pollHealth();
1017
+ }
1018
+
1019
+ async function doLogout() {
1020
+ if (!confirm('Log out? Sync jobs will stop working until you log in again.')) return;
1021
+ try {
1022
+ await postLocal('/api/local/logout');
1023
+ } catch (err) {
1024
+ alert(err.message);
1025
+ }
1026
+ await pollHealth();
1027
+ }
1028
+
906
1029
  async function loadDashboardData() {
907
1030
  showLoading(true);
908
1031
  try {
@@ -1043,7 +1166,7 @@
1043
1166
  // Parse timestamps [HH:MM:SS] if present
1044
1167
  let time = '';
1045
1168
  let text = line;
1046
- const match = line.match(/^(\[\d{1,2}:\d{2}:\d{2}\])\s*(.*)/);
1169
+ const match = line.match(/^(\[\d{1,2}:\d{2}:\d{2}(?: [AP]M)?\])\s*(.*)/);
1047
1170
  if (match) {
1048
1171
  time = match[1];
1049
1172
  text = match[2];
@@ -1069,6 +1192,17 @@
1069
1192
  }
1070
1193
  }
1071
1194
 
1195
+ async function deleteLogFile() {
1196
+ if (!confirm('Delete all lines from the log file on disk? This cannot be undone.')) return;
1197
+ try {
1198
+ const res = await fetch('/api/local/logs/clear', { method: 'POST' });
1199
+ if (!res.ok) throw new Error((await res.json()).error || 'Failed to clear log file.');
1200
+ clearLogs();
1201
+ } catch (err) {
1202
+ alert(err.message);
1203
+ }
1204
+ }
1205
+
1072
1206
  function clearLogs() {
1073
1207
  // Only hides the view; the log file on disk is untouched
1074
1208
  clearedThroughLine = lastLogLine;
@@ -1094,7 +1228,7 @@
1094
1228
  return;
1095
1229
  }
1096
1230
 
1097
- grid.style.display = 'grid';
1231
+ grid.style.display = 'flex';
1098
1232
  empty.style.display = 'none';
1099
1233
  grid.innerHTML = '';
1100
1234
 
@@ -1105,60 +1239,37 @@
1105
1239
 
1106
1240
  // Setup card HTML structure
1107
1241
  card.innerHTML = `
1108
- <div>
1109
- <div class="card-header">
1110
- <span class="profile-title">${escapeHtml(job.name)}</span>
1111
- <button class="status-badge ${job.enabled ? 'status-active' : 'status-inactive'}" style="cursor: pointer;" onclick="toggleEnabled('${escapeJs(job.name)}')" title="${job.enabled ? 'Pause this job in the sync service' : 'Resume this job in the sync service'}">
1112
- <span class="pulse-dot"></span>
1113
- <span class="badge-text">${job.enabled ? 'Enabled' : 'Paused'}</span>
1114
- </button>
1115
- </div>
1116
-
1117
- <div class="card-body">
1118
- <div class="path-group">
1119
- <span class="path-label">Local Directory</span>
1120
- <span class="path-value">${escapeHtml(job.localPath)}</span>
1121
- </div>
1122
- <div class="path-group">
1123
- <span class="path-label">Remote Destination</span>
1124
- <span class="path-value">${escapeHtml(job.remoteBucket)}:${escapeHtml(job.remotePath || '')}</span>
1125
- </div>
1126
-
1127
- <div class="meta-row">
1128
- <div class="meta-item">
1129
- <span class="path-label">Direction</span>
1130
- <span class="meta-val">${capitalize(job.direction)}</span>
1131
- </div>
1132
- <div class="meta-item">
1133
- <span class="path-label">Interval</span>
1134
- <span class="meta-val">${job.intervalMs ? job.intervalMs / 1000 : 10}s</span>
1135
- </div>
1136
- <div class="meta-item">
1137
- <span class="path-label">Conflict Copy</span>
1138
- <span class="meta-val">${job.conflictCopy ? 'Yes' : 'No'}</span>
1139
- </div>
1140
- </div>
1141
- </div>
1242
+ <div class="card-name">
1243
+ <span class="profile-title" title="${escapeHtml(job.name)}">${escapeHtml(job.name)}</span>
1244
+ <button class="status-badge ${job.enabled ? 'status-active' : 'status-inactive'}" style="cursor: pointer;" onclick="toggleEnabled('${escapeJs(job.name)}')" title="${job.enabled ? 'Pause this job in the sync service' : 'Resume this job in the sync service'}">
1245
+ <span class="pulse-dot"></span>
1246
+ <span class="badge-text">${job.enabled ? 'Enabled' : 'Paused'}</span>
1247
+ </button>
1248
+ </div>
1249
+
1250
+ <div class="card-body">
1251
+ <span class="path-value" title="${escapeHtml(job.localPath)}"><span class="path-label">Local</span>${escapeHtml(job.localPath)}</span>
1252
+ <span class="path-value" title="${escapeHtml(job.remoteBucket)}:${escapeHtml(job.remotePath || '')}"><span class="path-label">Remote</span>${escapeHtml(job.remoteBucket)}:${escapeHtml(job.remotePath || '')}</span>
1253
+ <span class="meta-row">
1254
+ <b>${capitalize(job.direction)}</b> · every <b>${job.intervalMs ? job.intervalMs / 1000 : 10}s</b>
1255
+ · conflict copy <b>${job.conflictCopy ? 'Yes' : 'No'}</b>
1256
+ · .gitignore <b>${job.respectGitignore !== false ? 'Yes' : 'No'}</b>
1257
+ </span>
1142
1258
  </div>
1143
1259
 
1144
1260
  <div class="card-actions">
1145
- <div class="card-actions-left">
1146
- <button class="btn btn-secondary btn-icon" onclick="openEditModal('${job.id}')" title="Edit Profile">
1147
- <!-- Edit icon -->
1148
- <svg style="width: 14px; height: 14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1149
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
1150
- <path d="M18.5 2.5a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
1151
- </svg>
1152
- </button>
1153
- <button class="btn btn-danger btn-icon" onclick="deleteProfile('${job.id}', '${escapeJs(job.name)}')" title="Delete Profile">
1154
- <!-- Trash icon -->
1155
- <svg style="width: 14px; height: 14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1156
- <polyline points="3 6 5 6 21 6"></polyline>
1157
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
1158
- </svg>
1159
- </button>
1160
- </div>
1161
-
1261
+ <button class="btn btn-secondary btn-icon" onclick="openEditModal('${job.id}')" title="Edit Profile">
1262
+ <svg style="width: 14px; height: 14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1263
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
1264
+ <path d="M18.5 2.5a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
1265
+ </svg>
1266
+ </button>
1267
+ <button class="btn btn-danger btn-icon" onclick="deleteProfile('${job.id}', '${escapeJs(job.name)}')" title="Delete Profile">
1268
+ <svg style="width: 14px; height: 14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1269
+ <polyline points="3 6 5 6 21 6"></polyline>
1270
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
1271
+ </svg>
1272
+ </button>
1162
1273
  <button class="btn btn-primary" id="run-btn-${job.id}" onclick="syncNow('${job.id}', '${escapeJs(job.name)}')" title="Run one sync pass now, even if paused">
1163
1274
  <svg style="width: 12px; height: 12px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg> Sync now
1164
1275
  </button>
@@ -1227,6 +1338,7 @@
1227
1338
  document.getElementById('jobForm').reset();
1228
1339
  document.getElementById('saveBtn').textContent = 'Register Profile';
1229
1340
  document.getElementById('jobConflictCopy').checked = true;
1341
+ document.getElementById('jobGitignore').checked = true;
1230
1342
 
1231
1343
  document.getElementById('jobModalBackdrop').classList.add('open');
1232
1344
  }
@@ -1245,6 +1357,9 @@
1245
1357
  document.getElementById('jobStrategy').value = job.conflictStrategy || 'newer-wins';
1246
1358
  document.getElementById('jobInterval').value = job.intervalMs ? job.intervalMs / 1000 : 10;
1247
1359
  document.getElementById('jobConflictCopy').checked = !!job.conflictCopy;
1360
+ document.getElementById('jobGitignore').checked = job.respectGitignore !== false;
1361
+ document.getElementById('jobInclude').value = (job.filePatterns || []).join('\n');
1362
+ document.getElementById('jobExclude').value = (job.excludePatterns || []).join('\n');
1248
1363
  document.getElementById('saveBtn').textContent = 'Save Changes';
1249
1364
 
1250
1365
  document.getElementById('jobModalBackdrop').classList.add('open');
@@ -1260,14 +1375,6 @@
1260
1375
  }
1261
1376
  }
1262
1377
 
1263
- function toggleCheckboxValue(e) {
1264
- // Allow clicking label/row to toggle the checkbox
1265
- if (e.target.id !== 'jobConflictCopy') {
1266
- const checkbox = document.getElementById('jobConflictCopy');
1267
- checkbox.checked = !checkbox.checked;
1268
- }
1269
- }
1270
-
1271
1378
  async function saveJob(e) {
1272
1379
  e.preventDefault();
1273
1380
 
@@ -1279,6 +1386,9 @@
1279
1386
  const strategy = document.getElementById('jobStrategy').value;
1280
1387
  const intervalSec = parseInt(document.getElementById('jobInterval').value, 10);
1281
1388
  const conflictCopy = document.getElementById('jobConflictCopy').checked;
1389
+ const gitignore = document.getElementById('jobGitignore').checked;
1390
+ const include = document.getElementById('jobInclude').value.split('\n').map(s => s.trim()).filter(Boolean);
1391
+ const exclude = document.getElementById('jobExclude').value.split('\n').map(s => s.trim()).filter(Boolean);
1282
1392
 
1283
1393
  // Sanitize name/ID to matching requirements: lowercased alphanumeric + hyphen + underscores
1284
1394
  const name = rawName.toLowerCase().replace(/[^a-z0-9_-]/g, '');
@@ -1300,7 +1410,10 @@
1300
1410
  direction,
1301
1411
  strategy,
1302
1412
  intervalSec,
1303
- noConflictCopy: !conflictCopy
1413
+ noConflictCopy: !conflictCopy,
1414
+ include,
1415
+ exclude,
1416
+ gitignore
1304
1417
  })
1305
1418
  });
1306
1419
 
@@ -1330,7 +1443,10 @@
1330
1443
  conflictStrategy: strategy,
1331
1444
  conflictCopy,
1332
1445
  intervalMs: intervalSec * 1000,
1333
- enabled: true
1446
+ enabled: true,
1447
+ filePatterns: include,
1448
+ excludePatterns: exclude,
1449
+ respectGitignore: gitignore
1334
1450
  };
1335
1451
 
1336
1452
  const configRes = await fetch('/api/local/config');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hsync-cli",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "hsync CLI for file sync operations",
5
5
  "bin": {
6
6
  "hsync": "./dist/bin.js"