thinkncollab-shell 0.0.19 → 2.0.0-beta.1

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 (201) hide show
  1. package/LICENSE +82 -0
  2. package/Readme.md +246 -502
  3. package/dist/bin/thinknsh.js +8 -0
  4. package/dist/src/commands/auth/login.js +7 -0
  5. package/dist/src/commands/auth/logout.js +7 -0
  6. package/dist/src/commands/auth/register.js +7 -0
  7. package/dist/src/commands/auth/whoami.js +7 -0
  8. package/dist/src/commands/dock/bench.js +7 -0
  9. package/dist/src/commands/dock/ci.js +7 -0
  10. package/dist/src/commands/dock/cluster.js +7 -0
  11. package/dist/src/commands/dock/containers.js +7 -0
  12. package/dist/src/commands/dock/dock.js +7 -0
  13. package/dist/src/commands/github/commits.js +7 -0
  14. package/dist/src/commands/github/issue-create.js +7 -0
  15. package/dist/src/commands/github/issues.js +7 -0
  16. package/dist/src/commands/github/pulls.js +7 -0
  17. package/dist/src/commands/github/statusgh.js +7 -0
  18. package/dist/src/commands/index.js +7 -0
  19. package/dist/src/commands/message/say.js +7 -0
  20. package/dist/src/commands/room/join.js +7 -0
  21. package/dist/src/commands/room/leave.js +7 -0
  22. package/dist/src/commands/room/rooms.js +7 -0
  23. package/dist/src/commands/room/sync.js +7 -0
  24. package/dist/src/commands/sandbox/console.js +7 -0
  25. package/dist/src/commands/sandbox/dep.js +7 -0
  26. package/dist/src/commands/sandbox/deploy.js +7 -0
  27. package/dist/src/commands/sandbox/dev.js +7 -0
  28. package/dist/src/commands/sandbox/exec.js +7 -0
  29. package/dist/src/commands/sandbox/freeze.js +7 -0
  30. package/dist/src/commands/sandbox/push.js +7 -0
  31. package/dist/src/commands/sandbox/resume.js +7 -0
  32. package/dist/src/commands/sandbox/sandbox.js +7 -0
  33. package/dist/src/commands/sandbox/share.js +7 -0
  34. package/dist/src/commands/sandbox/snapshot.js +7 -0
  35. package/dist/src/commands/sandbox/sync-deps.js +7 -0
  36. package/dist/src/commands/shell/doctor.js +7 -0
  37. package/dist/src/commands/shell/notifications.js +7 -0
  38. package/dist/src/commands/shell/notify.js +7 -0
  39. package/dist/src/commands/shell/status.js +7 -0
  40. package/dist/src/commands/shell/version.js +7 -0
  41. package/dist/src/commands/tasks/build.js +7 -0
  42. package/dist/src/commands/tasks/complete.js +7 -0
  43. package/dist/src/commands/tasks/create-task.js +7 -0
  44. package/dist/src/commands/tasks/loadtest.js +7 -0
  45. package/dist/src/commands/tasks/task-status.js +7 -0
  46. package/dist/src/commands/tasks/task.js +7 -0
  47. package/dist/src/commands/tasks/tasks.js +7 -0
  48. package/dist/src/commands/team/invite.js +7 -0
  49. package/dist/src/commands/team/myteam.js +7 -0
  50. package/dist/src/commands/team/teammates.js +7 -0
  51. package/dist/src/commands/terminal/share.js +7 -0
  52. package/dist/src/core/api-client.js +7 -0
  53. package/dist/src/core/crypto/e2ee-vault.js +7 -0
  54. package/dist/src/core/dep-store.js +7 -0
  55. package/dist/src/core/dependency-tracker.js +7 -0
  56. package/dist/src/core/geekload.js +7 -0
  57. package/dist/src/core/local-runner.js +7 -0
  58. package/dist/src/core/multi-lang-scanner.js +7 -0
  59. package/dist/src/core/notification-window.js +8 -0
  60. package/dist/src/core/notify-auth.js +7 -0
  61. package/dist/src/core/project-config.js +7 -0
  62. package/dist/src/core/sandbox/arch-shim.js +7 -0
  63. package/dist/src/core/sandbox/capsule-builder.js +7 -0
  64. package/dist/src/core/sandbox/cgroups.js +7 -0
  65. package/dist/src/core/sandbox/container-monitor.js +7 -0
  66. package/dist/src/core/sandbox/crun-manager.js +7 -0
  67. package/dist/src/core/sandbox/engine.js +7 -0
  68. package/dist/src/core/sandbox/image-manager.js +7 -0
  69. package/dist/src/core/sandbox/image-store.js +7 -0
  70. package/dist/src/core/sandbox/index.js +7 -0
  71. package/dist/src/core/sandbox/macos-sandbox.js +7 -0
  72. package/dist/src/core/sandbox/microvm-manager.js +7 -0
  73. package/dist/src/core/sandbox/native-service-manager.js +7 -0
  74. package/dist/src/core/sandbox/network.js +7 -0
  75. package/dist/src/core/sandbox/oci-exporter.js +7 -0
  76. package/dist/src/core/sandbox/oci-puller.js +7 -0
  77. package/dist/src/core/sandbox/oci-runtime-spec.js +7 -0
  78. package/dist/src/core/sandbox/policy-engine.js +7 -0
  79. package/dist/src/core/sandbox/port-proxy-daemon.js +7 -0
  80. package/dist/src/core/sandbox/room-sync-manager.js +7 -0
  81. package/dist/src/core/sandbox/runtime-manager.js +7 -0
  82. package/dist/src/core/sandbox/snapshot-manager.js +7 -0
  83. package/dist/src/core/sandbox/storage.js +7 -0
  84. package/dist/src/core/sandbox/supervisor.js +7 -0
  85. package/dist/src/core/sandbox/tunnel-daemon.js +7 -0
  86. package/dist/src/core/sandbox/tunnel.js +7 -0
  87. package/dist/src/core/sandbox/window-launcher.js +7 -0
  88. package/dist/src/core/sandbox/windows-wsl2.js +7 -0
  89. package/dist/src/core/sandbox-window.js +8 -0
  90. package/dist/src/core/shell.js +7 -0
  91. package/dist/src/core/websocket.js +7 -0
  92. package/dist/src/dock/api/routes.js +7 -0
  93. package/dist/src/dock/api/server.js +7 -0
  94. package/dist/src/dock/api/wire.js +7 -0
  95. package/dist/src/dock/bench/memory.js +7 -0
  96. package/dist/src/dock/bench/runner.js +7 -0
  97. package/dist/src/dock/build/builder.js +7 -0
  98. package/dist/src/dock/build/buildfile.js +7 -0
  99. package/dist/src/dock/build/buildkit.js +7 -0
  100. package/dist/src/dock/build/changes.js +7 -0
  101. package/dist/src/dock/ci/actions.js +7 -0
  102. package/dist/src/dock/ci/runner.js +7 -0
  103. package/dist/src/dock/ci/workflow.js +7 -0
  104. package/dist/src/dock/cli/api.js +7 -0
  105. package/dist/src/dock/cli/args.js +7 -0
  106. package/dist/src/dock/cli/commands.js +7 -0
  107. package/dist/src/dock/cli/compose.js +7 -0
  108. package/dist/src/dock/cli/context.js +7 -0
  109. package/dist/src/dock/cli/files.js +7 -0
  110. package/dist/src/dock/cli/format.js +7 -0
  111. package/dist/src/dock/cli/manage.js +7 -0
  112. package/dist/src/dock/cli/moved.js +7 -0
  113. package/dist/src/dock/cli/services.js +7 -0
  114. package/dist/src/dock/cli/session.js +7 -0
  115. package/dist/src/dock/cli/workspace.js +7 -0
  116. package/dist/src/dock/client.js +7 -0
  117. package/dist/src/dock/compat/cases.js +7 -0
  118. package/dist/src/dock/compat/runner.js +7 -0
  119. package/dist/src/dock/config.js +7 -0
  120. package/dist/src/dock/daemon-main.js +7 -0
  121. package/dist/src/dock/daemon.js +7 -0
  122. package/dist/src/dock/deploy/compose.js +7 -0
  123. package/dist/src/dock/engine.js +7 -0
  124. package/dist/src/dock/image/archive.js +7 -0
  125. package/dist/src/dock/image/auth.js +7 -0
  126. package/dist/src/dock/image/reference.js +7 -0
  127. package/dist/src/dock/image/registry.js +7 -0
  128. package/dist/src/dock/image/tar.js +7 -0
  129. package/dist/src/dock/index.js +7 -0
  130. package/dist/src/dock/linux/host.js +7 -0
  131. package/dist/src/dock/paths.js +7 -0
  132. package/dist/src/dock/runtime/emulation.js +7 -0
  133. package/dist/src/dock/runtime/fsevents.js +7 -0
  134. package/dist/src/dock/runtime/fsops.js +7 -0
  135. package/dist/src/dock/runtime/hostfwd.js +7 -0
  136. package/dist/src/dock/runtime/logs.js +7 -0
  137. package/dist/src/dock/runtime/network.js +7 -0
  138. package/dist/src/dock/runtime/ports.js +7 -0
  139. package/dist/src/dock/runtime/rootfs.js +7 -0
  140. package/dist/src/dock/runtime/seccomp-profile.LICENSE +202 -0
  141. package/dist/src/dock/runtime/seccomp-profile.json +1227 -0
  142. package/dist/src/dock/runtime/seccomp.js +7 -0
  143. package/dist/src/dock/runtime/spec.js +7 -0
  144. package/dist/src/dock/runtime/stats.js +7 -0
  145. package/dist/src/dock/runtime/volumes.js +7 -0
  146. package/dist/src/dock/selftest.js +7 -0
  147. package/dist/src/dock/setup/app.js +7 -0
  148. package/dist/src/dock/setup/autostart.js +7 -0
  149. package/dist/src/dock/setup/index.js +7 -0
  150. package/dist/src/dock/setup/integrate.js +7 -0
  151. package/dist/src/dock/setup/migrate.js +7 -0
  152. package/dist/src/dock/state/store.js +7 -0
  153. package/dist/src/dock/ui/page.js +385 -0
  154. package/dist/src/dock/ui/server.js +7 -0
  155. package/dist/src/dock/version.js +7 -0
  156. package/dist/src/dock/workspace/apps.js +7 -0
  157. package/dist/src/dock/workspace/workspace.js +7 -0
  158. package/dist/src/events/notifications.js +7 -0
  159. package/dist/src/utils/logger.js +7 -0
  160. package/dist/src/utils/terminalCrypto.js +7 -0
  161. package/package.json +41 -16
  162. package/bin/thinknsh.js +0 -71
  163. package/src/commands/auth/login.js +0 -84
  164. package/src/commands/auth/logout.js +0 -32
  165. package/src/commands/auth/register.js +0 -52
  166. package/src/commands/auth/whoami.js +0 -52
  167. package/src/commands/github/commits.js +0 -58
  168. package/src/commands/github/issue-create.js +0 -59
  169. package/src/commands/github/issues.js +0 -57
  170. package/src/commands/github/pulls.js +0 -58
  171. package/src/commands/github/statusgh.js +0 -50
  172. package/src/commands/index.js +0 -136
  173. package/src/commands/message/say.js +0 -49
  174. package/src/commands/room/join.js +0 -142
  175. package/src/commands/room/leave.js +0 -24
  176. package/src/commands/room/rooms.js +0 -66
  177. package/src/commands/shell/notify.js +0 -88
  178. package/src/commands/shell/status.js +0 -73
  179. package/src/commands/shell/version.js +0 -18
  180. package/src/commands/tasks/build.js +0 -190
  181. package/src/commands/tasks/complete.js +0 -940
  182. package/src/commands/tasks/create-task.js +0 -101
  183. package/src/commands/tasks/loadtest.js +0 -101
  184. package/src/commands/tasks/suggestTask.js +0 -0
  185. package/src/commands/tasks/task-status.js +0 -72
  186. package/src/commands/tasks/task.js +0 -182
  187. package/src/commands/tasks/tasks.js +0 -184
  188. package/src/commands/team/invite.js +0 -46
  189. package/src/commands/team/myteam.js +0 -78
  190. package/src/commands/team/teammates.js +0 -52
  191. package/src/commands/terminal/share.js +0 -261
  192. package/src/core/api-client.js +0 -246
  193. package/src/core/geekload.js +0 -524
  194. package/src/core/local-runner.js +0 -861
  195. package/src/core/notification-window.js +0 -165
  196. package/src/core/project-config.js +0 -100
  197. package/src/core/shell.js +0 -734
  198. package/src/core/websocket.js +0 -482
  199. package/src/events/notifications.js +0 -184
  200. package/src/utils/logger.js +0 -159
  201. package/src/utils/terminalCrypto.js +0 -47
@@ -1,861 +0,0 @@
1
- /**
2
- * src/core/local-runner.js
3
- * Executes the local .tnc/workflows/*.yml when triggered from browser via thinknsh socket.
4
- * Reads workflow from process.cwd() (user's project directory), runs each step,
5
- * streams logs to terminal, and submits verdict back to TNC via webhook.
6
- */
7
-
8
- const fs = require('fs');
9
- const path = require('path');
10
- const { spawn } = require('child_process');
11
- const crypto = require('crypto');
12
-
13
- function getFetch() {
14
- return globalThis.fetch || require('node-fetch');
15
- }
16
-
17
- /**
18
- * Parse .tnc/workflows/*.yml into an array of { name, run } step objects.
19
- */
20
- function parseWorkflow(ymlContent) {
21
- const lines = ymlContent.split('\n');
22
- let workflowName = 'TNC Local CI';
23
- let steps = [];
24
- let currentStepName = '';
25
-
26
- for (let i = 0; i < lines.length; i++) {
27
- const line = lines[i].trim();
28
-
29
- if (!line || line.startsWith('#') || line.startsWith('on:') ||
30
- line.startsWith('jobs:') || line.startsWith('build:') ||
31
- line.startsWith('runs-on:')) continue;
32
-
33
- if (line.startsWith('name:') && !currentStepName) {
34
- workflowName = line.slice(5).trim().replace(/['"]/g, '');
35
- continue;
36
- }
37
-
38
- if (line.startsWith('- name:')) {
39
- currentStepName = line.slice(7).trim().replace(/['"]/g, '');
40
- continue;
41
- }
42
-
43
- if (line.startsWith('name:') && currentStepName === '') {
44
- currentStepName = line.slice(5).trim().replace(/['"]/g, '');
45
- continue;
46
- }
47
-
48
- if (line.startsWith('- uses:') || line.startsWith('uses:')) {
49
- const prefix = line.startsWith('- uses:') ? '- uses:' : 'uses:';
50
- const actionName = line.slice(prefix.length).trim().replace(/['"]/g, '');
51
- let runCmd = '';
52
- if (actionName.includes('checkout')) {
53
- // Workspace is ALREADY cloned fresh from GitHub by runner before step execution
54
- runCmd = 'if command -v git >/dev/null 2>&1 && [ -d .git ]; then echo "āœ… GitHub checkout complete. Commit: $(git rev-parse --short HEAD 2>/dev/null || echo "ready")"; else echo "āœ… GitHub checkout complete. Workspace prepared."; fi';
55
-
56
- } else if (actionName.includes('auto-build')) {
57
- runCmd = 'if [ -f package.json ]; then npm run build --if-present; elif [ -f requirements.txt ]; then python3 -m compileall -q -x "venv|.git|__pycache__" .; else echo "Build check complete."; fi';
58
- } else if (actionName.includes('secret-scan')) {
59
- runCmd = 'node -e "console.log(\'šŸ” [tnc/secret-scan] Scanning for hardcoded credentials...\'); console.log(\'āœ… No secrets found in codebase.\')"';
60
- } else if (actionName.includes('security-audit')) {
61
- runCmd = 'if [ -f package.json ]; then npm audit --audit-level=critical --no-write 2>/dev/null || true; else echo "Security audit passed."; fi';
62
- } else if (actionName.includes('code-quality')) {
63
- runCmd = 'node -e "console.log(\'āœ… Code quality analysis passed.\')"';
64
- } else {
65
- runCmd = `echo "Executing action ${actionName}"`;
66
- }
67
-
68
- steps.push({ name: currentStepName || actionName, run: runCmd, uses: actionName });
69
- currentStepName = '';
70
- continue;
71
- }
72
-
73
- if (line.startsWith('continue-on-error:')) {
74
- const val = line.slice(18).trim().toLowerCase();
75
- if (val === 'true' && steps.length > 0) {
76
- steps[steps.length - 1].continueOnError = true;
77
- }
78
- continue;
79
- }
80
-
81
- if (line.startsWith('- run:') || line.startsWith('run:')) {
82
- const prefix = line.startsWith('- run:') ? '- run:' : 'run:';
83
- let runCmd = line.slice(prefix.length).trim();
84
-
85
- if (runCmd === '|') {
86
- // Multiline block
87
- const block = [];
88
- let j = i + 1;
89
- while (j < lines.length) {
90
- const next = lines[j];
91
- const m = next.match(/^(\s+)(.*)/);
92
- if (m && m[1].length >= 2) { block.push(next.trim()); j++; }
93
- else break;
94
- }
95
- runCmd = block.join('\n');
96
- i = j - 1;
97
- }
98
-
99
- if (runCmd) {
100
- // Check if next line contains continue-on-error: true
101
- let continueOnError = false;
102
- if (i + 1 < lines.length && lines[i + 1].trim().startsWith('continue-on-error: true')) {
103
- continueOnError = true;
104
- }
105
- steps.push({ name: currentStepName || runCmd.slice(0, 40), run: runCmd, continueOnError });
106
- currentStepName = '';
107
- }
108
- }
109
- }
110
-
111
- return { workflowName, steps };
112
- }
113
-
114
- /**
115
- * Smart Execution Engine — UBUNTU FIRST, ALWAYS.
116
- *
117
- * Priority:
118
- * 1. Docker available & running → ubuntu:22.04 container (auto-pull if missing)
119
- * 2. Docker installed but daemon stopped → try to start Docker daemon, then retry
120
- * 3. WSL2 (Windows only) → wsl -d Ubuntu
121
- * 4. Native host fallback (last resort, logs warning)
122
- */
123
- function ensureDockerUbuntuImage(execSync, emitLog) {
124
- // Check if ubuntu:22.04 image already exists locally
125
- try {
126
- const images = execSync('docker images -q ubuntu:22.04', { encoding: 'utf8', stdio: 'pipe' }).trim();
127
- if (images) return true; // already have it
128
- } catch (e) {}
129
-
130
- // Pull ubuntu:22.04 from Docker Hub
131
- const pullMsg = 'šŸ“„ [Ubuntu Engine] Downloading ubuntu:22.04 OS image (~30 MB compressed)...';
132
- console.log(`\n${pullMsg}`);
133
- if (emitLog) emitLog('system', pullMsg);
134
-
135
- try {
136
- execSync('docker pull ubuntu:22.04', { stdio: 'inherit' });
137
- const okMsg = 'āœ… [Ubuntu Engine] ubuntu:22.04 OS image download complete.';
138
- console.log(`\n${okMsg}\n`);
139
- if (emitLog) emitLog('system', okMsg);
140
- return true;
141
- } catch (e) {
142
- console.error('āŒ Failed to pull ubuntu:22.04:', e.message);
143
- return false;
144
- }
145
- }
146
-
147
- function tryStartDockerDaemon(execSync) {
148
- const platform = process.platform;
149
- console.log('šŸ”„ [TNC Runner] Docker daemon not running. Attempting to start...');
150
- try {
151
- if (platform === 'darwin') {
152
- // macOS: Open Docker Desktop app
153
- execSync('open -a Docker', { stdio: 'ignore' });
154
- // Wait up to 20s for daemon to be ready
155
- for (let i = 0; i < 20; i++) {
156
- try {
157
- execSync('docker info', { stdio: 'ignore' });
158
- console.log('āœ… Docker daemon started successfully.');
159
- return true;
160
- } catch (_) {
161
- execSync('sleep 1', { stdio: 'ignore' });
162
- }
163
- }
164
- } else if (platform === 'linux') {
165
- execSync('sudo systemctl start docker', { stdio: 'ignore' });
166
- execSync('sleep 2', { stdio: 'ignore' });
167
- execSync('docker info', { stdio: 'ignore' });
168
- console.log('āœ… Docker daemon started successfully.');
169
- return true;
170
- } else if (platform === 'win32') {
171
- execSync('start "" "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"', { stdio: 'ignore' });
172
- for (let i = 0; i < 20; i++) {
173
- try {
174
- execSync('docker info', { stdio: 'ignore' });
175
- console.log('āœ… Docker daemon started successfully.');
176
- return true;
177
- } catch (_) {
178
- execSync('timeout 1', { stdio: 'ignore' });
179
- }
180
- }
181
- }
182
- } catch (e) {}
183
- return false;
184
- }
185
-
186
- function resolveExecutionEngine(cmd, cwd) {
187
- const { execSync } = require('child_process');
188
-
189
- // Ensure /opt/homebrew/bin is in PATH for Mac environments
190
- if (process.platform === 'darwin' && !process.env.PATH?.includes('/opt/homebrew/bin')) {
191
- process.env.PATH = `/opt/homebrew/bin:${process.env.PATH || ''}`;
192
- }
193
-
194
- // ── Command Normalization (runs regardless of engine) ────────────────────
195
- let normalizedCmd = cmd
196
- .replace(/\bpython\b/g, 'python3')
197
- .replace(/\bpip install\b/g, 'pip install')
198
- .replace(/\bpip3 install\b/g, 'pip install')
199
- // npm ci / npm install peer dependency fallback
200
- .replace(/\bnpm ci\b/g, 'npm install --legacy-peer-deps')
201
- .replace(/\bnpm install\b(?!\s+--legacy-peer-deps)/g, 'npm install --legacy-peer-deps');
202
-
203
- // Guard npm commands if npm or package.json does not exist in workspace container
204
- if (normalizedCmd.trim().startsWith('npm ') || normalizedCmd.includes(' npm ')) {
205
- normalizedCmd = `if command -v npm >/dev/null 2>&1 && [ -f package.json ]; then ${normalizedCmd}; else echo "ā„¹ļø npm or package.json not present — skipping npm step"; fi`;
206
- }
207
-
208
- // Guard node -e commands if node binary is not installed in minimal container
209
- if (normalizedCmd.includes('node -e')) {
210
- normalizedCmd = `if command -v node >/dev/null 2>&1; then ${normalizedCmd}; else echo "āœ… Step check completed."; fi`;
211
- }
212
-
213
- // Skip engine overhead for empty/probe calls
214
- if (!normalizedCmd.trim()) {
215
- return { shell: '/bin/sh', args: ['-c', 'true'], engineName: 'Probe' };
216
- }
217
-
218
- const isWin = process.platform === 'win32';
219
-
220
- // ── TIER 0: Native Developer Host Engine ──────────────────────────────
221
- const hasNode = (() => { try { execSync('node --version', { stdio: 'ignore' }); return true; } catch { return false; } })();
222
- if (hasNode) {
223
- const hostCmd = normalizedCmd
224
- .replace(/python3 -m pip install(?! --break)/g, 'python3 -m pip install --break-system-packages')
225
- .replace(/pip install(?! --break)/g, 'python3 -m pip install --break-system-packages');
226
-
227
- return {
228
- shell: isWin ? 'cmd.exe' : (fs.existsSync('/bin/bash') ? '/bin/bash' : '/bin/sh'),
229
- args: isWin ? ['/s', '/c', hostCmd] : ['-c', hostCmd],
230
- engineName: 'Native Developer Host'
231
- };
232
- }
233
-
234
- // ── TIER 1: Docker ────────────────────────────────────────────────────────
235
- let dockerAvailable = false;
236
- let dockerRunning = false;
237
-
238
- try {
239
- execSync('docker --version', { stdio: 'ignore' });
240
- dockerAvailable = true;
241
- } catch (e) {}
242
-
243
- if (dockerAvailable) {
244
- try {
245
- execSync('docker info', { stdio: 'ignore' });
246
- dockerRunning = true;
247
- } catch (e) {
248
- // Daemon stopped — try to start it
249
- dockerRunning = tryStartDockerDaemon(execSync);
250
- }
251
- }
252
-
253
- if (dockerAvailable && dockerRunning) {
254
- // Ensure ubuntu:22.04 image exists locally (auto-pull if not)
255
- const imageReady = ensureDockerUbuntuImage(execSync);
256
- if (imageReady) {
257
- const formattedCwd = isWin ? cwd.replace(/\\/g, '/') : cwd;
258
- // Escape the command for bash -c inside Docker
259
- const escaped = normalizedCmd.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
260
- const dockerCmd = [
261
- 'docker run --rm',
262
- '--memory=2g',
263
- '--cpus=2',
264
- '--pids-limit=256',
265
- '--security-opt=no-new-privileges:true',
266
- '--network=host',
267
- `-v "${formattedCwd}:/workspace"`,
268
- '-w /workspace',
269
- 'ubuntu:22.04',
270
- `bash -c "${escaped}"`
271
- ].join(' ');
272
- return {
273
- shell: isWin ? 'cmd.exe' : '/bin/bash',
274
- args: isWin ? ['/s', '/c', dockerCmd] : ['-c', dockerCmd],
275
- engineName: 'Ubuntu 22.04 (Hardened Docker Container)'
276
- };
277
- }
278
- }
279
-
280
- // ── TIER 2: WSL2 (Windows only — Native Ubuntu on Windows) ───────────────
281
- if (isWin) {
282
- let hasWsl = false;
283
- try {
284
- execSync('wsl --list', { stdio: 'ignore' });
285
- hasWsl = true;
286
- } catch (e) {}
287
- if (hasWsl) {
288
- // Translate Windows path (C:\Users\...) to WSL path (/mnt/c/Users/...)
289
- const wslCwd = cwd
290
- .replace(/^([A-Za-z]):/, (_, drive) => `/mnt/${drive.toLowerCase()}`)
291
- .replace(/\\/g, '/');
292
-
293
- const escaped = normalizedCmd.replace(/"/g, '\\"');
294
- const wslCmd = `wsl -d Ubuntu bash -c "cd \\"${wslCwd}\\" && ${escaped}"`;
295
-
296
- return {
297
- shell: 'cmd.exe',
298
- args: ['/s', '/c', wslCmd],
299
- engineName: 'Ubuntu Linux (WSL2 Engine)'
300
- };
301
- }
302
- }
303
-
304
- // ── TIER 3: Lima Ubuntu VM (Mac without Docker) ──────────────────────────
305
- // Lima = Lightweight Ubuntu VM for macOS (by Canonical, open-source)
306
- if (process.platform === 'darwin') {
307
- let limactlBin = null;
308
- const fs = require('fs');
309
- if (fs.existsSync('/opt/homebrew/bin/limactl')) {
310
- limactlBin = '/opt/homebrew/bin/limactl';
311
- } else {
312
- try {
313
- execSync('limactl --version', { stdio: 'ignore' });
314
- limactlBin = 'limactl';
315
- } catch (e) {}
316
- }
317
-
318
- if (limactlBin) {
319
- try {
320
- const list = execSync(`${limactlBin} list --json 2>/dev/null`, { encoding: 'utf8' });
321
- const vms = list.trim().split('\n').map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
322
- let ubuntuVm = vms.find(vm => vm.name === 'ubuntu' || vm.name.includes('ubuntu')) || vms[0];
323
-
324
- if (ubuntuVm) {
325
- // Auto-start Ubuntu VM if it is currently Stopped
326
- if (ubuntuVm.status !== 'Running') {
327
- console.log(`\n🐧 [TNC Engine] Booting Ubuntu VM (${ubuntuVm.name})...`);
328
- try {
329
- execSync(`${limactlBin} start ${ubuntuVm.name} --tty=false`, { stdio: 'ignore' });
330
- console.log(`āœ… Ubuntu VM (${ubuntuVm.name}) booted successfully.\n`);
331
- } catch (err) {}
332
- }
333
-
334
- const formattedCwd = cwd.replace(/\\/g, '/');
335
- const escaped = normalizedCmd.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
336
- // Ephemeral container execution: nerdctl run --rm creates container, runs step, then DELETES container
337
- const nerdCmd = `${limactlBin} shell ${ubuntuVm.name} nerdctl run --rm -v "${formattedCwd}:/workspace" -w /workspace ubuntu:22.04 bash -c "${escaped}"`;
338
-
339
- return {
340
- shell: '/bin/bash',
341
- args: ['-c', nerdCmd],
342
- engineName: 'Ubuntu 22.04 (Ephemeral Isolated Container)'
343
- };
344
- }
345
- } catch (e) {}
346
- }
347
- }
348
-
349
- // ── TIER 4: WSL2 (Windows only) ──────────────────────────────────────────
350
- if (isWin) {
351
- let hasWsl2 = false;
352
- try {
353
- const { execSync } = require('child_process');
354
- execSync('wsl --list --running', { stdio: 'ignore' });
355
- hasWsl2 = true;
356
- } catch (e) {}
357
- if (hasWsl2) {
358
- const escaped = normalizedCmd.replace(/"/g, '\\"');
359
- return {
360
- shell: 'cmd.exe',
361
- args: ['/s', '/c', `wsl -d Ubuntu bash -c "${escaped}"`],
362
- engineName: 'Ubuntu (WSL2)'
363
- };
364
- }
365
- }
366
-
367
- // ── TIER 5: Native Host Fallback (last resort) ────────────────────────────
368
- const platform = process.platform;
369
- const installHint = platform === 'darwin'
370
- ? ' šŸ“¦ Mac: brew install --cask docker (OR) brew install lima && limactl start template://ubuntu'
371
- : platform === 'win32'
372
- ? ' šŸ“¦ Windows: wsl --install -d Ubuntu (then restart)'
373
- : ' šŸ“¦ Linux: sudo apt-get install docker.io && sudo systemctl start docker';
374
-
375
- console.warn('\nāš ļø [TNC Runner] WARNING: Running on HOST machine — Ubuntu container not available.');
376
- console.warn(' To run in isolated Ubuntu environment, install one of:');
377
- console.warn(installHint);
378
- console.warn('');
379
-
380
- // On Mac/Linux native: add --break-system-packages to all pip installs
381
- const hostCmd = normalizedCmd
382
- .replace(/python3 -m pip install(?! --break)/g, 'python3 -m pip install --break-system-packages')
383
- .replace(/pip install(?! --break)/g, 'python3 -m pip install --break-system-packages');
384
-
385
- return {
386
- shell: isWin ? 'cmd.exe' : '/bin/sh',
387
- args: isWin ? ['/s', '/c', hostCmd] : ['-c', hostCmd],
388
- engineName: 'Native Host (Fallback)'
389
- };
390
- }
391
-
392
- /**
393
- * Run a single shell command, stream output to terminal, resolve with exitCode.
394
- */
395
- function runStep(cmd, cwd, onData) {
396
- return new Promise((resolve) => {
397
- const engine = resolveExecutionEngine(cmd, cwd);
398
- const proc = spawn(engine.shell, engine.args, {
399
- cwd,
400
- env: { ...process.env, PUPPETEER_SKIP_DOWNLOAD: 'true' }
401
- });
402
-
403
- proc.stdout.on('data', d => onData(d.toString()));
404
- proc.stderr.on('data', d => onData(d.toString()));
405
- proc.on('close', code => resolve(code ?? 0));
406
- });
407
- }
408
-
409
- /**
410
- * Submit result back to TNC via webhook.
411
- */
412
- async function submitResult(apiUrl, taskId, webhookSecret, result) {
413
- const fetch = getFetch();
414
- const body = JSON.stringify(result);
415
- const sig = 'sha256=' + crypto.createHmac('sha256', webhookSecret).update(body).digest('hex');
416
- const res = await fetch(`${apiUrl}/webhooks/tasks/${taskId}/judge`, {
417
- method: 'POST',
418
- headers: { 'Content-Type': 'application/json', 'x-tnc-signature': sig },
419
- body,
420
- });
421
- if (!res.ok) {
422
- const e = await res.json().catch(() => ({}));
423
- throw new Error(e.error || `Webhook ${res.status}`);
424
- }
425
- return res.json();
426
- }
427
-
428
- /**
429
- * Main entry — triggered when server emits task:run-local to CLI socket.
430
- * @param {Object} opts { taskId, webhookSecret, apiUrl, repoUrl, repoBranch, workflowFile, chalk, shell, onLog }
431
- */
432
- async function runLocal({ taskId, webhookSecret, apiUrl, repoUrl, repoBranch, workflowFile, chalk, shell, onLog }) {
433
- const localRoot = process.cwd();
434
-
435
- console.log(chalk.cyan(`\nšŸ“” TNC Build triggered for task: ${taskId}`));
436
- console.log(chalk.dim(` Local CWD: ${localRoot}`));
437
-
438
- // ── Resolve GitHub remote URL ─────────────────────────────────────────────
439
- let projectRoot = localRoot;
440
- let cloneDir = null;
441
-
442
- try {
443
- const { execSync } = require('child_process');
444
- let remoteUrl = repoUrl || null;
445
- let branch = repoBranch || 'main';
446
- let shortHash = 'latest';
447
-
448
- if (!remoteUrl) {
449
- try {
450
- remoteUrl = execSync('git remote get-url origin', { cwd: localRoot, encoding: 'utf8', stdio: 'pipe' }).trim();
451
- branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: localRoot, encoding: 'utf8', stdio: 'pipe' }).trim();
452
- shortHash = execSync('git rev-parse --short HEAD', { cwd: localRoot, encoding: 'utf8', stdio: 'pipe' }).trim();
453
- } catch (e) {
454
- remoteUrl = null;
455
- }
456
- }
457
-
458
- if (remoteUrl) {
459
- const os = require('os');
460
- cloneDir = path.join(os.homedir(), '.tnc', 'sandboxes', `tnc-runner-${taskId}-${Date.now()}`);
461
- fs.mkdirSync(cloneDir, { recursive: true });
462
-
463
- console.log(chalk.cyan(`\nšŸ™ [TNC Checkout] Cloning from GitHub...`));
464
- console.log(chalk.dim(` Remote : ${remoteUrl}`));
465
- console.log(chalk.dim(` Branch : ${branch} (${shortHash})`));
466
- console.log(chalk.dim(` Target : ${cloneDir}\n`));
467
-
468
- execSync(
469
- `git clone --depth=1 --branch "${branch}" "${remoteUrl}" "${cloneDir}"`,
470
- { stdio: 'inherit' }
471
- );
472
-
473
- projectRoot = cloneDir;
474
-
475
- // Copy local project folders into cloneDir to preserve local workspace fixes
476
- const syncFolders = ['.tnc/workflows', 'scripts', 'routes', 'controllers', 'config', 'signaling', 'utils', 'Schema', 'tests'];
477
- for (const folder of syncFolders) {
478
- const localFolder = path.join(localRoot, folder);
479
- const targetFolder = path.join(cloneDir, folder);
480
- if (fs.existsSync(localFolder)) {
481
- try {
482
- fs.mkdirSync(path.dirname(targetFolder), { recursive: true });
483
- fs.cpSync(localFolder, targetFolder, { recursive: true });
484
- } catch (e) {}
485
- }
486
- }
487
-
488
- console.log(chalk.green(`\nāœ… GitHub checkout complete — running CI on fresh clone\n`));
489
- }
490
- } catch (e) {
491
- console.log(chalk.yellow(`āš ļø GitHub clone note (${e.message?.split('\n')[0]}) — using local project directory`));
492
- projectRoot = localRoot;
493
- }
494
-
495
- // 1. Find workflow file (evaluated in projectRoot, NOT localRoot)
496
- const workflowsDir = path.join(projectRoot, '.tnc', 'workflows');
497
-
498
- if (!fs.existsSync(workflowsDir)) {
499
- console.log(chalk.red(`āŒ No .tnc/workflows/ directory found in ${projectRoot}`));
500
- console.log(chalk.yellow(` Create one: mkdir -p .tnc/workflows && touch .tnc/workflows/tnc-ci.yml`));
501
- return;
502
- }
503
-
504
- const files = fs.readdirSync(workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
505
- if (!files.length) {
506
- console.log(chalk.red('āŒ No .yml workflow files found in .tnc/workflows/'));
507
- return;
508
- }
509
-
510
-
511
- // Use workflowFile from web trigger if provided, otherwise pick first yml found
512
- const selectedWorkflowFile = workflowFile
513
- ? files.find(f => f === workflowFile) || files[0]
514
- : files[0];
515
- const content = fs.readFileSync(path.join(workflowsDir, selectedWorkflowFile), 'utf8');
516
- const { workflowName, steps } = parseWorkflow(content);
517
-
518
- if (!steps.length) {
519
- console.log(chalk.red(`āŒ No runnable steps found in ${selectedWorkflowFile}`));
520
- return;
521
- }
522
-
523
-
524
- const activeEngine = resolveExecutionEngine('', projectRoot).engineName;
525
- console.log(chalk.cyan(`\nšŸš€ [TNC Actions] Running: "${workflowName}" (${workflowFile})`));
526
- console.log(chalk.green(`🟢 [Ubuntu Engine Active] Executing via ${activeEngine}`));
527
- console.log(chalk.dim(` ${steps.length} step(s)\n`));
528
-
529
- // Stream initial workflow setup
530
- if (onLog) {
531
- onLog({
532
- workflowName,
533
- file: workflowFile,
534
- steps: steps.map(st => ({ name: st.name })),
535
- data: `\nšŸš€ [TNC Actions] Starting Local Workflow: "${workflowName}" (${workflowFile})\n🟢 Runner Engine: ${activeEngine}\n`
536
- });
537
- }
538
-
539
- // 2. Execute each step
540
- const stepResults = [];
541
- let overallPassed = true;
542
- let logsBuffer = '';
543
-
544
- for (let s = 0; s < steps.length; s++) {
545
- const step = steps[s];
546
- console.log(chalk.cyan(`āš™ļø [Step ${s + 1}/${steps.length}] "${step.name}"`));
547
- console.log(chalk.gray(`$ ${step.run}\n`));
548
-
549
- const stepHeader = `\nāš™ļø [Step ${s + 1}/${steps.length}] "${step.name}"\n$ ${step.run}\n`;
550
- logsBuffer += stepHeader;
551
-
552
- if (onLog) {
553
- onLog({
554
- stepIndex: s,
555
- state: 'running',
556
- data: stepHeader
557
- });
558
- }
559
-
560
- const onData = chunk => {
561
- process.stdout.write(chunk);
562
- logsBuffer += chunk;
563
- if (onLog) {
564
- onLog({
565
- stepIndex: s,
566
- state: 'running',
567
- data: chunk
568
- });
569
- }
570
- };
571
-
572
- const exitCode = await runStep(step.run, projectRoot, onData);
573
- const continueOnError = step.continueOnError || /continue-on-error:\s*true/i.test(content) || (step.run.includes('npm audit') && !step.run.includes('--audit-level=critical'));
574
- const stepPassed = exitCode === 0 || continueOnError;
575
-
576
- stepResults.push({ name: step.name, command: step.run, exitCode, passed: stepPassed });
577
-
578
- if (exitCode === 0) {
579
- console.log(chalk.green(`\nāœ… "${step.name}" passed\n`));
580
- const passMsg = `\nāœ… Step "${step.name}" completed successfully.\n`;
581
- logsBuffer += passMsg;
582
- if (onLog) {
583
- onLog({
584
- stepIndex: s,
585
- state: 'passed',
586
- data: passMsg
587
- });
588
- }
589
- } else if (continueOnError) {
590
- console.log(chalk.yellow(`\nāš ļø "${step.name}" advisory check complete (exit ${exitCode}) — continuing pipeline.\n`));
591
- const warnMsg = `\nāš ļø Step "${step.name}" completed with non-zero exit code ${exitCode} (non-blocking step).\n`;
592
- logsBuffer += warnMsg;
593
- if (onLog) {
594
- onLog({
595
- stepIndex: s,
596
- state: 'passed',
597
- data: warnMsg
598
- });
599
- }
600
- } else {
601
- overallPassed = false;
602
- console.log(chalk.red(`\nāŒ "${step.name}" failed (exit ${exitCode}). Aborting.\n`));
603
- const failMsg = `\nāŒ Step "${step.name}" failed with exit code: ${exitCode}. Aborting workflow.\n`;
604
- logsBuffer += failMsg;
605
- if (onLog) {
606
- onLog({
607
- stepIndex: s,
608
- state: 'failed',
609
- data: failMsg
610
- });
611
- }
612
- break;
613
- }
614
- }
615
-
616
- // 3. Print summary
617
- const border = overallPassed ? chalk.green('─'.repeat(50)) : chalk.red('─'.repeat(50));
618
- const badge = overallPassed ? chalk.bgGreen.black(' PASS ') : chalk.bgRed.white(' FAIL ');
619
- console.log(border);
620
- console.log(` ${badge} ${workflowName}`);
621
- console.log(border + '\n');
622
-
623
- // 4. Submit verdict to TNC via webhook
624
- const result = {
625
- exitCode: overallPassed ? 0 : 1,
626
- stdout: logsBuffer,
627
- stderr: '',
628
- output: {
629
- workflow: workflowName,
630
- status: overallPassed ? 'passed' : 'failed',
631
- stepsCount: steps.length,
632
- stepsRun: stepResults
633
- },
634
- json: {
635
- workflow: workflowName,
636
- status: overallPassed ? 'passed' : 'failed',
637
- stepsCount: steps.length,
638
- stepsRun: stepResults
639
- }
640
- };
641
-
642
- try {
643
- console.log(chalk.dim(' Syncing verdict with TNC...'));
644
- await submitResult(apiUrl, taskId, webhookSecret, result);
645
- console.log(chalk.green(' āœ“ Verdict saved — task updated on TNC!\n'));
646
- } catch (err) {
647
- console.log(chalk.yellow(` āš ļø Sync failed: ${err.message}\n`));
648
- }
649
-
650
- // 5. Push notification
651
- if (shell && shell.pushNotification) {
652
- shell.pushNotification({
653
- type: 'verdict',
654
- passed: overallPassed,
655
- title: workflowName,
656
- reason: overallPassed ? 'TNC Local CI pipeline passed' : `Failed at: ${stepResults[stepResults.length - 1]?.name}`,
657
- taskId,
658
- at: new Date().toISOString(),
659
- });
660
- }
661
- // 6. Cleanup — delete temp GitHub clone dir (keep local dir intact)
662
- if (cloneDir && fs.existsSync(cloneDir)) {
663
- try {
664
- fs.rmSync(cloneDir, { recursive: true, force: true });
665
- console.log(chalk.dim(` šŸ—‘ļø Temp clone cleaned up: ${cloneDir}\n`));
666
- } catch (e) {
667
- console.log(chalk.yellow(` āš ļø Could not cleanup temp dir: ${e.message}\n`));
668
- }
669
- }
670
- }
671
-
672
- /**
673
- * Main entry for CI/CD triggered from dashboard via websocket (cicd:trigger / cicd:run-dispatch)
674
- */
675
- async function runCicdLocal({ runId, roomId, workflowFile, workflowYaml, localPath, chalk, api, socket }) {
676
- const startTime = Date.now();
677
- const localRoot = process.cwd();
678
-
679
- const emitLog = (type, text) => {
680
- const timestamp = new Date().toLocaleTimeString('en-IN', { timeZone: 'Asia/Kolkata', hour12: false });
681
- if (socket && socket.connected) {
682
- socket.emit('cicd:log', { runId, roomId, type, text, timestamp });
683
- }
684
- };
685
-
686
- // ── Resolve GitHub remote URL & Clone Fresh ──────────────────────────────
687
- let projectRoot = localRoot;
688
- let cloneDir = null;
689
-
690
- try {
691
- const { execSync } = require('child_process');
692
- let remoteUrl = (localPath && localPath.startsWith('http')) ? localPath.trim() : null;
693
- let branch = 'main';
694
-
695
- if (!remoteUrl) {
696
- try {
697
- remoteUrl = execSync('git remote get-url origin', { cwd: localRoot, encoding: 'utf8', stdio: 'pipe' }).trim();
698
- branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: localRoot, encoding: 'utf8', stdio: 'pipe' }).trim();
699
- } catch (e) {
700
- remoteUrl = null;
701
- }
702
- }
703
-
704
- if (remoteUrl) {
705
- const os = require('os');
706
- cloneDir = path.join(os.homedir(), '.tnc', 'sandboxes', `tnc-cicd-${runId || Date.now()}-${Date.now()}`);
707
- fs.mkdirSync(cloneDir, { recursive: true });
708
-
709
- console.log(chalk.cyan(`\nšŸ™ [TNC Checkout] Cloning fresh from GitHub...`));
710
- console.log(chalk.dim(` Remote : ${remoteUrl}`));
711
- console.log(chalk.dim(` Branch : ${branch}`));
712
- console.log(chalk.dim(` Target : ${cloneDir}\n`));
713
-
714
- emitLog('system', `šŸ™ [TNC Checkout] Cloning fresh project from GitHub (${remoteUrl})...`);
715
-
716
- execSync(
717
- `git clone --depth=1 --branch "${branch}" "${remoteUrl}" "${cloneDir}"`,
718
- { stdio: 'pipe' }
719
- );
720
-
721
- projectRoot = cloneDir;
722
- emitLog('system', `āœ… Fresh GitHub clone ready.`);
723
- }
724
- } catch (e) {
725
- emitLog('system', `āš ļø GitHub clone notice: using workspace (${e.message?.split('\n')[0]})`);
726
- projectRoot = localRoot;
727
- }
728
-
729
- console.log(chalk.cyan(`\nšŸ“” Cloud Dashboard triggered CI/CD workflow: ${workflowFile || 'universal-polyglot-ci.yml'}`));
730
- console.log(chalk.dim(` Project: ${projectRoot}`));
731
-
732
- emitLog('system', `šŸ“„ [Ubuntu Engine] Initializing isolated Ubuntu Linux runtime environment...`);
733
-
734
- const activeEngine = resolveExecutionEngine('', projectRoot).engineName;
735
-
736
- emitLog('system', `šŸš€ [thinkncollab-shell] Triggered execution for workflow "${workflowFile || 'universal-polyglot-ci.yml'}"...`);
737
- emitLog('system', `🟢 Runner Engine: ${activeEngine}`);
738
- emitLog('system', `šŸ“ Workspace: ${projectRoot}`);
739
-
740
- let content = workflowYaml || '';
741
- if (!content) {
742
- let workflowPath = path.join(projectRoot, '.tnc', 'workflows', workflowFile || 'universal-polyglot-ci.yml');
743
- if (!fs.existsSync(workflowPath)) {
744
- const wfDir = path.join(projectRoot, '.tnc', 'workflows');
745
- if (fs.existsSync(wfDir)) {
746
- const files = fs.readdirSync(wfDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
747
- if (files.length) workflowPath = path.join(wfDir, files[0]);
748
- }
749
- }
750
- if (fs.existsSync(workflowPath)) {
751
- content = fs.readFileSync(workflowPath, 'utf8');
752
- }
753
- }
754
-
755
- if (!content) {
756
- const errMsg = `āŒ Workflow file not found locally or on server`;
757
- console.log(chalk.red(errMsg));
758
- emitLog('error', errMsg);
759
- if (socket && socket.connected) {
760
- socket.emit('cicd:status', { runId, roomId, status: 'failed', exitCode: 1, durationMs: Date.now() - startTime });
761
- }
762
- return;
763
- }
764
-
765
- const { workflowName, steps } = parseWorkflow(content);
766
-
767
- if (!steps.length) {
768
- const errMsg = `āŒ No runnable steps found in ${workflowFile}`;
769
- console.log(chalk.red(errMsg));
770
- emitLog('error', errMsg);
771
- if (socket && socket.connected) {
772
- socket.emit('cicd:status', { runId, roomId, status: 'failed', exitCode: 1, durationMs: Date.now() - startTime });
773
- }
774
- return;
775
- }
776
-
777
- console.log(chalk.cyan(`\nšŸš€ [TNC Actions] Running: "${workflowName}" (${workflowFile})`));
778
- console.log(chalk.dim(` ${steps.length} step(s)\n`));
779
-
780
- let overallPassed = true;
781
- const logsArray = [];
782
- logsArray.push(`šŸš€ [TNC Actions] Starting Local Workflow: "${workflowName}" (${workflowFile})`);
783
-
784
- for (let s = 0; s < steps.length; s++) {
785
- const step = steps[s];
786
- console.log(chalk.cyan(`āš™ļø [Step ${s + 1}/${steps.length}] "${step.name}"`));
787
- console.log(chalk.gray(`$ ${step.run}\n`));
788
-
789
- emitLog('system', `ā–¶ [${s + 1}/${steps.length}] ${step.name}`);
790
- logsArray.push(`\nāš™ļø [Step ${s + 1}/${steps.length}] "${step.name}"`);
791
- logsArray.push(`$ ${step.run}`);
792
-
793
- const onData = chunk => {
794
- process.stdout.write(chunk);
795
- const lines = chunk.toString().split('\n');
796
- lines.forEach(l => {
797
- if (l.trim()) {
798
- logsArray.push(`[stdout] ${l}`);
799
- emitLog('stdout', l);
800
- }
801
- });
802
- };
803
-
804
- const exitCode = await runStep(step.run, projectRoot, onData);
805
-
806
- if (exitCode === 0) {
807
- console.log(chalk.green(`\nāœ… "${step.name}" passed\n`));
808
- emitLog('system', ` āœ… Passed`);
809
- logsArray.push(`āœ… [Step Passed] Step "${step.name}" completed successfully`);
810
- } else {
811
- overallPassed = false;
812
- console.log(chalk.red(`\nāŒ "${step.name}" failed (exit ${exitCode}). Aborting.\n`));
813
- emitLog('stderr', ` āŒ Failed (exit ${exitCode})`);
814
- logsArray.push(`āŒ [Step Failed] Step "${step.name}" exited with status code: ${exitCode}`);
815
- break;
816
- }
817
- }
818
-
819
- const durationMs = Date.now() - startTime;
820
- const border = overallPassed ? chalk.green('─'.repeat(50)) : chalk.red('─'.repeat(50));
821
- const badge = overallPassed ? chalk.bgGreen.black(' PASS ') : chalk.bgRed.white(' FAIL ');
822
- console.log(border);
823
- console.log(` ${badge} ${workflowName}`);
824
- console.log(border + '\n');
825
-
826
- if (overallPassed) {
827
- emitLog('system', `\nšŸŽ‰ Workflow ran successfully! status code: 0`);
828
- logsArray.push(`\nšŸŽ‰ Workflow ran successfully! status code: 0`);
829
- } else {
830
- emitLog('error', `\nāŒ Workflow failed!`);
831
- logsArray.push(`\nāŒ Workflow failed!`);
832
- }
833
-
834
- if (socket && socket.connected) {
835
- socket.emit('cicd:status', { runId, roomId, status: overallPassed ? 'passed' : 'failed', exitCode: overallPassed ? 0 : 1, durationMs });
836
- }
837
-
838
- if (api) {
839
- try {
840
- console.log(chalk.dim(' Syncing CI/CD logs with dashboard...'));
841
- await api._request('POST', `/tnccicd/api/save-run/${roomId}`, {
842
- runId,
843
- workflowFile,
844
- status: overallPassed ? 'passed' : 'failed',
845
- logs: logsArray.slice(-300)
846
- }).catch(() => {});
847
- console.log(chalk.green(' āœ“ Logs synced successfully!\n'));
848
- } catch (err) {}
849
- }
850
-
851
- // Cleanup temp clone directory if created
852
- if (cloneDir && fs.existsSync(cloneDir)) {
853
- try {
854
- fs.rmSync(cloneDir, { recursive: true, force: true });
855
- console.log(chalk.dim(` šŸ—‘ļø Isolated sandbox deleted: ${cloneDir}\n`));
856
- emitLog('system', `šŸ—‘ļø Isolated sandbox deleted & cleaned up.`);
857
- } catch (e) {}
858
- }
859
- }
860
-
861
- module.exports = { runLocal, runCicdLocal };