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
|
@@ -1,940 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const chalk = require('chalk');
|
|
4
|
-
const crypto = require('crypto');
|
|
5
|
-
const cp = require('child_process');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
|
|
8
|
-
// āāā Helpers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
9
|
-
|
|
10
|
-
function getFetchFn() {
|
|
11
|
-
return globalThis.fetch || require('node-fetch');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Parse raw Set-Cookie header into Playwright cookie objects
|
|
15
|
-
function parseCookieHeader(raw, base) {
|
|
16
|
-
const domain = new URL(base).hostname;
|
|
17
|
-
const cookies = [];
|
|
18
|
-
const parts = raw.split(/,(?=[^ ][^=]+=)/);
|
|
19
|
-
|
|
20
|
-
for (const part of parts) {
|
|
21
|
-
const segments = part.split(';').map(s => s.trim());
|
|
22
|
-
const [nameVal, ...attrs] = segments;
|
|
23
|
-
const eqIdx = nameVal.indexOf('=');
|
|
24
|
-
if (eqIdx === -1) continue;
|
|
25
|
-
|
|
26
|
-
const name = nameVal.slice(0, eqIdx).trim();
|
|
27
|
-
const value = nameVal.slice(eqIdx + 1).trim();
|
|
28
|
-
const attrMap = {};
|
|
29
|
-
|
|
30
|
-
for (const attr of attrs) {
|
|
31
|
-
const [k, v] = attr.split('=').map(s => s.trim());
|
|
32
|
-
attrMap[k.toLowerCase()] = v || true;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
cookies.push({
|
|
36
|
-
name,
|
|
37
|
-
value,
|
|
38
|
-
domain: attrMap.domain || domain,
|
|
39
|
-
path: attrMap.path || '/',
|
|
40
|
-
httpOnly: !!attrMap.httponly,
|
|
41
|
-
secure: !!attrMap.secure,
|
|
42
|
-
sameSite: attrMap.samesite === 'None' ? 'None'
|
|
43
|
-
: attrMap.samesite === 'Strict' ? 'Strict'
|
|
44
|
-
: 'Lax',
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return cookies;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function extractCsrf(html, customSelector) {
|
|
52
|
-
if (customSelector) {
|
|
53
|
-
const nameMatch = customSelector.match(/name=['"]([^'"]+)['"]/);
|
|
54
|
-
if (nameMatch) {
|
|
55
|
-
const re = new RegExp(
|
|
56
|
-
`name=["']${nameMatch[1]}["'][^>]*value=["']([^"']+)["']|value=["']([^"']+)["'][^>]*name=["']${nameMatch[1]}["']`
|
|
57
|
-
);
|
|
58
|
-
const m = html.match(re);
|
|
59
|
-
if (m) return { field: nameMatch[1], token: m[1] || m[2] };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const patterns = [
|
|
64
|
-
{ field: '_csrf', re: /name=["']_csrf["'][^>]*value=["']([^"']+)["']/ },
|
|
65
|
-
{ field: '_token', re: /name=["']_token["'][^>]*value=["']([^"']+)["']/ },
|
|
66
|
-
{ field: 'authenticity_token', re: /name=["']authenticity_token["'][^>]*value=["']([^"']+)["']/ },
|
|
67
|
-
{ field: '__RequestVerificationToken', re: /name=["']__RequestVerificationToken["'][^>]*value=["']([^"']+)["']/ },
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
for (const { field, re } of patterns) {
|
|
71
|
-
const m = html.match(re);
|
|
72
|
-
if (m) return { field, token: m[1] };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// āāā Strategy: request āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
79
|
-
async function authViaRequest(context, base, email, password, authConfig) {
|
|
80
|
-
const fetch = getFetchFn();
|
|
81
|
-
const signinPath = authConfig.signinUrl || '/signin';
|
|
82
|
-
const signinUrl = base.replace(/\/$/, '') + signinPath;
|
|
83
|
-
|
|
84
|
-
console.log(chalk.dim(` ā [request] GET ${signinUrl}`));
|
|
85
|
-
|
|
86
|
-
// 1. GET signin page ā grab CSRF + initial cookies
|
|
87
|
-
const getRes = await fetch(signinUrl, {
|
|
88
|
-
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; TNCBot/1.0)' },
|
|
89
|
-
redirect: 'follow',
|
|
90
|
-
});
|
|
91
|
-
const html = await getRes.text();
|
|
92
|
-
const getCookies = getRes.headers.get('set-cookie') || '';
|
|
93
|
-
const csrf = extractCsrf(html, authConfig.csrfSelector);
|
|
94
|
-
|
|
95
|
-
if (csrf) console.log(chalk.dim(` ā CSRF detected: ${csrf.field}`));
|
|
96
|
-
|
|
97
|
-
// 2. POST credentials
|
|
98
|
-
const formBody = new URLSearchParams({ email, password });
|
|
99
|
-
if (csrf) formBody.append(csrf.field, csrf.token);
|
|
100
|
-
|
|
101
|
-
console.log(chalk.dim(` ā [request] POST ${signinUrl}`));
|
|
102
|
-
|
|
103
|
-
const postRes = await fetch(signinUrl, {
|
|
104
|
-
method: 'POST',
|
|
105
|
-
headers: {
|
|
106
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
107
|
-
'Cookie': getCookies,
|
|
108
|
-
'User-Agent': 'Mozilla/5.0 (compatible; TNCBot/1.0)',
|
|
109
|
-
'Referer': signinUrl,
|
|
110
|
-
},
|
|
111
|
-
body: formBody.toString(),
|
|
112
|
-
redirect: 'manual',
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// 3. Extract session cookies ā follow redirect manually if needed
|
|
116
|
-
let finalCookies = postRes.headers.get('set-cookie');
|
|
117
|
-
|
|
118
|
-
if (!finalCookies && postRes.status >= 300 && postRes.status < 400) {
|
|
119
|
-
const loc = postRes.headers.get('location');
|
|
120
|
-
if (loc) {
|
|
121
|
-
const redirectRes = await fetch(
|
|
122
|
-
loc.startsWith('http') ? loc : base + loc,
|
|
123
|
-
{ headers: { 'Cookie': getCookies }, redirect: 'manual' }
|
|
124
|
-
);
|
|
125
|
-
finalCookies = redirectRes.headers.get('set-cookie');
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (!finalCookies) {
|
|
130
|
-
throw new Error(
|
|
131
|
-
'Auth failed ā no session cookie returned. ' +
|
|
132
|
-
'Check testUser credentials or switch to strategy: "form"'
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const parsed = parseCookieHeader(finalCookies, base);
|
|
137
|
-
if (!parsed.length) throw new Error('Could not parse session cookies from response');
|
|
138
|
-
|
|
139
|
-
await context.addCookies(parsed);
|
|
140
|
-
console.log(chalk.dim(` ā Cookies injected: ${parsed.map(c => c.name).join(', ')}`));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function authViaForm(page, base, email, password, authConfig) {
|
|
144
|
-
const signinPath = authConfig.signinUrl || '/signin';
|
|
145
|
-
await page.goto(base + signinPath, { waitUntil: 'networkidle', timeout: 15000 });
|
|
146
|
-
console.log(chalk.dim(` ā [form] ${page.url()}`));
|
|
147
|
-
|
|
148
|
-
if (!page.url().includes(signinPath.replace(/^\//, ''))) return;
|
|
149
|
-
|
|
150
|
-
await page.evaluate(() => {
|
|
151
|
-
document.querySelectorAll('form input, form button, form > div, form > section').forEach(el => {
|
|
152
|
-
el.style.display = '';
|
|
153
|
-
el.style.visibility = 'visible';
|
|
154
|
-
el.style.opacity = '1';
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const fields = authConfig.fields || {};
|
|
159
|
-
|
|
160
|
-
const emailSel = fields.email || [
|
|
161
|
-
'input[type="email"]', 'input[name="email"]',
|
|
162
|
-
'input[id="email"]', 'input[autocomplete="email"]',
|
|
163
|
-
].join(', ');
|
|
164
|
-
|
|
165
|
-
const passSel = fields.password || [
|
|
166
|
-
'input[type="password"]', 'input[name="password"]',
|
|
167
|
-
'input[id="password"]', 'input[autocomplete="current-password"]',
|
|
168
|
-
].join(', ');
|
|
169
|
-
|
|
170
|
-
const submitSel = fields.submit || [
|
|
171
|
-
'button[type="submit"]', 'input[type="submit"]',
|
|
172
|
-
'button:has-text("Sign in")', 'button:has-text("Login")',
|
|
173
|
-
'button:has-text("Log in")', 'button:has-text("Continue")',
|
|
174
|
-
'form button',
|
|
175
|
-
].join(', ');
|
|
176
|
-
|
|
177
|
-
await page.locator(emailSel).first().fill(email);
|
|
178
|
-
console.log(chalk.dim(` ā Email filled`));
|
|
179
|
-
await page.locator(passSel).first().fill(password);
|
|
180
|
-
console.log(chalk.dim(` ā Password filled`));
|
|
181
|
-
|
|
182
|
-
const hasSubmit = await page.locator(submitSel).first().count().catch(() => 0);
|
|
183
|
-
if (hasSubmit) {
|
|
184
|
-
await page.locator(submitSel).first().click();
|
|
185
|
-
console.log(chalk.dim(` ā Submit clicked`));
|
|
186
|
-
} else {
|
|
187
|
-
await page.locator(passSel).first().press('Enter');
|
|
188
|
-
console.log(chalk.dim(` ā Submitted via Enter`));
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const successPath = authConfig.successRedirect || null;
|
|
192
|
-
await page.waitForURL(
|
|
193
|
-
u => successPath
|
|
194
|
-
? u.includes(successPath)
|
|
195
|
-
: !u.includes('/signin') && !u.includes('/login') && !u.includes('/verify'),
|
|
196
|
-
{ timeout: 15000 }
|
|
197
|
-
).catch(() => {});
|
|
198
|
-
|
|
199
|
-
if (page.url().includes('/signin') || page.url().includes('/login')) {
|
|
200
|
-
const errText = await page
|
|
201
|
-
.locator('.flash, .error, .alert, [class*="error"], [role="alert"]')
|
|
202
|
-
.first().textContent().catch(() => null);
|
|
203
|
-
throw new Error(
|
|
204
|
-
`Auth failed ā still on signin.${errText ? ` Page: "${errText.trim()}"` : ' Check credentials.'}`
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
console.log(chalk.dim(` ā Redirected: ${page.url()}`));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// āāā Strategy: cookie āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
212
|
-
async function authViaCookie(context, base, authConfig) {
|
|
213
|
-
const cookies = authConfig.cookies;
|
|
214
|
-
if (!Array.isArray(cookies) || !cookies.length) {
|
|
215
|
-
throw new Error('auth.strategy "cookie" requires auth.cookies array in testConfig');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const domain = new URL(base).hostname;
|
|
219
|
-
const normalized = cookies.map(c => ({
|
|
220
|
-
name: c.name,
|
|
221
|
-
value: c.value,
|
|
222
|
-
domain: c.domain || domain,
|
|
223
|
-
path: c.path || '/',
|
|
224
|
-
httpOnly: c.httpOnly || false,
|
|
225
|
-
secure: c.secure || false,
|
|
226
|
-
sameSite: c.sameSite || 'Lax',
|
|
227
|
-
}));
|
|
228
|
-
|
|
229
|
-
await context.addCookies(normalized);
|
|
230
|
-
console.log(chalk.dim(` ā Cookies injected: ${normalized.map(c => c.name).join(', ')}`));
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// āāā Auth Dispatcher āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
234
|
-
async function authenticate(page, context, base, testUser, authConfig = {}) {
|
|
235
|
-
const strategy = authConfig.strategy || 'request';
|
|
236
|
-
console.log(chalk.dim(` ā Auth strategy: ${strategy}`));
|
|
237
|
-
|
|
238
|
-
switch (strategy) {
|
|
239
|
-
case 'request':
|
|
240
|
-
await authViaRequest(context, base, testUser.email, testUser.password, authConfig);
|
|
241
|
-
break;
|
|
242
|
-
case 'form':
|
|
243
|
-
await authViaForm(page, base, testUser.email, testUser.password, authConfig);
|
|
244
|
-
break;
|
|
245
|
-
case 'cookie':
|
|
246
|
-
await authViaCookie(context, base, authConfig);
|
|
247
|
-
break;
|
|
248
|
-
case 'skip':
|
|
249
|
-
console.log(chalk.dim(` ā Auth skipped`));
|
|
250
|
-
break;
|
|
251
|
-
default:
|
|
252
|
-
throw new Error(`Unknown auth strategy: "${strategy}". Use: request | form | cookie | skip`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// āāā HTTP Test āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
257
|
-
async function runHttpTest(testConfig, testUser, authConfig = {}) {
|
|
258
|
-
const { baseUrl, method = 'GET', endpoint, headers = {}, body } = testConfig;
|
|
259
|
-
if (!baseUrl || !endpoint) throw new Error('testConfig missing baseUrl or endpoint');
|
|
260
|
-
|
|
261
|
-
const url = baseUrl.replace(/\/$/, '') + endpoint;
|
|
262
|
-
const fetch = getFetchFn();
|
|
263
|
-
|
|
264
|
-
// āā Auth ā session cookie lo āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
265
|
-
let sessionCookie = '';
|
|
266
|
-
if (testUser && authConfig.strategy !== 'skip') {
|
|
267
|
-
const signinUrl = baseUrl.replace(/\/$/, '') + (authConfig.signinUrl || '/signin');
|
|
268
|
-
const getRes = await fetch(signinUrl, { redirect: 'follow' });
|
|
269
|
-
const html = await getRes.text();
|
|
270
|
-
const getCookie = getRes.headers.get('set-cookie') || '';
|
|
271
|
-
const csrf = extractCsrf(html, authConfig.csrfSelector);
|
|
272
|
-
|
|
273
|
-
const formBody = new URLSearchParams({ email: testUser.email, password: testUser.password });
|
|
274
|
-
if (csrf) formBody.append(csrf.field, csrf.token);
|
|
275
|
-
|
|
276
|
-
const postRes = await fetch(signinUrl, {
|
|
277
|
-
method: 'POST',
|
|
278
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': getCookie },
|
|
279
|
-
body: formBody.toString(),
|
|
280
|
-
redirect: 'manual',
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
const setCookie = postRes.headers.get('set-cookie');
|
|
284
|
-
if (setCookie) {
|
|
285
|
-
sessionCookie = setCookie.split(',').map(c => c.split(';')[0].trim()).join('; ');
|
|
286
|
-
console.log(chalk.dim(` ā HTTP auth: cookie obtained`));
|
|
287
|
-
} else {
|
|
288
|
-
console.log(chalk.yellow(` ā ļø HTTP auth: no cookie returned`));
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// āā API call āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
293
|
-
let res, responseBody;
|
|
294
|
-
try {
|
|
295
|
-
res = await fetch(url, {
|
|
296
|
-
method,
|
|
297
|
-
headers: {
|
|
298
|
-
'Content-Type': 'application/json',
|
|
299
|
-
...(sessionCookie ? { 'Cookie': sessionCookie } : {}),
|
|
300
|
-
...headers,
|
|
301
|
-
},
|
|
302
|
-
...(body && method !== 'GET' ? { body: JSON.stringify(body) } : {})
|
|
303
|
-
});
|
|
304
|
-
responseBody = await res.json().catch(() => ({}));
|
|
305
|
-
} catch (err) {
|
|
306
|
-
throw new Error(`Could not reach ${url} ā is your server running? (${err.message})`);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return { status: res.status, body: responseBody };
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// āāā Browser Test āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
313
|
-
async function runBrowserTest(testConfig, testUser, boardAuth = {}) {
|
|
314
|
-
let playwright;
|
|
315
|
-
try {
|
|
316
|
-
playwright = require('playwright');
|
|
317
|
-
} catch {
|
|
318
|
-
throw new Error(
|
|
319
|
-
'Playwright not installed.\n Run: npm install -g playwright && npx playwright install chromium'
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const { url, flow = [], expect, auth = {} } = testConfig;
|
|
324
|
-
if (!url) throw new Error('testConfig missing url');
|
|
325
|
-
|
|
326
|
-
const authConfig = { ...boardAuth, ...auth };
|
|
327
|
-
|
|
328
|
-
const base = new URL(url).origin;
|
|
329
|
-
const browser = await playwright.chromium.launch({ headless: true });
|
|
330
|
-
const context = await browser.newContext();
|
|
331
|
-
const page = await context.newPage();
|
|
332
|
-
|
|
333
|
-
try {
|
|
334
|
-
// āā Auth āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
335
|
-
const strategy = authConfig.strategy || (testUser ? 'request' : 'skip');
|
|
336
|
-
if (strategy === 'cookie') {
|
|
337
|
-
await authenticate(page, context, base, testUser, authConfig);
|
|
338
|
-
} else if (testUser && strategy !== 'skip') {
|
|
339
|
-
await authenticate(page, context, base, testUser, authConfig);
|
|
340
|
-
|
|
341
|
-
} else if (!testUser && strategy !== 'skip') {
|
|
342
|
-
console.log(chalk.yellow(` ā ļø No testUser ā auth skipped`));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// āā Navigate to target āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
346
|
-
await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 });
|
|
347
|
-
|
|
348
|
-
if (page.url().includes('/signin') || page.url().includes('/login')) {
|
|
349
|
-
throw new Error('Auth failed ā redirected to signin on target page');
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
console.log(chalk.dim(` ā Target: ${page.url()}`));
|
|
353
|
-
|
|
354
|
-
// āā Flow steps āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
355
|
-
for (const step of flow) {
|
|
356
|
-
switch (step.action) {
|
|
357
|
-
case 'fill': await page.fill(step.selector, step.value || ''); break;
|
|
358
|
-
case 'click': await page.click(step.selector); break;
|
|
359
|
-
case 'wait': await page.waitForTimeout(step.ms || 1000); break;
|
|
360
|
-
case 'waitForElement':await page.waitForSelector(step.selector, { timeout: 8000 }); break;
|
|
361
|
-
case 'select': await page.selectOption(step.selector, step.value); break;
|
|
362
|
-
case 'screenshot': await page.screenshot({ path: step.path || '/tmp/tnc.png' }); break;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// āā Capture result āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
367
|
-
const currentUrl = page.url();
|
|
368
|
-
const bodyText = await page.textContent('body').catch(() => '');
|
|
369
|
-
|
|
370
|
-
let elementExists = false;
|
|
371
|
-
let elementText = '';
|
|
372
|
-
if (expect?.element) {
|
|
373
|
-
try {
|
|
374
|
-
await page.waitForSelector(expect.element, { timeout: 5000 });
|
|
375
|
-
elementExists = true;
|
|
376
|
-
elementText = await page.textContent(expect.element).catch(() => '');
|
|
377
|
-
} catch { elementExists = false; }
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return { url: currentUrl, elementExists, elementText, bodyText };
|
|
381
|
-
} finally {
|
|
382
|
-
await browser.close();
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Helper to run a local command and capture exitCode & stdout/stderr
|
|
387
|
-
function runLocalCommand(command) {
|
|
388
|
-
return new Promise((resolve) => {
|
|
389
|
-
cp.exec(command, (error, stdout, stderr) => {
|
|
390
|
-
const exitCode = error ? (error.code || 1) : 0;
|
|
391
|
-
resolve({
|
|
392
|
-
exitCode,
|
|
393
|
-
stdout: stdout.toString(),
|
|
394
|
-
stderr: stderr.toString()
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// āāā Local Judge āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
401
|
-
function judgeLocally(testConfig, result) {
|
|
402
|
-
const { type, value, condition, expect } = testConfig;
|
|
403
|
-
const failures = [];
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
switch (type) {
|
|
407
|
-
case 'exitCode': {
|
|
408
|
-
const passed = result.exitCode === value;
|
|
409
|
-
return {
|
|
410
|
-
passed,
|
|
411
|
-
reason: passed
|
|
412
|
-
? `Exit code matched: ${value}`
|
|
413
|
-
: `Expected exit code ${value}, got ${result.exitCode}`
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
case 'stdout': {
|
|
418
|
-
const actualOut = (result.stdout || '').trim();
|
|
419
|
-
const expectedOut = (value || '').trim();
|
|
420
|
-
const passed = actualOut === expectedOut;
|
|
421
|
-
return {
|
|
422
|
-
passed,
|
|
423
|
-
reason: passed ? `Output matched` : `Output mismatch`,
|
|
424
|
-
diff: passed ? undefined : { expected: expectedOut, actual: actualOut }
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
case 'regex': {
|
|
429
|
-
const pattern = new RegExp(value);
|
|
430
|
-
const target = result.stdout || result.output || '';
|
|
431
|
-
const passed = pattern.test(target);
|
|
432
|
-
return {
|
|
433
|
-
passed,
|
|
434
|
-
reason: passed ? `Regex matched: ${value}` : `Regex did not match: ${value}`
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
case 'http': {
|
|
439
|
-
if (expect?.status !== undefined && result.status !== expect.status) {
|
|
440
|
-
failures.push(`Status: expected ${expect.status}, got ${result.status}`);
|
|
441
|
-
}
|
|
442
|
-
if (expect?.body) {
|
|
443
|
-
for (const [key, condition] of Object.entries(expect.body)) {
|
|
444
|
-
const actualVal = getNestedValue(result.body || {}, key);
|
|
445
|
-
if (condition?.exists) {
|
|
446
|
-
if (actualVal === undefined || actualVal === null)
|
|
447
|
-
failures.push(`body.${key}: expected to exist`);
|
|
448
|
-
} else if (condition?.eq !== undefined) {
|
|
449
|
-
if (actualVal !== condition.eq)
|
|
450
|
-
failures.push(`body.${key}: expected "${condition.eq}", got "${actualVal}"`);
|
|
451
|
-
} else if (condition?.contains !== undefined) {
|
|
452
|
-
if (!String(actualVal).includes(String(condition.contains)))
|
|
453
|
-
failures.push(`body.${key}: expected to contain "${condition.contains}"`);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
case 'json': {
|
|
461
|
-
const actualData = result.output || result.json || {};
|
|
462
|
-
if (condition) {
|
|
463
|
-
return judgeWithConditions(condition, actualData);
|
|
464
|
-
} else {
|
|
465
|
-
const passed = JSON.stringify(value) === JSON.stringify(actualData);
|
|
466
|
-
return {
|
|
467
|
-
passed,
|
|
468
|
-
reason: passed ? `JSON matched` : `JSON mismatch`,
|
|
469
|
-
diff: passed ? undefined : { expected: value, actual: actualData }
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
case 'loadtest':
|
|
474
|
-
case 'geekload': {
|
|
475
|
-
const actualData = result.output || result.json || {};
|
|
476
|
-
const cond = condition || expect?.condition || expect || {};
|
|
477
|
-
return judgeWithConditions(cond, actualData);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
case 'browser': {
|
|
481
|
-
if (expect?.element && !result.elementExists) {
|
|
482
|
-
failures.push(`Element "${expect.element}" not found on page`);
|
|
483
|
-
}
|
|
484
|
-
if (expect?.text && result.elementText) {
|
|
485
|
-
if (!result.elementText.includes(expect.text))
|
|
486
|
-
failures.push(`Element text: expected to contain "${expect.text}", got "${result.elementText}"`);
|
|
487
|
-
}
|
|
488
|
-
if (expect?.redirectUrl) {
|
|
489
|
-
try {
|
|
490
|
-
const actualPath = new URL(result.url).pathname;
|
|
491
|
-
if (!actualPath.includes(expect.redirectUrl))
|
|
492
|
-
failures.push(`URL: expected "${expect.redirectUrl}", got "${actualPath}"`);
|
|
493
|
-
} catch {
|
|
494
|
-
failures.push(`Invalid URL in result: "${result.url}"`);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
break;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
case 'tnc-build': {
|
|
501
|
-
const passed = result.exitCode === 0;
|
|
502
|
-
return {
|
|
503
|
-
passed,
|
|
504
|
-
reason: passed ? 'TNC Local CI pipeline passed' : 'TNC Local CI pipeline failed'
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
default:
|
|
509
|
-
return { passed: false, reason: `Unknown test type: ${type}` };
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
return {
|
|
513
|
-
passed: failures.length === 0,
|
|
514
|
-
reason: failures.length === 0 ? 'All checks passed' : failures.join(' | '),
|
|
515
|
-
failures,
|
|
516
|
-
};
|
|
517
|
-
} catch (err) {
|
|
518
|
-
return { passed: false, reason: `Judge error: ${err.message}` };
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function judgeWithConditions(conditions, actual) {
|
|
523
|
-
const failures = [];
|
|
524
|
-
|
|
525
|
-
for (const [key, ops] of Object.entries(conditions)) {
|
|
526
|
-
const actualVal = actual[key];
|
|
527
|
-
|
|
528
|
-
if (actualVal === undefined) {
|
|
529
|
-
failures.push(`Missing key: "${key}"`);
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
for (const [op, expected] of Object.entries(ops)) {
|
|
534
|
-
let pass = false;
|
|
535
|
-
switch (op) {
|
|
536
|
-
case 'gte': pass = actualVal >= expected; break;
|
|
537
|
-
case 'lte': pass = actualVal <= expected; break;
|
|
538
|
-
case 'gt': pass = actualVal > expected; break;
|
|
539
|
-
case 'lt': pass = actualVal < expected; break;
|
|
540
|
-
case 'eq': pass = actualVal === expected; break;
|
|
541
|
-
case 'contains': pass = String(actualVal).includes(String(expected)); break;
|
|
542
|
-
case 'regex': pass = new RegExp(expected).test(String(actualVal)); break;
|
|
543
|
-
default: pass = false;
|
|
544
|
-
}
|
|
545
|
-
if (!pass) {
|
|
546
|
-
failures.push(`"${key}" failed: ${actualVal} ${op} ${expected}`);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
return {
|
|
552
|
-
passed: failures.length === 0,
|
|
553
|
-
reason: failures.length === 0
|
|
554
|
-
? `All conditions passed`
|
|
555
|
-
: `Failed conditions: ${failures.join('; ')}`,
|
|
556
|
-
diff: failures.length > 0 ? { conditions, actual } : undefined
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function getNestedValue(obj, key) {
|
|
561
|
-
return key.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// āāā Webhook āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
565
|
-
async function hitWebhook(apiUrl, taskId, webhookSecret, payload) {
|
|
566
|
-
const body = JSON.stringify(payload);
|
|
567
|
-
const signature = 'sha256=' + crypto.createHmac('sha256', webhookSecret).update(body).digest('hex');
|
|
568
|
-
const fetch = getFetchFn();
|
|
569
|
-
|
|
570
|
-
const res = await fetch(`${apiUrl}/webhooks/tasks/${taskId}/judge`, {
|
|
571
|
-
method: 'POST',
|
|
572
|
-
headers: { 'Content-Type': 'application/json', 'x-tnc-signature': signature },
|
|
573
|
-
body,
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
if (!res.ok) {
|
|
577
|
-
const err = await res.json().catch(() => ({}));
|
|
578
|
-
throw new Error(err.error || `Webhook failed: ${res.status}`);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
return res.json();
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// āāā Command āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
585
|
-
module.exports = {
|
|
586
|
-
name: 'task-submit',
|
|
587
|
-
description: 'Auto-test and submit a task for verification',
|
|
588
|
-
aliases: ['submit'],
|
|
589
|
-
requiresAuth: true,
|
|
590
|
-
|
|
591
|
-
async execute(args, shell) {
|
|
592
|
-
if (!args[0]) {
|
|
593
|
-
console.log(chalk.red('Usage: task-submit <task-id>'));
|
|
594
|
-
console.log(chalk.dim(' Alias: submit <task-id>'));
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const taskId = args[0];
|
|
599
|
-
const apiUrl = shell.api.apiUrl || 'https://thinkncollab.com';
|
|
600
|
-
|
|
601
|
-
// āā Fetch task āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
602
|
-
let task;
|
|
603
|
-
try {
|
|
604
|
-
task = await shell.api._request('GET', `/thinknsh/${taskId}/cli`);
|
|
605
|
-
} catch (err) {
|
|
606
|
-
if (err.message?.includes('404')) console.log(chalk.red('ā Task not found'));
|
|
607
|
-
else if (err.message?.includes('403')) console.log(chalk.red('ā Access denied'));
|
|
608
|
-
else console.log(chalk.red(`ā ${err.message}`));
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (!task.testConfig) {
|
|
613
|
-
console.log(chalk.yellow('ā ļø This task has no test config defined.'));
|
|
614
|
-
console.log(chalk.dim(' complete <task-id> to mark manually\n'));
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const { title, testConfig, webhookSecret } = task;
|
|
619
|
-
|
|
620
|
-
const testUser = (task.testuserEmail && task.testuserPassword)
|
|
621
|
-
? { email: task.testuserEmail, password: task.testuserPassword }
|
|
622
|
-
: null;
|
|
623
|
-
|
|
624
|
-
const boardAuth = {
|
|
625
|
-
strategy: task.testauthStrategy || 'request',
|
|
626
|
-
signinUrl: task.testauthSigninUrl || '/signin',
|
|
627
|
-
csrfSelector: task.testauthCsrf || null,
|
|
628
|
-
};
|
|
629
|
-
|
|
630
|
-
// āā Print summary āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
631
|
-
console.log(chalk.cyan(`\n āā Submitting: "${title}" āāāāāāāāāāāāāāāāāāāāāā`));
|
|
632
|
-
console.log(chalk.dim(` Type : ${testConfig.type}`));
|
|
633
|
-
const authConfig = { ...boardAuth, ...(testConfig.auth || {}) };
|
|
634
|
-
if (testConfig.type === 'http') {
|
|
635
|
-
console.log(chalk.dim(` Target : ${testConfig.method || 'GET'} ${testConfig.baseUrl}${testConfig.endpoint}`));
|
|
636
|
-
} else if (testConfig.type === 'browser') {
|
|
637
|
-
const strategy = testConfig.auth?.strategy || (testUser ? 'request' : 'skip');
|
|
638
|
-
console.log(chalk.dim(` Target : ${testConfig.url}`));
|
|
639
|
-
console.log(chalk.dim(` Auth : ${strategy}`));
|
|
640
|
-
if (testUser) console.log(chalk.dim(` TestUser : ${testUser.email}`));
|
|
641
|
-
else console.log(chalk.yellow(` ā ļø No testUser ā auth will be skipped`));
|
|
642
|
-
}
|
|
643
|
-
console.log(chalk.dim(` Running virtual user...\n`));
|
|
644
|
-
|
|
645
|
-
// āā Run test āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
646
|
-
let result;
|
|
647
|
-
try {
|
|
648
|
-
if (testConfig.type === 'http') {
|
|
649
|
-
result = await runHttpTest(testConfig, testUser, authConfig);
|
|
650
|
-
console.log(chalk.dim(` ā Status : ${result.status}`));
|
|
651
|
-
console.log(chalk.dim(` ā Body : ${JSON.stringify(result.body).slice(0, 80)}`));
|
|
652
|
-
|
|
653
|
-
} else if (testConfig.type === 'browser') {
|
|
654
|
-
console.log(chalk.dim(` ā Launching headless browser...`));
|
|
655
|
-
result = await runBrowserTest(testConfig, testUser, authConfig);
|
|
656
|
-
console.log(chalk.dim(` ā Page : ${result.url}`));
|
|
657
|
-
console.log(chalk.dim(` ā Element: ${result.elementExists ? 'found ā' : 'not found ā'}`));
|
|
658
|
-
if (result.elementText)
|
|
659
|
-
console.log(chalk.dim(` ā Text : "${result.elementText.trim().slice(0, 60)}"`));
|
|
660
|
-
|
|
661
|
-
} else if (testConfig.type === 'exitCode' || testConfig.type === 'stdout' || testConfig.type === 'regex') {
|
|
662
|
-
const cmd = testConfig.command || testConfig.script;
|
|
663
|
-
if (!cmd) {
|
|
664
|
-
throw new Error(`testConfig type "${testConfig.type}" requires a "command" or "script" to execute.`);
|
|
665
|
-
}
|
|
666
|
-
console.log(chalk.dim(` ā Executing local command: "${cmd}"`));
|
|
667
|
-
const runRes = await runLocalCommand(cmd);
|
|
668
|
-
result = {
|
|
669
|
-
exitCode: runRes.exitCode,
|
|
670
|
-
stdout: runRes.stdout,
|
|
671
|
-
stderr: runRes.stderr
|
|
672
|
-
};
|
|
673
|
-
console.log(chalk.dim(` ā Exit Code: ${result.exitCode}`));
|
|
674
|
-
if (result.stdout) console.log(chalk.dim(` ā Stdout : ${result.stdout.trim().slice(0, 80)}`));
|
|
675
|
-
|
|
676
|
-
} else if (testConfig.type === 'json') {
|
|
677
|
-
const cmd = testConfig.command || testConfig.script;
|
|
678
|
-
if (cmd) {
|
|
679
|
-
console.log(chalk.dim(` ā Executing local command: "${cmd}"`));
|
|
680
|
-
const runRes = await runLocalCommand(cmd);
|
|
681
|
-
let parsedJson = {};
|
|
682
|
-
try {
|
|
683
|
-
parsedJson = JSON.parse(runRes.stdout.trim());
|
|
684
|
-
} catch (e) {
|
|
685
|
-
throw new Error(`Command executed but stdout was not valid JSON: ${runRes.stdout}`);
|
|
686
|
-
}
|
|
687
|
-
result = {
|
|
688
|
-
exitCode: runRes.exitCode,
|
|
689
|
-
stdout: runRes.stdout,
|
|
690
|
-
stderr: runRes.stderr,
|
|
691
|
-
json: parsedJson,
|
|
692
|
-
output: parsedJson
|
|
693
|
-
};
|
|
694
|
-
} else {
|
|
695
|
-
throw new Error('testConfig type "json" requires a "command" or "script" to output JSON.');
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
} else if (testConfig.type === 'loadtest' || testConfig.type === 'geekload') {
|
|
699
|
-
const script = testConfig.script || testConfig.command;
|
|
700
|
-
if (!script) {
|
|
701
|
-
throw new Error(`testConfig type "${testConfig.type}" requires a "script" or "command" specifying the load test script path.`);
|
|
702
|
-
}
|
|
703
|
-
const scriptPath = path.resolve(process.cwd(), script);
|
|
704
|
-
console.log(chalk.dim(` ā Running GeekLoad test script: ${script}`));
|
|
705
|
-
const { runLoadTest } = require('../../core/geekload');
|
|
706
|
-
const loadtestResult = await runLoadTest(scriptPath);
|
|
707
|
-
|
|
708
|
-
const Table = require('cli-table3');
|
|
709
|
-
const table = new Table({
|
|
710
|
-
head: [chalk.cyan('Metric'), chalk.cyan('Value')],
|
|
711
|
-
colWidths: [25, 20]
|
|
712
|
-
});
|
|
713
|
-
table.push(
|
|
714
|
-
['Virtual Users', loadtestResult.virtual_users],
|
|
715
|
-
['Duration (sec)', loadtestResult.duration_sec],
|
|
716
|
-
['Total Requests', loadtestResult.total_requests],
|
|
717
|
-
['Failed Requests', loadtestResult.failed_requests],
|
|
718
|
-
['Failure Rate', `${(loadtestResult.failure_rate * 100).toFixed(2)}%`],
|
|
719
|
-
['p95 Latency (ms)', loadtestResult.p95_latency_ms]
|
|
720
|
-
);
|
|
721
|
-
console.log('\n' + table.toString() + '\n');
|
|
722
|
-
|
|
723
|
-
result = {
|
|
724
|
-
exitCode: loadtestResult.failed_requests > 0 ? 1 : 0,
|
|
725
|
-
output: loadtestResult,
|
|
726
|
-
json: loadtestResult
|
|
727
|
-
};
|
|
728
|
-
|
|
729
|
-
} else if (testConfig.type === 'tnc-build') {
|
|
730
|
-
const fs = require('fs');
|
|
731
|
-
const path = require('path');
|
|
732
|
-
const { spawn } = require('child_process');
|
|
733
|
-
|
|
734
|
-
const projectRoot = process.cwd();
|
|
735
|
-
const workflowsDir = path.join(projectRoot, '.tnc/workflows');
|
|
736
|
-
|
|
737
|
-
if (!fs.existsSync(workflowsDir)) {
|
|
738
|
-
throw new Error('No .tnc/workflows directory found in project root.');
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const files = fs.readdirSync(workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
742
|
-
if (files.length === 0) {
|
|
743
|
-
throw new Error('No workflow files found in .tnc/workflows/');
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
const workflowFile = files[0];
|
|
747
|
-
console.log(chalk.dim(` ā Found workflow file: ${workflowFile}`));
|
|
748
|
-
const content = fs.readFileSync(path.join(workflowsDir, workflowFile), 'utf8');
|
|
749
|
-
|
|
750
|
-
// Parse steps
|
|
751
|
-
const lines = content.split('\n');
|
|
752
|
-
let workflowName = workflowFile;
|
|
753
|
-
let steps = [];
|
|
754
|
-
let currentStepName = '';
|
|
755
|
-
|
|
756
|
-
for (let i = 0; i < lines.length; i++) {
|
|
757
|
-
const line = lines[i].trim();
|
|
758
|
-
if (line.startsWith('name:')) {
|
|
759
|
-
workflowName = line.substring(5).trim().replace(/['"]/g, '');
|
|
760
|
-
continue;
|
|
761
|
-
}
|
|
762
|
-
if (line.startsWith('-')) {
|
|
763
|
-
const cleaned = line.substring(1).trim();
|
|
764
|
-
if (cleaned.startsWith('name:')) {
|
|
765
|
-
currentStepName = cleaned.substring(5).trim().replace(/['"]/g, '');
|
|
766
|
-
} else if (cleaned.startsWith('run:')) {
|
|
767
|
-
let runCmd = cleaned.substring(4).trim();
|
|
768
|
-
if ((runCmd.startsWith('"') && runCmd.endsWith('"')) || (runCmd.startsWith("'") && runCmd.endsWith("'"))) {
|
|
769
|
-
runCmd = runCmd.substring(1, runCmd.length - 1);
|
|
770
|
-
}
|
|
771
|
-
if (runCmd === '|') {
|
|
772
|
-
let block = [];
|
|
773
|
-
let j = i + 1;
|
|
774
|
-
while (j < lines.length) {
|
|
775
|
-
const next = lines[j];
|
|
776
|
-
const match = next.match(/^(\s+)(.*)/);
|
|
777
|
-
if (match && match[1].length > 4) {
|
|
778
|
-
block.push(next.trim());
|
|
779
|
-
j++;
|
|
780
|
-
} else {
|
|
781
|
-
break;
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
runCmd = block.join('\n');
|
|
785
|
-
i = j - 1;
|
|
786
|
-
}
|
|
787
|
-
steps.push({
|
|
788
|
-
name: currentStepName || `run: ${runCmd.substring(0, 30)}`,
|
|
789
|
-
run: runCmd
|
|
790
|
-
});
|
|
791
|
-
currentStepName = '';
|
|
792
|
-
}
|
|
793
|
-
} else if (line.startsWith('name:')) {
|
|
794
|
-
currentStepName = line.substring(5).trim().replace(/['"]/g, '');
|
|
795
|
-
} else if (line.startsWith('run:')) {
|
|
796
|
-
let runCmd = line.substring(4).trim();
|
|
797
|
-
if ((runCmd.startsWith('"') && runCmd.endsWith('"')) || (runCmd.startsWith("'") && runCmd.endsWith("'"))) {
|
|
798
|
-
runCmd = runCmd.substring(1, runCmd.length - 1);
|
|
799
|
-
}
|
|
800
|
-
if (runCmd === '|') {
|
|
801
|
-
let block = [];
|
|
802
|
-
let j = i + 1;
|
|
803
|
-
while (j < lines.length) {
|
|
804
|
-
const next = lines[j];
|
|
805
|
-
const match = next.match(/^(\s+)(.*)/);
|
|
806
|
-
if (match && match[1].length > 4) {
|
|
807
|
-
block.push(next.trim());
|
|
808
|
-
j++;
|
|
809
|
-
} else {
|
|
810
|
-
break;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
runCmd = block.join('\n');
|
|
814
|
-
i = j - 1;
|
|
815
|
-
}
|
|
816
|
-
steps.push({
|
|
817
|
-
name: currentStepName || `run: ${runCmd.substring(0, 30)}`,
|
|
818
|
-
run: runCmd
|
|
819
|
-
});
|
|
820
|
-
currentStepName = '';
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
if (steps.length === 0) {
|
|
825
|
-
throw new Error('No steps found in the workflow file.');
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
console.log(chalk.cyan(`\nš [TNC Actions] Starting Local Workflow: "${workflowName}"\n`));
|
|
829
|
-
|
|
830
|
-
const stepResults = [];
|
|
831
|
-
let overallPassed = true;
|
|
832
|
-
let logsBuffer = '';
|
|
833
|
-
|
|
834
|
-
for (let s = 0; s < steps.length; s++) {
|
|
835
|
-
const step = steps[s];
|
|
836
|
-
console.log(chalk.cyan(`āļø [Step ${s+1}/${steps.length}] "${step.name}"`));
|
|
837
|
-
console.log(chalk.gray(`$ ${step.run}`));
|
|
838
|
-
|
|
839
|
-
// Run command shell execution
|
|
840
|
-
const exitCode = await new Promise((resolve) => {
|
|
841
|
-
const shellCmd = process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
|
|
842
|
-
const shellArgs = process.platform === 'win32' ? ['/s', '/c', step.run] : ['-c', step.run];
|
|
843
|
-
|
|
844
|
-
const proc = spawn(shellCmd, shellArgs, {
|
|
845
|
-
cwd: projectRoot,
|
|
846
|
-
env: { ...process.env, PATH: process.env.PATH, PUPPETEER_SKIP_DOWNLOAD: 'true' }
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
proc.stdout.on('data', (chunk) => {
|
|
850
|
-
const chunkStr = chunk.toString();
|
|
851
|
-
process.stdout.write(chunkStr);
|
|
852
|
-
logsBuffer += chunkStr;
|
|
853
|
-
});
|
|
854
|
-
|
|
855
|
-
proc.stderr.on('data', (chunk) => {
|
|
856
|
-
const chunkStr = chunk.toString();
|
|
857
|
-
process.stderr.write(chalk.red(chunkStr));
|
|
858
|
-
logsBuffer += chunkStr;
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
proc.on('close', (code) => {
|
|
862
|
-
resolve(code);
|
|
863
|
-
});
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
const stepPassed = (exitCode === 0);
|
|
867
|
-
stepResults.push({
|
|
868
|
-
name: step.name,
|
|
869
|
-
command: step.run,
|
|
870
|
-
exitCode,
|
|
871
|
-
passed: stepPassed
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
if (!stepPassed) {
|
|
875
|
-
overallPassed = false;
|
|
876
|
-
console.log(chalk.red(`\nā Step "${step.name}" failed with exit code: ${exitCode}. Aborting.\n`));
|
|
877
|
-
break;
|
|
878
|
-
} else {
|
|
879
|
-
console.log(chalk.green(`\nā
Step "${step.name}" passed successfully.\n`));
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
result = {
|
|
884
|
-
exitCode: overallPassed ? 0 : 1,
|
|
885
|
-
stdout: logsBuffer,
|
|
886
|
-
stderr: '',
|
|
887
|
-
output: {
|
|
888
|
-
workflow: workflowName,
|
|
889
|
-
status: overallPassed ? 'passed' : 'failed',
|
|
890
|
-
stepsCount: steps.length,
|
|
891
|
-
stepsRun: stepResults
|
|
892
|
-
},
|
|
893
|
-
json: {
|
|
894
|
-
workflow: workflowName,
|
|
895
|
-
status: overallPassed ? 'passed' : 'failed',
|
|
896
|
-
stepsCount: steps.length,
|
|
897
|
-
stepsRun: stepResults
|
|
898
|
-
}
|
|
899
|
-
};
|
|
900
|
-
|
|
901
|
-
} else {
|
|
902
|
-
console.log(chalk.red(`ā Unsupported test type: ${testConfig.type}`));
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
} catch (err) {
|
|
906
|
-
console.log(chalk.red(`\n ā Virtual user failed: ${err.message}\n`));
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// āā Judge āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
911
|
-
const verdict = judgeLocally(testConfig, result);
|
|
912
|
-
const border = verdict.passed ? chalk.green('ā'.repeat(50)) : chalk.red('ā'.repeat(50));
|
|
913
|
-
const badge = verdict.passed ? chalk.bgGreen.black(' PASS ') : chalk.bgRed.white(' FAIL ');
|
|
914
|
-
|
|
915
|
-
console.log('\n' + border);
|
|
916
|
-
console.log(` ${badge} ${chalk.bold(title)}`);
|
|
917
|
-
console.log(` ${chalk.dim('Result :')} ${verdict.passed ? chalk.green(verdict.reason) : chalk.red(verdict.reason)}`);
|
|
918
|
-
console.log(border);
|
|
919
|
-
|
|
920
|
-
// āā Webhook āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
921
|
-
try {
|
|
922
|
-
console.log(chalk.dim('\n Syncing with ThinkNCollab...'));
|
|
923
|
-
await hitWebhook(apiUrl, taskId, webhookSecret, result);
|
|
924
|
-
console.log(chalk.green(' ā Verdict saved ā task updated\n'));
|
|
925
|
-
} catch (err) {
|
|
926
|
-
console.log(chalk.yellow(` ā ļø Webhook sync failed: ${err.message}`));
|
|
927
|
-
console.log(chalk.dim(' Local result shown above is accurate.\n'));
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// āā Notification āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
931
|
-
shell.pushNotification({
|
|
932
|
-
type: 'verdict',
|
|
933
|
-
passed: verdict.passed,
|
|
934
|
-
title,
|
|
935
|
-
reason: verdict.reason,
|
|
936
|
-
taskId,
|
|
937
|
-
at: new Date().toISOString(),
|
|
938
|
-
});
|
|
939
|
-
},
|
|
940
|
-
};
|