multi-lang-build 0.2.2__tar.gz → 0.2.4__tar.gz

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 (22) hide show
  1. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/PKG-INFO +1 -1
  2. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/pyproject.toml +1 -1
  3. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/__init__.py +1 -1
  4. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/cli.py +17 -10
  5. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/compiler/base.py +135 -22
  6. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/compiler/go.py +54 -38
  7. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/compiler/pnpm.py +83 -33
  8. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/compiler/python.py +24 -15
  9. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/register.py +29 -17
  10. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/.gitignore +0 -0
  11. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/LICENSE +0 -0
  12. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/README.md +0 -0
  13. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/compiler/__init__.py +0 -0
  14. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/mirror/__init__.py +0 -0
  15. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/mirror/cli.py +0 -0
  16. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/mirror/config.py +0 -0
  17. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/py.typed +0 -0
  18. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/src/multi_lang_build/types.py +0 -0
  19. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/tests/__init__.py +0 -0
  20. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/tests/conftest.py +0 -0
  21. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/tests/test_compiler.py +0 -0
  22. {multi_lang_build-0.2.2 → multi_lang_build-0.2.4}/tests/test_mirror.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-lang-build
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Multi-language automated build tool with domestic mirror acceleration support
5
5
  Project-URL: Homepage, https://github.com/example/multi-lang-build
6
6
  Project-URL: Repository, https://github.com/example/multi-lang-build
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "multi-lang-build"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "Multi-language automated build tool with domestic mirror acceleration support"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -7,7 +7,7 @@ from multi_lang_build.compiler.python import PythonCompiler
7
7
  from multi_lang_build.mirror.config import MirrorConfig, get_mirror_config
8
8
  from multi_lang_build.register import register_skill
9
9
 
10
- __version__ = "0.2.1"
10
+ __version__ = "0.2.4"
11
11
  __all__ = [
12
12
  "BuildConfig",
13
13
  "BuildResult",
@@ -61,7 +61,8 @@ Supported languages:
61
61
  pnpm_parser.add_argument("--mirror/--no-mirror", default=True, help="Enable/disable mirror acceleration")
62
62
  pnpm_parser.add_argument("--script", type=str, help="Run specific npm script")
63
63
  pnpm_parser.add_argument("--install", action="store_true", help="Install dependencies only")
64
-
64
+ pnpm_parser.add_argument("--stream/--no-stream", dest="stream_output", default=True, help="Enable/disable real-time output streaming (default: enabled)")
65
+
65
66
  go_parser = subparsers.add_parser(
66
67
  "go",
67
68
  help="Build Go projects",
@@ -75,7 +76,8 @@ Supported languages:
75
76
  go_parser.add_argument("--test", action="store_true", help="Run tests instead of building")
76
77
  go_parser.add_argument("--tidy", action="store_true", help="Run go mod tidy")
77
78
  go_parser.add_argument("--all", action="store_true", help="Build all packages")
78
-
79
+ go_parser.add_argument("--stream/--no-stream", dest="stream_output", default=True, help="Enable/disable real-time output streaming (default: enabled)")
80
+
79
81
  python_parser = subparsers.add_parser(
80
82
  "python",
81
83
  help="Build Python projects",
@@ -88,6 +90,7 @@ Supported languages:
88
90
  python_parser.add_argument("--dev", action="store_true", help="Include development dependencies")
89
91
  python_parser.add_argument("--poetry", action="store_true", help="Force using poetry")
90
92
  python_parser.add_argument("--create-venv", type=Path, help="Create virtual environment at path")
93
+ python_parser.add_argument("--stream/--no-stream", dest="stream_output", default=True, help="Enable/disable real-time output streaming (default: enabled)")
91
94
 
92
95
  mirror_parser = subparsers.add_parser(
93
96
  "mirror",
@@ -175,9 +178,9 @@ Examples:
175
178
  try:
176
179
  if parsed_args.language == "pnpm":
177
180
  from multi_lang_build.compiler.pnpm import PnpmCompiler
178
-
181
+
179
182
  compiler = PnpmCompiler()
180
-
183
+
181
184
  if parsed_args.script:
182
185
  result = compiler.run_script(
183
186
  parsed_args.source_dir,
@@ -194,13 +197,14 @@ Examples:
194
197
  parsed_args.source_dir,
195
198
  parsed_args.output,
196
199
  mirror_enabled=parsed_args.mirror,
200
+ stream_output=parsed_args.stream_output,
197
201
  )
198
-
202
+
199
203
  elif parsed_args.language == "go":
200
204
  from multi_lang_build.compiler.go import GoCompiler
201
-
205
+
202
206
  compiler = GoCompiler()
203
-
207
+
204
208
  if parsed_args.test:
205
209
  result = compiler.run_tests(
206
210
  parsed_args.source_dir,
@@ -216,6 +220,7 @@ Examples:
216
220
  parsed_args.source_dir,
217
221
  parsed_args.output,
218
222
  mirror_enabled=parsed_args.mirror,
223
+ stream_output=parsed_args.stream_output,
219
224
  )
220
225
  else:
221
226
  tags = parsed_args.tags.split(",") if parsed_args.tags else None
@@ -225,13 +230,14 @@ Examples:
225
230
  mirror_enabled=parsed_args.mirror,
226
231
  ldflags=parsed_args.ldflags,
227
232
  tags=tags,
233
+ stream_output=parsed_args.stream_output,
228
234
  )
229
-
235
+
230
236
  elif parsed_args.language == "python":
231
237
  from multi_lang_build.compiler.python import PythonCompiler
232
-
238
+
233
239
  compiler = PythonCompiler()
234
-
240
+
235
241
  if parsed_args.create_venv:
236
242
  result = compiler.create_venv(
237
243
  parsed_args.create_venv,
@@ -249,6 +255,7 @@ Examples:
249
255
  parsed_args.source_dir,
250
256
  parsed_args.output,
251
257
  mirror_enabled=parsed_args.mirror,
258
+ stream_output=parsed_args.stream_output,
252
259
  )
253
260
 
254
261
  elif parsed_args.language == "mirror":
@@ -4,6 +4,8 @@ from abc import ABC, abstractmethod
4
4
  from typing import TypeAlias, TypedDict, NotRequired
5
5
  from pathlib import Path
6
6
  import time
7
+ import subprocess
8
+ import sys
7
9
 
8
10
 
9
11
  class BuildConfig(TypedDict, total=False):
@@ -37,30 +39,30 @@ class CompilerInfo(TypedDict):
37
39
 
38
40
  class CompilerBase(ABC):
39
41
  """Abstract base class for all language compilers."""
40
-
42
+
41
43
  @property
42
44
  @abstractmethod
43
45
  def name(self) -> str:
44
46
  """Get the compiler name."""
45
47
  ...
46
-
48
+
47
49
  @property
48
50
  @abstractmethod
49
51
  def version(self) -> str:
50
52
  """Get the compiler version."""
51
53
  ...
52
-
54
+
53
55
  @property
54
56
  @abstractmethod
55
57
  def supported_mirrors(self) -> list[str]:
56
58
  """Get list of supported mirror configurations."""
57
59
  ...
58
-
60
+
59
61
  @abstractmethod
60
62
  def get_info(self) -> CompilerInfo:
61
63
  """Get compiler information."""
62
64
  ...
63
-
65
+
64
66
  @abstractmethod
65
67
  def build(
66
68
  self,
@@ -70,33 +72,35 @@ class CompilerBase(ABC):
70
72
  environment: dict[str, str] | None = None,
71
73
  mirror_enabled: bool = True,
72
74
  extra_args: list[str] | None = None,
75
+ stream_output: bool = True,
73
76
  ) -> BuildResult:
74
77
  """Execute the build process.
75
-
78
+
76
79
  Args:
77
80
  source_dir: Source code directory
78
81
  output_dir: Build output directory
79
82
  environment: Additional environment variables
80
83
  mirror_enabled: Whether to use mirror acceleration
81
84
  extra_args: Additional arguments to pass to the build command
82
-
85
+ stream_output: Whether to stream output in real-time (default: True)
86
+
83
87
  Returns:
84
88
  BuildResult containing success status and output information.
85
89
  """
86
90
  ...
87
-
91
+
88
92
  @abstractmethod
89
93
  def clean(self, directory: Path) -> bool:
90
94
  """Clean build artifacts in the specified directory.
91
-
95
+
92
96
  Args:
93
97
  directory: Directory to clean
94
-
98
+
95
99
  Returns:
96
100
  True if successful, False otherwise.
97
101
  """
98
102
  ...
99
-
103
+
100
104
  def _run_build(
101
105
  self,
102
106
  command: list[str],
@@ -105,42 +109,151 @@ class CompilerBase(ABC):
105
109
  *,
106
110
  environment: dict[str, str] | None = None,
107
111
  extra_args: list[str] | None = None,
112
+ stream_output: bool = True,
108
113
  ) -> BuildResult:
109
114
  """Execute a build command with timing and error handling.
110
-
115
+
111
116
  Args:
112
117
  command: Build command to execute
113
118
  source_dir: Source directory for the build
114
119
  output_dir: Output directory for build artifacts
115
120
  environment: Additional environment variables
116
121
  extra_args: Additional arguments to append to command
117
-
122
+ stream_output: Whether to stream output in real-time (default: True)
123
+
118
124
  Returns:
119
125
  BuildResult with success status and output information.
120
126
  """
121
- import subprocess
122
-
123
127
  full_command = command.copy()
124
128
  if extra_args:
125
129
  full_command.extend(extra_args)
126
-
130
+
127
131
  env = environment.copy() if environment else {}
128
132
  env.setdefault("PATH", "")
129
-
133
+
130
134
  start_time = time.perf_counter()
131
-
135
+
136
+ if stream_output:
137
+ return self._run_build_stream(
138
+ full_command, source_dir, output_dir, env, start_time
139
+ )
140
+ else:
141
+ return self._run_build_capture(
142
+ full_command, source_dir, output_dir, env, start_time
143
+ )
144
+
145
+ def _run_build_stream(
146
+ self,
147
+ command: list[str],
148
+ source_dir: Path,
149
+ output_dir: Path,
150
+ env: dict[str, str],
151
+ start_time: float,
152
+ ) -> BuildResult:
153
+ """Execute build command with real-time output streaming.
154
+
155
+ Args:
156
+ command: Build command to execute
157
+ source_dir: Source directory for the build
158
+ output_dir: Output directory for build artifacts
159
+ env: Environment variables
160
+ start_time: Start time for duration calculation
161
+
162
+ Returns:
163
+ BuildResult with success status and output information.
164
+ """
165
+ stdout_buffer = []
166
+ stderr_buffer = []
167
+
168
+ try:
169
+ process = subprocess.Popen(
170
+ command,
171
+ cwd=source_dir,
172
+ stdout=subprocess.PIPE,
173
+ stderr=subprocess.PIPE,
174
+ text=True,
175
+ env=env,
176
+ )
177
+
178
+ # Read stdout in real-time
179
+ for line in process.stdout:
180
+ line = line.rstrip('\n\r')
181
+ stdout_buffer.append(line)
182
+ print(line)
183
+ sys.stdout.flush()
184
+
185
+ # Read stderr in real-time
186
+ for line in process.stderr:
187
+ line = line.rstrip('\n\r')
188
+ stderr_buffer.append(line)
189
+ print(line, file=sys.stderr)
190
+ sys.stderr.flush()
191
+
192
+ return_code = process.wait()
193
+ duration = time.perf_counter() - start_time
194
+
195
+ return BuildResult(
196
+ success=return_code == 0,
197
+ return_code=return_code,
198
+ stdout='\n'.join(stdout_buffer),
199
+ stderr='\n'.join(stderr_buffer),
200
+ output_path=output_dir if return_code == 0 else None,
201
+ duration_seconds=duration,
202
+ )
203
+
204
+ except subprocess.TimeoutExpired:
205
+ duration = time.perf_counter() - start_time
206
+ return BuildResult(
207
+ success=False,
208
+ return_code=-1,
209
+ stdout='\n'.join(stdout_buffer),
210
+ stderr="Build timed out after 1 hour",
211
+ output_path=None,
212
+ duration_seconds=duration,
213
+ )
214
+ except Exception as e:
215
+ duration = time.perf_counter() - start_time
216
+ return BuildResult(
217
+ success=False,
218
+ return_code=-2,
219
+ stdout='\n'.join(stdout_buffer),
220
+ stderr=f"Build error: {str(e)}",
221
+ output_path=None,
222
+ duration_seconds=duration,
223
+ )
224
+
225
+ def _run_build_capture(
226
+ self,
227
+ command: list[str],
228
+ source_dir: Path,
229
+ output_dir: Path,
230
+ env: dict[str, str],
231
+ start_time: float,
232
+ ) -> BuildResult:
233
+ """Execute build command with captured output (no real-time display).
234
+
235
+ Args:
236
+ command: Build command to execute
237
+ source_dir: Source directory for the build
238
+ output_dir: Output directory for build artifacts
239
+ env: Environment variables
240
+ start_time: Start time for duration calculation
241
+
242
+ Returns:
243
+ BuildResult with success status and output information.
244
+ """
132
245
  try:
133
246
  result = subprocess.run(
134
- full_command,
247
+ command,
135
248
  cwd=source_dir,
136
249
  capture_output=True,
137
250
  text=True,
138
251
  env=env,
139
252
  timeout=3600, # 1 hour timeout
140
253
  )
141
-
254
+
142
255
  duration = time.perf_counter() - start_time
143
-
256
+
144
257
  return BuildResult(
145
258
  success=result.returncode == 0,
146
259
  return_code=result.returncode,
@@ -149,7 +262,7 @@ class CompilerBase(ABC):
149
262
  output_path=output_dir if result.returncode == 0 else None,
150
263
  duration_seconds=duration,
151
264
  )
152
-
265
+
153
266
  except subprocess.TimeoutExpired:
154
267
  duration = time.perf_counter() - start_time
155
268
  return BuildResult(
@@ -110,16 +110,18 @@ class GoCompiler(CompilerBase):
110
110
  environment: dict[str, str] | None = None,
111
111
  mirror_enabled: bool = True,
112
112
  extra_args: list[str] | None = None,
113
+ stream_output: bool = True,
113
114
  ) -> BuildResult:
114
115
  """Execute the Go build process.
115
-
116
+
116
117
  Args:
117
118
  source_dir: Source code directory
118
119
  output_dir: Build output directory (for binary)
119
120
  environment: Additional environment variables
120
121
  mirror_enabled: Whether to use Go proxy mirror
121
122
  extra_args: Additional arguments to pass to go build
122
-
123
+ stream_output: Whether to stream output in real-time (default: True)
124
+
123
125
  Returns:
124
126
  BuildResult containing success status and output information.
125
127
  """
@@ -147,22 +149,24 @@ class GoCompiler(CompilerBase):
147
149
  source_dir,
148
150
  output_dir,
149
151
  environment=env,
152
+ stream_output=stream_output,
150
153
  )
151
-
154
+
152
155
  if not deps_result["success"]:
153
156
  return deps_result
154
-
157
+
155
158
  # Build the project
156
159
  build_args = [go_executable, "build", "-o", str(output_dir)]
157
-
160
+
158
161
  if extra_args:
159
162
  build_args.extend(extra_args)
160
-
163
+
161
164
  return self._run_build(
162
165
  build_args,
163
166
  source_dir,
164
167
  output_dir,
165
168
  environment=env,
169
+ stream_output=stream_output,
166
170
  )
167
171
 
168
172
  def build_binary(
@@ -175,9 +179,10 @@ class GoCompiler(CompilerBase):
175
179
  mirror_enabled: bool = True,
176
180
  ldflags: str | None = None,
177
181
  tags: list[str] | None = None,
182
+ stream_output: bool = True,
178
183
  ) -> BuildResult:
179
184
  """Build a specific Go binary.
180
-
185
+
181
186
  Args:
182
187
  source_dir: Source code directory (working directory for build)
183
188
  output_path: Output path for the binary
@@ -187,40 +192,42 @@ class GoCompiler(CompilerBase):
187
192
  mirror_enabled: Whether to use Go proxy mirror
188
193
  ldflags: Linker flags to pass to the compiler
189
194
  tags: Build tags to pass to the compiler
190
-
195
+ stream_output: Whether to stream output in real-time (default: True)
196
+
191
197
  Returns:
192
198
  BuildResult containing success status and output information.
193
199
  """
194
200
  go_executable = self._get_executable_path()
195
-
201
+
196
202
  source_dir = self._validate_directory(source_dir, create_if_not_exists=False)
197
-
203
+
198
204
  # Ensure output directory exists
199
205
  output_path.parent.mkdir(parents=True, exist_ok=True)
200
-
206
+
201
207
  env = environment.copy() if environment else {}
202
-
208
+
203
209
  if mirror_enabled:
204
210
  mirror_key = self._mirror or "go"
205
211
  env = apply_mirror_environment(mirror_key, env)
206
-
212
+
207
213
  build_args = [go_executable, "build", "-o", str(output_path)]
208
-
214
+
209
215
  if ldflags:
210
216
  build_args.extend(["-ldflags", ldflags])
211
-
217
+
212
218
  if tags:
213
219
  build_args.extend(["-tags", ",".join(tags)])
214
-
220
+
215
221
  # Add target if specified (target is relative to source_dir)
216
222
  if target:
217
223
  build_args.append(str(target))
218
-
224
+
219
225
  return self._run_build(
220
226
  build_args,
221
227
  source_dir,
222
228
  output_path.parent,
223
229
  environment=env,
230
+ stream_output=stream_output,
224
231
  )
225
232
 
226
233
  def build_all(
@@ -231,42 +238,45 @@ class GoCompiler(CompilerBase):
231
238
  environment: dict[str, str] | None = None,
232
239
  mirror_enabled: bool = True,
233
240
  platform: str | None = None,
241
+ stream_output: bool = True,
234
242
  ) -> BuildResult:
235
243
  """Build all binaries defined in a Makefile or build script.
236
-
244
+
237
245
  Args:
238
246
  source_dir: Source code directory
239
247
  output_dir: Output directory for all binaries
240
248
  environment: Additional environment variables
241
249
  mirror_enabled: Whether to use Go proxy mirror
242
250
  platform: Target platform (e.g., "linux/amd64", "windows/amd64")
243
-
251
+ stream_output: Whether to stream output in real-time (default: True)
252
+
244
253
  Returns:
245
254
  BuildResult containing success status and output information.
246
255
  """
247
256
  go_executable = self._get_executable_path()
248
-
257
+
249
258
  source_dir = self._validate_directory(source_dir, create_if_not_exists=False)
250
259
  output_dir = self._validate_directory(output_dir, create_if_not_exists=True)
251
-
260
+
252
261
  env = environment.copy() if environment else {}
253
-
262
+
254
263
  if mirror_enabled:
255
264
  mirror_key = self._mirror or "go"
256
265
  env = apply_mirror_environment(mirror_key, env)
257
-
266
+
258
267
  build_args = [go_executable, "build", "-o", str(output_dir), "./..."]
259
-
268
+
260
269
  if platform:
261
270
  env["GOOS"], env["GOARCH"] = platform.split("/")
262
-
271
+
263
272
  return self._run_build(
264
273
  build_args,
265
274
  source_dir,
266
275
  output_dir,
267
276
  environment=env,
277
+ stream_output=stream_output,
268
278
  )
269
-
279
+
270
280
  def run_tests(
271
281
  self,
272
282
  source_dir: Path,
@@ -275,16 +285,18 @@ class GoCompiler(CompilerBase):
275
285
  mirror_enabled: bool = True,
276
286
  verbose: bool = True,
277
287
  race: bool = False,
288
+ stream_output: bool = True,
278
289
  ) -> BuildResult:
279
290
  """Run Go tests.
280
-
291
+
281
292
  Args:
282
293
  source_dir: Source code directory
283
294
  environment: Additional environment variables
284
295
  mirror_enabled: Whether to use Go proxy mirror
285
296
  verbose: Enable verbose test output
286
297
  race: Enable race detector
287
-
298
+ stream_output: Whether to stream output in real-time (default: True)
299
+
288
300
  Returns:
289
301
  BuildResult containing success status and output information.
290
302
  """
@@ -299,52 +311,56 @@ class GoCompiler(CompilerBase):
299
311
  env = apply_mirror_environment(mirror_key, env)
300
312
 
301
313
  test_args = [go_executable, "test"]
302
-
314
+
303
315
  if verbose:
304
316
  test_args.append("-v")
305
-
317
+
306
318
  if race:
307
319
  test_args.append("-race")
308
-
320
+
309
321
  return self._run_build(
310
322
  test_args,
311
323
  source_dir,
312
324
  source_dir,
313
325
  environment=env,
326
+ stream_output=stream_output,
314
327
  )
315
-
328
+
316
329
  def tidy_modules(
317
330
  self,
318
331
  source_dir: Path,
319
332
  *,
320
333
  environment: dict[str, str] | None = None,
321
334
  mirror_enabled: bool = True,
335
+ stream_output: bool = True,
322
336
  ) -> BuildResult:
323
337
  """Run go mod tidy to clean up dependencies.
324
-
338
+
325
339
  Args:
326
340
  source_dir: Source code directory
327
341
  environment: Additional environment variables
328
342
  mirror_enabled: Whether to use Go proxy mirror
329
-
343
+ stream_output: Whether to stream output in real-time (default: True)
344
+
330
345
  Returns:
331
346
  BuildResult containing success status and output information.
332
347
  """
333
348
  go_executable = self._get_executable_path()
334
-
349
+
335
350
  source_dir = self._validate_directory(source_dir, create_if_not_exists=False)
336
-
351
+
337
352
  env = environment.copy() if environment else {}
338
-
353
+
339
354
  if mirror_enabled:
340
355
  mirror_key = self._mirror or "go"
341
356
  env = apply_mirror_environment(mirror_key, env)
342
-
357
+
343
358
  return self._run_build(
344
359
  [go_executable, "mod", "tidy"],
345
360
  source_dir,
346
361
  source_dir,
347
362
  environment=env,
363
+ stream_output=stream_output,
348
364
  )
349
365
 
350
366
  def clean(self, directory: Path) -> bool:
@@ -108,48 +108,94 @@ class PnpmCompiler(CompilerBase):
108
108
  command: list[str],
109
109
  working_dir: Path,
110
110
  environment: dict[str, str],
111
+ stream_output: bool = True,
111
112
  ) -> BuildResult:
112
113
  """Execute a command in the specified directory, returning to original cwd afterwards.
113
-
114
+
114
115
  Args:
115
116
  command: Command to execute
116
117
  working_dir: Working directory for the command
117
118
  environment: Environment variables
118
-
119
+ stream_output: Whether to stream output in real-time (default: True)
120
+
119
121
  Returns:
120
122
  BuildResult containing success status and output information
121
123
  """
122
124
  import subprocess
123
-
125
+ import sys
126
+
124
127
  original_cwd = Path.cwd()
125
128
  full_command = command.copy()
126
-
129
+
127
130
  start_time: float = 0.0
128
-
131
+
129
132
  try:
130
133
  os.chdir(working_dir)
131
-
134
+
132
135
  start_time = time.perf_counter()
133
-
134
- result = subprocess.run(
135
- full_command,
136
- capture_output=True,
137
- text=True,
138
- timeout=3600,
139
- env={**os.environ, **environment},
140
- )
141
-
142
- duration = time.perf_counter() - start_time
143
-
144
- return BuildResult(
145
- success=result.returncode == 0,
146
- return_code=result.returncode,
147
- stdout=result.stdout,
148
- stderr=result.stderr,
149
- output_path=working_dir,
150
- duration_seconds=duration,
151
- )
152
-
136
+
137
+ if stream_output:
138
+ stdout_buffer = []
139
+ stderr_buffer = []
140
+
141
+ process = subprocess.Popen(
142
+ full_command,
143
+ stdout=subprocess.PIPE,
144
+ stderr=subprocess.PIPE,
145
+ text=True,
146
+ env={**os.environ, **environment},
147
+ )
148
+
149
+ stdout_buffer = []
150
+ stderr_buffer = []
151
+
152
+ # Read stdout in real-time
153
+ if process.stdout:
154
+ for line in process.stdout:
155
+ line = line.rstrip('\n\r')
156
+ stdout_buffer.append(line)
157
+ print(line)
158
+ sys.stdout.flush()
159
+
160
+ # Read stderr in real-time
161
+ if process.stderr:
162
+ for line in process.stderr:
163
+ line = line.rstrip('\n\r')
164
+ stderr_buffer.append(line)
165
+ print(line, file=sys.stderr)
166
+ sys.stderr.flush()
167
+
168
+ return_code = process.wait()
169
+ duration = time.perf_counter() - start_time
170
+
171
+ return BuildResult(
172
+ success=return_code == 0,
173
+ return_code=return_code,
174
+ stdout='\n'.join(stdout_buffer),
175
+ stderr='\n'.join(stderr_buffer),
176
+ output_path=working_dir,
177
+ duration_seconds=duration,
178
+ )
179
+ else:
180
+ result = subprocess.run(
181
+ full_command,
182
+ capture_output=True,
183
+ text=True,
184
+ timeout=3600,
185
+ env={**os.environ, **environment},
186
+ )
187
+
188
+ duration = time.perf_counter() - start_time
189
+
190
+ return BuildResult(
191
+ success=result.returncode == 0,
192
+ return_code=result.returncode,
193
+ stdout=result.stdout,
194
+ stderr=result.stderr,
195
+ output_path=working_dir,
196
+ duration_seconds=duration,
197
+ )
198
+
153
199
  except subprocess.TimeoutExpired:
154
200
  duration = time.perf_counter() - start_time
155
201
  return BuildResult(
@@ -171,7 +217,6 @@ class PnpmCompiler(CompilerBase):
171
217
  duration_seconds=duration,
172
218
  )
173
219
  finally:
174
- # Always return to original working directory
175
220
  os.chdir(original_cwd)
176
221
 
177
222
  def build(
@@ -182,16 +227,18 @@ class PnpmCompiler(CompilerBase):
182
227
  environment: dict[str, str] | None = None,
183
228
  mirror_enabled: bool = True,
184
229
  extra_args: list[str] | None = None,
230
+ stream_output: bool = True,
185
231
  ) -> BuildResult:
186
232
  """Execute the pnpm build process with auto-detection of project root.
187
-
233
+
188
234
  Args:
189
235
  source_dir: Source code directory or subdirectory
190
236
  output_dir: Build output directory
191
237
  environment: Additional environment variables
192
238
  mirror_enabled: Whether to use mirror acceleration
193
239
  extra_args: Additional arguments to pass to pnpm build
194
-
240
+ stream_output: Whether to stream output in real-time (default: True)
241
+
195
242
  Returns:
196
243
  BuildResult containing success status and output information.
197
244
  """
@@ -225,20 +272,22 @@ class PnpmCompiler(CompilerBase):
225
272
  [pnpm_executable, "install"],
226
273
  project_root,
227
274
  env,
275
+ stream_output=stream_output,
228
276
  )
229
-
277
+
230
278
  if not install_result["success"]:
231
279
  return install_result
232
-
280
+
233
281
  # Run build
234
282
  build_args = [pnpm_executable, "build"]
235
283
  if extra_args:
236
284
  build_args.extend(extra_args)
237
-
285
+
238
286
  return self._execute_in_directory(
239
287
  build_args,
240
288
  project_root,
241
289
  env,
290
+ stream_output=stream_output,
242
291
  )
243
292
 
244
293
  def install_dependencies(
@@ -282,11 +331,12 @@ class PnpmCompiler(CompilerBase):
282
331
  command = [pnpm_executable, "install"]
283
332
  if production:
284
333
  command.append("--prod")
285
-
334
+
286
335
  return self._execute_in_directory(
287
336
  command,
288
337
  project_root,
289
338
  env,
339
+ stream_output=True,
290
340
  )
291
341
 
292
342
  def run_script(
@@ -106,16 +106,18 @@ class PythonCompiler(CompilerBase):
106
106
  environment: dict[str, str] | None = None,
107
107
  mirror_enabled: bool = True,
108
108
  extra_args: list[str] | None = None,
109
+ stream_output: bool = True,
109
110
  ) -> BuildResult:
110
111
  """Execute the Python build process.
111
-
112
+
112
113
  Args:
113
114
  source_dir: Source code directory
114
115
  output_dir: Build output directory
115
116
  environment: Additional environment variables
116
117
  mirror_enabled: Whether to use PyPI mirror
117
118
  extra_args: Additional arguments to pass to build command
118
-
119
+ stream_output: Whether to stream output in real-time (default: True)
120
+
119
121
  Returns:
120
122
  BuildResult containing success status and output information.
121
123
  """
@@ -134,18 +136,18 @@ class PythonCompiler(CompilerBase):
134
136
 
135
137
  # Determine build system
136
138
  build_system = self._detect_build_system(source_dir)
137
-
139
+
138
140
  if build_system == "poetry":
139
141
  return self._build_with_poetry(
140
- python_executable, source_dir, output_dir, env, extra_args
142
+ python_executable, source_dir, output_dir, env, extra_args, stream_output
141
143
  )
142
144
  elif build_system == "setuptools":
143
145
  return self._build_with_setuptools(
144
- python_executable, source_dir, output_dir, env, extra_args
146
+ python_executable, source_dir, output_dir, env, extra_args, stream_output
145
147
  )
146
148
  elif build_system == "pdm":
147
149
  return self._build_with_pdm(
148
- python_executable, source_dir, output_dir, env, extra_args
150
+ python_executable, source_dir, output_dir, env, extra_args, stream_output
149
151
  )
150
152
  else:
151
153
  return BuildResult(
@@ -200,34 +202,37 @@ class PythonCompiler(CompilerBase):
200
202
  output_dir: Path,
201
203
  env: dict[str, str],
202
204
  extra_args: list[str] | None = None,
205
+ stream_output: bool = True,
203
206
  ) -> BuildResult:
204
207
  """Build using Poetry."""
205
208
  # Install dependencies
206
209
  install_cmd = [python_executable, "-m", "poetry", "install"]
207
-
210
+
208
211
  install_result = self._run_build(
209
212
  install_cmd,
210
213
  source_dir,
211
214
  output_dir,
212
215
  environment=env,
216
+ stream_output=stream_output,
213
217
  )
214
-
218
+
215
219
  if not install_result["success"]:
216
220
  return install_result
217
-
221
+
218
222
  # Build package
219
223
  build_cmd = [python_executable, "-m", "poetry", "build"]
220
-
224
+
221
225
  if extra_args:
222
226
  build_cmd.extend(extra_args)
223
-
227
+
224
228
  return self._run_build(
225
229
  build_cmd,
226
230
  source_dir,
227
231
  output_dir,
228
232
  environment=env,
233
+ stream_output=stream_output,
229
234
  )
230
-
235
+
231
236
  def _build_with_setuptools(
232
237
  self,
233
238
  python_executable: str,
@@ -235,6 +240,7 @@ class PythonCompiler(CompilerBase):
235
240
  output_dir: Path,
236
241
  env: dict[str, str],
237
242
  extra_args: list[str] | None = None,
243
+ stream_output: bool = True,
238
244
  ) -> BuildResult:
239
245
  """Build using setuptools."""
240
246
  # Install build dependencies first
@@ -253,8 +259,9 @@ class PythonCompiler(CompilerBase):
253
259
  source_dir,
254
260
  output_dir,
255
261
  environment=env,
262
+ stream_output=stream_output,
256
263
  )
257
-
264
+
258
265
  def _build_with_pdm(
259
266
  self,
260
267
  python_executable: str,
@@ -262,19 +269,21 @@ class PythonCompiler(CompilerBase):
262
269
  output_dir: Path,
263
270
  env: dict[str, str],
264
271
  extra_args: list[str] | None = None,
272
+ stream_output: bool = True,
265
273
  ) -> BuildResult:
266
274
  """Build using PDM."""
267
275
  # Build with PDM
268
276
  build_cmd = [python_executable, "-m", "pdm", "build"]
269
-
277
+
270
278
  if extra_args:
271
279
  build_cmd.extend(extra_args)
272
-
280
+
273
281
  return self._run_build(
274
282
  build_cmd,
275
283
  source_dir,
276
284
  output_dir,
277
285
  environment=env,
286
+ stream_output=stream_output,
278
287
  )
279
288
 
280
289
  def install_dependencies(
@@ -29,17 +29,18 @@ def register_claude_code(project_level: bool = True) -> bool:
29
29
  """Register as Claude Code skill.
30
30
 
31
31
  Args:
32
- project_level: If True (default), register to project-level .claude/CLAUDE.md.
32
+ project_level: If True (default), register to project-level .claude/multi-lang-build.md.
33
33
  If False, register to global ~/.claude/CLAUDE.md.
34
34
  """
35
35
  try:
36
36
  # Determine registration path
37
37
  if project_level:
38
- # Project-level: .claude/CLAUDE.md in current directory
38
+ # Project-level: .claude/multi-lang-build.md in current directory
39
39
  config_dir = Path.cwd() / ".claude"
40
40
  config_dir.mkdir(exist_ok=True)
41
- claude_md = config_dir / "CLAUDE.md"
41
+ skill_file = config_dir / "multi-lang-build.md"
42
42
  level_name = "project-level"
43
+ is_new = not skill_file.exists()
43
44
  else:
44
45
  # Global-level: ~/.claude/CLAUDE.md (only check CLI for global)
45
46
  result = subprocess.run(
@@ -55,17 +56,11 @@ def register_claude_code(project_level: bool = True) -> bool:
55
56
 
56
57
  claude_dir = Path.home() / ".claude"
57
58
  claude_dir.mkdir(exist_ok=True)
58
- claude_md = claude_dir / "CLAUDE.md"
59
+ skill_file = claude_dir / "CLAUDE.md"
59
60
  level_name = "global"
61
+ is_new = not skill_file.exists()
60
62
 
61
- # Read existing content
62
- existing_content = ""
63
- if claude_md.exists():
64
- existing_content = claude_md.read_text()
65
-
66
- skill_section = """
67
-
68
- ## Multi-Lang Build Tool
63
+ skill_content = """## Multi-Lang Build Tool
69
64
 
70
65
  You can use the `multi-lang-build` tool to compile projects:
71
66
 
@@ -73,6 +68,7 @@ You can use the `multi-lang-build` tool to compile projects:
73
68
  - `multi-lang-build pnpm <source> --output <dir>` - Build pnpm projects
74
69
  - `multi-lang-build go <source> --output <bin>` - Build Go projects
75
70
  - `multi-lang-build python <source> --output <dir>` - Build Python projects
71
+ - `multi-lang-build mirror list|set|show` - Configure domestic mirrors
76
72
 
77
73
  ### Examples:
78
74
  ```bash
@@ -87,23 +83,39 @@ multi-lang-build python ./src --output ./dist --mirror
87
83
 
88
84
  # Build pnpm project
89
85
  multi-lang-build pnpm ./src --output ./dist --mirror
86
+
87
+ # Configure mirror
88
+ multi-lang-build mirror set pip pip_aliyun
89
+ multi-lang-build mirror set go
90
90
  ```
91
91
 
92
92
  When working with Go projects, check if there are multiple main packages and use the `--target` flag to specify which one to build.
93
93
  """
94
94
 
95
- if "## Multi-Lang Build Tool" not in existing_content:
96
- with open(claude_md, "a") as f:
97
- f.write(skill_section)
98
- print(f"✅ Registered with Claude Code ({level_name}, added to {claude_md})")
95
+ if project_level:
96
+ # Project-level: create dedicated file
97
+ with open(skill_file, "w") as f:
98
+ f.write(skill_content)
99
+ print(f"✅ Registered with Claude Code ({level_name}, created {skill_file})")
99
100
  else:
100
- print(f"ℹ️ Already registered with Claude Code ({level_name}, {claude_md})")
101
+ # Global-level: append to existing CLAUDE.md if not already present
102
+ existing_content = ""
103
+ if skill_file.exists():
104
+ existing_content = skill_file.read_text()
105
+
106
+ if "## Multi-Lang Build Tool" not in existing_content:
107
+ with open(skill_file, "a") as f:
108
+ f.write(skill_content)
109
+ print(f"✅ Registered with Claude Code ({level_name}, added to {skill_file})")
110
+ else:
111
+ print(f"ℹ️ Already registered with Claude Code ({level_name}, {skill_file})")
101
112
 
102
113
  return True
103
114
 
104
115
  except Exception as e:
105
116
  print(f"❌ Failed to register with Claude Code: {e}")
106
117
  return False
118
+ return False
107
119
 
108
120
 
109
121
  def register_opencode(project_level: bool = True) -> bool: