teambridge-candles 1.0.0__tar.gz

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.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: teambridge-candles
3
+ Version: 1.0.0
4
+ Requires-Dist: requests>=2.31.0
5
+ Requires-Dist: click>=8.1.0
6
+ Requires-Dist: watchdog>=3.0.0
7
+ Requires-Dist: python-socketio[client]>=5.11.0
8
+ Requires-Dist: inquirer>=3.1.3
9
+ Dynamic: requires-dist
File without changes
@@ -0,0 +1,61 @@
1
+ # 📄 Location: d:/Ptojects/TeamBridge/cli/candles/auth.py
2
+ import os
3
+ import json
4
+ import socket
5
+ from pathlib import Path
6
+
7
+ HOME_DIR = str(Path.home())
8
+ CONFIG_DIR = os.path.join(HOME_DIR, '.candles')
9
+ CONFIG_PATH = os.path.join(CONFIG_DIR, 'config.json')
10
+
11
+ def save_session(token, email, team_code=None, project_name=None):
12
+ os.makedirs(CONFIG_DIR, exist_ok=True)
13
+ config_data = {
14
+ "auth_token": token,
15
+ "user_email": email,
16
+ "device_name": socket.gethostname()
17
+ }
18
+ if team_code:
19
+ config_data["team_code"] = team_code
20
+ if project_name:
21
+ config_data["project_name"] = project_name
22
+
23
+ with open(CONFIG_PATH, 'w') as f:
24
+ json.dump(config_data, f, indent=4)
25
+
26
+ def load_session():
27
+ if not os.path.exists(CONFIG_PATH):
28
+ return None
29
+ with open(CONFIG_PATH, 'r') as f:
30
+ return json.load(f)
31
+
32
+ def clear_session():
33
+ if os.path.exists(CONFIG_PATH):
34
+ try:
35
+ os.remove(CONFIG_PATH)
36
+ except Exception:
37
+ pass
38
+
39
+ def save_local_context(team_code, project_name):
40
+ """Saves project reference context inside the current working directory."""
41
+ local_dir = os.path.join(os.getcwd(), '.candles')
42
+ os.makedirs(local_dir, exist_ok=True)
43
+ local_path = os.path.join(local_dir, 'project_context.json')
44
+ context_data = {
45
+ "team_code": team_code,
46
+ "project_name": project_name,
47
+ "initialized_at": os.getcwd()
48
+ }
49
+ with open(local_path, 'w') as f:
50
+ json.dump(context_data, f, indent=4)
51
+
52
+ def load_local_context():
53
+ """Loads project reference context inside the current working directory."""
54
+ local_path = os.path.join(os.getcwd(), '.candles', 'project_context.json')
55
+ if not os.path.exists(local_path):
56
+ return None
57
+ try:
58
+ with open(local_path, 'r') as f:
59
+ return json.load(f)
60
+ except Exception:
61
+ return None
@@ -0,0 +1,383 @@
1
+ # 📄 Location: d:/Ptojects/TeamBridge/cli/candles/main.py
2
+ import os
3
+ import json
4
+ import click
5
+ import requests
6
+ import socket
7
+ import base64
8
+ import webbrowser
9
+ import inquirer
10
+ from .auth import save_session, load_session, clear_session, save_local_context, load_local_context
11
+ from .tracker import start_workspace_monitoring
12
+
13
+ BACKEND_URL = "https://candels.onrender.com/api/cli"
14
+
15
+ def get_headers(session):
16
+ return {
17
+ "Authorization": f"Bearer {session.get('auth_token')}"
18
+ }
19
+
20
+ def print_base_folder_warning():
21
+ """Prints warning about running commands in subfolders rather than base folders."""
22
+ click.echo(click.style("\n[WARNING] Candles should be initialized and linked only on the base/root folder", fg="yellow", bold=True))
23
+ click.echo(click.style("so that the full project folder and its files can be accessed easily and displayed on the web.", fg="yellow"))
24
+
25
+ def is_base_directory():
26
+ """Helper to check if the current directory is likely a project base directory."""
27
+ base_indicators = ['.git', 'package.json', 'requirements.txt', 'README.md', 'app.py', 'index.html', 'candles.config']
28
+ return any(os.path.exists(os.path.join(os.getcwd(), ind)) for ind in base_indicators)
29
+
30
+ def check_internet_connection():
31
+ """Quick socket routine to verify if public DNS route is accessible."""
32
+ try:
33
+ socket.create_connection(("8.8.8.8", 53), timeout=3)
34
+ return True
35
+ except OSError:
36
+ return False
37
+
38
+ @click.group()
39
+ def cli():
40
+ """Candles Global CLI Tool - Evolve your workspace into a collaborative SaaS environment."""
41
+ pass
42
+
43
+ @cli.command()
44
+ def create():
45
+ """Redirect user to the website portal to configure structural team setups."""
46
+ click.echo(click.style("\n[Redirecting] Opening Candles Form Console...", fg="cyan", bold=True))
47
+ click.echo(click.style("Please create a project, confirm them and complete the declaration form to create project to use that in the local system.", fg="yellow"))
48
+ webbrowser.open("http://127.0.0.1:5000/create-project")
49
+
50
+ @cli.command()
51
+ def login():
52
+ """Securely connect your terminal environment to your Candles profile with masked security inputs."""
53
+ click.echo(click.style("[Cloud] Connecting to Candles Cloud Services...", fg="cyan"))
54
+
55
+ email = click.prompt("Enter your registered email")
56
+ # hide_input=True replaces typed credentials with clean secure shell masking
57
+ password = click.prompt("Enter your password", hide_input=True)
58
+ device_name = socket.gethostname()
59
+
60
+ try:
61
+ response = requests.post(
62
+ f"{BACKEND_URL}/login",
63
+ json={"email": email, "password": password, "device_name": device_name}
64
+ )
65
+
66
+ if response.status_code == 200:
67
+ result = response.json()
68
+ token = result.get("token")
69
+
70
+ # Save initialized global state profiles variables data models
71
+ save_session(token, email, None, None)
72
+
73
+ current_path = os.getcwd()
74
+ click.echo(click.style(f"\n[Success] Authenticated cleanly on path context: {current_path}>", fg="green", bold=True))
75
+ click.echo("You can now safely execute 'cn create', 'cn link', 'cn add' etc. across structural system pathways.")
76
+ else:
77
+ click.echo(click.style("\n[Error] Access Denied: Check password / email and re-enter again.", fg="red", bold=True))
78
+
79
+ except requests.exceptions.ConnectionError:
80
+ click.echo(click.style("[Error] Connection Error: Could not reach Flask backend server. Make sure your app is running!", fg="red"))
81
+
82
+ def login_script():
83
+ """Wrapper entry point for cn-login command."""
84
+ from click.testing import CliRunner
85
+ cli(args=['login'])
86
+
87
+ def live_script():
88
+ """Wrapper entry point for cn-live command running background synchronization loop."""
89
+ from click.testing import CliRunner
90
+ cli(args=['link'])
91
+
92
+ @cli.command()
93
+ def init():
94
+ """Initialize a project workspace in the current folder."""
95
+ session = load_session()
96
+ if not session:
97
+ click.echo(click.style("[Warning] No active profile found. Please run 'cn login' first.", fg="yellow"))
98
+ return
99
+
100
+ team_code = session.get("team_code")
101
+ project_name = session.get("project_name")
102
+
103
+ if not team_code:
104
+ click.echo(click.style("[Error] No project selected in your profile. Run 'cn login' to select one.", fg="red"))
105
+ return
106
+
107
+ if not is_base_directory():
108
+ print_base_folder_warning()
109
+ if not click.confirm("Do you want to proceed with initialization in the current directory?", default=False):
110
+ click.echo(click.style("[Aborted] Initialization cancelled. Please navigate to the project base folder.", fg="red"))
111
+ return
112
+
113
+ save_local_context(team_code, project_name)
114
+ click.echo(click.style(f"[Local] Folder initialized and linked to Candles workspace: {project_name} ({team_code})", fg="green", bold=True))
115
+
116
+ @cli.command()
117
+ def get():
118
+ """Download all scaffolding files and folders from the online repository."""
119
+ session = load_session()
120
+ local_ctx = load_local_context()
121
+
122
+ if not session:
123
+ click.echo(click.style("[Warning] No active profile found. Please run 'cn login' first.", fg="yellow"))
124
+ return
125
+
126
+ team_code = local_ctx.get("team_code") if local_ctx else session.get("team_code")
127
+ if not team_code:
128
+ click.echo(click.style("[Error] Directory not initialized. Run 'cn init' or 'cn login' first.", fg="red"))
129
+ return
130
+
131
+ click.echo(click.style(f"[Sync] Pulling files for Team: {team_code} from cloud...", fg="cyan"))
132
+
133
+ try:
134
+ response = requests.get(
135
+ f"{BACKEND_URL}/files?team_code={team_code}",
136
+ headers=get_headers(session)
137
+ )
138
+
139
+ if response.status_code == 200:
140
+ files = response.json().get("files", [])
141
+ if not files:
142
+ click.echo(click.style("[Info] No files found in remote workspace yet.", fg="yellow"))
143
+ return
144
+
145
+ for file_data in files:
146
+ path = file_data["path"]
147
+ content = base64.b64decode(file_data["content"])
148
+
149
+ full_path = os.path.join(os.getcwd(), path)
150
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
151
+ with open(full_path, 'wb') as f:
152
+ f.write(content)
153
+ click.echo(f" [Downloading] {path}")
154
+
155
+ click.echo(click.style("[Success] Workspace download complete. Local folder matching cloud filesystem.", fg="green", bold=True))
156
+ else:
157
+ click.echo(click.style(f"[Error] Failed to fetch files: {response.text}", fg="red"))
158
+
159
+ except requests.exceptions.ConnectionError:
160
+ click.echo(click.style("[Error] Connection Error: Backend server is offline.", fg="red"))
161
+
162
+ @cli.command()
163
+ def link():
164
+ """Upload local folder structure, allow custom project selection cloning, and sync changes real-time."""
165
+ session = load_session()
166
+ if not session:
167
+ click.echo(click.style("[Warning] No active profile found. Please run 'cn login' first.", fg="yellow"))
168
+ return
169
+
170
+ # Connection environment indicator checks
171
+ if check_internet_connection():
172
+ click.echo(click.style("🌐 Connected [Online Real-time Pipeline Enabled]", fg="green"))
173
+ else:
174
+ click.echo(click.style("🛑 Disconnected [Offline Core Layer Running]", fg="red"))
175
+
176
+ local_ctx = load_local_context()
177
+
178
+ # If the user runs cn-link inside a directory that is already an active cloned repository context
179
+ if local_ctx:
180
+ team_code = local_ctx["team_code"]
181
+ project_name = local_ctx["project_name"]
182
+
183
+ click.echo(click.style(f"[Link] Linking current local directory to Candles cloud folder: {project_name}...", fg="cyan"))
184
+
185
+ if not is_base_directory():
186
+ print_base_folder_warning()
187
+ if not click.confirm("Are you sure you want to run link / synchronize here?"):
188
+ click.echo(click.style("[Aborted] Action aborted.", fg="red"))
189
+ return
190
+
191
+ click.echo(click.style("\n[Sync] Synchronizing local files to Cloud Workspace...", fg="cyan"))
192
+ ignored_folders = {'.git', 'node_modules', '__pycache__', '.candles', 'venv', 'env'}
193
+
194
+ for root, dirs, files in os.walk(os.getcwd()):
195
+ dirs[:] = [d for d in dirs if d not in ignored_folders]
196
+ for file in files:
197
+ full_path = os.path.join(root, file)
198
+ rel_path = os.path.relpath(full_path, os.getcwd()).replace('\\', '/')
199
+
200
+ canonical_root = os.path.realpath(os.getcwd())
201
+ canonical_file = os.path.realpath(full_path)
202
+ try:
203
+ if os.path.commonpath([canonical_root, canonical_file]) != canonical_root:
204
+ continue
205
+ except ValueError:
206
+ continue
207
+
208
+ try:
209
+ file_size = os.path.getsize(canonical_file)
210
+ if file_size > 2 * 1024 * 1024:
211
+ click.echo(click.style(f" [Binary Guard] Skipping heavy file (>2MB): {rel_path}", fg="yellow"))
212
+ continue
213
+
214
+ from .tracker import is_binary_file
215
+ if is_binary_file(canonical_file):
216
+ ext = os.path.splitext(rel_path)[1].lower()
217
+ if ext not in ['.docx', '.doc', '.pdf']:
218
+ continue
219
+
220
+ with open(full_path, 'rb') as f:
221
+ content_b64 = base64.b64encode(f.read()).decode('utf-8')
222
+
223
+ payload = {
224
+ "team_code": team_code,
225
+ "path": rel_path,
226
+ "content": content_b64
227
+ }
228
+
229
+ res = requests.post(f"{BACKEND_URL}/upload", json=payload, headers=get_headers(session))
230
+ if res.status_code == 200:
231
+ click.echo(f" [Syncing] {rel_path}")
232
+ except Exception as e:
233
+ click.echo(click.style(f" [Warning] Skipping file {rel_path}: {e}", fg="yellow"))
234
+
235
+ click.echo(click.style("[Success] Initial sync complete. Watching for local modifications...", fg="green"))
236
+ start_workspace_monitoring(team_code, session["auth_token"])
237
+ return
238
+
239
+ # If run in an unlinked/new folder (e.g., testfolder), prompt user to select and download an existing database project
240
+ click.echo(click.style("\n[Fetch] Contacting database to fetch accessible active user projects...", fg="cyan"))
241
+ try:
242
+ response = requests.get(f"{BACKEND_URL}/projects", headers=get_headers(session))
243
+ if response.status_code != 200:
244
+ click.echo(click.style("[Error] Failed to fetch active projects from backend registry.", fg="red"))
245
+ return
246
+
247
+ projects = response.json().get("projects", [])
248
+ if not projects:
249
+ click.echo(click.style("[Info] No remote projects found. Use 'cn create' to set up a new workspace registry.", fg="yellow"))
250
+ return
251
+
252
+ # Use terminal arrow selections key mappings arrays maps
253
+ project_map = {f"{p['project_title']} (Code: {p['team_code']})": p for p in projects}
254
+ choices_list = list(project_map.keys())
255
+
256
+ questions = [
257
+ inquirer.List(
258
+ 'chosen_project_str',
259
+ message="Select a project workspace (Use Up/Down Arrows, Space/Enter to confirm choice)",
260
+ choices=choices_list,
261
+ )
262
+ ]
263
+
264
+ answers = inquirer.prompt(questions)
265
+ if not answers:
266
+ click.echo(click.style("[Aborted] No workspace project targeted.", fg="red"))
267
+ return
268
+
269
+ selected_record = project_map[answers['chosen_project_str']]
270
+ team_code = selected_record["team_code"]
271
+ project_name = selected_record["project_title"]
272
+
273
+ if not click.confirm(f"\nDo you allow Candles to clone project database entries here inside folder context?"):
274
+ click.echo(click.style("[Aborted] Core cloning sequence cancelled.", fg="red"))
275
+ return
276
+
277
+ # Dynamically allocate cloning directory matching exactly your custom project form text
278
+ target_clone_folder = os.path.join(os.getcwd(), project_name)
279
+ os.makedirs(target_clone_folder, exist_ok=True)
280
+
281
+ # Save dynamic profile indicators inside local data markers structures
282
+ save_session(session["auth_token"], session["user_email"], team_code, project_name)
283
+
284
+ local_marker_path = os.path.join(target_clone_folder, '.candles')
285
+ os.makedirs(local_marker_path, exist_ok=True)
286
+ with open(os.path.join(local_marker_path, 'project_context.json'), 'w') as f:
287
+ json.dump({"team_code": team_code, "project_name": project_name, "initialized_at": target_clone_folder}, f, indent=4)
288
+
289
+ click.echo(click.style(f"\nExecuting command utility pipeline: cn-{project_name.lower()}-clone...", fg="cyan"))
290
+ files_res = requests.get(f"{BACKEND_URL}/files?team_code={team_code}", headers=get_headers(session))
291
+
292
+ if files_res.status_code == 200:
293
+ files = files_res.json().get("files", [])
294
+ for file_data in files:
295
+ path = file_data["path"]
296
+ content = base64.b64decode(file_data["content"])
297
+ full_path = os.path.join(target_clone_folder, path)
298
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
299
+ with open(full_path, 'wb') as f:
300
+ f.write(content)
301
+
302
+ click.echo(click.style(f"\n[Success] Your project '{project_name}' has been successfully cloned to the local filesystem wrapper context!", fg="green", bold=True))
303
+ click.echo(click.style(f"To activate dynamic updates, navigate using: 'cd {project_name}' and run 'cn link'. Files will dynamically update to the workspace of candles.", fg="yellow"))
304
+
305
+ except Exception as e:
306
+ click.echo(click.style(f"[Error] Repository initialization pipeline failed: {e}", fg="red"))
307
+
308
+ @cli.command()
309
+ @click.argument('file_path', required=False)
310
+ def drop(file_path):
311
+ """Delete a file locally and from the remote server workspace explorer."""
312
+ session = load_session()
313
+ local_ctx = load_local_context()
314
+
315
+ if not session:
316
+ click.echo(click.style("[Warning] No active profile found. Please run 'cn login' first.", fg="yellow"))
317
+ return
318
+
319
+ team_code = local_ctx["team_code"] if local_ctx else session.get("team_code")
320
+ if not team_code:
321
+ click.echo(click.style("[Error] Directory not linked. Run 'cn init' first.", fg="red"))
322
+ return
323
+
324
+ if not file_path:
325
+ file_path = click.prompt("Enter the relative file path to delete/drop")
326
+
327
+ if not click.confirm(f"[Warning] Are you sure you want to drop '{file_path}' from local and remote workspaces?"):
328
+ click.echo(click.style("[Aborted] Drop action aborted.", fg="yellow"))
329
+ return
330
+
331
+ local_file = os.path.join(os.getcwd(), file_path)
332
+ if os.path.exists(local_file):
333
+ try:
334
+ if os.path.isdir(local_file):
335
+ import shutil
336
+ shutil.rmtree(local_file)
337
+ else:
338
+ os.remove(local_file)
339
+ click.echo(click.style(f"[Success] Deleted local file: {file_path}", fg="green"))
340
+ except Exception as e:
341
+ click.echo(click.style(f"[Error] Failed to delete local file: {e}", fg="red"))
342
+
343
+ try:
344
+ response = requests.post(
345
+ f"{BACKEND_URL}/drop",
346
+ json={"team_code": team_code, "path": file_path.replace('\\', '/')},
347
+ headers=get_headers(session)
348
+ )
349
+ if response.status_code == 200:
350
+ click.echo(click.style(f"[Success] Successfully dropped '{file_path}' from Candles Cloud Explorer.", fg="green", bold=True))
351
+ else:
352
+ click.echo(click.style(f"[Error] Failed to drop '{file_path}' on server: {response.text}", fg="red"))
353
+ except Exception as e:
354
+ click.echo(click.style("[Error] Remote drop connection failed: " + str(e), fg="red"))
355
+
356
+ @cli.command()
357
+ def status():
358
+ """Check the status of your current local terminal workspace profile."""
359
+ session = load_session()
360
+ local_ctx = load_local_context()
361
+
362
+ if not session:
363
+ click.echo(click.style("[Warning] No active profile found. Run 'cn login' first.", fg="yellow"))
364
+ return
365
+
366
+ click.echo(click.style("[Status] Candles Connection Status:", fg="cyan", bold=True))
367
+ click.echo(f" Logged In As: {session['user_email']}")
368
+ click.echo(f" Device Name: {session['device_name']}")
369
+ if session.get("team_code"):
370
+ click.echo(f" Active Project (Global): {session.get('project_name', 'Unknown')} ({session.get('team_code')})")
371
+ if local_ctx:
372
+ click.echo(click.style(f"[Success] Linked Folder (Local): {local_ctx.get('project_name', 'Unknown')} ({local_ctx.get('team_code')})", fg="green"))
373
+ else:
374
+ click.echo(click.style("[Warning] Current folder is not linked locally. Run 'cn init'.", fg="yellow"))
375
+
376
+ @cli.command()
377
+ def logout():
378
+ """Clear local session storage files to log out safely."""
379
+ clear_session()
380
+ click.echo(click.style("[Success] Logged out of Candles. Local configuration purged.", fg="yellow", bold=True))
381
+
382
+ if __name__ == '__main__':
383
+ cli()
@@ -0,0 +1,314 @@
1
+ # 📄 Location: d:/Ptojects/TeamBridge/cli/candles/tracker.py
2
+ import os
3
+ import time
4
+ import json
5
+ import base64
6
+ import requests
7
+ import threading
8
+ from watchdog.observers import Observer
9
+ from watchdog.events import FileSystemEventHandler
10
+
11
+ try:
12
+ import socketio
13
+ except ImportError:
14
+ socketio = None
15
+
16
+ BACKEND_URL = "https://candels.onrender.com/api/cli"
17
+
18
+ def is_binary_file(filepath):
19
+ """Check if a file is binary by reading the first 1024 bytes and searching for null byte or high non-text byte ratio."""
20
+ try:
21
+ if not os.path.exists(filepath) or os.path.isdir(filepath):
22
+ return False
23
+ with open(filepath, 'rb') as f:
24
+ chunk = f.read(1024)
25
+ if b'\x00' in chunk:
26
+ return True
27
+ non_printable = sum(1 for byte in chunk if byte < 32 and byte not in (9, 10, 13))
28
+ if chunk and (non_printable / len(chunk)) > 0.3:
29
+ return True
30
+ except Exception:
31
+ pass
32
+ return False
33
+
34
+ class ProjectActivityHandler(FileSystemEventHandler):
35
+ """Listens natively to file system events and aggregates activity + syncs changes to server."""
36
+ def __init__(self, team_code, auth_token, settings=None, document_paths=None):
37
+ self.team_code = team_code
38
+ self.auth_token = auth_token
39
+ self.document_paths = document_paths or set()
40
+ self.pending_changes = {}
41
+ self.last_event_time = 0
42
+ self.lock = threading.Lock()
43
+
44
+ self.max_file_size_bytes = 2 * 1024 * 1024
45
+ self.allowed_exts = None
46
+ self.ignored_folders = ['.git', 'node_modules', '__pycache__', '.candles', 'venv', 'env']
47
+
48
+ if settings:
49
+ if settings.get("max_file_size_mb") is not None:
50
+ self.max_file_size_bytes = int(settings["max_file_size_mb"] * 1024 * 1024)
51
+ if settings.get("allowed_extensions"):
52
+ self.allowed_exts = {ext.strip().lower() for ext in settings["allowed_extensions"].split(",") if ext.strip()}
53
+ if settings.get("ignored_folders"):
54
+ self.ignored_folders = [f.strip() for f in settings["ignored_folders"].split(",") if f.strip()]
55
+
56
+ self.sio = None
57
+ if socketio:
58
+ try:
59
+ self.sio = socketio.Client()
60
+ @self.sio.on('cli_stream_response')
61
+ def on_cli_stream_response(data):
62
+ status = data.get('status')
63
+ msg = data.get('message', '')
64
+ if status == 'success':
65
+ print(f"[WebSocket Sync] Success: {msg}")
66
+ else:
67
+ print(f"[WebSocket Sync Error] {msg}")
68
+ except Exception as e:
69
+ print(f"[Warning] Failed to initialize Socket.IO client: {e}")
70
+ self.sio = None
71
+
72
+ def start_worker(self):
73
+ if self.sio:
74
+ try:
75
+ # 🛠️ SAFE FIX: Ensure it extracts the root domain properly even if trailing slashes vary
76
+ if '/api' in BACKEND_URL:
77
+ backend_root = BACKEND_URL.rsplit('/api', 1)[0]
78
+ else:
79
+ backend_root = BACKEND_URL.replace('/cli', '')
80
+
81
+ self.sio.connect(backend_root)
82
+ print(f"[WebSocket] Connected to real-time sync engine at {backend_root}")
83
+ except Exception as e:
84
+ print(f"[WebSocket Warning] Connection failed: {e}. Falling back to HTTP batch uploads.")
85
+ self.sio = None
86
+
87
+ # Thread 1: Real-time quick debounce stream worker
88
+ threading.Thread(target=self.worker_loop, daemon=True).start()
89
+
90
+ # Thread 2: Periodic absolute check routine loop running every 5 minutes and 1 second precisely
91
+ threading.Thread(target=self.five_minute_ticker_loop, daemon=True).start()
92
+
93
+ def worker_loop(self):
94
+ while True:
95
+ time.sleep(0.5)
96
+ self.flush_payloads()
97
+
98
+ def five_minute_ticker_loop(self):
99
+ """Force system tree checks alignment every 5 minutes and 1 second (301s total) precisely."""
100
+ while True:
101
+ time.sleep(301)
102
+ print("\n[Clock Update Engine] Standard 5-minute synchronization interval tick running updates checklist indices...")
103
+ self.force_full_directory_sync()
104
+
105
+ def force_full_directory_sync(self):
106
+ """Walks workspace tree structural components queuing all non-ignored updates changes."""
107
+ for root, dirs, files in os.walk(os.getcwd()):
108
+ dirs[:] = [d for d in dirs if d not in self.ignored_folders]
109
+ for file in files:
110
+ full_path = os.path.join(root, file)
111
+ rel_path = os.path.relpath(full_path, os.getcwd())
112
+ with self.lock:
113
+ self.pending_changes[rel_path] = {
114
+ "absolute_path": full_path,
115
+ "timestamp": time.time(),
116
+ "deleted": False
117
+ }
118
+ self.flush_payloads()
119
+
120
+ def flush_payloads(self):
121
+ batch_payload = []
122
+
123
+ with self.lock:
124
+ if not self.pending_changes:
125
+ return
126
+ # Debounce filter validation check logic
127
+ if time.time() - self.last_event_time < 1.5:
128
+ # If an event was captured very recently, let the next loop pass clean
129
+ return
130
+ changes = list(self.pending_changes.items())
131
+ self.pending_changes.clear()
132
+
133
+ for rel_path, info in changes:
134
+ if info["deleted"]:
135
+ batch_payload.append({
136
+ "path": rel_path.replace('\\', '/'),
137
+ "deleted": True
138
+ })
139
+ else:
140
+ try:
141
+ if not os.path.exists(info["absolute_path"]):
142
+ continue
143
+
144
+ file_size = os.path.getsize(info["absolute_path"])
145
+ if file_size > self.max_file_size_bytes:
146
+ continue
147
+
148
+ if is_binary_file(info["absolute_path"]):
149
+ ext = os.path.splitext(rel_path)[1].lower()
150
+ if ext not in ['.docx', '.doc', '.pdf']:
151
+ continue
152
+
153
+ with open(info["absolute_path"], 'rb') as f:
154
+ content_b64 = base64.b64encode(f.read()).decode('utf-8')
155
+
156
+ batch_payload.append({
157
+ "path": rel_path.replace('\\', '/'),
158
+ "content": content_b64,
159
+ "deleted": False
160
+ })
161
+ except Exception as e:
162
+ print(f"[Sync Error] Could not read file {rel_path} for batch: {e}")
163
+
164
+ if not batch_payload:
165
+ return
166
+
167
+ if self.sio and self.sio.connected:
168
+ try:
169
+ payload = {
170
+ "auth_token": self.auth_token,
171
+ "team_code": self.team_code,
172
+ "files": batch_payload
173
+ }
174
+ self.sio.emit('cli_file_stream', payload)
175
+ print(f"[Dynamic Update Hub] Streamed batch of {len(batch_payload)} actions via WebSocket.")
176
+ except Exception:
177
+ self.send_http_batch(batch_payload)
178
+ else:
179
+ self.send_http_batch(batch_payload)
180
+
181
+ def send_http_batch(self, batch_payload):
182
+ try:
183
+ headers = {"Authorization": f"Bearer {self.auth_token}"}
184
+ payload = {
185
+ "team_code": self.team_code,
186
+ "files": batch_payload
187
+ }
188
+ response = requests.post(
189
+ f"{BACKEND_URL}/upload-batch",
190
+ json=payload,
191
+ headers=headers,
192
+ timeout=15
193
+ )
194
+ if response.status_code == 200:
195
+ print(f"[Dynamic Update Hub] Synchronized batch update payload changes cleanly to cloud repositories context layer.")
196
+ except Exception as e:
197
+ print(f"[Sync Error HTTP] Consolidated batch request failed: {e}")
198
+
199
+ def process_event(self, event, is_deletion=False):
200
+ if event.is_directory:
201
+ return
202
+
203
+ try:
204
+ canonical_path = os.path.realpath(event.src_path)
205
+ except Exception:
206
+ canonical_path = os.path.abspath(event.src_path)
207
+
208
+ is_registered_doc = False
209
+ registered_doc_path = None
210
+ for doc_path in self.document_paths:
211
+ if os.path.normcase(os.path.realpath(canonical_path)) == os.path.normcase(os.path.realpath(doc_path)):
212
+ is_registered_doc = True
213
+ registered_doc_path = doc_path
214
+ break
215
+
216
+ if not is_registered_doc:
217
+ cwd_path = os.path.realpath(os.getcwd())
218
+ try:
219
+ if os.path.commonpath([cwd_path, canonical_path]) != cwd_path:
220
+ return
221
+ except ValueError:
222
+ return
223
+
224
+ relative_path = os.path.relpath(canonical_path, cwd_path)
225
+ else:
226
+ relative_path = registered_doc_path
227
+
228
+ path_parts = relative_path.replace('\\', '/').split('/')
229
+ if any(ignored in path_parts for ignored in self.ignored_folders):
230
+ return
231
+
232
+ if not is_deletion:
233
+ ext = os.path.splitext(canonical_path)[1].lower()
234
+ if self.allowed_exts and ext not in self.allowed_exts:
235
+ if ext not in ['.docx', '.doc', '.pdf']:
236
+ return
237
+
238
+ try:
239
+ file_size = os.path.getsize(canonical_path)
240
+ if file_size > self.max_file_size_bytes:
241
+ return
242
+ except Exception:
243
+ pass
244
+
245
+ with self.lock:
246
+ self.pending_changes[relative_path] = {
247
+ "absolute_path": canonical_path,
248
+ "timestamp": time.time(),
249
+ "deleted": is_deletion
250
+ }
251
+ self.last_event_time = time.time()
252
+
253
+ def on_modified(self, event):
254
+ self.process_event(event, is_deletion=False)
255
+
256
+ def on_created(self, event):
257
+ self.process_event(event, is_deletion=False)
258
+
259
+ def on_deleted(self, event):
260
+ self.process_event(event, is_deletion=True)
261
+
262
+ def start_workspace_monitoring(team_code, auth_token):
263
+ """Starts the background thread worker watching the current directory and registered documents."""
264
+ path_to_watch = os.getcwd()
265
+ settings = {}
266
+ try:
267
+ response = requests.get(f"https://candels.onrender.com/api/workspace/settings?team_code={team_code}", headers={"Authorization": f"Bearer {auth_token}"}, timeout=5)
268
+ if response.status_code == 200:
269
+ settings = response.json().get("settings", {})
270
+ except Exception:
271
+ pass
272
+
273
+ document_paths = set()
274
+ try:
275
+ response = requests.get(
276
+ f"http://127.0.0.1:5000/api/workspace/documents?team_code={team_code}",
277
+ headers={"Authorization": f"Bearer {auth_token}"},
278
+ timeout=5
279
+ )
280
+ if response.status_code == 200:
281
+ docs = response.json().get("documents", [])
282
+ for doc in docs:
283
+ file_path = doc.get("file_path")
284
+ if file_path:
285
+ document_paths.add(os.path.abspath(file_path))
286
+ except Exception:
287
+ pass
288
+
289
+ event_handler = ProjectActivityHandler(team_code, auth_token, settings, document_paths)
290
+ event_handler.start_worker()
291
+
292
+ observer = Observer()
293
+ observer.schedule(event_handler, path=path_to_watch, recursive=True)
294
+
295
+ watched_dirs = {os.path.realpath(path_to_watch)}
296
+ for doc_path in document_paths:
297
+ dir_to_watch = os.path.dirname(doc_path)
298
+ if os.path.exists(dir_to_watch) and os.path.realpath(dir_to_watch) not in watched_dirs:
299
+ try:
300
+ observer.schedule(event_handler, path=dir_to_watch, recursive=False)
301
+ watched_dirs.add(os.path.realpath(dir_to_watch))
302
+ except Exception:
303
+ pass
304
+
305
+ observer.start()
306
+ print(f"[Start] Candles background file-watcher active on: {path_to_watch}")
307
+ print("Press Ctrl+C to terminate the synchronization monitoring session.")
308
+ try:
309
+ while True:
310
+ time.sleep(1)
311
+ except KeyboardInterrupt:
312
+ observer.stop()
313
+ print("\n[Stop] Workspace monitoring suspended cleanly.")
314
+ observer.join()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ # 📄 Location: d:/Ptojects/TeamBridge/cli/setup.py
2
+ from setuptools import setup, find_packages
3
+
4
+ setup(
5
+ name="teambridge-candles",
6
+ version="1.0.0",
7
+ packages=find_packages(),
8
+ install_requires=[
9
+ "requests>=2.31.0",
10
+ "click>=8.1.0",
11
+ "watchdog>=3.0.0",
12
+ "python-socketio[client]>=5.11.0",
13
+ "inquirer>=3.1.3" # Added for premium arrow-key and spacebar selections
14
+ ],
15
+ entry_points={
16
+ 'console_scripts': [
17
+ 'candles=candles.main:cli',
18
+ 'cn=candles.main:cli',
19
+ 'cn-login=candles.main:login_script',
20
+ 'cn-live=candles.main:live_script',
21
+ ],
22
+ },
23
+ )
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: teambridge-candles
3
+ Version: 1.0.0
4
+ Requires-Dist: requests>=2.31.0
5
+ Requires-Dist: click>=8.1.0
6
+ Requires-Dist: watchdog>=3.0.0
7
+ Requires-Dist: python-socketio[client]>=5.11.0
8
+ Requires-Dist: inquirer>=3.1.3
9
+ Dynamic: requires-dist
@@ -0,0 +1,11 @@
1
+ setup.py
2
+ candles/__init__.py
3
+ candles/auth.py
4
+ candles/main.py
5
+ candles/tracker.py
6
+ teambridge_candles.egg-info/PKG-INFO
7
+ teambridge_candles.egg-info/SOURCES.txt
8
+ teambridge_candles.egg-info/dependency_links.txt
9
+ teambridge_candles.egg-info/entry_points.txt
10
+ teambridge_candles.egg-info/requires.txt
11
+ teambridge_candles.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ candles = candles.main:cli
3
+ cn = candles.main:cli
4
+ cn-live = candles.main:live_script
5
+ cn-login = candles.main:login_script
@@ -0,0 +1,5 @@
1
+ requests>=2.31.0
2
+ click>=8.1.0
3
+ watchdog>=3.0.0
4
+ python-socketio[client]>=5.11.0
5
+ inquirer>=3.1.3