Files
NetMesh/domain/snippetScript.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

182 lines
7.3 KiB
TypeScript

import type { Snippet, SnippetKind } from './models';
export function getSnippetKind(snippet: Pick<Snippet, 'kind'>): SnippetKind {
return snippet.kind === 'script' ? 'script' : 'snippet';
}
export function isScriptSnippet(snippet: Pick<Snippet, 'kind'>): boolean {
return getSnippetKind(snippet) === 'script';
}
/** Common interactive shell prompts (root uses #, regular user uses $). */
export const DEFAULT_SHELL_PROMPT_PATTERNS = ['# ', '$ ', '~# ', '~$ ', '% '];
/** Default wait after each recorded command — matches waitForPrompt in generated scripts. */
export const DEFAULT_RECORDING_PROMPT_TIMEOUT_MS = 30000;
/** Regex matching a shell prompt on the last line (~# / ~$ / user@host:path#). */
export const SHELL_PROMPT_END_REGEX = /(?:~[#$]\s*|[@][^\n]{0,120}[:][^\n]{0,120}[#$%]\s*)$/m;
/** Minimal smoke script for verifying manual run and onOutput triggers. */
export const SCRIPT_SMOKE_TEST = `// === Smoke test ===
// Manual: trigger=manual, pick a target host, click "Run now"
// onOutput: trigger=onOutput, pattern=NETCATTY_SMOKE, save, connect to target host, then run: echo NETCATTY_SMOKE
//
await nct.screen.waitForPrompt(30000);
await nct.screen.sendLine('echo netcatty-smoke-ok');
nct.log('Smoke test passed');
await nct.dialog.alert('Netcatty script smoke test OK');
`;
/** Full integration test for onConnect / manual run; dialog API enabled by default. */
export const SCRIPT_INTEGRATION_TEST = `// Netcatty Integration Test — onConnect / manual full API exercise
// Trigger: onConnect or Run now | Permission: Auto or Confirm (dialogs need non-Observer)
const CONFIG = {
SAMPLE_COUNT: 8,
SAMPLE_INTERVAL_MS: 2000,
PROMPT_TIMEOUT_MS: 60000,
STEP_TIMEOUT_MS: 20000,
RUN_DIALOGS: true,
RUN_SESSION_LOG: false,
RUN_SCREEN_CLEAR: false,
};
async function main() {
const tag = \`nc-it-\${Date.now().toString(36)}\`;
nct.log('=== Netcatty Integration Test START ===');
nct.log(\`tag=\${tag} nct.version=\${nct.version}\`);
nct.log(\`session: name=\${nct.session.name} host=\${nct.session.hostname} user=\${nct.session.username} connected=\${nct.session.connected}\`);
if (!nct.session.connected) {
throw new Error('Session not connected');
}
nct.log('[1/12] waitForPrompt');
await nct.screen.waitForPrompt(CONFIG.PROMPT_TIMEOUT_MS);
nct.log('[2/12] sendLine + waitForText');
await nct.screen.sendLine(\`echo "\${tag}_BOOTSTRAP_OK"\`);
await nct.screen.waitForText(\`\${tag}_BOOTSTRAP_OK\`, CONFIG.STEP_TIMEOUT_MS);
nct.log('[3/12] waitForAny');
await nct.screen.sendLine('echo ANY_CHECK && uname -s');
await nct.screen.waitForAny(['ANY_CHECK', /Linux/], CONFIG.STEP_TIMEOUT_MS);
nct.log('[4/12] getText / rows / cols / currentRow');
const text = await nct.screen.getText();
const lineCount = text.split('\\n').filter(Boolean).length;
nct.log(\`captured \${lineCount} lines, rows=\${nct.screen.rows} cols=\${nct.screen.cols} cursorRow=\${nct.screen.currentRow}\`);
nct.log('[5/12] send raw + Enter');
await nct.screen.send(\`echo -n "\${tag}_RAW"\`);
await nct.screen.sendLine('');
await nct.screen.waitForText(\`\${tag}_RAW\`, CONFIG.STEP_TIMEOUT_MS);
nct.log('[6/12] waitForRegex');
await nct.screen.sendLine(\`echo BUILD_ID=\${tag}\`);
await nct.screen.waitForRegex(new RegExp(\`BUILD_ID=\${tag}\`), CONFIG.STEP_TIMEOUT_MS);
nct.log('[7/12] session.sleep / nct.sleep');
await nct.session.sleep(800);
await nct.sleep(800);
nct.log('[8/12] progress loop health sampling');
nct.progress.start('Health sampling', CONFIG.SAMPLE_COUNT);
for (let i = 1; i <= CONFIG.SAMPLE_COUNT; i += 1) {
const cmd = [
\`echo "== Sample \${i}/\${CONFIG.SAMPLE_COUNT} =="\`,
'date',
'uptime',
"free -h | awk 'NR==2{print $1,$2,$3,$4,$7}'",
"df -h / | awk 'NR==2{print $1,$2,$3,$5,$6}'",
\`echo "\${tag}_SAMPLE_\${i}_DONE"\`,
].join(' && ');
await nct.screen.sendLine(cmd);
await nct.screen.waitForRegex(new RegExp(\`\${tag}_SAMPLE_\${i}_DONE\`), CONFIG.STEP_TIMEOUT_MS);
await nct.screen.waitForPrompt(CONFIG.PROMPT_TIMEOUT_MS);
nct.progress.step(\`sample \${i}/\${CONFIG.SAMPLE_COUNT}\`);
nct.log(\`sample \${i}/\${CONFIG.SAMPLE_COUNT} ok\`);
if (i < CONFIG.SAMPLE_COUNT) {
await nct.session.sleep(CONFIG.SAMPLE_INTERVAL_MS);
}
}
nct.progress.done();
nct.log('[9/12] nested loop activity mode');
const checks = ['whoami', 'id -u', 'pwd', 'echo $SHELL'];
for (const check of checks) {
await nct.screen.sendLine(check);
await nct.screen.waitForPrompt(CONFIG.PROMPT_TIMEOUT_MS);
}
if (CONFIG.RUN_SESSION_LOG) {
nct.log('[10/12] startLog / stopLog');
await nct.session.startLog(\`./netcatty-it-\${tag}.log\`);
await nct.screen.sendLine(\`echo "\${tag}_LOGGED"\`);
await nct.screen.waitForText(\`\${tag}_LOGGED\`, CONFIG.STEP_TIMEOUT_MS);
await nct.session.stopLog();
} else {
nct.log('[10/12] startLog/stopLog skipped');
}
if (CONFIG.RUN_SCREEN_CLEAR) {
nct.log('[11/12] screen.clear');
await nct.screen.clear();
await nct.session.sleep(500);
await nct.screen.waitForPrompt(CONFIG.PROMPT_TIMEOUT_MS);
} else {
nct.log('[11/12] screen.clear skipped');
}
if (CONFIG.RUN_DIALOGS) {
nct.log('[12/12] dialog confirm / prompt / alert');
const go = await nct.dialog.confirm(\`Integration test \${tag} finished OK. Continue to prompt/alert?\`);
nct.log(\`confirm => \${go}\`);
if (go) {
const note = await nct.dialog.prompt('Optional note:', 'all-good');
nct.log(\`prompt => \${note}\`);
await nct.dialog.alert(\`Done. note=\${note}\`);
} else {
nct.log('confirm declined — skipping prompt/alert');
}
} else {
nct.log('[12/12] dialog skipped');
}
await nct.screen.sendLine(\`echo "=== \${tag} ALL_PASSED ==="\`);
await nct.screen.waitForText(\`\${tag} ALL_PASSED\`, CONFIG.STEP_TIMEOUT_MS);
nct.log('=== Netcatty Integration Test PASSED ===');
}
await main();
`;
export const DEFAULT_SCRIPT_TEMPLATE = `// Netcatty automation script - async JS in the active terminal session
//
// nct.screen.waitForPrompt(ms?) wait for shell prompt (# root / $ user)
// nct.screen.waitForText(text, ms?) wait for exact output text
// nct.screen.waitForRegex(pattern, ms?) wait for regex output, including multiline
// nct.screen.waitForAny([patterns], ms?) wait until any pattern matches
// nct.screen.sendLine(cmd) type command + Enter; send(text) raw keys only
// nct.screen.getText(start?, end?) | clear()
// nct.session.connected | name | hostname | username (read-only metadata)
// nct.session.sleep(ms) | startLog(path) | stopLog() | disconnect()
// nct.dialog.confirm(msg)->bool | prompt(msg, def?)->string | alert(msg)
// nct.dialog.form({ fields })->object; fields: select/radio/checkbox/textarea/number, optional visibleWhen
// nct.dialog.select/radio/checkbox are convenience helpers
// nct.progress.start(label, total) opt-in determinate progress for loops
// nct.progress.step(detail?) | set(n, detail?) | done()
// nct.log(msg) run log panel. Type "nct." in editor for autocomplete snippets.
//
await nct.screen.waitForPrompt(30000);
await nct.screen.sendLine('echo hello');
nct.log('Done');
`;
const SCRIPT_WRITE_PATTERN = /nct\.screen\.(send(?:Line)?|clear)\s*\(|nct\.session\.(disconnect|startLog)\s*\(/;
export function scriptContainsWriteOperations(content: string): boolean {
return SCRIPT_WRITE_PATTERN.test(String(content || ''));
}