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.
- package/LICENSE +82 -0
- package/Readme.md +246 -502
- package/dist/bin/thinknsh.js +8 -0
- package/dist/src/commands/auth/login.js +7 -0
- package/dist/src/commands/auth/logout.js +7 -0
- package/dist/src/commands/auth/register.js +7 -0
- package/dist/src/commands/auth/whoami.js +7 -0
- package/dist/src/commands/dock/bench.js +7 -0
- package/dist/src/commands/dock/ci.js +7 -0
- package/dist/src/commands/dock/cluster.js +7 -0
- package/dist/src/commands/dock/containers.js +7 -0
- package/dist/src/commands/dock/dock.js +7 -0
- package/dist/src/commands/github/commits.js +7 -0
- package/dist/src/commands/github/issue-create.js +7 -0
- package/dist/src/commands/github/issues.js +7 -0
- package/dist/src/commands/github/pulls.js +7 -0
- package/dist/src/commands/github/statusgh.js +7 -0
- package/dist/src/commands/index.js +7 -0
- package/dist/src/commands/message/say.js +7 -0
- package/dist/src/commands/room/join.js +7 -0
- package/dist/src/commands/room/leave.js +7 -0
- package/dist/src/commands/room/rooms.js +7 -0
- package/dist/src/commands/room/sync.js +7 -0
- package/dist/src/commands/sandbox/console.js +7 -0
- package/dist/src/commands/sandbox/dep.js +7 -0
- package/dist/src/commands/sandbox/deploy.js +7 -0
- package/dist/src/commands/sandbox/dev.js +7 -0
- package/dist/src/commands/sandbox/exec.js +7 -0
- package/dist/src/commands/sandbox/freeze.js +7 -0
- package/dist/src/commands/sandbox/push.js +7 -0
- package/dist/src/commands/sandbox/resume.js +7 -0
- package/dist/src/commands/sandbox/sandbox.js +7 -0
- package/dist/src/commands/sandbox/share.js +7 -0
- package/dist/src/commands/sandbox/snapshot.js +7 -0
- package/dist/src/commands/sandbox/sync-deps.js +7 -0
- package/dist/src/commands/shell/doctor.js +7 -0
- package/dist/src/commands/shell/notifications.js +7 -0
- package/dist/src/commands/shell/notify.js +7 -0
- package/dist/src/commands/shell/status.js +7 -0
- package/dist/src/commands/shell/version.js +7 -0
- package/dist/src/commands/tasks/build.js +7 -0
- package/dist/src/commands/tasks/complete.js +7 -0
- package/dist/src/commands/tasks/create-task.js +7 -0
- package/dist/src/commands/tasks/loadtest.js +7 -0
- package/dist/src/commands/tasks/task-status.js +7 -0
- package/dist/src/commands/tasks/task.js +7 -0
- package/dist/src/commands/tasks/tasks.js +7 -0
- package/dist/src/commands/team/invite.js +7 -0
- package/dist/src/commands/team/myteam.js +7 -0
- package/dist/src/commands/team/teammates.js +7 -0
- package/dist/src/commands/terminal/share.js +7 -0
- package/dist/src/core/api-client.js +7 -0
- package/dist/src/core/crypto/e2ee-vault.js +7 -0
- package/dist/src/core/dep-store.js +7 -0
- package/dist/src/core/dependency-tracker.js +7 -0
- package/dist/src/core/geekload.js +7 -0
- package/dist/src/core/local-runner.js +7 -0
- package/dist/src/core/multi-lang-scanner.js +7 -0
- package/dist/src/core/notification-window.js +8 -0
- package/dist/src/core/notify-auth.js +7 -0
- package/dist/src/core/project-config.js +7 -0
- package/dist/src/core/sandbox/arch-shim.js +7 -0
- package/dist/src/core/sandbox/capsule-builder.js +7 -0
- package/dist/src/core/sandbox/cgroups.js +7 -0
- package/dist/src/core/sandbox/container-monitor.js +7 -0
- package/dist/src/core/sandbox/crun-manager.js +7 -0
- package/dist/src/core/sandbox/engine.js +7 -0
- package/dist/src/core/sandbox/image-manager.js +7 -0
- package/dist/src/core/sandbox/image-store.js +7 -0
- package/dist/src/core/sandbox/index.js +7 -0
- package/dist/src/core/sandbox/macos-sandbox.js +7 -0
- package/dist/src/core/sandbox/microvm-manager.js +7 -0
- package/dist/src/core/sandbox/native-service-manager.js +7 -0
- package/dist/src/core/sandbox/network.js +7 -0
- package/dist/src/core/sandbox/oci-exporter.js +7 -0
- package/dist/src/core/sandbox/oci-puller.js +7 -0
- package/dist/src/core/sandbox/oci-runtime-spec.js +7 -0
- package/dist/src/core/sandbox/policy-engine.js +7 -0
- package/dist/src/core/sandbox/port-proxy-daemon.js +7 -0
- package/dist/src/core/sandbox/room-sync-manager.js +7 -0
- package/dist/src/core/sandbox/runtime-manager.js +7 -0
- package/dist/src/core/sandbox/snapshot-manager.js +7 -0
- package/dist/src/core/sandbox/storage.js +7 -0
- package/dist/src/core/sandbox/supervisor.js +7 -0
- package/dist/src/core/sandbox/tunnel-daemon.js +7 -0
- package/dist/src/core/sandbox/tunnel.js +7 -0
- package/dist/src/core/sandbox/window-launcher.js +7 -0
- package/dist/src/core/sandbox/windows-wsl2.js +7 -0
- package/dist/src/core/sandbox-window.js +8 -0
- package/dist/src/core/shell.js +7 -0
- package/dist/src/core/websocket.js +7 -0
- package/dist/src/dock/api/routes.js +7 -0
- package/dist/src/dock/api/server.js +7 -0
- package/dist/src/dock/api/wire.js +7 -0
- package/dist/src/dock/bench/memory.js +7 -0
- package/dist/src/dock/bench/runner.js +7 -0
- package/dist/src/dock/build/builder.js +7 -0
- package/dist/src/dock/build/buildfile.js +7 -0
- package/dist/src/dock/build/buildkit.js +7 -0
- package/dist/src/dock/build/changes.js +7 -0
- package/dist/src/dock/ci/actions.js +7 -0
- package/dist/src/dock/ci/runner.js +7 -0
- package/dist/src/dock/ci/workflow.js +7 -0
- package/dist/src/dock/cli/api.js +7 -0
- package/dist/src/dock/cli/args.js +7 -0
- package/dist/src/dock/cli/commands.js +7 -0
- package/dist/src/dock/cli/compose.js +7 -0
- package/dist/src/dock/cli/context.js +7 -0
- package/dist/src/dock/cli/files.js +7 -0
- package/dist/src/dock/cli/format.js +7 -0
- package/dist/src/dock/cli/manage.js +7 -0
- package/dist/src/dock/cli/moved.js +7 -0
- package/dist/src/dock/cli/services.js +7 -0
- package/dist/src/dock/cli/session.js +7 -0
- package/dist/src/dock/cli/workspace.js +7 -0
- package/dist/src/dock/client.js +7 -0
- package/dist/src/dock/compat/cases.js +7 -0
- package/dist/src/dock/compat/runner.js +7 -0
- package/dist/src/dock/config.js +7 -0
- package/dist/src/dock/daemon-main.js +7 -0
- package/dist/src/dock/daemon.js +7 -0
- package/dist/src/dock/deploy/compose.js +7 -0
- package/dist/src/dock/engine.js +7 -0
- package/dist/src/dock/image/archive.js +7 -0
- package/dist/src/dock/image/auth.js +7 -0
- package/dist/src/dock/image/reference.js +7 -0
- package/dist/src/dock/image/registry.js +7 -0
- package/dist/src/dock/image/tar.js +7 -0
- package/dist/src/dock/index.js +7 -0
- package/dist/src/dock/linux/host.js +7 -0
- package/dist/src/dock/paths.js +7 -0
- package/dist/src/dock/runtime/emulation.js +7 -0
- package/dist/src/dock/runtime/fsevents.js +7 -0
- package/dist/src/dock/runtime/fsops.js +7 -0
- package/dist/src/dock/runtime/hostfwd.js +7 -0
- package/dist/src/dock/runtime/logs.js +7 -0
- package/dist/src/dock/runtime/network.js +7 -0
- package/dist/src/dock/runtime/ports.js +7 -0
- package/dist/src/dock/runtime/rootfs.js +7 -0
- package/dist/src/dock/runtime/seccomp-profile.LICENSE +202 -0
- package/dist/src/dock/runtime/seccomp-profile.json +1227 -0
- package/dist/src/dock/runtime/seccomp.js +7 -0
- package/dist/src/dock/runtime/spec.js +7 -0
- package/dist/src/dock/runtime/stats.js +7 -0
- package/dist/src/dock/runtime/volumes.js +7 -0
- package/dist/src/dock/selftest.js +7 -0
- package/dist/src/dock/setup/app.js +7 -0
- package/dist/src/dock/setup/autostart.js +7 -0
- package/dist/src/dock/setup/index.js +7 -0
- package/dist/src/dock/setup/integrate.js +7 -0
- package/dist/src/dock/setup/migrate.js +7 -0
- package/dist/src/dock/state/store.js +7 -0
- package/dist/src/dock/ui/page.js +385 -0
- package/dist/src/dock/ui/server.js +7 -0
- package/dist/src/dock/version.js +7 -0
- package/dist/src/dock/workspace/apps.js +7 -0
- package/dist/src/dock/workspace/workspace.js +7 -0
- package/dist/src/events/notifications.js +7 -0
- package/dist/src/utils/logger.js +7 -0
- package/dist/src/utils/terminalCrypto.js +7 -0
- package/package.json +41 -16
- package/bin/thinknsh.js +0 -71
- package/src/commands/auth/login.js +0 -84
- package/src/commands/auth/logout.js +0 -32
- package/src/commands/auth/register.js +0 -52
- package/src/commands/auth/whoami.js +0 -52
- package/src/commands/github/commits.js +0 -58
- package/src/commands/github/issue-create.js +0 -59
- package/src/commands/github/issues.js +0 -57
- package/src/commands/github/pulls.js +0 -58
- package/src/commands/github/statusgh.js +0 -50
- package/src/commands/index.js +0 -136
- package/src/commands/message/say.js +0 -49
- package/src/commands/room/join.js +0 -142
- package/src/commands/room/leave.js +0 -24
- package/src/commands/room/rooms.js +0 -66
- package/src/commands/shell/notify.js +0 -88
- package/src/commands/shell/status.js +0 -73
- package/src/commands/shell/version.js +0 -18
- package/src/commands/tasks/build.js +0 -190
- package/src/commands/tasks/complete.js +0 -940
- package/src/commands/tasks/create-task.js +0 -101
- package/src/commands/tasks/loadtest.js +0 -101
- package/src/commands/tasks/suggestTask.js +0 -0
- package/src/commands/tasks/task-status.js +0 -72
- package/src/commands/tasks/task.js +0 -182
- package/src/commands/tasks/tasks.js +0 -184
- package/src/commands/team/invite.js +0 -46
- package/src/commands/team/myteam.js +0 -78
- package/src/commands/team/teammates.js +0 -52
- package/src/commands/terminal/share.js +0 -261
- package/src/core/api-client.js +0 -246
- package/src/core/geekload.js +0 -524
- package/src/core/local-runner.js +0 -861
- package/src/core/notification-window.js +0 -165
- package/src/core/project-config.js +0 -100
- package/src/core/shell.js +0 -734
- package/src/core/websocket.js +0 -482
- package/src/events/notifications.js +0 -184
- package/src/utils/logger.js +0 -159
- package/src/utils/terminalCrypto.js +0 -47
package/src/core/local-runner.js
DELETED
|
@@ -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 };
|