jac-client 0.2.9__py3-none-any.whl → 0.2.10__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.
jac_client/plugin/cli.jac CHANGED
@@ -9,10 +9,89 @@ For creating client projects, use: `jac create --use client`
9
9
  """
10
10
 
11
11
  import from jaclang.cli.registry { get_registry }
12
- import from jaclang.cli.command { Arg, HookContext }
12
+ import from jaclang.cli.command { Arg, ArgKind, HookContext }
13
13
  import from jaclang.pycore.runtime { hookimpl }
14
14
  import os;
15
15
 
16
+ glob registry = get_registry();
17
+
18
+ # Register targets when module loads
19
+ # Import base target classes - this should trigger annex pass to auto-discover .impl.jac files
20
+ import from jac_client.plugin.src.targets.desktop_target {
21
+ DesktopTarget
22
+ }
23
+ import from jac_client.plugin.src.targets.web_target { WebTarget }
24
+ import from jac_client.plugin.src.targets.register { register_targets }
25
+
26
+ with entry {
27
+ register_targets();
28
+ }
29
+
30
+ """Register 'build' command at module level."""
31
+ @registry.command(
32
+ name="build",
33
+ help="Build a Jac application for a specific target",
34
+ args=[
35
+ Arg.create(
36
+ "filename",
37
+ kind=ArgKind.POSITIONAL,
38
+ default="main.jac",
39
+ help="Path to .jac file (default: main.jac)"
40
+ ),
41
+ Arg.create(
42
+ "client",
43
+ typ=str,
44
+ default="web",
45
+ help="Build client (web, desktop)",
46
+ choices=["web", "desktop"],
47
+ short="" # Disable auto-generated -t (may conflict with other plugins)
48
+ ),
49
+ Arg.create(
50
+ "platform",
51
+ typ=str,
52
+ default=None,
53
+ help="Platform for desktop builds (windows, macos, linux, all)",
54
+ choices=["windows", "macos", "linux", "all"],
55
+ short="p" # Use -p for platform
56
+ ),
57
+
58
+ ],
59
+ examples=[
60
+ ("jac build", "Build web target (default)"),
61
+ ("jac build main.jac", "Build specific file"),
62
+ ("jac build --client desktop", "Build desktop app"),
63
+ ("jac build --client desktop --platform windows", "Build for Windows"),
64
+
65
+ ],
66
+ group="build",
67
+ source="jac-client"
68
+ )
69
+ def build(
70
+ filename: str = "main.jac", target: str = "web", platform: str | None = None
71
+ ) -> int {
72
+ # This will be handled by the pre_hook
73
+ return 0;
74
+ }
75
+
76
+ """Register 'setup' command at module level."""
77
+ @registry.command(
78
+ name="setup",
79
+ help="Setup a build target (one-time initialization)",
80
+ args=[
81
+ Arg.create(
82
+ "target", kind=ArgKind.POSITIONAL, help="Target to setup (e.g., desktop)"
83
+ ),
84
+
85
+ ],
86
+ examples=[("jac setup desktop", "Setup desktop target (Tauri)"), ],
87
+ group="project",
88
+ source="jac-client"
89
+ )
90
+ def setup(target: str) -> int {
91
+ # This will be handled by the pre_hook
92
+ return 0;
93
+ }
94
+
16
95
  """Jac CLI extensions for client-side development."""
17
96
  class JacCmd {
18
97
  """Create Jac CLI cmds."""
@@ -68,6 +147,37 @@ class JacCmd {
68
147
  pre_hook=_handle_npm_remove,
69
148
  source="jac-client"
70
149
  );
150
+
151
+ # Add pre-hook to build command (command is registered at module level)
152
+ registry.extend_command(
153
+ command_name="build", pre_hook=_handle_build_target, source="jac-client"
154
+ );
155
+
156
+ # Add pre-hook to setup command (command is registered at module level)
157
+ registry.extend_command(
158
+ command_name="setup", pre_hook=_handle_setup_target, source="jac-client"
159
+ );
160
+
161
+ # Extend 'start' command with --client flag
162
+ # Note: jac-scale also uses --client, so we use --client to avoid conflict
163
+ if registry.has_command("start") {
164
+ registry.extend_command(
165
+ command_name="start",
166
+ args=[
167
+ Arg.create(
168
+ "client", # Use client to avoid conflict with jac-scale's --client
169
+ typ=str,
170
+ default="web",
171
+ help="Client build target for dev server (web, desktop)",
172
+ choices=["web", "desktop"],
173
+ short="" # Disable auto-generated short flag
174
+ ),
175
+
176
+ ],
177
+ pre_hook=_handle_start_target,
178
+ source="jac-client"
179
+ );
180
+ }
71
181
  }
72
182
  }
73
183
 
@@ -174,6 +284,249 @@ def _handle_npm_add(ctx: HookContext) -> None {
174
284
  }
175
285
  }
176
286
 
287
+ """Pre-hook to handle --client flag for build command."""
288
+ def _handle_build_target(ctx: HookContext) -> None {
289
+ import from jaclang.cli.console { console }
290
+ import from jac_client.plugin.src.targets.registry { get_target_registry }
291
+ import from pathlib { Path }
292
+ import from jaclang.project.config { get_config }
293
+
294
+ target_name = ctx.get_arg("client", "web");
295
+ platform = ctx.get_arg("platform", None);
296
+
297
+ registry = get_target_registry();
298
+ target = registry.get(target_name);
299
+
300
+ if not target {
301
+ console.error(f"Unknown client target: {target_name}");
302
+ console.print(
303
+ f"Available client targets: {', '.join(
304
+ [t.name for t in registry.get_all()]
305
+ )}",
306
+ style="muted"
307
+ );
308
+ ctx.set_data("cancel_execution", True);
309
+ ctx.set_data("cancel_return_code", 1);
310
+ return;
311
+ }
312
+
313
+ # Store target info in context for use by build handler
314
+ ctx.set_data("build_target", target);
315
+ ctx.set_data("build_platform", platform);
316
+
317
+ # If target requires setup, warn user
318
+ if target.requires_setup {
319
+ console.warning(
320
+ f"Client target '{target_name}' requires setup. Run 'jac setup {target_name}' first."
321
+ );
322
+ }
323
+
324
+ # Build using target's build method
325
+ try {
326
+ config = get_config();
327
+ if not config {
328
+ console.error("No jac.toml found. Run 'jac create' to create a project.");
329
+ ctx.set_data("cancel_execution", True);
330
+ ctx.set_data("cancel_return_code", 1);
331
+ return;
332
+ }
333
+
334
+ entry_file = ctx.get_arg("filename", None);
335
+ if not entry_file {
336
+ entry_file = config.entry_point;
337
+ }
338
+ if not entry_file {
339
+ console.error("No entry file specified. Use: jac build <file>");
340
+ ctx.set_data("cancel_execution", True);
341
+ ctx.set_data("cancel_return_code", 1);
342
+ return;
343
+ }
344
+
345
+ entry_path = Path(entry_file);
346
+ project_dir = config.project_root or Path.cwd();
347
+ if not entry_path.is_absolute() {
348
+ entry_path = project_dir / entry_path;
349
+ }
350
+ bundle_path = target.build(entry_path, project_dir, platform);
351
+
352
+ console.success(f"Build complete: {bundle_path}");
353
+ ctx.set_data("cancel_execution", True);
354
+ ctx.set_data("cancel_return_code", 0);
355
+ } except NotImplementedError as e {
356
+ console.error(
357
+ f"Target '{target_name}' is not yet implemented. Coming in Phase 2!"
358
+ );
359
+ ctx.set_data("cancel_execution", True);
360
+ ctx.set_data("cancel_return_code", 1);
361
+ } except Exception as e {
362
+ console.error(f"Build failed: {e}");
363
+ ctx.set_data("cancel_execution", True);
364
+ ctx.set_data("cancel_return_code", 1);
365
+ }
366
+ }
367
+
368
+ """Pre-hook to handle --client flag for start command."""
369
+ def _handle_start_target(ctx: HookContext) -> None {
370
+ import from jaclang.cli.console { console }
371
+ import from jac_client.plugin.src.targets.registry { get_target_registry }
372
+
373
+ target_name = ctx.get_arg("client", "web");
374
+
375
+ registry = get_target_registry();
376
+ target = registry.get(target_name);
377
+
378
+ if not target {
379
+ console.error(f"Unknown client target: {target_name}");
380
+ console.print(
381
+ f"Available client targets: {', '.join(
382
+ [t.name for t in registry.get_all()]
383
+ )}",
384
+ style="muted"
385
+ );
386
+ ctx.set_data("cancel_execution", True);
387
+ ctx.set_data("cancel_return_code", 1);
388
+ return;
389
+ }
390
+
391
+ # Store target info in context
392
+ ctx.set_data("start_client_target", target);
393
+
394
+ # Handle desktop target
395
+ if target_name == "desktop" {
396
+ import from pathlib { Path }
397
+ import from jaclang.project.config { get_config }
398
+ config = get_config();
399
+ if not config {
400
+ console.error("No jac.toml found. Run 'jac create' to create a project.");
401
+ ctx.set_data("cancel_execution", True);
402
+ ctx.set_data("cancel_return_code", 1);
403
+ return;
404
+ }
405
+ # Get entry file
406
+ entry_file = ctx.get_arg("filename", None);
407
+ if not entry_file {
408
+ entry_file = config.entry_point;
409
+ }
410
+ if not entry_file {
411
+ console.error("No entry file specified. Use: jac start <file>");
412
+ ctx.set_data("cancel_execution", True);
413
+ ctx.set_data("cancel_return_code", 1);
414
+ return;
415
+ }
416
+ entry_path = Path(entry_file);
417
+ project_dir = config.project_root or Path.cwd();
418
+ if not entry_path.is_absolute() {
419
+ entry_path = project_dir / entry_path;
420
+ }
421
+ # Check if setup has been run
422
+ tauri_dir = project_dir / "src-tauri";
423
+ if not tauri_dir.exists() {
424
+ console.error("Desktop target not set up. Run 'jac setup desktop' first.");
425
+ ctx.set_data("cancel_execution", True);
426
+ ctx.set_data("cancel_return_code", 1);
427
+ return;
428
+ }
429
+ # Check if --dev flag is set
430
+ dev_mode = ctx.get_arg("dev", False);
431
+ try {
432
+ if dev_mode {
433
+ # Desktop target with --dev: launch Tauri dev mode (hot reload)
434
+ target.dev(entry_path, project_dir);
435
+ } else {
436
+ # Desktop target without --dev: build web bundle and launch Tauri with built bundle
437
+ target.start(entry_path, project_dir);
438
+ }
439
+ ctx.set_data("cancel_execution", True);
440
+ ctx.set_data("cancel_return_code", 0);
441
+ } except Exception as e {
442
+ import traceback;
443
+ console.error(f"Desktop start failed: {e}");
444
+ console.print(traceback.format_exc(), style="muted");
445
+ ctx.set_data("cancel_execution", True);
446
+ ctx.set_data("cancel_return_code", 1);
447
+ }
448
+ return;
449
+ }
450
+ # Web target: let existing start command handle it
451
+ # (no cancellation, let it run normally)
452
+ }
453
+
454
+ """Pre-hook to handle setup command."""
455
+ def _handle_setup_target(ctx: HookContext) -> None {
456
+ import from jaclang.cli.console { console }
457
+ import from jac_client.plugin.src.targets.registry { get_target_registry }
458
+ import from pathlib { Path }
459
+ import from jaclang.project.config { get_config }
460
+
461
+ target_name = ctx.get_arg("target", None);
462
+ if not target_name {
463
+ console.error("No target specified. Use: jac setup <target>");
464
+ console.print("Available targets: desktop", style="muted");
465
+ ctx.set_data("cancel_execution", True);
466
+ ctx.set_data("cancel_return_code", 1);
467
+ return;
468
+ }
469
+
470
+ registry = get_target_registry();
471
+ target = registry.get(target_name);
472
+
473
+ if not target {
474
+ console.error(f"Unknown client target: {target_name}");
475
+ console.print(
476
+ f"Available targets: {', '.join([t.name for t in registry.get_all()])}",
477
+ style="muted"
478
+ );
479
+ ctx.set_data("cancel_execution", True);
480
+ ctx.set_data("cancel_return_code", 1);
481
+ return;
482
+ }
483
+
484
+ # Setup using target's setup method
485
+ try {
486
+ config = get_config();
487
+ if not config {
488
+ console.error("No jac.toml found. Run 'jac create' to create a project.");
489
+ ctx.set_data("cancel_execution", True);
490
+ ctx.set_data("cancel_return_code", 1);
491
+ return;
492
+ }
493
+
494
+ project_dir = config.project_root or Path.cwd();
495
+ console.print(
496
+ f"Setting up target '{target_name}' in: {project_dir}", style="muted"
497
+ );
498
+ target.setup(project_dir);
499
+
500
+ # Verify setup completed successfully
501
+ tauri_dir = project_dir / "src-tauri";
502
+ if not tauri_dir.exists() {
503
+ console.error(
504
+ f"Setup completed but src-tauri directory not found at: {tauri_dir}"
505
+ );
506
+ console.print(
507
+ "This may indicate the setup function failed silently.", style="muted"
508
+ );
509
+ ctx.set_data("cancel_execution", True);
510
+ ctx.set_data("cancel_return_code", 1);
511
+ return;
512
+ }
513
+
514
+ console.success(f"Target '{target_name}' setup complete!");
515
+ ctx.set_data("cancel_execution", True);
516
+ ctx.set_data("cancel_return_code", 0);
517
+ } except NotImplementedError as e {
518
+ console.error(f"Target '{target_name}' setup is not yet implemented.");
519
+ ctx.set_data("cancel_execution", True);
520
+ ctx.set_data("cancel_return_code", 1);
521
+ } except Exception as e {
522
+ import traceback;
523
+ console.error(f"Setup failed: {e}");
524
+ console.print(traceback.format_exc(), style="muted");
525
+ ctx.set_data("cancel_execution", True);
526
+ ctx.set_data("cancel_return_code", 1);
527
+ }
528
+ }
529
+
177
530
  """Pre-hook to handle --npm flag for removing npm dependencies."""
178
531
  def _handle_npm_remove(ctx: HookContext) -> None {
179
532
  # Check if --npm flag is set
@@ -0,0 +1,31 @@
1
+ """Configuration loader for Desktop target (Tauri).
2
+
3
+ This module provides configuration access for the desktop target.
4
+ Configuration is loaded from jac.toml under [desktop] section.
5
+
6
+ Similar to JacClientConfig but loads [desktop] directly (not under [plugins]).
7
+ """
8
+ import from pathlib { Path }
9
+ import from typing { Any }
10
+ import from jaclang.project.config { JacConfig, get_config }
11
+
12
+ """Desktop target configuration loader.
13
+
14
+ Provides access to desktop/Tauri configuration
15
+ from the [desktop] section of jac.toml.
16
+ """
17
+ class DesktopConfig {
18
+ has project_dir: Path | None = None,
19
+ _config: dict[str, Any] | None = None,
20
+ _jac_config: JacConfig | None = None;
21
+
22
+ def init(self: DesktopConfig, project_dir: Path | None = None) -> None;
23
+ def load(self: DesktopConfig) -> dict[str, Any];
24
+ def get_desktop_config(self: DesktopConfig) -> dict[str, Any];
25
+ def get_window_config(self: DesktopConfig) -> dict[str, Any];
26
+ def get_platforms_config(self: DesktopConfig) -> dict[str, Any];
27
+ def get_features_config(self: DesktopConfig) -> dict[str, Any];
28
+ def validate(self: DesktopConfig) -> None;
29
+ def get_default_config(self: DesktopConfig) -> dict[str, Any];
30
+ # def _get_jac_config(self: DesktopConfig) -> JacConfig;
31
+ }
@@ -0,0 +1,191 @@
1
+ """Implementation of Desktop target configuration loader."""
2
+ import from pathlib { Path }
3
+ import from typing { Any }
4
+ import from jaclang.project.config { JacConfig, get_config }
5
+ import from jaclang.project.plugin_config { deep_merge }
6
+ import from jaclang.cli.console { console }
7
+
8
+ """Initialize DesktopConfig with project directory."""
9
+ impl DesktopConfig.init(self: DesktopConfig, project_dir: Path | None = None) -> None {
10
+ if project_dir is None {
11
+ self.project_dir = Path.cwd();
12
+ } else {
13
+ self.project_dir = project_dir;
14
+ }
15
+ self._config = None;
16
+ self._jac_config = None;
17
+ }
18
+
19
+ """Get default configuration structure for desktop target."""
20
+ impl DesktopConfig.get_default_config(self: DesktopConfig) -> dict[str, Any] {
21
+ # Try to get project name/version from JacConfig for defaults
22
+ # If config is not available, use fallback defaults
23
+ project_name = "my-jac-app";
24
+ project_version = "1.0.0";
25
+ try {
26
+ jac_config = self._get_jac_config();
27
+ if jac_config and jac_config.project {
28
+ project_name = jac_config.project.name or project_name;
29
+ project_version = jac_config.project.version or project_version;
30
+ }
31
+ } except Exception {
32
+ # If config loading fails, use defaults
33
+ console.warning("Failed to load JacConfig, using fallback defaults");
34
+ }
35
+ # Generate identifier from project name
36
+ identifier = _generate_identifier(project_name);
37
+ return {
38
+ 'name': project_name,
39
+ 'identifier': identifier,
40
+ 'version': project_version,
41
+ 'window': {
42
+ 'title': project_name,
43
+ 'width': 1200,
44
+ 'height': 800,
45
+ 'min_width': 800,
46
+ 'min_height': 600,
47
+ 'resizable': True,
48
+ 'fullscreen': False
49
+ },
50
+ 'platforms': {'windows': True, 'macos': True, 'linux': True},
51
+ 'features': {
52
+ 'system_tray': False,
53
+ 'auto_update': False,
54
+ 'notifications': False
55
+ }
56
+ };
57
+ }
58
+
59
+ """Get or load the core JacConfig instance."""
60
+ impl DesktopConfig._get_jac_config(self: DesktopConfig) -> JacConfig {
61
+ if self._jac_config is None {
62
+ self._jac_config = get_config();
63
+ if self._jac_config is None {
64
+ raise RuntimeError(
65
+ "No jac.toml found. Run 'jac create' to create a project."
66
+ ) ;
67
+ }
68
+ }
69
+ return self._jac_config;
70
+ }
71
+
72
+ """Load configuration from jac.toml, merging with defaults."""
73
+ impl DesktopConfig.load(self: DesktopConfig) -> dict[str, Any] {
74
+ if self._config is not None {
75
+ return self._config;
76
+ }
77
+ default_config = self.get_default_config();
78
+ # Try to get [desktop] section from jac.toml
79
+ desktop_config = {};
80
+ try {
81
+ jac_config = self._get_jac_config();
82
+ # Get [desktop] section from raw TOML data
83
+ # The [desktop] section is not under [plugins], so we access it directly
84
+ if jac_config and jac_config._raw_data {
85
+ raw_data = jac_config._raw_data;
86
+ desktop_config = raw_data.get('desktop', {});
87
+ }
88
+ } except Exception {
89
+ # If config loading fails, use defaults only
90
+ console.warning("Failed to load JacConfig, using fallback defaults");
91
+ }
92
+ # Deep merge defaults with user configuration
93
+ if desktop_config {
94
+ self._config = deep_merge(default_config, desktop_config);
95
+ } else {
96
+ self._config = default_config;
97
+ }
98
+ return self._config;
99
+ }
100
+
101
+ """Get desktop configuration (name, identifier, version)."""
102
+ impl DesktopConfig.get_desktop_config(self: DesktopConfig) -> dict[str, Any] {
103
+ config = self.load();
104
+ return {
105
+ 'name': config.get('name', ''),
106
+ 'identifier': config.get('identifier', ''),
107
+ 'version': config.get('version', '1.0.0')
108
+ };
109
+ }
110
+
111
+ """Get window configuration."""
112
+ impl DesktopConfig.get_window_config(self: DesktopConfig) -> dict[str, Any] {
113
+ config = self.load();
114
+ return config.get('window', {});
115
+ }
116
+
117
+ """Get platforms configuration."""
118
+ impl DesktopConfig.get_platforms_config(self: DesktopConfig) -> dict[str, Any] {
119
+ config = self.load();
120
+ return config.get('platforms', {});
121
+ }
122
+
123
+ """Get features configuration."""
124
+ impl DesktopConfig.get_features_config(self: DesktopConfig) -> dict[str, Any] {
125
+ config = self.load();
126
+ return config.get('features', {});
127
+ }
128
+
129
+ """Validate desktop configuration."""
130
+ impl DesktopConfig.validate(self: DesktopConfig) -> None {
131
+ config = self.load();
132
+ # Required fields
133
+ name = config.get('name', '');
134
+ if not name {
135
+ raise ValueError(
136
+ "Desktop configuration error: 'name' is required in [desktop] section"
137
+ ) ;
138
+ }
139
+ identifier = config.get('identifier', '');
140
+ if not identifier {
141
+ raise ValueError(
142
+ "Desktop configuration error: 'identifier' is required in [desktop] section"
143
+ ) ;
144
+ }
145
+ # Validate identifier format (should be reverse domain notation)
146
+ if not _is_valid_identifier(identifier) {
147
+ console.warning(
148
+ f"Desktop identifier '{identifier}' may not be valid. Use reverse domain notation (e.g., 'com.example.myapp')"
149
+ );
150
+ }
151
+ # Validate window dimensions
152
+ window = config.get('window', {});
153
+ width = window.get('width', 1200);
154
+ height = window.get('height', 800);
155
+ min_width = window.get('min_width', 800);
156
+ min_height = window.get('min_height', 600);
157
+ if min_width > width {
158
+ raise ValueError(
159
+ f"Desktop configuration error: window.min_width ({min_width}) cannot be greater than window.width ({width})"
160
+ ) ;
161
+ }
162
+ if min_height > height {
163
+ raise ValueError(
164
+ f"Desktop configuration error: window.min_height ({min_height}) cannot be greater than window.height ({height})"
165
+ ) ;
166
+ }
167
+ }
168
+
169
+ """Generate identifier from project name."""
170
+ def _generate_identifier(name: str) -> str {
171
+ # Convert to lowercase, replace spaces/special chars with dots
172
+ import re;
173
+ # Remove special characters, keep alphanumeric, dots, hyphens
174
+ cleaned = re.sub(r'[^a-zA-Z0-9.\-]', '', name.lower());
175
+ # Replace multiple dots/hyphens with single dot
176
+ cleaned = re.sub(r'[.\-]+', '.', cleaned);
177
+ # Remove leading/trailing dots
178
+ cleaned = cleaned.strip('.');
179
+ # If empty or doesn't look like reverse domain, prefix with 'com.'
180
+ if not cleaned or not '.' in cleaned {
181
+ cleaned = f"com.{cleaned}" if cleaned else "com.myapp";
182
+ }
183
+ return cleaned;
184
+ }
185
+
186
+ """Check if identifier is valid (reverse domain notation)."""
187
+ def _is_valid_identifier(identifier: str) -> bool {
188
+ import re;
189
+ # Should be like: com.example.myapp (at least one dot, alphanumeric + dots)
190
+ return bool(re.match(r'^[a-z0-9]+(\.[a-z0-9]+)+$', identifier.lower()));
191
+ }