jac-client 0.2.7__py3-none-any.whl → 0.2.9__py3-none-any.whl

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.
Files changed (72) hide show
  1. jac_client/examples/all-in-one/{src/app.jac → main.jac} +5 -5
  2. jac_client/examples/all-in-one/{src/pages → pages}/BudgetPlanner.jac +8 -1
  3. jac_client/examples/all-in-one/{src/pages → pages}/FeaturesTest.jac +16 -1
  4. jac_client/examples/all-in-one/{src/pages/FeaturesTest.cl.jac → pages/features_test_ui.cl.jac} +11 -0
  5. jac_client/examples/all-in-one/{src/pages → pages}/nestedDemo.jac +1 -1
  6. jac_client/examples/all-in-one/{src/pages → pages}/notFound.jac +2 -7
  7. jac_client/plugin/cli.jac +162 -430
  8. jac_client/plugin/client.jac +30 -12
  9. jac_client/plugin/client_runtime.cl.jac +19 -15
  10. jac_client/plugin/impl/client.impl.jac +107 -69
  11. jac_client/plugin/impl/client_runtime.impl.jac +181 -9
  12. jac_client/plugin/plugin_config.jac +243 -15
  13. jac_client/plugin/src/config_loader.jac +1 -0
  14. jac_client/plugin/src/impl/compiler.impl.jac +2 -4
  15. jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
  16. jac_client/plugin/src/impl/vite_bundler.impl.jac +241 -11
  17. jac_client/plugin/src/vite_bundler.jac +14 -1
  18. jac_client/plugin/utils/__init__.jac +1 -0
  19. jac_client/plugin/utils/impl/node_installer.impl.jac +249 -0
  20. jac_client/plugin/utils/node_installer.jac +41 -0
  21. jac_client/templates/client.jacpack +72 -0
  22. jac_client/templates/fullstack.jacpack +61 -0
  23. jac_client/tests/conftest.py +48 -7
  24. jac_client/tests/test_cli.py +189 -73
  25. jac_client/tests/test_e2e.py +232 -0
  26. jac_client/tests/test_helpers.py +65 -0
  27. jac_client/tests/test_it.py +97 -137
  28. {jac_client-0.2.7.dist-info → jac_client-0.2.9.dist-info}/METADATA +4 -4
  29. jac_client-0.2.9.dist-info/RECORD +104 -0
  30. {jac_client-0.2.7.dist-info → jac_client-0.2.9.dist-info}/WHEEL +1 -1
  31. jac_client-0.2.7.dist-info/RECORD +0 -97
  32. /jac_client/examples/all-in-one/{src/button.jac → button.jac} +0 -0
  33. /jac_client/examples/all-in-one/{src/components → components}/CategoryFilter.jac +0 -0
  34. /jac_client/examples/all-in-one/{src/components → components}/Header.jac +0 -0
  35. /jac_client/examples/all-in-one/{src/components → components}/ProfitOverview.jac +0 -0
  36. /jac_client/examples/all-in-one/{src/components → components}/Summary.jac +0 -0
  37. /jac_client/examples/all-in-one/{src/components → components}/TransactionForm.jac +0 -0
  38. /jac_client/examples/all-in-one/{src/components → components}/TransactionItem.jac +0 -0
  39. /jac_client/examples/all-in-one/{src/components → components}/TransactionList.jac +0 -0
  40. /jac_client/examples/all-in-one/{src/components → components}/button.jac +0 -0
  41. /jac_client/examples/all-in-one/{src/components → components}/navigation.jac +0 -0
  42. /jac_client/examples/all-in-one/{src/constants → constants}/categories.jac +0 -0
  43. /jac_client/examples/all-in-one/{src/constants → constants}/clients.jac +0 -0
  44. /jac_client/examples/all-in-one/{src/context → context}/BudgetContext.jac +0 -0
  45. /jac_client/examples/all-in-one/{src/hooks → hooks}/useBudget.jac +0 -0
  46. /jac_client/examples/all-in-one/{src/hooks → hooks}/useLocalStorage.jac +0 -0
  47. /jac_client/examples/all-in-one/{src/pages → pages}/LandingPage.jac +0 -0
  48. /jac_client/examples/all-in-one/{src/pages/BudgetPlanner.cl.jac → pages/budget_planner_ui.cl.jac} +0 -0
  49. /jac_client/examples/all-in-one/{src/pages → pages}/loginPage.jac +0 -0
  50. /jac_client/examples/all-in-one/{src/pages → pages}/signupPage.jac +0 -0
  51. /jac_client/examples/all-in-one/{src/utils → utils}/formatters.jac +0 -0
  52. /jac_client/examples/asset-serving/css-with-image/{src/app.jac → main.jac} +0 -0
  53. /jac_client/examples/asset-serving/image-asset/{src/app.jac → main.jac} +0 -0
  54. /jac_client/examples/asset-serving/import-alias/{src/app.jac → main.jac} +0 -0
  55. /jac_client/examples/basic/{src/app.jac → main.jac} +0 -0
  56. /jac_client/examples/basic-auth/{src/app.jac → main.jac} +0 -0
  57. /jac_client/examples/basic-auth-with-router/{src/app.jac → main.jac} +0 -0
  58. /jac_client/examples/basic-full-stack/{src/app.jac → main.jac} +0 -0
  59. /jac_client/examples/css-styling/js-styling/{src/app.jac → main.jac} +0 -0
  60. /jac_client/examples/css-styling/material-ui/{src/app.jac → main.jac} +0 -0
  61. /jac_client/examples/css-styling/pure-css/{src/app.jac → main.jac} +0 -0
  62. /jac_client/examples/css-styling/sass-example/{src/app.jac → main.jac} +0 -0
  63. /jac_client/examples/css-styling/styled-components/{src/app.jac → main.jac} +0 -0
  64. /jac_client/examples/css-styling/tailwind-example/{src/app.jac → main.jac} +0 -0
  65. /jac_client/examples/full-stack-with-auth/{src/app.jac → main.jac} +0 -0
  66. /jac_client/examples/little-x/{src/app.jac → main.jac} +0 -0
  67. /jac_client/examples/nested-folders/nested-advance/{src/app.jac → main.jac} +0 -0
  68. /jac_client/examples/nested-folders/nested-basic/{src/app.jac → main.jac} +0 -0
  69. /jac_client/examples/ts-support/{src/app.jac → main.jac} +0 -0
  70. /jac_client/examples/with-router/{src/app.jac → main.jac} +0 -0
  71. {jac_client-0.2.7.dist-info → jac_client-0.2.9.dist-info}/entry_points.txt +0 -0
  72. {jac_client-0.2.7.dist-info → jac_client-0.2.9.dist-info}/top_level.txt +0 -0
@@ -7,9 +7,7 @@ impl ViteBundler._get_client_dir(self: ViteBundler) -> Path {
7
7
  if config is not None {
8
8
  return config.get_client_dir();
9
9
  }
10
- } except ImportError {
11
- pass;
12
- }
10
+ } except ImportError { }
13
11
  # Fallback to default
14
12
  return self.project_dir / '.jac' / 'client';
15
13
  }
@@ -80,6 +78,8 @@ impl ViteBundler.create_package_json(
80
78
  }
81
79
  # Generate tsconfig.json during build time
82
80
  self.create_tsconfig();
81
+ # Generate config files (postcss, tailwind, etc.) from jac.toml
82
+ self.create_config_files();
83
83
  return package_json_path;
84
84
  }
85
85
 
@@ -377,6 +377,8 @@ impl ViteBundler.find_bundle(self: ViteBundler) -> Optional[Path] {
377
377
 
378
378
  """Run Vite build with generated config in .jac/client/configs/."""
379
379
  impl ViteBundler.build(self: ViteBundler, entry_file: Optional[Path] = None) -> None {
380
+ import sys;
381
+ import time;
380
382
  self.output_dir.mkdir(parents=True, exist_ok=True);
381
383
  generated_package_json = self._get_client_dir() / 'configs' / 'package.json';
382
384
  if not generated_package_json.exists() {
@@ -397,18 +399,31 @@ impl ViteBundler.build(self: ViteBundler, entry_file: Optional[Path] = None) ->
397
399
  shutil.copy2(configs_package_json, build_package_json);
398
400
  }
399
401
  try {
400
- # Install to .jac/client/node_modules
401
- subprocess.run(
402
- ['npm', 'install'],
402
+ # Install to .jac/client/node_modules with progress feedback
403
+ print(
404
+ " ⏳ Installing npm dependencies (this may take a minute)...",
405
+ flush=True
406
+ );
407
+ start_time = time.time();
408
+ result = subprocess.run(
409
+ ['npm', 'install', '--progress'],
403
410
  cwd=build_dir,
404
- check=True,
411
+ check=False,
405
412
  capture_output=True,
406
413
  text=True
407
414
  );
408
- } except subprocess.CalledProcessError as e {
409
- raise e from ClientBundleError(
410
- f"Failed to install npm dependencies: {e.stderr}"
411
- ) ;
415
+ elapsed = time.time() - start_time;
416
+ if result.returncode != 0 {
417
+ print(
418
+ f"\n ✖ npm install failed after {elapsed:.1f}s",
419
+ file=sys.stderr
420
+ );
421
+ raise ClientBundleError(
422
+ f"Failed to install npm dependencies: {result.stderr
423
+ or result.stdout}"
424
+ ) ;
425
+ }
426
+ print(f" ✔ Dependencies installed ({elapsed:.1f}s)", flush=True);
412
427
  } except FileNotFoundError {
413
428
  raise None from ClientBundleError(
414
429
  'npm command not found. Ensure Node.js and npm are installed.'
@@ -433,15 +448,20 @@ impl ViteBundler.build(self: ViteBundler, entry_file: Optional[Path] = None) ->
433
448
  command = ['npm', 'run', 'build'];
434
449
  }
435
450
  # Run vite from client build directory so it can find node_modules
451
+ print(" ⏳ Building client bundle...", flush=True);
452
+ start_time = time.time();
436
453
  result = subprocess.run(
437
454
  command, cwd=build_dir, check=False, capture_output=True, text=True
438
455
  );
456
+ elapsed = time.time() - start_time;
439
457
  if result.returncode != 0 {
458
+ print(f"\n ✖ Vite build failed after {elapsed:.1f}s", file=sys.stderr);
440
459
  error_msg = result.stderr or result.stdout or 'Unknown error';
441
460
  raise ClientBundleError(
442
461
  f"Vite build failed:\n{error_msg}\nCommand: {' '.join(command)}"
443
462
  ) from None ;
444
463
  }
464
+ print(f" ✔ Client bundle built ({elapsed:.1f}s)", flush=True);
445
465
  } finally {
446
466
  # Clean up temporary package.json in client build dir
447
467
  build_package_json = build_dir / 'package.json';
@@ -475,3 +495,213 @@ impl ViteBundler.init(
475
495
  # Set output_dir after config_loader is initialized so _get_client_dir works
476
496
  self.output_dir = output_dir or (self._get_client_dir() / 'dist');
477
497
  }
498
+
499
+ """Create a dev-mode vite config with API proxy for HMR."""
500
+ impl ViteBundler.create_dev_vite_config(
501
+ self: ViteBundler, entry_file: Path, api_port: int = 8000
502
+ ) -> Path {
503
+ build_dir = self._get_client_dir();
504
+ build_dir.mkdir(parents=True, exist_ok=True);
505
+ configs_dir = build_dir / 'configs';
506
+ configs_dir.mkdir(exist_ok=True);
507
+ config_path = configs_dir / 'vite.dev.config.js';
508
+ # Get entry file relative path
509
+ try {
510
+ entry_relative = entry_file.relative_to(build_dir).as_posix();
511
+ } except ValueError {
512
+ entry_relative = entry_file.as_posix();
513
+ }
514
+ # Calculate paths for aliases
515
+ if entry_relative.endswith('/build/main.js') {
516
+ compiled_utils_relative = entry_relative[:-13] + '/compiled/client_runtime.js';
517
+ compiled_assets_relative = entry_relative[:-13] + '/compiled/assets';
518
+ } elif entry_relative.endswith('build/main.js') {
519
+ compiled_utils_relative = 'compiled/client_runtime.js';
520
+ compiled_assets_relative = 'compiled/assets';
521
+ } else {
522
+ compiled_utils_relative = 'compiled/client_runtime.js';
523
+ compiled_assets_relative = 'compiled/assets';
524
+ }
525
+ # Extensions for TypeScript
526
+ extensions = ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'];
527
+ extensions_str = ', '.join(f'"{ext}"' for ext in extensions);
528
+ # Generate dev config with proxy for API routes
529
+ config_content = f'''import {{ defineConfig }} from "vite";
530
+ import path from "path";
531
+ import {{ fileURLToPath }} from "url";
532
+ import react from "@vitejs/plugin-react";
533
+
534
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
535
+ const buildDir = path.resolve(__dirname, "..");
536
+ const projectRoot = path.resolve(__dirname, "../../..");
537
+
538
+ /**
539
+ * Vite DEV configuration for HMR mode
540
+ * Proxies API routes to Python server at localhost:{api_port}
541
+ */
542
+ export default defineConfig({{
543
+ plugins: [react()],
544
+ root: buildDir,
545
+ publicDir: false,
546
+ server: {{
547
+ watch: {{
548
+ usePolling: true,
549
+ interval: 100,
550
+ }},
551
+ proxy: {{
552
+ "/walker": {{
553
+ target: "http://localhost:{api_port}",
554
+ changeOrigin: true,
555
+ }},
556
+ "/function": {{
557
+ target: "http://localhost:{api_port}",
558
+ changeOrigin: true,
559
+ }},
560
+ "/user": {{
561
+ target: "http://localhost:{api_port}",
562
+ changeOrigin: true,
563
+ }},
564
+ "/introspect": {{
565
+ target: "http://localhost:{api_port}",
566
+ changeOrigin: true,
567
+ }},
568
+ }},
569
+ }},
570
+ resolve: {{
571
+ alias: {{
572
+ "@jac-client/utils": path.resolve(buildDir, "{compiled_utils_relative}"),
573
+ "@jac-client/assets": path.resolve(buildDir, "{compiled_assets_relative}"),
574
+ }},
575
+ extensions: [{extensions_str}],
576
+ }},
577
+ }});
578
+ ''';
579
+ config_path.write_text(config_content, encoding='utf-8');
580
+ return config_path;
581
+ }
582
+
583
+ """Create config files from jac.toml [plugins.client.configs].
584
+
585
+ Generates JavaScript config files (e.g., postcss.config.js, tailwind.config.js)
586
+ from TOML configuration. Each key in [plugins.client.configs] becomes a config file.
587
+
588
+ Example jac.toml:
589
+ [plugins.client.configs.postcss]
590
+ plugins = ["tailwindcss", "autoprefixer"]
591
+
592
+ [plugins.client.configs.tailwind]
593
+ content = ["./src/**/*.{js,jsx}"]
594
+
595
+ This generates:
596
+ - .jac/client/configs/postcss.config.js
597
+ - .jac/client/configs/tailwind.config.js
598
+ """
599
+ impl ViteBundler.create_config_files(self: ViteBundler) -> list[Path] {
600
+ configs = self.config_loader.get_configs();
601
+ if not configs {
602
+ return [];
603
+ }
604
+ build_dir = self._get_client_dir();
605
+ configs_dir = build_dir / 'configs';
606
+ configs_dir.mkdir(parents=True, exist_ok=True);
607
+ created_files: list[Path] = [];
608
+ for (config_name, config_data) in configs.items() {
609
+ config_path = configs_dir / f'{config_name}.config.js';
610
+
611
+ # Convert the TOML config to JavaScript module.exports
612
+ js_content = _toml_config_to_js(config_name, config_data);
613
+ config_path.write_text(js_content, encoding='utf-8');
614
+ created_files.append(config_path);
615
+ }
616
+ return created_files;
617
+ }
618
+
619
+ """Convert TOML config data to JavaScript config file content.
620
+
621
+ Generates a generic module.exports with the config data as JSON.
622
+ """
623
+ def _toml_config_to_js(config_name: str, config_data: dict) -> str {
624
+ return f"module.exports = {json.dumps(config_data, indent=2)};\n";
625
+ }
626
+
627
+ """Start Vite dev server as a subprocess."""
628
+ impl ViteBundler.start_dev_server(self: ViteBundler, port: int = 3000) -> Any {
629
+ import sys;
630
+ import time;
631
+ build_dir = self._get_client_dir();
632
+ node_modules = build_dir / 'node_modules';
633
+ # Create/update index.html for dev server (load from compiled/ for HMR)
634
+ index_html = build_dir / 'index.html';
635
+ index_content = '''<!DOCTYPE html>
636
+ <html lang="en">
637
+ <head>
638
+ <meta charset="UTF-8" />
639
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
640
+ <title>Jac App (Dev)</title>
641
+ </head>
642
+ <body>
643
+ <div id="root"></div>
644
+ <script type="module" src="/compiled/_entry.js"></script>
645
+ </body>
646
+ </html>
647
+ ''';
648
+ index_html.write_text(index_content, encoding='utf-8');
649
+ # Ensure dependencies are installed
650
+ if not node_modules.exists() {
651
+ generated_package_json = build_dir / 'configs' / 'package.json';
652
+ if not generated_package_json.exists() {
653
+ self.create_package_json();
654
+ }
655
+ # Temporarily copy package.json for npm install
656
+ build_package_json = build_dir / 'package.json';
657
+ if not build_package_json.exists() {
658
+ shutil.copy2(generated_package_json, build_package_json);
659
+ }
660
+ try {
661
+ print(
662
+ " ⏳ Installing npm dependencies (this may take a minute)...",
663
+ flush=True
664
+ );
665
+ start_time = time.time();
666
+ result = subprocess.run(
667
+ ['npm', 'install', '--progress'],
668
+ cwd=build_dir,
669
+ check=False,
670
+ capture_output=True,
671
+ text=True
672
+ );
673
+ elapsed = time.time() - start_time;
674
+ if result.returncode != 0 {
675
+ print(
676
+ f"\n ✖ npm install failed after {elapsed:.1f}s", file=sys.stderr
677
+ );
678
+ print(f" Error: {result.stderr or result.stdout}", file=sys.stderr);
679
+ raise ClientBundleError(
680
+ f"Failed to install npm dependencies: {result.stderr
681
+ or result.stdout}"
682
+ ) ;
683
+ }
684
+ print(f" ✔ Dependencies installed ({elapsed:.1f}s)", flush=True);
685
+ } finally {
686
+ # Clean up temp package.json
687
+ if build_package_json.exists() {
688
+ build_package_json.unlink();
689
+ }
690
+ }
691
+ }
692
+ # Find the dev config
693
+ dev_config = build_dir / 'configs' / 'vite.dev.config.js';
694
+ if not dev_config.exists() {
695
+ raise ClientBundleError(
696
+ "Dev config not found. Call create_dev_vite_config first."
697
+ ) ;
698
+ }
699
+ config_rel = dev_config.relative_to(build_dir);
700
+ logger.debug(f"Starting Vite dev server on port {port}");
701
+ # Start Vite in dev mode (let output go to terminal for HMR visibility)
702
+ process = subprocess.Popen(
703
+ ['npx', 'vite', '--config', str(config_rel), '--port', str(port)],
704
+ cwd=build_dir
705
+ );
706
+ return process;
707
+ }
@@ -1,12 +1,15 @@
1
1
  """Vite bundling module."""
2
2
  import hashlib;
3
3
  import json;
4
+ import logging;
4
5
  import shutil;
5
6
  import subprocess;
6
7
  import from pathlib { Path }
7
- import from typing { Optional }
8
+ import from typing { Any, Optional }
8
9
  import from jaclang.runtimelib.client_bundle { ClientBundleError }
9
10
  import from .config_loader { JacClientConfig }
11
+
12
+ glob logger = logging.getLogger(__name__);
10
13
  """Handles Vite bundling operations."""
11
14
  class ViteBundler {
12
15
  def init(
@@ -34,4 +37,14 @@ class ViteBundler {
34
37
  ) -> Path;
35
38
 
36
39
  def create_tsconfig(self: ViteBundler) -> Path;
40
+ """Create config files from jac.toml [plugins.client.configs]."""
41
+ def create_config_files(self: ViteBundler) -> list[Path];
42
+
43
+ """Create a dev-mode vite config with API proxy for HMR."""
44
+ def create_dev_vite_config(
45
+ self: ViteBundler, entry_file: Path, api_port: int = 8000
46
+ ) -> Path;
47
+
48
+ """Start Vite dev server as a subprocess."""
49
+ def start_dev_server(self: ViteBundler, port: int = 3000) -> Any;
37
50
  }
@@ -0,0 +1 @@
1
+ """Utility modules for jac-client plugin."""
@@ -0,0 +1,249 @@
1
+ """Implementation of Node.js installer methods."""
2
+
3
+ """Check if Node.js is installed and accessible."""
4
+ impl NodeInstaller.is_node_installed -> bool {
5
+ try {
6
+ result = subprocess.run(
7
+ ['node', '--version'], capture_output=True, text=True, timeout=5
8
+ );
9
+ return result.returncode == 0;
10
+ } except (FileNotFoundError, subprocess.TimeoutExpired) {
11
+ return False;
12
+ }
13
+ }
14
+
15
+ """Check if npm is installed and accessible."""
16
+ impl NodeInstaller.is_npm_installed -> bool {
17
+ try {
18
+ result = subprocess.run(
19
+ ['npm', '--version'], capture_output=True, text=True, timeout=5
20
+ );
21
+ return result.returncode == 0;
22
+ } except (FileNotFoundError, subprocess.TimeoutExpired) {
23
+ return False;
24
+ }
25
+ }
26
+
27
+ """Check if NVM is installed."""
28
+ impl NodeInstaller.is_nvm_installed -> bool {
29
+ nvm_dir = Path.home() / '.nvm';
30
+ return nvm_dir.exists() and (nvm_dir / 'nvm.sh').exists();
31
+ }
32
+
33
+ """Get the currently installed Node.js version."""
34
+ impl NodeInstaller.get_node_version -> (str | None) {
35
+ try {
36
+ result = subprocess.run(
37
+ ['node', '--version'], capture_output=True, text=True, timeout=5
38
+ );
39
+ if result.returncode == 0 {
40
+ return result.stdout.strip();
41
+ }
42
+ } except (FileNotFoundError, subprocess.TimeoutExpired) { }
43
+ return None;
44
+ }
45
+
46
+ """Install NVM (Node Version Manager)."""
47
+ impl NodeInstaller.install_nvm -> tuple[bool, str] {
48
+ system = platform.system();
49
+ if system == 'Windows' {
50
+ return (
51
+ False,
52
+ 'Windows detected. Please install Node.js manually:\n' + ' 1. Download from: https://nodejs.org/\n' + ' 2. Or use nvm-windows: https://github.com/coreybutler/nvm-windows\n' + ' After installation, run "jac add --cl" again.'
53
+ );
54
+ }
55
+ print('Installing NVM (Node Version Manager)...');
56
+ try {
57
+ # Download and run NVM installation script
58
+ install_script = subprocess.run(
59
+ ['curl', '-o-', NodeInstaller.NVM_INSTALL_URL],
60
+ capture_output=True,
61
+ text=True,
62
+ timeout=30
63
+ );
64
+
65
+ if install_script.returncode != 0 {
66
+ return (
67
+ False,
68
+ f'Failed to download NVM installer: {install_script.stderr}'
69
+ );
70
+ }
71
+
72
+ # Execute the installation script
73
+ result = subprocess.run(
74
+ ['bash', '-c', install_script.stdout],
75
+ capture_output=True,
76
+ text=True,
77
+ timeout=60
78
+ );
79
+
80
+ if result.returncode != 0 {
81
+ return (False, f'NVM installation failed: {result.stderr}');
82
+ }
83
+
84
+ print('NVM installed successfully!');
85
+ return (True, 'NVM installed successfully');
86
+ } except subprocess.TimeoutExpired {
87
+ return (
88
+ False,
89
+ 'NVM installation timed out. Please check your internet connection.'
90
+ );
91
+ } except FileNotFoundError as e {
92
+ if 'curl' in str(e) {
93
+ return (
94
+ False,
95
+ 'curl command not found. Please install curl first:\n' + ' Ubuntu/Debian: sudo apt-get install curl\n' + ' macOS: curl is pre-installed\n' + ' Fedora/RHEL: sudo dnf install curl'
96
+ );
97
+ }
98
+ return (False, f'Required command not found: {e}');
99
+ } except Exception as e {
100
+ return (False, f'Unexpected error during NVM installation: {e}');
101
+ }
102
+ }
103
+
104
+ """Install Node.js using NVM."""
105
+ impl NodeInstaller.install_node_via_nvm(version: str = "20") -> tuple[bool, str] {
106
+ print(f'Installing Node.js v{version} via NVM...');
107
+ nvm_dir = Path.home() / '.nvm';
108
+ nvm_script = nvm_dir / 'nvm.sh';
109
+ if not nvm_script.exists() {
110
+ return (False, 'NVM is not installed or nvm.sh not found');
111
+ }
112
+ try {
113
+ # Source NVM and install Node.js
114
+ command = (
115
+ 'export NVM_DIR="$HOME/.nvm"\n' + '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"\n' + f'nvm install {version}\n' + f'nvm use {version}\n' + f'nvm alias default {version}\n'
116
+ );
117
+
118
+ result = subprocess.run(
119
+ ['bash', '-c', command], capture_output=True, text=True, timeout=300
120
+ );
121
+
122
+ if result.returncode != 0 {
123
+ return (False, f'Node.js installation failed: {result.stderr}');
124
+ }
125
+
126
+ print(f'Node.js v{version} installed successfully!');
127
+ return (True, f'Node.js v{version} installed successfully');
128
+ } except subprocess.TimeoutExpired {
129
+ return (False, 'Node.js installation timed out. Please try again.');
130
+ } except Exception as e {
131
+ return (False, f'Unexpected error during Node.js installation: {e}');
132
+ }
133
+ }
134
+
135
+ """Ensure Node.js is installed, installing automatically if needed.
136
+
137
+ Returns tuple of (success: bool, message: str, was_just_installed: bool)
138
+ """
139
+ impl NodeInstaller.ensure_node_installed(
140
+ interactive: bool = True
141
+ ) -> tuple[bool, str, bool] {
142
+ # Common fallback message for manual installation
143
+ manual_install_msg = (
144
+ 'Please install Node.js manually:\n' + ' • Download from: https://nodejs.org/\n' + ' • Or install NVM: https://github.com/nvm-sh/nvm\n' + ' After installation, run "jac add --cl" again.'
145
+ );
146
+ # Check if Node.js is already available
147
+ if NodeInstaller.is_node_installed() and NodeInstaller.is_npm_installed() {
148
+ version = NodeInstaller.get_node_version();
149
+ return (True, f'Node.js {version} is already installed', False);
150
+ }
151
+ print('\nNode.js is not installed or not found in PATH.');
152
+ # Check for interactive mode
153
+ if interactive {
154
+ print(
155
+ '\nJac client requires Node.js to build and run client-side applications.'
156
+ );
157
+ print(
158
+ f'Would you like to automatically install Node.js v{NodeInstaller.DEFAULT_NODE_VERSION} using NVM?'
159
+ );
160
+ response = input('Install Node.js? [Y/n]: ').strip().lower();
161
+ if response and response not in ('y', 'yes') {
162
+ return (
163
+ False,
164
+ f'Node.js installation cancelled. {manual_install_msg}',
165
+ False
166
+ );
167
+ }
168
+ }
169
+ # Wrap entire installation process in error handling
170
+ try {
171
+ # Check if NVM is installed
172
+ if not NodeInstaller.is_nvm_installed() {
173
+ success_nvm = NodeInstaller.install_nvm();
174
+ if not success_nvm[0] {
175
+ return (
176
+ False,
177
+ f'Unable to automatically install Node.js.\n\nError: {success_nvm[
178
+ 1
179
+ ]}\n\n{manual_install_msg}',
180
+ False
181
+ );
182
+ }
183
+ }
184
+
185
+ # Install Node.js via NVM
186
+ success_node = NodeInstaller.install_node_via_nvm();
187
+ if not success_node[0] {
188
+ return (
189
+ False,
190
+ f'Unable to automatically install Node.js.\n\nError: {success_node[1]}\n\n{manual_install_msg}',
191
+ False
192
+ );
193
+ }
194
+
195
+ # Node.js is now available via NVM sourcing in subprocesses
196
+ print('\n' + '=' * 70);
197
+ print('Node.js has been installed successfully!');
198
+ print('Continuing with package installation...');
199
+ print('=' * 70 + '\n');
200
+
201
+ return (
202
+ True,
203
+ f'Node.js {NodeInstaller.DEFAULT_NODE_VERSION} installed successfully',
204
+ True
205
+ );
206
+ } except Exception as e {
207
+ # Catch any unexpected errors during installation
208
+ return (
209
+ False,
210
+ f'Unable to automatically install Node.js.\n\nUnexpected error: {str(e)}\n\n{manual_install_msg}',
211
+ False
212
+ );
213
+ }
214
+ }
215
+
216
+ """Run npm command with NVM environment properly sourced.
217
+
218
+ This method automatically sources NVM in a subprocess, so npm commands work
219
+ immediately after NVM installation without requiring the user to reload their shell.
220
+ """
221
+ impl NodeInstaller.run_npm_with_nvm(
222
+ args: list, cwd: Path, timeout: int = 300
223
+ ) -> object {
224
+ # First, try running npm directly (in case it's already in PATH)
225
+ try {
226
+ return subprocess.run(
227
+ ['npm'] + args,
228
+ cwd=cwd,
229
+ capture_output=True,
230
+ text=True,
231
+ timeout=timeout,
232
+ check=True
233
+ );
234
+ } except FileNotFoundError { }
235
+ # If npm is not in PATH, source NVM and run npm in a subprocess
236
+ # This allows npm to work immediately after installation without shell reload
237
+ args_str = ' '.join(args);
238
+ command = (
239
+ 'export NVM_DIR="$HOME/.nvm"\n' + '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"\n' + f'npm {args_str}\n'
240
+ );
241
+ return subprocess.run(
242
+ ['bash', '-c', command],
243
+ cwd=cwd,
244
+ capture_output=True,
245
+ text=True,
246
+ timeout=timeout,
247
+ check=True
248
+ );
249
+ }
@@ -0,0 +1,41 @@
1
+ """Automatic Node.js installation utility using NVM."""
2
+
3
+ import os;
4
+ import platform;
5
+ import subprocess;
6
+ import from pathlib { Path }
7
+
8
+ """Handles Node.js installation and detection."""
9
+ obj NodeInstaller {
10
+ static has DEFAULT_NODE_VERSION: str = "20",
11
+ NVM_INSTALL_URL: str = "https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh";
12
+
13
+ """Check if Node.js is installed and accessible."""
14
+ static def is_node_installed -> bool;
15
+
16
+ """Check if npm is installed and accessible."""
17
+ static def is_npm_installed -> bool;
18
+
19
+ """Check if NVM is installed."""
20
+ static def is_nvm_installed -> bool;
21
+
22
+ """Get the currently installed Node.js version."""
23
+ static def get_node_version -> (str | None);
24
+
25
+ """Install NVM (Node Version Manager)."""
26
+ static def install_nvm -> tuple[bool, str];
27
+
28
+ """Install Node.js using NVM."""
29
+ static def install_node_via_nvm(version: str = "20") -> tuple[bool, str];
30
+
31
+ """Ensure Node.js is installed, installing automatically if needed.
32
+
33
+ Returns tuple of (success: bool, message: str, was_just_installed: bool)
34
+ """
35
+ static def ensure_node_installed(
36
+ interactive: bool = True
37
+ ) -> tuple[bool, str, bool];
38
+
39
+ """Run npm command with NVM environment properly sourced."""
40
+ static def run_npm_with_nvm(args: list, cwd: Path, timeout: int = 300) -> object;
41
+ }