jac-client 0.2.6__py3-none-any.whl → 0.2.11__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 (119) hide show
  1. jac_client/examples/all-in-one/{src/button.jac → button.jac} +4 -3
  2. jac_client/examples/all-in-one/components/CategoryFilter.jac +47 -0
  3. jac_client/examples/all-in-one/components/Header.jac +17 -0
  4. jac_client/examples/all-in-one/components/ProfitOverview.jac +64 -0
  5. jac_client/examples/all-in-one/components/Summary.jac +76 -0
  6. jac_client/examples/all-in-one/components/TransactionForm.jac +188 -0
  7. jac_client/examples/all-in-one/components/TransactionItem.jac +62 -0
  8. jac_client/examples/all-in-one/components/TransactionList.jac +44 -0
  9. jac_client/examples/all-in-one/components/button.jac +8 -0
  10. jac_client/examples/all-in-one/components/navigation.jac +126 -0
  11. jac_client/examples/all-in-one/constants/categories.jac +36 -0
  12. jac_client/examples/all-in-one/constants/clients.jac +12 -0
  13. jac_client/examples/all-in-one/context/BudgetContext.jac +31 -0
  14. jac_client/examples/all-in-one/hooks/useBudget.jac +122 -0
  15. jac_client/examples/all-in-one/hooks/useLocalStorage.jac +37 -0
  16. jac_client/examples/all-in-one/main.jac +542 -0
  17. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +140 -0
  18. jac_client/examples/all-in-one/pages/FeaturesTest.jac +157 -0
  19. jac_client/examples/all-in-one/pages/LandingPage.jac +124 -0
  20. jac_client/examples/all-in-one/pages/budget_planner_ui.cl.jac +65 -0
  21. jac_client/examples/all-in-one/pages/features_test_ui.cl.jac +675 -0
  22. jac_client/examples/all-in-one/pages/loginPage.jac +127 -0
  23. jac_client/examples/all-in-one/pages/nestedDemo.jac +54 -0
  24. jac_client/examples/all-in-one/pages/notFound.jac +18 -0
  25. jac_client/examples/all-in-one/pages/signupPage.jac +127 -0
  26. jac_client/examples/all-in-one/utils/formatters.jac +49 -0
  27. jac_client/examples/asset-serving/css-with-image/main.jac +92 -0
  28. jac_client/examples/asset-serving/image-asset/main.jac +56 -0
  29. jac_client/examples/asset-serving/import-alias/main.jac +109 -0
  30. jac_client/examples/basic/main.jac +23 -0
  31. jac_client/examples/basic-auth/main.jac +363 -0
  32. jac_client/examples/basic-auth-with-router/main.jac +451 -0
  33. jac_client/examples/basic-full-stack/main.jac +362 -0
  34. jac_client/examples/css-styling/js-styling/main.jac +63 -0
  35. jac_client/examples/css-styling/material-ui/main.jac +122 -0
  36. jac_client/examples/css-styling/pure-css/main.jac +55 -0
  37. jac_client/examples/css-styling/sass-example/main.jac +55 -0
  38. jac_client/examples/css-styling/styled-components/main.jac +62 -0
  39. jac_client/examples/css-styling/tailwind-example/main.jac +74 -0
  40. jac_client/examples/full-stack-with-auth/main.jac +696 -0
  41. jac_client/examples/little-x/main.jac +681 -0
  42. jac_client/examples/little-x/src/submit-button.jac +15 -14
  43. jac_client/examples/nested-folders/nested-advance/main.jac +26 -0
  44. jac_client/examples/nested-folders/nested-advance/src/ButtonRoot.jac +4 -6
  45. jac_client/examples/nested-folders/nested-advance/src/level1/ButtonSecondL.jac +9 -13
  46. jac_client/examples/nested-folders/nested-advance/src/level1/Card.jac +29 -32
  47. jac_client/examples/nested-folders/nested-advance/src/level1/level2/ButtonThirdL.jac +12 -18
  48. jac_client/examples/nested-folders/nested-basic/{src/app.jac → main.jac} +7 -5
  49. jac_client/examples/nested-folders/nested-basic/src/button.jac +4 -3
  50. jac_client/examples/nested-folders/nested-basic/src/components/button.jac +4 -3
  51. jac_client/examples/ts-support/main.jac +35 -0
  52. jac_client/examples/with-router/main.jac +286 -0
  53. jac_client/plugin/cli.jac +507 -470
  54. jac_client/plugin/client.jac +30 -12
  55. jac_client/plugin/client_runtime.cl.jac +25 -15
  56. jac_client/plugin/impl/client.impl.jac +126 -26
  57. jac_client/plugin/impl/client_runtime.impl.jac +182 -10
  58. jac_client/plugin/plugin_config.jac +216 -34
  59. jac_client/plugin/src/__init__.jac +0 -2
  60. jac_client/plugin/src/compiler.jac +2 -2
  61. jac_client/plugin/src/config_loader.jac +1 -0
  62. jac_client/plugin/src/desktop_config.jac +31 -0
  63. jac_client/plugin/src/impl/compiler.impl.jac +99 -30
  64. jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
  65. jac_client/plugin/src/impl/desktop_config.impl.jac +191 -0
  66. jac_client/plugin/src/impl/jac_to_js.impl.jac +5 -1
  67. jac_client/plugin/src/impl/package_installer.impl.jac +20 -20
  68. jac_client/plugin/src/impl/vite_bundler.impl.jac +384 -144
  69. jac_client/plugin/src/package_installer.jac +1 -1
  70. jac_client/plugin/src/targets/desktop/sidecar/main.py +144 -0
  71. jac_client/plugin/src/targets/desktop_target.jac +37 -0
  72. jac_client/plugin/src/targets/impl/desktop_target.impl.jac +2347 -0
  73. jac_client/plugin/src/targets/impl/registry.impl.jac +64 -0
  74. jac_client/plugin/src/targets/impl/web_target.impl.jac +157 -0
  75. jac_client/plugin/src/targets/register.jac +21 -0
  76. jac_client/plugin/src/targets/registry.jac +87 -0
  77. jac_client/plugin/src/targets/web_target.jac +35 -0
  78. jac_client/plugin/src/vite_bundler.jac +15 -1
  79. jac_client/plugin/utils/__init__.jac +3 -0
  80. jac_client/plugin/utils/bun_installer.jac +16 -0
  81. jac_client/plugin/utils/impl/bun_installer.impl.jac +99 -0
  82. jac_client/templates/client.jacpack +72 -0
  83. jac_client/templates/fullstack.jacpack +61 -0
  84. jac_client/tests/conftest.py +110 -52
  85. jac_client/tests/fixtures/spawn_test/app.jac +64 -70
  86. jac_client/tests/fixtures/with-ts/app.jac +28 -28
  87. jac_client/tests/test_cli.py +280 -113
  88. jac_client/tests/test_e2e.py +232 -0
  89. jac_client/tests/test_helpers.py +58 -0
  90. jac_client/tests/test_it.py +325 -154
  91. jac_client/tests/test_it_desktop.py +891 -0
  92. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/METADATA +20 -11
  93. jac_client-0.2.11.dist-info/RECORD +113 -0
  94. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/WHEEL +1 -1
  95. jac_client/examples/all-in-one/src/app.jac +0 -841
  96. jac_client/examples/all-in-one/src/components/button.jac +0 -7
  97. jac_client/examples/asset-serving/css-with-image/src/app.jac +0 -88
  98. jac_client/examples/asset-serving/image-asset/src/app.jac +0 -55
  99. jac_client/examples/asset-serving/import-alias/src/app.jac +0 -111
  100. jac_client/examples/basic/src/app.jac +0 -21
  101. jac_client/examples/basic-auth/src/app.jac +0 -377
  102. jac_client/examples/basic-auth-with-router/src/app.jac +0 -464
  103. jac_client/examples/basic-full-stack/src/app.jac +0 -365
  104. jac_client/examples/css-styling/js-styling/src/app.jac +0 -84
  105. jac_client/examples/css-styling/material-ui/src/app.jac +0 -122
  106. jac_client/examples/css-styling/pure-css/src/app.jac +0 -64
  107. jac_client/examples/css-styling/sass-example/src/app.jac +0 -64
  108. jac_client/examples/css-styling/styled-components/src/app.jac +0 -71
  109. jac_client/examples/css-styling/tailwind-example/src/app.jac +0 -63
  110. jac_client/examples/full-stack-with-auth/src/app.jac +0 -722
  111. jac_client/examples/little-x/src/app.jac +0 -719
  112. jac_client/examples/nested-folders/nested-advance/src/app.jac +0 -35
  113. jac_client/examples/ts-support/src/app.jac +0 -35
  114. jac_client/examples/with-router/src/app.jac +0 -323
  115. jac_client/plugin/src/babel_processor.jac +0 -18
  116. jac_client/plugin/src/impl/babel_processor.impl.jac +0 -84
  117. jac_client-0.2.6.dist-info/RECORD +0 -74
  118. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/entry_points.txt +0 -0
  119. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2347 @@
1
+ """Implementation of DesktopTarget methods."""
2
+ import from pathlib { Path }
3
+ import from typing { Optional, Any }
4
+ import from jaclang.cli.console { console }
5
+ import from jaclang.project.config { get_config }
6
+ import subprocess;
7
+ import json;
8
+ import shutil;
9
+ import os;
10
+ import platform;
11
+ import stat;
12
+
13
+ """Setup desktop target - scaffold Tauri project structure."""
14
+ impl DesktopTarget.setup(self: DesktopTarget, project_dir: Path) -> None {
15
+ # Define tauri_dir early so we can use it in error handling
16
+ tauri_dir = project_dir / "src-tauri";
17
+ # Create src-tauri directory structure FIRST (before ANY other operations)
18
+ # This ensures the directory exists even if later steps fail
19
+ console.print("\n🖥️ Setting up desktop target (Tauri)", style="bold");
20
+ console.print(f" Project directory: {project_dir}", style="muted");
21
+ # Check if already set up
22
+ if tauri_dir.exists() {
23
+ console.warning("Desktop target already set up. Skipping...");
24
+ return;
25
+ }
26
+ # Ensure project directory exists
27
+ if not project_dir.exists() {
28
+ try {
29
+ project_dir.mkdir(parents=True, exist_ok=True);
30
+ } except Exception as e {
31
+ raise RuntimeError(f"Failed to create project directory: {e}") ;
32
+ }
33
+ }
34
+ # Create src-tauri directory structure IMMEDIATELY (before imports or any other operations)
35
+ console.print(" Creating src-tauri/ directory structure...", style="muted");
36
+ try {
37
+ tauri_dir.mkdir(parents=True, exist_ok=True);
38
+ (tauri_dir / "src").mkdir(exist_ok=True);
39
+ (tauri_dir / "binaries").mkdir(exist_ok=True);
40
+ console.print(f" ✔ Created {tauri_dir}", style="success");
41
+ } except Exception as e {
42
+ raise RuntimeError(f"Failed to create src-tauri directory: {e}") ;
43
+ }
44
+ # Wrap rest of setup in try-finally to ensure directory exists even if exceptions occur
45
+ try {
46
+ # Now import and do other operations (directory already exists, so if these fail, directory is still there)
47
+ import from jac_client.plugin.src.desktop_config { DesktopConfig }
48
+
49
+ # Load desktop config (will use defaults if [desktop] section doesn't exist)
50
+ # Wrap in try-except to handle config loading errors gracefully
51
+ try {
52
+ desktop_config = DesktopConfig(project_dir=project_dir);
53
+ config_data = desktop_config.load();
54
+
55
+ project_name = config_data.get('name', 'my-jac-app');
56
+ project_version = config_data.get('version', '1.0.0');
57
+ identifier = config_data.get('identifier', 'com.myapp');
58
+ } except Exception as e {
59
+ console.warning(f" Failed to load desktop config: {e}, using defaults");
60
+ project_name = 'my-jac-app';
61
+ project_version = '1.0.0';
62
+ identifier = 'com.myapp';
63
+ }
64
+
65
+ console.print(
66
+ f" Project name: {project_name}, version: {project_version}",
67
+ style="muted"
68
+ );
69
+ console.print(f" Identifier: {identifier}", style="muted");
70
+
71
+ # Generate tauri.conf.json (don't fail setup if this fails)
72
+ try {
73
+ _generate_tauri_config(
74
+ tauri_dir, project_name, identifier, project_version
75
+ );
76
+ } except Exception as e {
77
+ console.warning(f" Failed to generate tauri.conf.json: {e}");
78
+ console.print(
79
+ " Setup will continue, but tauri.conf.json may be missing.",
80
+ style="muted"
81
+ );
82
+ }
83
+
84
+ # Generate Cargo.toml (don't fail setup if this fails)
85
+ try {
86
+ _generate_cargo_toml(tauri_dir, project_name, identifier);
87
+ } except Exception as e {
88
+ console.warning(f" Failed to generate Cargo.toml: {e}");
89
+ console.print(
90
+ " Setup will continue, but Cargo.toml may be missing.", style="muted"
91
+ );
92
+ }
93
+
94
+ # Generate build.rs (don't fail setup if this fails)
95
+ try {
96
+ _generate_build_rs(tauri_dir);
97
+ } except Exception as e {
98
+ console.warning(f" Failed to generate build.rs: {e}");
99
+ console.print(
100
+ " Setup will continue, but build.rs may be missing.", style="muted"
101
+ );
102
+ }
103
+
104
+ # Generate icons (don't fail setup if this fails)
105
+ try {
106
+ _generate_default_icons(tauri_dir);
107
+ } except Exception as e {
108
+ console.warning(f" Failed to generate icons: {e}");
109
+ console.print(
110
+ " Setup will continue, but icons may be missing.", style="muted"
111
+ );
112
+ }
113
+
114
+ # Update tauri.conf.json to include icons
115
+ try {
116
+ config_path = tauri_dir / "tauri.conf.json";
117
+ if config_path.exists() {
118
+ with open(config_path, "r") as f {
119
+ config = json.load(f);
120
+ }
121
+ _populate_icon_array(tauri_dir, config);
122
+ with open(config_path, "w") as f {
123
+ json.dump(config, f, indent=2);
124
+ }
125
+ }
126
+ } except Exception as e {
127
+ console.warning(f" Failed to update icon array in tauri.conf.json: {e}");
128
+ }
129
+
130
+ # Generate main.rs (don't fail setup if this fails)
131
+ try {
132
+ _generate_main_rs(tauri_dir);
133
+ } except Exception as e {
134
+ console.warning(f" Failed to generate main.rs: {e}");
135
+ console.print(
136
+ " Setup will continue, but main.rs may be missing.", style="muted"
137
+ );
138
+ }
139
+
140
+ # Add [desktop] section to jac.toml (don't fail setup if this fails)
141
+ try {
142
+ _add_desktop_config(project_dir, project_name, identifier, project_version);
143
+ } except Exception as e {
144
+ console.warning(f" Failed to add [desktop] section to jac.toml: {e}");
145
+ console.print(
146
+ " Setup will continue, but jac.toml may not be updated.",
147
+ style="muted"
148
+ );
149
+ }
150
+
151
+ # Check and install required dependencies (wrap in try-except to not fail setup)
152
+ console.print("\n📦 Checking required dependencies...", style="bold");
153
+ try {
154
+ _check_and_install_dependencies();
155
+ } except Exception as e {
156
+ console.warning(f" Dependency check encountered an issue: {e}");
157
+ console.print(
158
+ " Setup will continue, but some dependencies may be missing.",
159
+ style="muted"
160
+ );
161
+ }
162
+
163
+ # Verify essential files exist, recreate if missing
164
+ essential_files = {
165
+ "tauri.conf.json": lambda :
166
+ _generate_tauri_config(
167
+ tauri_dir, project_name, identifier, project_version
168
+ ),
169
+ "Cargo.toml": lambda :
170
+ _generate_cargo_toml(tauri_dir, project_name, identifier),
171
+ "build.rs": lambda : _generate_build_rs(tauri_dir),
172
+ "src/main.rs": lambda : _generate_main_rs(tauri_dir)
173
+ };
174
+
175
+ for file_path in essential_files {
176
+ full_path = tauri_dir / file_path;
177
+ if not full_path.exists() {
178
+ console.warning(f" {file_path} not found, regenerating...");
179
+ try {
180
+ generator = essential_files[file_path];
181
+ generator();
182
+ } except Exception as e {
183
+ console.warning(f" Failed to regenerate {file_path}: {e}");
184
+ }
185
+ }
186
+ }
187
+
188
+ console.success("Desktop target setup complete!");
189
+ console.print("\nNext steps:", style="bold");
190
+ console.print(" 1. Build: jac build main.jac --client desktop");
191
+ console.print(" 2. Dev: jac start main.jac --client desktop");
192
+ } finally {
193
+ # ABSOLUTE GUARANTEE: Ensure src-tauri directory exists no matter what
194
+ if not tauri_dir.exists() {
195
+ try {
196
+ tauri_dir.mkdir(parents=True, exist_ok=True);
197
+ (tauri_dir / "src").mkdir(exist_ok=True);
198
+ (tauri_dir / "binaries").mkdir(exist_ok=True);
199
+ console.print(f" ✔ Ensured {tauri_dir} exists", style="success");
200
+ } except Exception as e {
201
+ console.error(f" CRITICAL: Could not create src-tauri directory: {e}");
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ """Generate identifier from project name."""
208
+ def _generate_identifier(name: str) -> str {
209
+ # Convert to lowercase, replace spaces/special chars with dots
210
+ identifier = name.lower();
211
+ identifier = identifier.replace(" ", ".");
212
+ identifier = identifier.replace("_", ".");
213
+ identifier = identifier.replace("-", ".");
214
+ # Remove invalid characters (keep only alphanumeric and dots)
215
+ filtered = "";
216
+ for char in identifier {
217
+ if char.isalnum() or char == "." {
218
+ filtered += char;
219
+ }
220
+ }
221
+ identifier = filtered;
222
+ # Ensure it starts with a letter
223
+ if identifier and not identifier[0].isalpha() {
224
+ identifier = "com." + identifier;
225
+ }
226
+ # Default if empty
227
+ if not identifier {
228
+ identifier = "com.example.myapp";
229
+ }
230
+ return identifier;
231
+ }
232
+
233
+ """Generate tauri.conf.json."""
234
+ def _generate_tauri_config(
235
+ tauri_dir: Path, name: str, identifier: str, version: str
236
+ ) -> None {
237
+ config = {
238
+ "productName": name,
239
+ "version": version,
240
+ "identifier": identifier,
241
+ "build": {
242
+ "devUrl": "http://localhost:5173",
243
+ "frontendDist": "../.jac/client/dist"
244
+ },
245
+ "app": {
246
+ "windows": [
247
+ {
248
+ "title": name,
249
+ "width": 1200,
250
+ "height": 800,
251
+ "minWidth": 800,
252
+ "minHeight": 600,
253
+ "resizable": True,
254
+ "fullscreen": False
255
+ }
256
+ ],
257
+ "security": {"csp": None}
258
+ },
259
+ "bundle": {"active": True, "targets": "all", "icon": []},
260
+ "plugins": {}
261
+ };
262
+
263
+ config_path = tauri_dir / "tauri.conf.json";
264
+ with open(config_path, "w") as f {
265
+ json.dump(config, f, indent=2);
266
+ }
267
+ try {
268
+ rel_path = config_path.relative_to(tauri_dir.parent);
269
+ console.print(f" ✔ Generated {rel_path}", style="success");
270
+ } except ValueError {
271
+ console.print(f" ✔ Generated {config_path.name}", style="success");
272
+ }
273
+ }
274
+
275
+ """Generate Cargo.toml."""
276
+ def _generate_cargo_toml(tauri_dir: Path, name: str, identifier: str) -> None {
277
+ # Sanitize name for Cargo (Rust package names)
278
+ cargo_name = name.lower().replace(" ", "-").replace("_", "-");
279
+ cargo_name = "".join(c if c.isalnum() or c == "-" else "" for c in cargo_name);
280
+ if not cargo_name {
281
+ cargo_name = "my-jac-app";
282
+ }
283
+
284
+ cargo_toml = f'''[package]
285
+ name = "{cargo_name}"
286
+ version = "0.1.0"
287
+ description = "A Tauri desktop app built with Jac"
288
+ authors = ["you"]
289
+ license = ""
290
+ repository = ""
291
+ edition = "2021"
292
+
293
+ [build-dependencies]
294
+ tauri-build = {{ version = "2.0", features = [] }}
295
+
296
+ [dependencies]
297
+ tauri = {{ version = "2.0", features = [] }}
298
+ serde = {{ version = "1", features = ["derive"] }}
299
+ serde_json = "1"
300
+
301
+ [features]
302
+ # This feature is used for production builds or when `devPath` points to the filesystem
303
+ custom-protocol = ["tauri/custom-protocol"]
304
+ ''';
305
+
306
+ cargo_path = tauri_dir / "Cargo.toml";
307
+ with open(cargo_path, "w") as f {
308
+ f.write(cargo_toml);
309
+ }
310
+ try {
311
+ rel_path = cargo_path.relative_to(tauri_dir.parent);
312
+ console.print(f" ✔ Generated {rel_path}", style="success");
313
+ } except ValueError {
314
+ console.print(f" ✔ Generated {cargo_path.name}", style="success");
315
+ }
316
+ }
317
+
318
+ """Generate build.rs (required for Tauri v2)."""
319
+ def _generate_build_rs(tauri_dir: Path) -> None {
320
+ build_rs = '''fn main() {
321
+ tauri_build::build()
322
+ }
323
+ ''';
324
+
325
+ build_path = tauri_dir / "build.rs";
326
+ with open(build_path, "w") as f {
327
+ f.write(build_rs);
328
+ }
329
+ try {
330
+ rel_path = build_path.relative_to(tauri_dir.parent);
331
+ console.print(f" ✔ Generated {rel_path}", style="success");
332
+ } except ValueError {
333
+ console.print(f" ✔ Generated build.rs", style="success");
334
+ }
335
+ }
336
+
337
+ """Generate default placeholder icons for Tauri."""
338
+ def _generate_default_icons(tauri_dir: Path) -> None {
339
+ icons_dir = tauri_dir / "icons";
340
+ icons_dir.mkdir(exist_ok=True);
341
+
342
+ # Create a 1024x1024 PNG icon as placeholder (AppImage requires square icons, 1024x1024 recommended)
343
+ # Try using PIL/Pillow first (most common), then fallback to subprocess with ImageMagick/convert,
344
+ # finally use a simple Python-based PNG generator
345
+ icon_path = icons_dir / "icon.png";
346
+
347
+ # Method 1: Try using Python with PIL to generate icon
348
+ try {
349
+ import subprocess;
350
+ # Use Python to create a 1024x1024 PNG using PIL if available
351
+ python_code = '''
352
+ import sys
353
+ try:
354
+ from PIL import Image, ImageDraw
355
+ size = 1024
356
+ img = Image.new("RGBA", (size, size), color=(66, 139, 202, 255))
357
+ draw = ImageDraw.Draw(img)
358
+ draw.rectangle([20, 20, size-20, size-20], outline=(255, 255, 255, 255), width=10)
359
+ img.save(sys.argv[1], "PNG")
360
+ sys.exit(0)
361
+ except ImportError:
362
+ sys.exit(1)
363
+ ''';
364
+ result = subprocess.run(
365
+ ["python3", "-c", python_code, str(icon_path)],
366
+ capture_output=True,
367
+ check=True,
368
+ timeout=5
369
+ );
370
+ console.print(" ✔ Generated default icon (1024x1024)", style="success");
371
+ console.warning(
372
+ " Note: Replace icons/icon.png with your app icon (1024x1024 PNG recommended)"
373
+ );
374
+ return;
375
+ } except (
376
+ subprocess.CalledProcessError,
377
+ FileNotFoundError,
378
+ subprocess.TimeoutExpired
379
+ ) {
380
+ # PIL not available, try next method
381
+ }
382
+
383
+ # Method 2: Try ImageMagick convert command
384
+ try {
385
+ import subprocess;
386
+ # Create a 1024x1024 solid blue PNG with alpha channel using ImageMagick
387
+ result = subprocess.run(
388
+ [
389
+ "convert",
390
+ "-size",
391
+ "1024x1024",
392
+ "xc:#428BCA",
393
+ "-alpha",
394
+ "set",
395
+ "-channel",
396
+ "RGBA",
397
+ str(icon_path)
398
+ ],
399
+ capture_output=True,
400
+ check=True,
401
+ timeout=5
402
+ );
403
+ console.print(" ✔ Generated default icon (1024x1024)", style="success");
404
+ console.warning(
405
+ " Note: Replace icons/icon.png with your app icon (1024x1024 PNG recommended)"
406
+ );
407
+ return;
408
+ } except (
409
+ subprocess.CalledProcessError,
410
+ FileNotFoundError,
411
+ subprocess.TimeoutExpired
412
+ ) {
413
+ # ImageMagick not available, try next method
414
+ }
415
+
416
+ # Method 3: Fallback - create a minimal but valid 1024x1024 PNG using Python
417
+ # This creates a simple solid color PNG
418
+ import struct;
419
+ import zlib;
420
+
421
+ width = 1024;
422
+ height = 1024;
423
+
424
+ # PNG signature
425
+ png = bytearray([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
426
+
427
+ # IHDR chunk - RGBA, 8-bit per channel (color type 6 = RGBA)
428
+ ihdr_data = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0);
429
+ ihdr_crc = zlib.crc32(b"IHDR" + ihdr_data) & 0xffffffff;
430
+ png.extend(struct.pack(">I", 13));
431
+ png.extend(b"IHDR");
432
+ png.extend(ihdr_data);
433
+ png.extend(struct.pack(">I", ihdr_crc));
434
+
435
+ # IDAT chunk - solid blue color with alpha (RGBA: 66, 139, 202, 255 = #428BCA)
436
+ # PNG scanlines: each row is prefixed with filter byte (0 = none)
437
+ row_size = width * 4; # RGBA = 4 bytes per pixel
438
+ scanline = bytes([0]) + bytes([66, 139, 202, 255]) * width; # Filter byte + RGBA data
439
+ image_data = scanline * height;
440
+
441
+ # Compress
442
+ compressed = zlib.compress(image_data, level=9);
443
+ idat_crc = zlib.crc32(b"IDAT" + compressed) & 0xffffffff;
444
+ png.extend(struct.pack(">I", len(compressed)));
445
+ png.extend(b"IDAT");
446
+ png.extend(compressed);
447
+ png.extend(struct.pack(">I", idat_crc));
448
+
449
+ # IEND chunk
450
+ png.extend(struct.pack(">I", 0));
451
+ png.extend(b"IEND");
452
+ png.extend(struct.pack(">I", 0xAE426082));
453
+
454
+ # Write icon.png
455
+ with open(icon_path, "wb") as f {
456
+ f.write(png);
457
+ }
458
+
459
+ console.print(" ✔ Generated default icon (1024x1024)", style="success");
460
+ console.warning(
461
+ " Note: Replace icons/icon.png with your app icon (1024x1024 PNG recommended)"
462
+ );
463
+ }
464
+
465
+ """Generate main.rs."""
466
+ def _generate_main_rs(tauri_dir: Path) -> None {
467
+ main_rs = '''// Prevents additional console window on Windows in release, DO NOT REMOVE!!
468
+ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
469
+
470
+ use std::process::{Command, Child};
471
+ use std::sync::Mutex;
472
+ use tauri::Manager;
473
+
474
+ // Global storage for sidecar process
475
+ static SIDECAR_PROCESS: Mutex<Option<Child>> = Mutex::new(None);
476
+
477
+ fn find_and_start_sidecar(app: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {
478
+ // Try to find the sidecar in bundled resources
479
+ let resource_dir = app.path().resource_dir()?;
480
+
481
+ // Possible sidecar names
482
+ let sidecar_names = if cfg!(windows) {
483
+ vec!["binaries/jac-sidecar.exe", "binaries/jac-sidecar.bat"]
484
+ } else {
485
+ vec!["binaries/jac-sidecar", "binaries/jac-sidecar.sh"]
486
+ };
487
+
488
+ let mut sidecar_path = None;
489
+ for name in &sidecar_names {
490
+ let path = resource_dir.join(name);
491
+ if path.exists() {
492
+ sidecar_path = Some(path);
493
+ break;
494
+ }
495
+ }
496
+
497
+ // If not found in resources, try relative to executable
498
+ if sidecar_path.is_none() {
499
+ if let Ok(exe_path) = std::env::current_exe() {
500
+ if let Some(exe_dir) = exe_path.parent() {
501
+ let exe_dir = exe_dir.to_path_buf();
502
+ for name in &sidecar_names {
503
+ let path = exe_dir.join(name);
504
+ if path.exists() {
505
+ sidecar_path = Some(path);
506
+ break;
507
+ }
508
+ }
509
+ }
510
+ }
511
+ }
512
+
513
+ if let Some(sidecar_path) = sidecar_path {
514
+ // Determine module path (try to find main.jac relative to app)
515
+ let module_path = if let Ok(exe_path) = std::env::current_exe() {
516
+ if let Some(exe_dir) = exe_path.parent() {
517
+ // Look for main.jac in parent directories
518
+ let mut current = exe_dir.to_path_buf();
519
+ loop {
520
+ let main_jac = current.join("main.jac");
521
+ if main_jac.exists() {
522
+ break Some(main_jac);
523
+ }
524
+ if !current.pop() {
525
+ break None;
526
+ }
527
+ }
528
+ } else {
529
+ None
530
+ }
531
+ } else {
532
+ None
533
+ };
534
+
535
+ // Build command to start sidecar
536
+ let mut cmd = if cfg!(windows) {
537
+ if sidecar_path.extension().and_then(|s| s.to_str()) == Some("bat") {
538
+ let mut c = Command::new("cmd");
539
+ c.arg("/C");
540
+ c.arg(&sidecar_path);
541
+ c
542
+ } else {
543
+ Command::new(&sidecar_path)
544
+ }
545
+ } else {
546
+ if sidecar_path.extension().and_then(|s| s.to_str()) == Some("sh") {
547
+ let mut c = Command::new("sh");
548
+ c.arg(&sidecar_path);
549
+ c
550
+ } else {
551
+ Command::new(&sidecar_path)
552
+ }
553
+ };
554
+
555
+ // Add arguments
556
+ if let Some(ref mp) = module_path {
557
+ cmd.arg("--module-path").arg(mp);
558
+ } else {
559
+ cmd.arg("--module-path").arg("main.jac");
560
+ }
561
+ cmd.arg("--port").arg("8000");
562
+ cmd.arg("--host").arg("127.0.0.1");
563
+
564
+ // Spawn sidecar process
565
+ match cmd.spawn() {
566
+ Ok(child) => {
567
+ let mut process = SIDECAR_PROCESS.lock().unwrap();
568
+ *process = Some(child);
569
+ eprintln!("Sidecar started successfully");
570
+ Ok(())
571
+ }
572
+ Err(e) => {
573
+ eprintln!("Failed to start sidecar: {}", e);
574
+ Err(Box::new(e))
575
+ }
576
+ }
577
+ } else {
578
+ eprintln!("Sidecar not found in resources, skipping auto-start");
579
+ Ok(())
580
+ }
581
+ }
582
+
583
+ fn stop_sidecar() {
584
+ let mut process = SIDECAR_PROCESS.lock().unwrap();
585
+ if let Some(mut child) = process.take() {
586
+ let _ = child.kill();
587
+ let _ = child.wait();
588
+ eprintln!("Sidecar stopped");
589
+ }
590
+ }
591
+
592
+ fn main() {
593
+ tauri::Builder::default()
594
+ .setup(|app| {
595
+ // Try to start sidecar on app startup
596
+ if let Err(e) = find_and_start_sidecar(app.handle()) {
597
+ eprintln!("Warning: Could not start sidecar: {}", e);
598
+ }
599
+ Ok(())
600
+ })
601
+ .on_window_event(|_window, event| {
602
+ // Clean up sidecar when last window closes
603
+ if matches!(event, tauri::WindowEvent::CloseRequested { .. }) {
604
+ stop_sidecar();
605
+ }
606
+ })
607
+ .run(tauri::generate_context!())
608
+ .expect("error while running tauri application");
609
+
610
+ // Ensure sidecar is stopped on exit
611
+ stop_sidecar();
612
+ }
613
+ ''';
614
+
615
+ main_path = tauri_dir / "src" / "main.rs";
616
+ with open(main_path, "w") as f {
617
+ f.write(main_rs);
618
+ }
619
+ try {
620
+ rel_path = main_path.relative_to(tauri_dir.parent);
621
+ console.print(f" ✔ Generated {rel_path}", style="success");
622
+ } except ValueError {
623
+ console.print(f" ✔ Generated src/main.rs", style="success");
624
+ }
625
+ }
626
+
627
+ """Add [desktop] section to jac.toml."""
628
+ def _add_desktop_config(
629
+ project_dir: Path, name: str, identifier: str, version: str
630
+ ) -> None {
631
+ # Read existing jac.toml
632
+ jac_toml_path = project_dir / "jac.toml";
633
+ if not jac_toml_path.exists() {
634
+ raise RuntimeError("jac.toml not found") ;
635
+ }
636
+
637
+ # Read current content
638
+ with open(jac_toml_path, "r") as f {
639
+ content = f.read();
640
+ }
641
+
642
+ # Check if [desktop] section already exists
643
+ if "[desktop]" in content {
644
+ console.warning(" [desktop] section already exists in jac.toml. Skipping...");
645
+ return;
646
+ }
647
+
648
+ # Append [desktop] section
649
+ desktop_section = f'''
650
+
651
+ # Desktop target configuration (Tauri)
652
+ [desktop]
653
+ name = "{name}"
654
+ identifier = "{identifier}"
655
+ version = "{version}"
656
+
657
+ [desktop.window]
658
+ title = "{name}"
659
+ width = 1200
660
+ height = 800
661
+ min_width = 800
662
+ min_height = 600
663
+ resizable = true
664
+ fullscreen = false
665
+
666
+ [desktop.platforms]
667
+ windows = true
668
+ macos = true
669
+ linux = true
670
+
671
+ [desktop.features]
672
+ system_tray = false
673
+ auto_update = false
674
+ notifications = false
675
+ ''';
676
+
677
+ # Append the section
678
+ with open(jac_toml_path, "a") as f {
679
+ f.write(desktop_section);
680
+ }
681
+ console.print(" ✔ Added [desktop] section to jac.toml", style="success");
682
+ }
683
+
684
+ """Check and install all required dependencies for desktop target."""
685
+ def _check_and_install_dependencies -> None {
686
+ import platform as platform_module;
687
+ import sys;
688
+
689
+ system = platform_module.system();
690
+
691
+ # Check Rust toolchain
692
+ rust_installed = _check_and_install_rust();
693
+
694
+ # Check build tools
695
+ build_tools_installed = _check_and_install_build_tools(system);
696
+
697
+ # Check system dependencies (Linux only)
698
+ if system == "Linux" {
699
+ _check_and_install_linux_dependencies();
700
+ } elif system == "Darwin" {
701
+ _check_macos_dependencies();
702
+ } elif system == "Windows" {
703
+ _check_windows_dependencies();
704
+ }
705
+
706
+ # Check Tauri CLI
707
+ _check_and_install_tauri_cli();
708
+
709
+ # Check Python and jaclang (required for sidecar)
710
+ _check_python_and_jaclang();
711
+
712
+ # Summary
713
+ console.print("\n Dependency check complete!", style="success");
714
+ if not rust_installed {
715
+ console.warning(
716
+ " Rust is required but not installed. Please install it manually."
717
+ );
718
+ }
719
+ if not build_tools_installed {
720
+ console.warning(
721
+ " Build tools are required but not installed. Please install them manually."
722
+ );
723
+ }
724
+ }
725
+
726
+ """Check if Rust toolchain is installed, and offer to install if missing."""
727
+ def _check_and_install_rust -> bool {
728
+ try {
729
+ result = subprocess.run(
730
+ ["rustc", "--version"], capture_output=True, text=True, timeout=5
731
+ );
732
+ if result.returncode == 0 {
733
+ version = result.stdout.strip();
734
+ console.print(f" ✔ Rust toolchain found: {version}", style="success");
735
+ return True;
736
+ }
737
+ } except Exception { }
738
+
739
+ # Rust not found
740
+ console.warning(" Rust toolchain not found");
741
+ console.print(
742
+ " Rust is required for building desktop applications with Tauri.",
743
+ style="muted"
744
+ );
745
+ console.print(" Install Rust: https://rustup.rs/", style="muted");
746
+
747
+ # Try to detect if we can install automatically
748
+ try {
749
+ # Check if curl is available
750
+ subprocess.run(
751
+ ["curl", "--version"], capture_output=True, check=True, timeout=2
752
+ );
753
+ console.print(
754
+ "\n Would you like to install Rust automatically using rustup?",
755
+ style="info"
756
+ );
757
+ response = input(" Install Rust? [Y/n]: ").strip().lower();
758
+ if not response or response in ('y', 'yes') {
759
+ console.print(" Installing Rust...", style="muted");
760
+ console.print(" This may take a few minutes...", style="muted");
761
+ try {
762
+ # Run rustup installer directly via curl pipe
763
+ result = subprocess.run(
764
+ [
765
+ "sh",
766
+ "-c",
767
+ "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"
768
+ ],
769
+ timeout=600, # 10 minute timeout
770
+ check=False
771
+ );
772
+ if result.returncode == 0 {
773
+ console.print(" ✔ Rust installed successfully!", style="success");
774
+ console.print(
775
+ " Note: You may need to restart your terminal or run: source $HOME/.cargo/env",
776
+ style="muted"
777
+ );
778
+ # Try to source cargo env and verify
779
+ import os;
780
+ cargo_bin = os.path.expanduser("~/.cargo/bin");
781
+ if os.path.exists(cargo_bin) {
782
+ # Add to PATH for current session
783
+ current_path = os.environ.get("PATH", "");
784
+ os.environ["PATH"] = f"{cargo_bin}:{current_path}";
785
+ # Verify installation
786
+ try {
787
+ verify_result = subprocess.run(
788
+ ["rustc", "--version"],
789
+ capture_output=True,
790
+ text=True,
791
+ timeout=5
792
+ );
793
+ if verify_result.returncode == 0 {
794
+ version = verify_result.stdout.strip();
795
+ console.print(
796
+ f" ✔ Verified: {version}", style="success"
797
+ );
798
+ return True;
799
+ }
800
+ } except Exception { }
801
+ return True;
802
+ } else {
803
+ console.warning(
804
+ " Rust installation failed. Please install manually."
805
+ );
806
+ }
807
+ }
808
+ } except Exception as e {
809
+ error_str = str(e);
810
+ if "TimeoutExpired" in error_str or "timeout" in error_str.lower() {
811
+ console.warning(
812
+ " Rust installation timed out. Please install manually."
813
+ );
814
+ } else {
815
+ console.warning(f" Failed to install Rust automatically: {e}");
816
+ console.print(
817
+ " Please install manually: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh",
818
+ style="muted"
819
+ );
820
+ }
821
+ }
822
+ } else {
823
+ console.print(
824
+ " Skipping Rust installation. Please install manually.", style="muted"
825
+ );
826
+ }
827
+ } except Exception {
828
+ console.print(
829
+ " curl not found. Cannot install Rust automatically.", style="muted"
830
+ );
831
+ console.print(
832
+ " Please install manually: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh",
833
+ style="muted"
834
+ );
835
+ }
836
+
837
+ return False;
838
+ }
839
+
840
+ """Check and install build tools based on OS."""
841
+ def _check_and_install_build_tools(system: str) -> bool {
842
+ if system == "Linux" {
843
+ # Check for gcc/cc
844
+ try {
845
+ subprocess.run(
846
+ ["gcc", "--version"], capture_output=True, check=True, timeout=5
847
+ );
848
+ console.print(" ✔ Build tools (gcc) found", style="success");
849
+ return True;
850
+ } except Exception {
851
+ console.warning(" Build tools (gcc) not found");
852
+ _try_install_linux_build_tools();
853
+ return False;
854
+ }
855
+ } elif system == "Darwin" {
856
+ # Check for Xcode Command Line Tools
857
+ try {
858
+ result = subprocess.run(
859
+ ["xcode-select", "-p"], capture_output=True, text=True, timeout=5
860
+ );
861
+ if result.returncode == 0 {
862
+ console.print(" ✔ Xcode Command Line Tools found", style="success");
863
+ return True;
864
+ }
865
+ } except Exception { }
866
+ console.warning(" Xcode Command Line Tools not found");
867
+ console.print(" Install with: xcode-select --install", style="muted");
868
+ console.print("\n Would you like to open the installer?", style="info");
869
+ response = input(" Open installer? [Y/n]: ").strip().lower();
870
+ if not response or response in ('y', 'yes') {
871
+ try {
872
+ subprocess.run(["xcode-select", "--install"], check=False);
873
+ console.print(
874
+ " ✔ Installer opened. Please complete the installation.",
875
+ style="success"
876
+ );
877
+ } except Exception {
878
+ console.warning(
879
+ " Failed to open installer. Please run: xcode-select --install"
880
+ );
881
+ }
882
+ }
883
+ return False;
884
+ } elif system == "Windows" {
885
+ # Check for Visual Studio Build Tools
886
+ console.warning(" Build tools check not implemented for Windows");
887
+ console.print(
888
+ " Please install Visual Studio Build Tools manually:", style="muted"
889
+ );
890
+ console.print(
891
+ " https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022",
892
+ style="muted"
893
+ );
894
+ return False;
895
+ }
896
+ return False;
897
+ }
898
+
899
+ """Try to install Linux build tools."""
900
+ def _try_install_linux_build_tools -> None {
901
+ import platform as platform_module;
902
+ import os;
903
+
904
+ # Detect Linux distribution
905
+ distro = _detect_linux_distro();
906
+
907
+ if not distro {
908
+ console.print(
909
+ " Could not detect Linux distribution. Please install build tools manually.",
910
+ style="muted"
911
+ );
912
+ return;
913
+ }
914
+
915
+ console.print(f" Detected: {distro}", style="muted");
916
+ console.print(
917
+ "\n Would you like to install build tools automatically?", style="info"
918
+ );
919
+ response = input(" Install build tools? [Y/n]: ").strip().lower();
920
+ if not response or response in ('y', 'yes') {
921
+ try {
922
+ if distro in ("ubuntu", "debian") {
923
+ console.print(" Installing build-essential...", style="muted");
924
+ result = subprocess.run(
925
+ ["sudo", "apt-get", "update"], check=False, timeout=60
926
+ );
927
+ if result.returncode == 0 {
928
+ result = subprocess.run(
929
+ ["sudo", "apt-get", "install", "-y", "build-essential"],
930
+ check=False,
931
+ timeout=300
932
+ );
933
+ if result.returncode == 0 {
934
+ console.print(" ✔ Build tools installed", style="success");
935
+ return;
936
+ }
937
+ }
938
+ } elif distro == "fedora" {
939
+ console.print(" Installing gcc gcc-c++...", style="muted");
940
+ result = subprocess.run(
941
+ ["sudo", "dnf", "install", "-y", "gcc", "gcc-c++"],
942
+ check=False,
943
+ timeout=300
944
+ );
945
+ if result.returncode == 0 {
946
+ console.print(" ✔ Build tools installed", style="success");
947
+ return;
948
+ }
949
+ } elif distro == "arch" {
950
+ console.print(" Installing base-devel...", style="muted");
951
+ result = subprocess.run(
952
+ ["sudo", "pacman", "-S", "--noconfirm", "base-devel"],
953
+ check=False,
954
+ timeout=300
955
+ );
956
+ if result.returncode == 0 {
957
+ console.print(" ✔ Build tools installed", style="success");
958
+ return;
959
+ }
960
+ }
961
+ console.warning(" Failed to install build tools automatically");
962
+ } except Exception as e {
963
+ console.warning(f" Error installing build tools: {e}");
964
+ }
965
+ }
966
+ }
967
+
968
+ """Detect Linux distribution."""
969
+ def _detect_linux_distro -> str | None {
970
+ import os;
971
+ try {
972
+ # Check /etc/os-release
973
+ if os.path.exists("/etc/os-release") {
974
+ with open("/etc/os-release", "r") as f {
975
+ content = f.read().lower();
976
+ if "ubuntu" in content or "debian" in content {
977
+ if "ubuntu" in content {
978
+ return "ubuntu";
979
+ }
980
+ return "debian";
981
+ } elif "fedora" in content {
982
+ return "fedora";
983
+ } elif "arch" in content or "manjaro" in content {
984
+ return "arch";
985
+ }
986
+ }
987
+ }
988
+ } except Exception {
989
+ return None;
990
+ }
991
+ }
992
+
993
+ """Check and install Linux system dependencies."""
994
+ def _check_and_install_linux_dependencies -> None {
995
+ # Check pkg-config
996
+ pkg_config_ok = False;
997
+ try {
998
+ subprocess.run(
999
+ ["pkg-config", "--version"], capture_output=True, check=True, timeout=5
1000
+ );
1001
+ console.print(" ✔ pkg-config found", style="success");
1002
+ pkg_config_ok = True;
1003
+ } except Exception {
1004
+ console.warning(" pkg-config not found");
1005
+ }
1006
+
1007
+ # Check for GTK/WebKit libraries
1008
+ webkit_ok = False;
1009
+ try {
1010
+ result = subprocess.run(
1011
+ ["pkg-config", "--exists", "webkit2gtk-4.1"],
1012
+ capture_output=True,
1013
+ timeout=5
1014
+ );
1015
+ if result.returncode == 0 {
1016
+ console.print(" ✔ GTK/WebKit libraries found", style="success");
1017
+ webkit_ok = True;
1018
+ }
1019
+ } except Exception {
1020
+ console.warning(" GTK/WebKit libraries not found");
1021
+ }
1022
+
1023
+ if pkg_config_ok and webkit_ok {
1024
+ return; # All dependencies satisfied
1025
+
1026
+ }
1027
+
1028
+ # Missing dependencies
1029
+ console.warning(" Some Linux system dependencies are missing");
1030
+ distro = _detect_linux_distro();
1031
+
1032
+ if distro {
1033
+ console.print(f" Detected: {distro}", style="muted");
1034
+ console.print(
1035
+ "\n Would you like to install missing dependencies automatically?",
1036
+ style="info"
1037
+ );
1038
+ response = input(" Install dependencies? [Y/n]: ").strip().lower();
1039
+ if not response or response in ('y', 'yes') {
1040
+ _try_install_linux_system_deps(distro);
1041
+ } else {
1042
+ _print_manual_install_instructions(distro);
1043
+ }
1044
+ } else {
1045
+ _print_manual_install_instructions(None);
1046
+ }
1047
+ }
1048
+
1049
+ """Try to install Linux system dependencies."""
1050
+ def _try_install_linux_system_deps(distro: str) -> None {
1051
+ try {
1052
+ if distro in ("ubuntu", "debian") {
1053
+ deps = [
1054
+ "libwebkit2gtk-4.1-dev",
1055
+ "build-essential",
1056
+ "curl",
1057
+ "wget",
1058
+ "libssl-dev",
1059
+ "libgtk-3-dev",
1060
+ "libayatana-appindicator3-dev",
1061
+ "librsvg2-dev"
1062
+ ];
1063
+ console.print(" Installing dependencies...", style="muted");
1064
+ result = subprocess.run(
1065
+ ["sudo", "apt-get", "update"], check=False, timeout=60
1066
+ );
1067
+ if result.returncode == 0 {
1068
+ result = subprocess.run(
1069
+ ["sudo", "apt-get", "install", "-y"] + deps,
1070
+ check=False,
1071
+ timeout=600
1072
+ );
1073
+ if result.returncode == 0 {
1074
+ console.print(" ✔ Dependencies installed", style="success");
1075
+ return;
1076
+ }
1077
+ }
1078
+ } elif distro == "fedora" {
1079
+ deps = [
1080
+ "webkit2gtk3-devel.x86_64",
1081
+ "openssl-devel",
1082
+ "curl",
1083
+ "wget",
1084
+ "libappindicator-gtk3",
1085
+ "librsvg2-devel"
1086
+ ];
1087
+ console.print(" Installing dependencies...", style="muted");
1088
+ result = subprocess.run(
1089
+ ["sudo", "dnf", "install", "-y"] + deps, check=False, timeout=600
1090
+ );
1091
+ if result.returncode == 0 {
1092
+ console.print(" ✔ Dependencies installed", style="success");
1093
+ return;
1094
+ }
1095
+ } elif distro == "arch" {
1096
+ deps = [
1097
+ "webkit2gtk",
1098
+ "base-devel",
1099
+ "curl",
1100
+ "wget",
1101
+ "openssl",
1102
+ "appmenu-gtk-module",
1103
+ "gtk3",
1104
+ "libappindicator-gtk3",
1105
+ "librsvg",
1106
+ "libvips"
1107
+ ];
1108
+ console.print(" Installing dependencies...", style="muted");
1109
+ result = subprocess.run(
1110
+ ["sudo", "pacman", "-S", "--noconfirm"] + deps,
1111
+ check=False,
1112
+ timeout=600
1113
+ );
1114
+ if result.returncode == 0 {
1115
+ console.print(" ✔ Dependencies installed", style="success");
1116
+ return;
1117
+ }
1118
+ }
1119
+ console.warning(" Failed to install dependencies automatically");
1120
+ _print_manual_install_instructions(distro);
1121
+ } except Exception as e {
1122
+ console.warning(f" Error installing dependencies: {e}");
1123
+ _print_manual_install_instructions(distro);
1124
+ }
1125
+ }
1126
+
1127
+ """Print manual installation instructions."""
1128
+ def _print_manual_install_instructions(distro: str | None) -> None {
1129
+ console.print(" Install system dependencies manually:", style="muted");
1130
+ if distro in ("ubuntu", "debian") {
1131
+ console.print(
1132
+ " sudo apt-get install libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev",
1133
+ style="muted"
1134
+ );
1135
+ } elif distro == "fedora" {
1136
+ console.print(
1137
+ " sudo dnf install webkit2gtk3-devel.x86_64 openssl-devel curl wget libappindicator-gtk3 librsvg2-devel",
1138
+ style="muted"
1139
+ );
1140
+ } elif distro == "arch" {
1141
+ console.print(
1142
+ " sudo pacman -S webkit2gtk base-devel curl wget openssl appmenu-gtk-module gtk3 libappindicator-gtk3 librsvg libvips",
1143
+ style="muted"
1144
+ );
1145
+ } else {
1146
+ console.print(
1147
+ " Ubuntu/Debian: sudo apt-get install libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev",
1148
+ style="muted"
1149
+ );
1150
+ console.print(
1151
+ " Fedora: sudo dnf install webkit2gtk3-devel.x86_64 openssl-devel curl wget libappindicator-gtk3 librsvg2-devel",
1152
+ style="muted"
1153
+ );
1154
+ console.print(
1155
+ " Arch: sudo pacman -S webkit2gtk base-devel curl wget openssl appmenu-gtk-module gtk3 libappindicator-gtk3 librsvg libvips",
1156
+ style="muted"
1157
+ );
1158
+ }
1159
+ }
1160
+
1161
+ """Check macOS dependencies."""
1162
+ def _check_macos_dependencies -> None {
1163
+ # macOS dependencies are typically handled by Xcode Command Line Tools
1164
+ # which is checked in _check_and_install_build_tools
1165
+ console.print(
1166
+ " ✔ macOS dependencies (handled by Xcode Command Line Tools)", style="success"
1167
+ );
1168
+ }
1169
+
1170
+ """Check Windows dependencies."""
1171
+ def _check_windows_dependencies -> None {
1172
+ console.warning(" Windows dependencies check not fully implemented");
1173
+ console.print(
1174
+ " Please ensure Visual Studio Build Tools are installed:", style="muted"
1175
+ );
1176
+ console.print(
1177
+ " https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022",
1178
+ style="muted"
1179
+ );
1180
+ }
1181
+
1182
+ """Check Python and jaclang (required for sidecar functionality)."""
1183
+ def _check_python_and_jaclang -> None {
1184
+ # Check Python - try python3 first, then python
1185
+ python_ok = False;
1186
+ python_cmd = None;
1187
+ python_version = None;
1188
+
1189
+ try {
1190
+ result = subprocess.run(
1191
+ ["python3", "--version"], capture_output=True, text=True, timeout=5
1192
+ );
1193
+ if result.returncode == 0 {
1194
+ python_version = result.stdout.strip();
1195
+ console.print(f" ✔ Python found: {python_version}", style="success");
1196
+ python_ok = True;
1197
+ python_cmd = "python3";
1198
+ }
1199
+ } except Exception { }
1200
+
1201
+ # Try python command as fallback if python3 not found
1202
+ if not python_ok {
1203
+ try {
1204
+ result = subprocess.run(
1205
+ ["python", "--version"], capture_output=True, text=True, timeout=5
1206
+ );
1207
+ if result.returncode == 0 {
1208
+ python_version = result.stdout.strip();
1209
+ console.print(f" ✔ Python found: {python_version}", style="success");
1210
+ python_ok = True;
1211
+ python_cmd = "python";
1212
+ }
1213
+ } except Exception { }
1214
+ }
1215
+
1216
+ if not python_ok {
1217
+ console.warning(" Python not found");
1218
+ console.print(
1219
+ " Python is required for sidecar functionality (Jac backend).",
1220
+ style="muted"
1221
+ );
1222
+ console.print(
1223
+ " Install Python: https://www.python.org/downloads/", style="muted"
1224
+ );
1225
+ return;
1226
+ }
1227
+
1228
+ # Check jaclang using the detected python command
1229
+ jaclang_ok = False;
1230
+ try {
1231
+ result = subprocess.run(
1232
+ [python_cmd, "-m", "jaclang", "--version"],
1233
+ capture_output=True,
1234
+ text=True,
1235
+ timeout=5
1236
+ );
1237
+ if result.returncode == 0 {
1238
+ version = result.stdout.strip();
1239
+ console.print(f" ✔ jaclang found: {version}", style="success");
1240
+ jaclang_ok = True;
1241
+ }
1242
+ } except Exception { }
1243
+
1244
+ if not jaclang_ok {
1245
+ console.warning(" jaclang not found");
1246
+ console.print(
1247
+ " jaclang is required for sidecar functionality (Jac backend).",
1248
+ style="muted"
1249
+ );
1250
+ console.print("\n Would you like to install jaclang?", style="info");
1251
+ response = input(" Install jaclang? [Y/n]: ").strip().lower();
1252
+ if not response or response in ('y', 'yes') {
1253
+ console.print(" Installing jaclang...", style="muted");
1254
+ try {
1255
+ result = subprocess.run(
1256
+ [python_cmd, "-m", "pip", "install", "jaclang"],
1257
+ check=False,
1258
+ timeout=300,
1259
+ capture_output=False # Show output
1260
+ );
1261
+ if result.returncode == 0 {
1262
+ console.print(" ✔ jaclang installed", style="success");
1263
+ } else {
1264
+ console.warning(" Failed to install jaclang");
1265
+ console.print(
1266
+ " Please install manually: pip install jaclang", style="muted"
1267
+ );
1268
+ }
1269
+ } except Exception as e {
1270
+ console.warning(f" Error installing jaclang: {e}");
1271
+ console.print(
1272
+ " Please install manually: pip install jaclang", style="muted"
1273
+ );
1274
+ }
1275
+ } else {
1276
+ console.print(" Skipping jaclang installation.", style="muted");
1277
+ console.print(
1278
+ " Sidecar will not work without jaclang. Install with: pip install jaclang",
1279
+ style="muted"
1280
+ );
1281
+ }
1282
+ }
1283
+ }
1284
+
1285
+ """Check and install Tauri CLI if needed."""
1286
+ def _check_and_install_tauri_cli -> None {
1287
+ # Check if cargo tauri is available (preferred method)
1288
+ cargo_tauri_ok = False;
1289
+ try {
1290
+ result = subprocess.run(
1291
+ ["cargo", "tauri", "--version"], capture_output=True, text=True, timeout=5
1292
+ );
1293
+ if result.returncode == 0 {
1294
+ version = result.stdout.strip();
1295
+ console.print(f" ✔ Tauri CLI found (cargo): {version}", style="success");
1296
+ return;
1297
+ }
1298
+ } except Exception { }
1299
+
1300
+ # Check if bun tauri CLI is available (global install)
1301
+ bun_tauri_ok = False;
1302
+ try {
1303
+ result = subprocess.run(
1304
+ ["bun", "pm", "ls", "-g"], capture_output=True, text=True, timeout=5
1305
+ );
1306
+ if result.returncode == 0 and "@tauri-apps/cli" in result.stdout {
1307
+ console.print(" ✔ Tauri CLI found (bun)", style="success");
1308
+ return;
1309
+ }
1310
+ } except Exception { }
1311
+
1312
+ # Tauri CLI not found
1313
+ console.warning(" Tauri CLI not found");
1314
+ console.print(
1315
+ " Tauri CLI is required for building desktop applications.", style="muted"
1316
+ );
1317
+
1318
+ # Check if Rust/Cargo is available (required for cargo install)
1319
+ cargo_available = False;
1320
+ try {
1321
+ subprocess.run(
1322
+ ["cargo", "--version"], capture_output=True, check=True, timeout=5
1323
+ );
1324
+ cargo_available = True;
1325
+ } except Exception { }
1326
+
1327
+ # Check if bun is available
1328
+ bun_available = False;
1329
+ try {
1330
+ subprocess.run(
1331
+ ["bun", "--version"], capture_output=True, check=True, timeout=5
1332
+ );
1333
+ bun_available = True;
1334
+ } except Exception { }
1335
+
1336
+ if not cargo_available and not bun_available {
1337
+ console.print(
1338
+ " Neither cargo nor bun is available. Cannot install Tauri CLI automatically.",
1339
+ style="muted"
1340
+ );
1341
+ console.print(
1342
+ " Please install Rust (for cargo) or Bun (https://bun.sh) first.",
1343
+ style="muted"
1344
+ );
1345
+ return;
1346
+ }
1347
+
1348
+ # Offer to install
1349
+ console.print("\n Would you like to install Tauri CLI?", style="info");
1350
+ response = input(" Install Tauri CLI? [Y/n]: ").strip().lower();
1351
+ if not response or response in ('y', 'yes') {
1352
+ if cargo_available {
1353
+ console.print(" Installing Tauri CLI via cargo...", style="muted");
1354
+ try {
1355
+ result = subprocess.run(
1356
+ ["cargo", "install", "tauri-cli"],
1357
+ check=False,
1358
+ timeout=600,
1359
+ capture_output=False # Show output
1360
+ );
1361
+ if result.returncode == 0 {
1362
+ console.print(" ✔ Tauri CLI installed", style="success");
1363
+ return;
1364
+ } else {
1365
+ console.warning(" Failed to install via cargo. Trying npm...");
1366
+ }
1367
+ } except Exception as e {
1368
+ console.warning(f" Error installing via cargo: {e}");
1369
+ }
1370
+ }
1371
+ if bun_available {
1372
+ console.print(" Installing Tauri CLI via bun...", style="muted");
1373
+ try {
1374
+ result = subprocess.run(
1375
+ ["bun", "add", "-g", "@tauri-apps/cli"],
1376
+ check=False,
1377
+ timeout=300,
1378
+ capture_output=False # Show output
1379
+ );
1380
+ if result.returncode == 0 {
1381
+ console.print(" ✔ Tauri CLI installed", style="success");
1382
+ return;
1383
+ } else {
1384
+ console.warning(" Failed to install via bun");
1385
+ }
1386
+ } except Exception as e {
1387
+ console.warning(f" Error installing via bun: {e}");
1388
+ }
1389
+ }
1390
+ console.print(" Please install manually:", style="muted");
1391
+ if cargo_available {
1392
+ console.print(" cargo install tauri-cli", style="muted");
1393
+ }
1394
+ if bun_available {
1395
+ console.print(" bun add -g @tauri-apps/cli", style="muted");
1396
+ }
1397
+ } else {
1398
+ console.print(" Skipping Tauri CLI installation.", style="muted");
1399
+ console.print(
1400
+ " It will be needed when building. Install with:", style="muted"
1401
+ );
1402
+ if cargo_available {
1403
+ console.print(" cargo install tauri-cli", style="muted");
1404
+ }
1405
+ if bun_available {
1406
+ console.print(" bun add -g @tauri-apps/cli", style="muted");
1407
+ }
1408
+ }
1409
+ }
1410
+
1411
+ """Build desktop app - build web bundle first, then wrap with Tauri."""
1412
+ impl DesktopTarget.build(
1413
+ self: DesktopTarget,
1414
+ entry_file: Path,
1415
+ project_dir: Path,
1416
+ platform: Optional[str] = None
1417
+ ) -> Path {
1418
+ import from jac_client.plugin.src.targets.web_target { WebTarget }
1419
+ import from jac_client.plugin.src.vite_bundler { ViteBundler }
1420
+ console.print("\n🖥️ Building desktop app (Tauri)", style="bold");
1421
+ # Check if setup has been run
1422
+ tauri_dir = project_dir / "src-tauri";
1423
+ if not tauri_dir.exists() {
1424
+ raise RuntimeError("Desktop target not set up. Run 'jac setup desktop' first.") ;
1425
+ }
1426
+ # Step 1: Build web bundle first (reuse existing pipeline)
1427
+ console.print(" Step 1: Building web bundle...", style="muted");
1428
+ web_target = WebTarget();
1429
+ web_bundle_path = web_target.build(entry_file, project_dir, platform);
1430
+ console.print(f" ✔ Web bundle built: {web_bundle_path}", style="success");
1431
+ # Step 1.5: Bundle sidecar (optional - can be skipped if not needed)
1432
+ # This bundles the Jac backend as an executable for local use
1433
+ sidecar_bundled = False;
1434
+ try {
1435
+ console.print(" Step 1.5: Bundling sidecar (Jac backend)...", style="muted");
1436
+ sidecar_path = _bundle_sidecar(entry_file, project_dir, tauri_dir, platform);
1437
+ if sidecar_path and sidecar_path.exists() {
1438
+ console.print(f" ✔ Sidecar bundled: {sidecar_path}", style="success");
1439
+ sidecar_bundled = True;
1440
+ }
1441
+ } except Exception as e {
1442
+ console.warning(f" Sidecar bundling skipped: {e}");
1443
+ console.print(
1444
+ " Note: Desktop app will need external API server", style="muted"
1445
+ );
1446
+ sidecar_bundled = False;
1447
+ }
1448
+ # Step 1.5: Check and regenerate icon if needed (AppImage requires square icons)
1449
+ icon_path = tauri_dir / "icons" / "icon.png";
1450
+ if icon_path.exists() {
1451
+ try {
1452
+ # Try to check icon size and squareness using PIL
1453
+ import subprocess;
1454
+ check_code = '''
1455
+ import sys
1456
+ from PIL import Image
1457
+ try:
1458
+ img = Image.open(sys.argv[1])
1459
+ # AppImage requires square icons, and 1024x1024 is recommended
1460
+ if img.width != img.height or img.width < 512:
1461
+ sys.exit(1) # Not square or too small
1462
+ # Verify it's RGBA format
1463
+ if img.mode != "RGBA":
1464
+ sys.exit(1) # Not RGBA
1465
+ sys.exit(0) # Icon OK
1466
+ except:
1467
+ sys.exit(1) # Error or invalid
1468
+ ''';
1469
+ result = subprocess.run(
1470
+ ["python3", "-c", check_code, str(icon_path)],
1471
+ capture_output=True,
1472
+ timeout=5
1473
+ );
1474
+ if result.returncode != 0 {
1475
+ console.warning(
1476
+ " Icon is invalid or too small for AppImage, regenerating..."
1477
+ );
1478
+ # Delete old icon before regenerating
1479
+ if icon_path.exists() {
1480
+ icon_path.unlink();
1481
+ }
1482
+ _generate_default_icons(tauri_dir);
1483
+ }
1484
+ } except Exception {
1485
+ # If check fails, try to regenerate anyway
1486
+ console.warning(" Could not verify icon, regenerating...");
1487
+ _generate_default_icons(tauri_dir);
1488
+ }
1489
+ } else {
1490
+ # Icon doesn't exist, generate it
1491
+ _generate_default_icons(tauri_dir);
1492
+ }
1493
+ # Step 2: Update tauri.conf.json to point to web bundle
1494
+ console.print(" Step 2: Updating Tauri configuration...", style="muted");
1495
+ _update_tauri_config_for_build(tauri_dir, project_dir, web_bundle_path);
1496
+ # Step 3: Run cargo tauri build
1497
+ console.print(" Step 3: Building Tauri app...", style="muted");
1498
+ bundle_path = _run_tauri_build(tauri_dir, platform);
1499
+ console.success(f"Desktop app built successfully!");
1500
+ console.print(f" Output: {bundle_path}", style="muted");
1501
+ return bundle_path;
1502
+ }
1503
+
1504
+ """Add sidecar binary to tauri.conf.json if it exists."""
1505
+ def _add_sidecar_to_config(tauri_dir: Path, config: dict) -> None {
1506
+ binaries_dir = tauri_dir / "binaries";
1507
+ if not binaries_dir.exists() {
1508
+ return; # No binaries directory, skip
1509
+
1510
+ }
1511
+
1512
+ # Find sidecar wrapper script (must exist and be executable)
1513
+ sidecar_files = [];
1514
+ for pattern in [
1515
+ "jac-sidecar.sh",
1516
+ "jac-sidecar.bat",
1517
+ "jac-sidecar",
1518
+ "jac-sidecar.exe"
1519
+ ] {
1520
+ found = list(binaries_dir.glob(pattern));
1521
+ for f in found {
1522
+ if f.is_file() and f.exists() {
1523
+ sidecar_files.append(f);
1524
+ }
1525
+ }
1526
+ }
1527
+
1528
+ if not sidecar_files {
1529
+ # Remove sidecar from resources if it was previously added but no longer exists
1530
+ if "bundle" in config and "resources" in config["bundle"] {
1531
+ resources = config["bundle"]["resources"];
1532
+ # Remove any jac-sidecar entries
1533
+ config["bundle"]["resources"] = [
1534
+ r
1535
+ for r in resources
1536
+ if not ("jac-sidecar" in r or "binaries/jac-sidecar" in r)
1537
+ ];
1538
+ }
1539
+ return; # No sidecar found, skip
1540
+
1541
+ }
1542
+
1543
+ # Tauri v2 uses "resources" in bundle section for sidecars
1544
+ if "bundle" not in config {
1545
+ config["bundle"] = {};
1546
+ }
1547
+
1548
+ # Add resources array if it doesn't exist
1549
+ if "resources" not in config["bundle"] {
1550
+ config["bundle"]["resources"] = [];
1551
+ }
1552
+
1553
+ # Add sidecar binary path (relative to tauri_dir)
1554
+ for sidecar_file in sidecar_files {
1555
+ rel_path = sidecar_file.relative_to(tauri_dir);
1556
+ rel_str = str(rel_path.as_posix());
1557
+ if rel_str not in config["bundle"]["resources"] {
1558
+ config["bundle"]["resources"].append(rel_str);
1559
+ console.print(f" ✔ Added sidecar to config: {rel_str}", style="success");
1560
+ }
1561
+ }
1562
+ }
1563
+
1564
+ """Populate icon array in tauri.conf.json from icons directory."""
1565
+ def _populate_icon_array(tauri_dir: Path, config: dict) -> None {
1566
+ # Populate icon array if empty or missing
1567
+ if "bundle" not in config {
1568
+ config["bundle"] = {};
1569
+ }
1570
+ if "icon" not in config["bundle"]
1571
+ or not config["bundle"]["icon"]
1572
+ or len(config["bundle"]["icon"]) == 0 {
1573
+ # Scan icons directory for PNG files
1574
+ icons_dir = tauri_dir / "icons";
1575
+ icon_files = [];
1576
+ if icons_dir.exists() {
1577
+ for icon_file in icons_dir.iterdir() {
1578
+ if icon_file.is_file() and icon_file.suffix.lower() == ".png" {
1579
+ # Path relative to tauri.conf.json (which is in tauri_dir)
1580
+ # So icons/icon.png is the correct relative path
1581
+ icon_rel_path = icon_file.relative_to(tauri_dir);
1582
+ icon_files.append(str(icon_rel_path.as_posix()));
1583
+ }
1584
+ }
1585
+ }
1586
+ # Sort to ensure consistent ordering (icon.png first if it exists)
1587
+ icon_files.sort();
1588
+ config["bundle"]["icon"] = icon_files;
1589
+ if icon_files {
1590
+ console.print(
1591
+ f" ✔ Populated icon array with {len(icon_files)} icon(s)",
1592
+ style="success"
1593
+ );
1594
+ }
1595
+ }
1596
+ }
1597
+
1598
+ """Update tauri.conf.json to point to the built web bundle."""
1599
+ def _update_tauri_config_for_build(
1600
+ tauri_dir: Path,
1601
+ project_dir: Path,
1602
+ web_bundle_path: Path,
1603
+ sidecar_bundled: bool = False
1604
+ ) -> None {
1605
+ import json;
1606
+
1607
+ config_path = tauri_dir / "tauri.conf.json";
1608
+ if not config_path.exists() {
1609
+ raise RuntimeError("tauri.conf.json not found. Run 'jac setup desktop' first.") ;
1610
+ }
1611
+
1612
+ # Read existing config
1613
+ with open(config_path, "r") as f {
1614
+ config = json.load(f);
1615
+ }
1616
+
1617
+ # Calculate relative path from tauri_dir to dist directory (not the bundle file)
1618
+ # web_bundle_path is the JS file, but frontendDist needs the directory containing index.html
1619
+ dist_dir = web_bundle_path.parent;
1620
+
1621
+ try {
1622
+ # Dist directory is typically in .jac/client/dist
1623
+ # Relative to src-tauri, that's ../.jac/client/dist
1624
+ dist_relative = dist_dir.relative_to(project_dir);
1625
+ dist_relative_str = "../" + str(dist_relative.as_posix());
1626
+ } except ValueError {
1627
+ # If paths don't share a common root, use absolute path
1628
+ dist_relative_str = str(dist_dir.as_posix());
1629
+ }
1630
+
1631
+ # Update build config (Tauri v2 structure)
1632
+ if "build" not in config {
1633
+ config["build"] = {};
1634
+ }
1635
+ # Tauri v2 uses 'frontendDist' to point to the directory containing index.html
1636
+ config["build"]["frontendDist"] = dist_relative_str;
1637
+ # Remove devUrl and other invalid properties
1638
+ if "devUrl" in config["build"] {
1639
+ del config["build"]["devUrl"];
1640
+ }
1641
+ if "devPath" in config["build"] {
1642
+ del config["build"]["devPath"];
1643
+ }
1644
+ if "distDir" in config["build"] {
1645
+ del config["build"]["distDir"];
1646
+ }
1647
+ if "withGlobalTauri" in config["build"] {
1648
+ del config["build"]["withGlobalTauri"];
1649
+ }
1650
+ # Clean up null values
1651
+ if "beforeBuildCommand" in config["build"]
1652
+ and config["build"]["beforeBuildCommand"] is None {
1653
+ del config["build"]["beforeBuildCommand"];
1654
+ }
1655
+
1656
+ # Populate icon array if empty or missing
1657
+ _populate_icon_array(tauri_dir, config);
1658
+
1659
+ # Add sidecar binary if it exists
1660
+ _add_sidecar_to_config(tauri_dir, config);
1661
+
1662
+ # Write updated config
1663
+ with open(config_path, "w") as f {
1664
+ json.dump(config, f, indent=2);
1665
+ }
1666
+
1667
+ console.print(" ✔ Updated tauri.conf.json", style="success");
1668
+ }
1669
+
1670
+ """Check if appimagetool is available and can run."""
1671
+ def _check_appimagetool -> tuple[bool, str | None] {
1672
+ # First check if the command exists
1673
+ try {
1674
+ result = subprocess.run(
1675
+ ["which", "appimagetool"], capture_output=True, timeout=2
1676
+ );
1677
+ if result.returncode != 0 {
1678
+ return (False, "appimagetool not found in PATH");
1679
+ }
1680
+ } except Exception {
1681
+ return (False, "Could not check for appimagetool");
1682
+ }
1683
+
1684
+ # Try to run appimagetool to see if it works
1685
+ try {
1686
+ result = subprocess.run(
1687
+ ["appimagetool", "--version"], capture_output=True, timeout=5, text=True
1688
+ );
1689
+ if result.returncode == 0 {
1690
+ return (True, None);
1691
+ } else {
1692
+ # Check if it's a FUSE error
1693
+ error_output = result.stderr or result.stdout or "";
1694
+ if "fuse" in error_output.lower() or "libfuse" in error_output.lower() {
1695
+ return (
1696
+ False,
1697
+ "FUSE library not available. Install with: sudo apt-get install libfuse2"
1698
+ );
1699
+ }
1700
+ return (False, f"appimagetool failed: {error_output[:100]}");
1701
+ }
1702
+ } except FileNotFoundError {
1703
+ return (False, "appimagetool not found");
1704
+ } except subprocess.TimeoutExpired {
1705
+ return (False, "appimagetool check timed out");
1706
+ } except Exception as e {
1707
+ return (False, f"Error checking appimagetool: {str(e)[:100]}");
1708
+ }
1709
+ }
1710
+
1711
+ """Create AppImage from AppDir if appimagetool is available."""
1712
+ def _create_appimage_from_appdir(appdir_path: Path) -> Path | None {
1713
+ if not appdir_path.exists() or not appdir_path.is_dir() {
1714
+ return None;
1715
+ }
1716
+
1717
+ # Check if appimagetool is available and working
1718
+ (is_available, error_msg) = _check_appimagetool();
1719
+ if not is_available {
1720
+ console.warning(" appimagetool not available. AppImage will not be created.");
1721
+ if error_msg {
1722
+ console.print(f" {error_msg}", style="muted");
1723
+ }
1724
+ # Provide installation instructions based on the error
1725
+ if error_msg and "fuse" in error_msg.lower() {
1726
+ console.print(" For WSL2, you may need to install FUSE:", style="muted");
1727
+ console.print(
1728
+ " sudo apt-get update && sudo apt-get install -y libfuse2",
1729
+ style="muted"
1730
+ );
1731
+ console.print(
1732
+ " Or use --appimage-extract to extract appimagetool first",
1733
+ style="muted"
1734
+ );
1735
+ } else {
1736
+ console.print(" Install it with:", style="muted");
1737
+ console.print(
1738
+ " wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage",
1739
+ style="muted"
1740
+ );
1741
+ console.print(" chmod +x appimagetool-x86_64.AppImage", style="muted");
1742
+ console.print(
1743
+ " sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool",
1744
+ style="muted"
1745
+ );
1746
+ }
1747
+ return None;
1748
+ }
1749
+
1750
+ # Determine AppImage filename from AppDir name
1751
+ appimage_name = f"{appdir_path.name}.AppImage";
1752
+ appimage_path = appdir_path.parent / appimage_name;
1753
+
1754
+ # Skip if AppImage already exists and is newer than AppDir
1755
+ if appimage_path.exists() {
1756
+ appdir_mtime = appdir_path.stat().st_mtime;
1757
+ appimage_mtime = appimage_path.stat().st_mtime;
1758
+ if appimage_mtime >= appdir_mtime {
1759
+ console.print(
1760
+ f" ✔ AppImage already exists: {appimage_path.name}", style="success"
1761
+ );
1762
+ return appimage_path;
1763
+ }
1764
+ }
1765
+
1766
+ try {
1767
+ console.print(f" Creating AppImage from {appdir_path.name}...", style="muted");
1768
+ result = subprocess.run(
1769
+ ["appimagetool", str(appdir_path), str(appimage_path)],
1770
+ cwd=appdir_path.parent,
1771
+ check=False, # Don't raise on error, we'll check manually
1772
+ capture_output=True,
1773
+ text=True,
1774
+ timeout=300 # 5 minute timeout
1775
+ );
1776
+
1777
+ if result.returncode != 0 {
1778
+ error_output = result.stderr or result.stdout or "Unknown error";
1779
+ # Check for FUSE-related errors
1780
+ if "fuse" in error_output.lower()
1781
+ or "libfuse" in error_output.lower()
1782
+ or "FUSE" in error_output {
1783
+ console.warning(
1784
+ " Failed to create AppImage: FUSE library not available"
1785
+ );
1786
+ console.print(
1787
+ " Install FUSE with: sudo apt-get install -y libfuse2",
1788
+ style="muted"
1789
+ );
1790
+ console.print(
1791
+ " For WSL2, you may also need: sudo apt-get install -y fuse",
1792
+ style="muted"
1793
+ );
1794
+ } else {
1795
+ console.warning(f" Failed to create AppImage: {error_output[:200]}");
1796
+ }
1797
+ return None;
1798
+ }
1799
+
1800
+ # Verify the AppImage was created
1801
+ if not appimage_path.exists() {
1802
+ console.warning(" AppImage creation reported success but file not found");
1803
+ return None;
1804
+ }
1805
+
1806
+ # Make AppImage executable
1807
+ appimage_path.chmod(appimage_path.stat().st_mode | stat.S_IEXEC);
1808
+
1809
+ console.print(f" ✔ AppImage created: {appimage_path.name}", style="success");
1810
+ return appimage_path;
1811
+ } except subprocess.TimeoutExpired {
1812
+ console.warning(" AppImage creation timed out");
1813
+ return None;
1814
+ } except Exception as e {
1815
+ error_msg = str(e);
1816
+ if "fuse" in error_msg.lower() or "libfuse" in error_msg.lower() {
1817
+ console.warning(
1818
+ " FUSE library error. Install with: sudo apt-get install -y libfuse2"
1819
+ );
1820
+ } else {
1821
+ console.warning(f" Error creating AppImage: {error_msg[:200]}");
1822
+ }
1823
+ return None;
1824
+ }
1825
+ }
1826
+
1827
+ """Run cargo tauri build and return path to bundle."""
1828
+ def _run_tauri_build(tauri_dir: Path, platform: Optional[str] = None) -> Path {
1829
+ import os;
1830
+
1831
+ # Determine target based on platform
1832
+ target = None;
1833
+ if platform == "windows" {
1834
+ target = "x86_64-pc-windows-msvc";
1835
+ } elif platform == "macos" {
1836
+ # Try to detect architecture
1837
+ if platform.machine() == "arm64" {
1838
+ target = "aarch64-apple-darwin";
1839
+ } else {
1840
+ target = "x86_64-apple-darwin";
1841
+ }
1842
+ } elif platform == "linux" {
1843
+ target = "x86_64-unknown-linux-gnu";
1844
+ }
1845
+
1846
+ # Build command - prefer cargo tauri build, fallback to bun if package.json has tauri scripts
1847
+ build_cmd = ["cargo", "tauri", "build"];
1848
+ if target {
1849
+ build_cmd.extend(["--target", target]);
1850
+ }
1851
+
1852
+ # Check if package.json has tauri scripts, use bun run if so
1853
+ package_json = tauri_dir.parent / "package.json";
1854
+ use_bun = False;
1855
+ if package_json.exists() {
1856
+ try {
1857
+ with open(package_json, "r") as f {
1858
+ package_data = json.load(f);
1859
+ scripts = package_data.get("scripts", {});
1860
+ if "tauri" in scripts or "tauri:build" in scripts {
1861
+ use_bun = True;
1862
+ }
1863
+ }
1864
+ } except Exception {
1865
+ console.warning(" Failed to check package.json for tauri scripts");
1866
+ }
1867
+ }
1868
+
1869
+ if use_bun {
1870
+ # Ensure bun is available
1871
+ import from jac_client.plugin.utils { ensure_bun_available }
1872
+ if not ensure_bun_available() {
1873
+ console.error(
1874
+ "Bun is required for this project. Install manually: https://bun.sh"
1875
+ );
1876
+ raise RuntimeError("Bun is required") from None ;
1877
+ }
1878
+ # Use bun run tauri build
1879
+ build_cmd = ["bun", "run", "tauri", "build"];
1880
+ if target {
1881
+ build_cmd.extend(["--", "--target", target]);
1882
+ }
1883
+ }
1884
+
1885
+ # Change to tauri directory and run build
1886
+ original_cwd = os.getcwd();
1887
+ try {
1888
+ os.chdir(tauri_dir.parent);
1889
+ console.print(f" Running: {' '.join(build_cmd)}", style="muted");
1890
+
1891
+ result = subprocess.run(
1892
+ build_cmd,
1893
+ cwd=tauri_dir.parent,
1894
+ check=True,
1895
+ capture_output=False # Show output to user
1896
+ );
1897
+ } finally {
1898
+ os.chdir(original_cwd);
1899
+ }
1900
+
1901
+ # Find the bundle output
1902
+ # Tauri outputs to src-tauri/target/{target}/release/bundle/
1903
+ if target {
1904
+ bundle_dir = tauri_dir / "target" / target / "release" / "bundle";
1905
+ } else {
1906
+ bundle_dir = tauri_dir / "target" / "release" / "bundle";
1907
+ }
1908
+
1909
+ if not bundle_dir.exists() {
1910
+ raise RuntimeError(f"Build completed but bundle not found at: {bundle_dir}") ;
1911
+ }
1912
+
1913
+ # Find the actual bundle file (varies by platform)
1914
+ bundle_files = list(bundle_dir.rglob("*"));
1915
+ installers = [
1916
+ f
1917
+ for f in bundle_files
1918
+ if f.is_file()
1919
+ and f.suffix in [".exe", ".dmg", ".AppImage", ".deb", ".rpm", ".msi"]
1920
+ ];
1921
+
1922
+ # For Linux: if no AppImage but AppDir exists, try to create AppImage
1923
+ is_linux_build = (platform == "linux")
1924
+ or (not platform and target and "linux" in target);
1925
+ if not installers and is_linux_build {
1926
+ appimage_dir = bundle_dir / "appimage";
1927
+ if appimage_dir.exists() {
1928
+ # Look for AppDir directories
1929
+ appdirs = [
1930
+ d
1931
+ for d in appimage_dir.iterdir()
1932
+ if d.is_dir() and d.name.endswith(".AppDir")
1933
+ ];
1934
+ if appdirs {
1935
+ # Try to create AppImage from the first AppDir found
1936
+ appimage_path = _create_appimage_from_appdir(appdirs[0]);
1937
+ if appimage_path and appimage_path.exists() {
1938
+ installers = [appimage_path];
1939
+ }
1940
+ }
1941
+ }
1942
+ }
1943
+
1944
+ if installers {
1945
+ return installers[0];
1946
+ }
1947
+
1948
+ # Fallback: return the bundle directory
1949
+ return bundle_dir;
1950
+ }
1951
+
1952
+ """Create Python-based sidecar wrapper script (no PyInstaller needed)."""
1953
+ def _bundle_sidecar(
1954
+ entry_file: Path,
1955
+ project_dir: Path,
1956
+ tauri_dir: Path,
1957
+ platform: Optional[str] = None
1958
+ ) -> Path | None {
1959
+ import platform as platform_module;
1960
+ import stat;
1961
+
1962
+ # Determine output directory
1963
+ binaries_dir = tauri_dir / "binaries";
1964
+ binaries_dir.mkdir(parents=True, exist_ok=True);
1965
+
1966
+ # Determine script name based on platform
1967
+ if platform == "windows"
1968
+ or (platform is None and platform_module.system() == "Windows") {
1969
+ script_name = "jac-sidecar.bat";
1970
+ is_windows = True;
1971
+ } else {
1972
+ script_name = "jac-sidecar.sh";
1973
+ is_windows = False;
1974
+ }
1975
+
1976
+ output_path = binaries_dir / script_name;
1977
+
1978
+ # Create wrapper script that runs Python module
1979
+ # This approach avoids PyInstaller issues and is much simpler
1980
+ if is_windows {
1981
+ # Windows batch script
1982
+ script_content = f'''@echo off
1983
+ REM Jac Sidecar Wrapper - Runs Jac backend using system Python
1984
+ REM This requires Python and jaclang to be installed
1985
+
1986
+ python -m jac_client.plugin.src.targets.desktop.sidecar.main %*
1987
+ ''';
1988
+ } else {
1989
+ # Unix shell script
1990
+ script_content = '''#!/bin/bash
1991
+ # Jac Sidecar Wrapper - Runs Jac backend using system Python
1992
+ # This requires Python and jaclang to be installed
1993
+
1994
+ exec python -m jac_client.plugin.src.targets.desktop.sidecar.main "$@"
1995
+ ''';
1996
+ }
1997
+
1998
+ # Write the wrapper script
1999
+ with open(output_path, "w") as f {
2000
+ f.write(script_content);
2001
+ }
2002
+
2003
+ # Make executable on Unix systems
2004
+ if not is_windows {
2005
+ st = output_path.stat();
2006
+ output_path.chmod(st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH);
2007
+ }
2008
+
2009
+ console.print(f" ✔ Created sidecar wrapper: {script_name}", style="success");
2010
+ console.print(
2011
+ f" Note: Requires Python and jaclang to be installed", style="muted"
2012
+ );
2013
+ console.print(f" Run: pip install jaclang", style="muted");
2014
+
2015
+ return output_path;
2016
+ }
2017
+
2018
+ """Start desktop dev server - start web dev server and launch tauri dev."""
2019
+ impl DesktopTarget.dev(
2020
+ self: DesktopTarget, entry_file: Path, project_dir: Path
2021
+ ) -> None {
2022
+ import from jac_client.plugin.src.vite_bundler { ViteBundler }
2023
+ import signal;
2024
+ import sys;
2025
+ console.print("\n🖥️ Starting desktop dev server (Tauri)", style="bold");
2026
+ # Check if setup has been run
2027
+ tauri_dir = project_dir / "src-tauri";
2028
+ if not tauri_dir.exists() {
2029
+ raise RuntimeError("Desktop target not set up. Run 'jac setup desktop' first.") ;
2030
+ }
2031
+ # Step 1: Update tauri.conf.json for dev mode
2032
+ console.print(" Configuring Tauri for dev mode...", style="muted");
2033
+ _update_tauri_config_for_dev(tauri_dir);
2034
+ # Step 1.5: Prepare package.json and get compiler
2035
+ # We need compiled/_entry.js and compiled/main.js to exist before Vite can serve them
2036
+ console.print(" Preparing compilation setup...", style="muted");
2037
+ vite_port = 5173;
2038
+ bundler = ViteBundler(
2039
+ project_dir=project_dir, output_dir=None, minify=False, config_path=None
2040
+ );
2041
+ # Ensure package.json exists (needed for ViteCompiler)
2042
+ package_json_path = bundler._get_client_dir() / 'configs' / 'package.json';
2043
+ if not package_json_path.exists() {
2044
+ bundler.create_package_json();
2045
+ }
2046
+ # Get the compiler via JacClient (this provides all the compile functions)
2047
+ import from jac_client.plugin.client { JacClient }
2048
+ builder = JacClient.get_client_bundle_builder();
2049
+ # Set the package.json path if not already set
2050
+ if not builder.vite_package_json or not builder.vite_package_json.exists() {
2051
+ builder.vite_package_json = package_json_path;
2052
+ }
2053
+ compiler = builder._get_compiler();
2054
+ # Ensure compiled directory exists
2055
+ compiler.compiled_dir.mkdir(parents=True, exist_ok=True);
2056
+ # Compile runtime utils (creates client_runtime.js)
2057
+ compiler.compile_runtime_utils();
2058
+ # Compile the module to create compiled/main.js (or whatever the module name is)
2059
+ (module_js, mod, module_manifest) = compiler.jac_compiler.compile_module(
2060
+ entry_file
2061
+ );
2062
+ # Write the compiled module to compiled directory
2063
+ module_name = entry_file.stem;
2064
+ compiled_module_path = compiler.compiled_dir / f"{module_name}.js";
2065
+ compiled_module_path.write_text(module_js, encoding='utf-8');
2066
+ # Create the entry file (this will be used by Vite dev server)
2067
+ compiler.create_entry_file(entry_file);
2068
+ console.print(" ✔ Module compiled for dev mode", style="success");
2069
+ # Step 2: Start web dev server
2070
+ console.print(" Starting web dev server...", style="muted");
2071
+ # Create dev vite config
2072
+ dev_config_path = bundler.create_dev_vite_config(entry_file, api_port=8000);
2073
+ # Start Vite dev server
2074
+ vite_process = bundler.start_dev_server(port=vite_port);
2075
+ if not vite_process {
2076
+ raise RuntimeError("Failed to start Vite dev server") ;
2077
+ }
2078
+ console.print(
2079
+ f" ✔ Web dev server running on http://localhost:{vite_port}", style="success"
2080
+ );
2081
+ # Step 3: Launch tauri dev
2082
+ console.print(" Launching Tauri dev window...", style="muted");
2083
+ console.print(" (Press Ctrl+C to stop)", style="muted");
2084
+ # Setup signal handlers for cleanup
2085
+ def cleanup -> None {
2086
+ console.print("\n Stopping dev servers...", style="muted");
2087
+ if vite_process {
2088
+ try {
2089
+ vite_process.terminate();
2090
+ vite_process.wait(timeout=5);
2091
+ } except Exception {
2092
+ vite_process.kill();
2093
+ }
2094
+ }
2095
+ console.print(" ✔ Dev servers stopped", style="success");
2096
+ }
2097
+ def signal_handler(signum: int, frame: Any) -> None {
2098
+ cleanup();
2099
+ sys.exit(0);
2100
+ }
2101
+ signal.signal(signal.SIGINT, signal_handler);
2102
+ signal.signal(signal.SIGTERM, signal_handler);
2103
+ try {
2104
+ # Run tauri dev
2105
+ tauri_process = _run_tauri_dev(tauri_dir);
2106
+
2107
+ # Wait for tauri process to finish
2108
+ tauri_process.wait();
2109
+ } except KeyboardInterrupt {
2110
+ console.print(
2111
+ "\n Keyboard interrupt detected. Stopping dev servers...", style="muted"
2112
+ );
2113
+ } finally {
2114
+ cleanup();
2115
+ }
2116
+ }
2117
+
2118
+ """Update tauri.conf.json for dev mode (point to dev server)."""
2119
+ def _update_tauri_config_for_dev(tauri_dir: Path) -> None {
2120
+ import json;
2121
+
2122
+ config_path = tauri_dir / "tauri.conf.json";
2123
+ if not config_path.exists() {
2124
+ raise RuntimeError("tauri.conf.json not found. Run 'jac setup desktop' first.") ;
2125
+ }
2126
+
2127
+ # Read existing config
2128
+ with open(config_path, "r") as f {
2129
+ config = json.load(f);
2130
+ }
2131
+
2132
+ # Update build config for dev mode (Tauri v2 structure)
2133
+ if "build" not in config {
2134
+ config["build"] = {};
2135
+ }
2136
+ # Tauri v2 uses 'devUrl' instead of 'devPath'
2137
+ config["build"]["devUrl"] = "http://localhost:5173";
2138
+ # Remove distDir and other invalid properties
2139
+ if "distDir" in config["build"] {
2140
+ del config["build"]["distDir"];
2141
+ }
2142
+ if "devPath" in config["build"] {
2143
+ del config["build"]["devPath"];
2144
+ }
2145
+ if "withGlobalTauri" in config["build"] {
2146
+ del config["build"]["withGlobalTauri"];
2147
+ }
2148
+ # Keep only valid properties
2149
+ if "beforeDevCommand" in config["build"]
2150
+ and config["build"]["beforeDevCommand"] is None {
2151
+ del config["build"]["beforeDevCommand"];
2152
+ }
2153
+ if "beforeBuildCommand" in config["build"]
2154
+ and config["build"]["beforeBuildCommand"] is None {
2155
+ del config["build"]["beforeBuildCommand"];
2156
+ }
2157
+
2158
+ # Populate icon array if empty or missing
2159
+ _populate_icon_array(tauri_dir, config);
2160
+
2161
+ # Add sidecar binary if it exists
2162
+ _add_sidecar_to_config(tauri_dir, config);
2163
+
2164
+ # Write updated config
2165
+ with open(config_path, "w") as f {
2166
+ json.dump(config, f, indent=2);
2167
+ }
2168
+
2169
+ console.print(" ✔ Updated tauri.conf.json for dev mode", style="success");
2170
+ }
2171
+
2172
+ """Run tauri dev command."""
2173
+ def _run_tauri_dev(tauri_dir: Path) -> subprocess.Popen {
2174
+ # Check if cargo is available
2175
+ try {
2176
+ subprocess.run(
2177
+ ["cargo", "--version"], capture_output=True, check=True, timeout=5
2178
+ );
2179
+ } except (
2180
+ subprocess.CalledProcessError,
2181
+ FileNotFoundError,
2182
+ subprocess.TimeoutExpired
2183
+ ) {
2184
+ raise RuntimeError(
2185
+ "Rust/Cargo not found. Install Rust from https://rustup.rs/\n"
2186
+ "Or run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
2187
+ ) ;
2188
+ }
2189
+
2190
+ # Check if build tools (gcc/cc) are available
2191
+ try {
2192
+ subprocess.run(["cc", "--version"], capture_output=True, check=True, timeout=5);
2193
+ } except (
2194
+ subprocess.CalledProcessError,
2195
+ FileNotFoundError,
2196
+ subprocess.TimeoutExpired
2197
+ ) {
2198
+ raise RuntimeError(
2199
+ "Build tools not found. Install build-essential:\n"
2200
+ " Ubuntu/Debian: sudo apt-get install build-essential\n"
2201
+ " Fedora: sudo dnf install gcc gcc-c++\n"
2202
+ " Arch: sudo pacman -S base-devel"
2203
+ ) ;
2204
+ }
2205
+
2206
+ # Check if tauri CLI is available via cargo
2207
+ try {
2208
+ subprocess.run(
2209
+ ["cargo", "tauri", "--version"], capture_output=True, check=True, timeout=5
2210
+ );
2211
+ } except (
2212
+ subprocess.CalledProcessError,
2213
+ FileNotFoundError,
2214
+ subprocess.TimeoutExpired
2215
+ ) {
2216
+ console.warning("Tauri CLI not found.");
2217
+ console.print(" Install manually: cargo install tauri-cli", style="muted");
2218
+ console.print(" Or use bun: bun add -g @tauri-apps/cli", style="muted");
2219
+ raise RuntimeError(
2220
+ "Tauri CLI not installed. Install it first:\n"
2221
+ " cargo install tauri-cli\n"
2222
+ " Or: bun add -g @tauri-apps/cli"
2223
+ ) ;
2224
+ }
2225
+
2226
+ # Check if package.json has tauri scripts
2227
+ package_json = tauri_dir.parent / "package.json";
2228
+ use_bun = False;
2229
+ if package_json.exists() {
2230
+ try {
2231
+ with open(package_json, "r") as f {
2232
+ package_data = json.load(f);
2233
+ scripts = package_data.get("scripts", {});
2234
+ if "tauri" in scripts or "tauri:dev" in scripts {
2235
+ use_bun = True;
2236
+ }
2237
+ }
2238
+ } except Exception { }
2239
+ }
2240
+
2241
+ if use_bun {
2242
+ # Ensure bun is available
2243
+ import from jac_client.plugin.utils { ensure_bun_available }
2244
+ if not ensure_bun_available() {
2245
+ console.error(
2246
+ "Bun is required for this project. Install manually: https://bun.sh"
2247
+ );
2248
+ raise RuntimeError("Bun is required") from None ;
2249
+ }
2250
+ # Use bun run tauri dev
2251
+ dev_cmd = ["bun", "run", "tauri", "dev"];
2252
+ } else {
2253
+ # Use cargo tauri dev directly
2254
+ dev_cmd = ["cargo", "tauri", "dev"];
2255
+ }
2256
+
2257
+ console.print(f" Running: {' '.join(dev_cmd)}", style="muted");
2258
+
2259
+ # Run tauri dev (this will open a window)
2260
+ # Use None for stdout/stderr to show output directly in terminal
2261
+ try {
2262
+ process = subprocess.Popen(
2263
+ dev_cmd,
2264
+ cwd=tauri_dir.parent,
2265
+ stdout=None, # Show output directly
2266
+ stderr=None, # Show errors directly
2267
+ text=True
2268
+ );
2269
+ return process;
2270
+ } except Exception as e {
2271
+ console.error(f" Failed to start Tauri: {e}");
2272
+ raise ;
2273
+ }
2274
+ }
2275
+
2276
+ """Start desktop app - build web bundle and launch Tauri with built bundle."""
2277
+ impl DesktopTarget.start(
2278
+ self: DesktopTarget, entry_file: Path, project_dir: Path
2279
+ ) -> None {
2280
+ import from jac_client.plugin.src.targets.web_target { WebTarget }
2281
+ import signal;
2282
+ import sys;
2283
+ console.print("\n🖥️ Starting desktop app (Tauri)", style="bold");
2284
+ # Check if setup has been run
2285
+ tauri_dir = project_dir / "src-tauri";
2286
+ if not tauri_dir.exists() {
2287
+ raise RuntimeError("Desktop target not set up. Run 'jac setup desktop' first.") ;
2288
+ }
2289
+ # Step 1: Build web bundle first
2290
+ console.print(" Step 1: Building web bundle...", style="muted");
2291
+ web_target = WebTarget();
2292
+ web_bundle_path = web_target.build(entry_file, project_dir, None);
2293
+ console.print(f" ✔ Web bundle built: {web_bundle_path}", style="success");
2294
+ # Step 2: Update tauri.conf.json to point to web bundle
2295
+ console.print(" Step 2: Updating Tauri configuration...", style="muted");
2296
+ _update_tauri_config_for_build(tauri_dir, project_dir, web_bundle_path);
2297
+ # Step 3: Launch tauri dev (which will use the built bundle)
2298
+ console.print(" Step 3: Launching Tauri app...", style="muted");
2299
+ console.print(" (Press Ctrl+C to stop)", style="muted");
2300
+ # Setup signal handlers for cleanup
2301
+ def cleanup -> None {
2302
+ console.print("\n Stopping app...", style="muted");
2303
+ console.print(" ✔ App stopped", style="success");
2304
+ }
2305
+ def signal_handler(signum: int, frame: Any) -> None {
2306
+ cleanup();
2307
+ sys.exit(0);
2308
+ }
2309
+ signal.signal(signal.SIGINT, signal_handler);
2310
+ signal.signal(signal.SIGTERM, signal_handler);
2311
+ tauri_process = None;
2312
+ try {
2313
+ # Run tauri dev (it will use the built bundle from distDir)
2314
+ tauri_process = _run_tauri_dev(tauri_dir);
2315
+
2316
+ if not tauri_process {
2317
+ console.error(" Failed to start Tauri process");
2318
+ return;
2319
+ }
2320
+
2321
+ # Wait for tauri process to finish
2322
+ return_code = tauri_process.wait();
2323
+ if return_code != 0 {
2324
+ console.warning(f" Tauri process exited with code {return_code}");
2325
+ }
2326
+ } except KeyboardInterrupt {
2327
+ console.print(
2328
+ "\n Keyboard interrupt detected. Stopping app...", style="muted"
2329
+ );
2330
+ if tauri_process {
2331
+ try {
2332
+ tauri_process.terminate();
2333
+ tauri_process.wait(timeout=5);
2334
+ } except Exception {
2335
+ if tauri_process {
2336
+ tauri_process.kill();
2337
+ }
2338
+ }
2339
+ }
2340
+ } except Exception as e {
2341
+ console.error(f" Error starting desktop app: {e}");
2342
+ import traceback;
2343
+ console.print(traceback.format_exc(), style="muted");
2344
+ } finally {
2345
+ cleanup();
2346
+ }
2347
+ }