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
182 lines
7.3 KiB
TypeScript
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 || ''));
|
|
}
|