feat(cnode): add standalone CLI for Docker server management

- Reorganized server management code:
  - Moved server_cli.py -> deploy/docker/cnode_cli.py
  - Moved server_manager.py -> deploy/docker/server_manager.py

- Created fast Python-based installation (0.1s startup):
  - deploy/installer/cnode_pkg/ - Standalone package
  - deploy/installer/install-cnode.sh - Local installer
  - deploy/installer/deploy.sh - Remote installer for users

- Added backward compatibility:
  - crawl4ai/cli.py: 'crwl server' redirects to 'cnode'
  - Updated tests to match new CLI structure (12/12 passing)

- Automated sync workflow:
  - .githooks/pre-commit - Auto-syncs source to package
  - setup-hooks.sh - One-time setup for contributors
  - deploy/installer/sync-cnode.sh - Manual sync script

Performance:
  - Startup time: 0.1s (49x faster than PyInstaller)
  - Size: ~50KB wrapper vs 8.8MB binary

Commands:
  cnode start [--replicas N]  # Start server/cluster
  cnode status                # Check status
  cnode scale N               # Scale replicas
  cnode logs [-f]             # View logs
  cnode stop                  # Stop server
This commit is contained in:
unclecode
2025-10-21 09:31:18 +08:00
parent 342fc52b47
commit cd02616218
16 changed files with 4181 additions and 11 deletions

View File

@@ -65,14 +65,14 @@ class TestCLIBasics:
assert 'Crawl4AI CLI' in result.output
def test_examples(self, runner):
result = runner.invoke(cli, ['--example'])
result = runner.invoke(cli, ['examples'])
assert result.exit_code == 0
assert 'Examples' in result.output
def test_missing_url(self, runner):
result = runner.invoke(cli)
result = runner.invoke(cli, ['crawl'])
assert result.exit_code != 0
assert 'URL argument is required' in result.output
assert ('Missing argument' in result.output or 'required' in result.output.lower())
class TestConfigParsing:
def test_parse_key_values_basic(self):
@@ -101,18 +101,19 @@ class TestConfigLoading:
class TestLLMConfig:
def test_llm_config_creation(self, temp_config_dir, runner):
def input_simulation(inputs):
return runner.invoke(cli, ['https://example.com', '-q', 'test question'],
return runner.invoke(cli, ['crawl', 'https://example.com', '-q', 'test question'],
input='\n'.join(inputs))
class TestCrawlingFeatures:
def test_basic_crawl(self, runner):
result = runner.invoke(cli, ['https://example.com'])
result = runner.invoke(cli, ['crawl', 'https://example.com'])
assert result.exit_code == 0
class TestErrorHandling:
def test_invalid_config_file(self, runner):
result = runner.invoke(cli, [
'crawl',
'https://example.com',
'--browser-config', 'nonexistent.yml'
])
@@ -122,8 +123,9 @@ class TestErrorHandling:
invalid_schema = temp_config_dir / 'invalid_schema.json'
with open(invalid_schema, 'w') as f:
f.write('invalid json')
result = runner.invoke(cli, [
'crawl',
'https://example.com',
'--schema', str(invalid_schema)
])