ptn 0.4.2__py3-none-any.whl → 0.4.6__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.
porterminal/__init__.py CHANGED
@@ -19,7 +19,7 @@ import subprocess
19
19
  import sys
20
20
  import time
21
21
  from pathlib import Path
22
- from threading import Thread
22
+ from threading import Event, Thread
23
23
 
24
24
  from rich.console import Console
25
25
 
@@ -287,9 +287,15 @@ def main() -> int:
287
287
  if tunnel_process is not None:
288
288
  Thread(target=drain_process_output, args=(tunnel_process,), daemon=True).start()
289
289
 
290
- # Wait for Ctrl+C or process exit
290
+ # Use an event for responsive Ctrl+C handling on Windows
291
+ shutdown_event = Event()
292
+
293
+ def signal_handler(signum: int, frame: object) -> None:
294
+ shutdown_event.set()
295
+
296
+ old_handler = signal.signal(signal.SIGINT, signal_handler)
291
297
  try:
292
- while True:
298
+ while not shutdown_event.is_set():
293
299
  if server_process is not None and server_process.poll() is not None:
294
300
  code = server_process.returncode
295
301
  if code == 0 or code < 0:
@@ -304,9 +310,11 @@ def main() -> int:
304
310
  else:
305
311
  console.print(f"\n[yellow]Tunnel stopped (exit code {code})[/yellow]")
306
312
  break
307
- time.sleep(1)
313
+ shutdown_event.wait(0.1)
314
+ finally:
315
+ signal.signal(signal.SIGINT, old_handler)
308
316
 
309
- except KeyboardInterrupt:
317
+ if shutdown_event.is_set():
310
318
  console.print("\n[dim]Shutting down...[/dim]")
311
319
 
312
320
  # Cleanup - terminate gracefully, then kill if needed
porterminal/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.4.2'
32
- __version_tuple__ = version_tuple = (0, 4, 2)
31
+ __version__ = version = '0.4.6'
32
+ __version_tuple__ = version_tuple = (0, 4, 6)
33
33
 
34
34
  __commit_id__ = commit_id = None
porterminal/cli/args.py CHANGED
@@ -65,8 +65,11 @@ def parse_args() -> argparse.Namespace:
65
65
  parser.add_argument(
66
66
  "-i",
67
67
  "--init",
68
- action="store_true",
69
- help="Create .ptn/ptn.yaml config file in current directory",
68
+ nargs="?",
69
+ const=True,
70
+ default=False,
71
+ metavar="URL_OR_PATH",
72
+ help="Create .ptn/ptn.yaml config (optionally from URL or file path)",
70
73
  )
71
74
  parser.add_argument(
72
75
  "-p",
@@ -108,7 +111,7 @@ def parse_args() -> argparse.Namespace:
108
111
  sys.exit(0 if success else 1)
109
112
 
110
113
  if args.init:
111
- _init_config()
114
+ _init_config(args.init if args.init is not True else None)
112
115
  # Continue to launch ptn after creating config
113
116
 
114
117
  if args.default_password:
@@ -118,9 +121,16 @@ def parse_args() -> argparse.Namespace:
118
121
  return args
119
122
 
120
123
 
121
- def _init_config() -> None:
122
- """Create .ptn/ptn.yaml in current directory with auto-discovered scripts."""
124
+ def _init_config(source: str | None = None) -> None:
125
+ """Create .ptn/ptn.yaml in current directory.
126
+
127
+ Args:
128
+ source: Optional URL or file path to use as config source.
129
+ If None, auto-discovers scripts and creates default config.
130
+ """
123
131
  from pathlib import Path
132
+ from urllib.error import URLError
133
+ from urllib.request import urlopen
124
134
 
125
135
  import yaml
126
136
 
@@ -130,6 +140,37 @@ def _init_config() -> None:
130
140
  config_dir = cwd / ".ptn"
131
141
  config_file = config_dir / "ptn.yaml"
132
142
 
143
+ # If source is provided, fetch/copy it
144
+ if source:
145
+ config_dir.mkdir(exist_ok=True)
146
+
147
+ if source.startswith(("http://", "https://")):
148
+ # Download from URL
149
+ try:
150
+ print(f"Downloading config from {source}...")
151
+ with urlopen(source, timeout=10) as response:
152
+ content = response.read().decode("utf-8")
153
+ config_file.write_text(content)
154
+ print(f"Created: {config_file}")
155
+ except (URLError, OSError, TimeoutError) as e:
156
+ print(f"Error downloading config: {e}")
157
+ return
158
+ else:
159
+ # Copy from local file
160
+ source_path = Path(source).expanduser().resolve()
161
+ if not source_path.exists():
162
+ print(f"Error: File not found: {source_path}")
163
+ return
164
+ try:
165
+ content = source_path.read_text(encoding="utf-8")
166
+ config_file.write_text(content)
167
+ print(f"Created: {config_file} (from {source_path})")
168
+ except OSError as e:
169
+ print(f"Error reading config: {e}")
170
+ return
171
+ return
172
+
173
+ # No source - use auto-discovery
133
174
  if config_file.exists():
134
175
  print(f"Config already exists: {config_file}")
135
176
  return
@@ -5,15 +5,61 @@ import re
5
5
  import tomllib
6
6
  from pathlib import Path
7
7
 
8
+ import yaml
9
+
8
10
  # Pattern for safe script names (alphanumeric, hyphens, underscores only)
9
11
  _SAFE_NAME = re.compile(r"^[a-zA-Z0-9_-]+$")
10
12
 
13
+ # Maximum buttons to return from each discovery function
14
+ _MAX_BUTTONS = 6
15
+
11
16
 
12
17
  def _is_safe_name(name: str) -> bool:
13
18
  """Check if script name contains only safe characters."""
14
19
  return bool(_SAFE_NAME.match(name)) and len(name) <= 50
15
20
 
16
21
 
22
+ def _find_file(base: Path, filenames: list[str]) -> Path | None:
23
+ """Find the first existing file from a list of candidates."""
24
+ for filename in filenames:
25
+ path = base / filename
26
+ if path.exists():
27
+ return path
28
+ return None
29
+
30
+
31
+ def _build_buttons(
32
+ tasks: dict,
33
+ priority: list[str],
34
+ command_prefix: str,
35
+ *,
36
+ priority_only: bool = False,
37
+ ) -> list[dict]:
38
+ """Build button configs from tasks dict with priority ordering.
39
+
40
+ Args:
41
+ tasks: Dict of task names to their definitions
42
+ priority: List of task names to prioritize
43
+ command_prefix: Command prefix (e.g., "deno task", "task", "just")
44
+ priority_only: If True, only include tasks from priority list
45
+ """
46
+ buttons = []
47
+ priority_set = set(priority)
48
+
49
+ # Add priority tasks first
50
+ for name in priority:
51
+ if name in tasks and _is_safe_name(name):
52
+ buttons.append({"label": name, "send": f"{command_prefix} {name}\r", "row": 2})
53
+
54
+ # Add remaining tasks (unless priority_only is set)
55
+ if not priority_only:
56
+ for name in tasks:
57
+ if name not in priority_set and _is_safe_name(name) and len(buttons) < _MAX_BUTTONS:
58
+ buttons.append({"label": name, "send": f"{command_prefix} {name}\r", "row": 2})
59
+
60
+ return buttons[:_MAX_BUTTONS]
61
+
62
+
17
63
  def discover_scripts(cwd: Path | None = None) -> list[dict]:
18
64
  """Discover project scripts in current directory.
19
65
 
@@ -24,9 +70,13 @@ def discover_scripts(cwd: Path | None = None) -> list[dict]:
24
70
  buttons = []
25
71
 
26
72
  # Check each project type (only those with explicit scripts)
27
- buttons.extend(_discover_npm_scripts(base))
73
+ # Order matters: first match wins for deduplication
74
+ buttons.extend(_discover_npm_scripts(base)) # Also handles Bun
75
+ buttons.extend(_discover_deno_tasks(base))
28
76
  buttons.extend(_discover_python_scripts(base))
29
77
  buttons.extend(_discover_makefile_targets(base))
78
+ buttons.extend(_discover_just_recipes(base))
79
+ buttons.extend(_discover_taskfile_tasks(base))
30
80
 
31
81
  # Dedupe by label, keep first occurrence
32
82
  unique: dict[str, dict] = {}
@@ -36,7 +86,11 @@ def discover_scripts(cwd: Path | None = None) -> list[dict]:
36
86
 
37
87
 
38
88
  def _discover_npm_scripts(base: Path) -> list[dict]:
39
- """Extract scripts from package.json."""
89
+ """Extract scripts from package.json.
90
+
91
+ Uses 'bun run' if bun.lockb exists, otherwise 'npm run'.
92
+ Only includes scripts from the priority list (build, dev, start, etc.).
93
+ """
40
94
  pkg_file = base / "package.json"
41
95
  if not pkg_file.exists():
42
96
  return []
@@ -45,21 +99,21 @@ def _discover_npm_scripts(base: Path) -> list[dict]:
45
99
  data = json.loads(pkg_file.read_text(encoding="utf-8"))
46
100
  scripts = data.get("scripts", {})
47
101
 
48
- # Common useful scripts to include (if defined)
102
+ # Detect package manager: bun if bun.lockb exists
103
+ runner = "bun run" if (base / "bun.lockb").exists() else "npm run"
49
104
  priority = ["build", "dev", "start", "test", "lint", "format", "watch"]
50
105
 
51
- buttons = []
52
- for name in priority:
53
- if name in scripts:
54
- buttons.append({"label": name, "send": f"npm run {name}\r", "row": 2})
55
-
56
- return buttons[:6] # Limit to 6 buttons
106
+ return _build_buttons(scripts, priority, runner, priority_only=True)
57
107
  except Exception:
58
108
  return []
59
109
 
60
110
 
61
111
  def _discover_python_scripts(base: Path) -> list[dict]:
62
- """Extract scripts from pyproject.toml."""
112
+ """Extract scripts from pyproject.toml.
113
+
114
+ Checks [project.scripts] (PEP 621) first, then [tool.poetry.scripts].
115
+ Takes up to 4 from each source, deduplicates, and caps at 6 total.
116
+ """
63
117
  toml_file = base / "pyproject.toml"
64
118
  if not toml_file.exists():
65
119
  return []
@@ -68,19 +122,20 @@ def _discover_python_scripts(base: Path) -> list[dict]:
68
122
  data = tomllib.loads(toml_file.read_text(encoding="utf-8"))
69
123
  buttons = []
70
124
 
71
- # Check [project.scripts] (PEP 621)
125
+ # Check [project.scripts] (PEP 621) - take up to 4
72
126
  project_scripts = data.get("project", {}).get("scripts", {})
73
127
  for name in list(project_scripts.keys())[:4]:
74
128
  if _is_safe_name(name):
75
129
  buttons.append({"label": name, "send": f"{name}\r", "row": 2})
76
130
 
77
- # Check [tool.poetry.scripts]
131
+ # Check [tool.poetry.scripts] - take up to 4, skip duplicates
132
+ existing_labels = {b["label"] for b in buttons}
78
133
  poetry_scripts = data.get("tool", {}).get("poetry", {}).get("scripts", {})
79
134
  for name in list(poetry_scripts.keys())[:4]:
80
- if _is_safe_name(name) and not any(b["label"] == name for b in buttons):
135
+ if _is_safe_name(name) and name not in existing_labels:
81
136
  buttons.append({"label": name, "send": f"{name}\r", "row": 2})
82
137
 
83
- return buttons[:6]
138
+ return buttons[:_MAX_BUTTONS]
84
139
  except Exception:
85
140
  return []
86
141
 
@@ -98,15 +153,114 @@ def _discover_makefile_targets(base: Path) -> list[dict]:
98
153
  pattern = r"^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:"
99
154
  targets = re.findall(pattern, content, re.MULTILINE)
100
155
 
101
- # Priority order for common targets (use set for O(1) lookup)
156
+ # Convert list to dict for _build_buttons compatibility
157
+ targets_dict = {t: True for t in targets}
102
158
  priority = ["build", "test", "run", "clean", "install", "dev", "lint", "all"]
103
- priority_set = set(priority)
104
- target_set = set(targets)
105
159
 
106
- # Priority targets first, then remaining targets
107
- ordered = [t for t in priority if t in target_set]
108
- ordered.extend(t for t in targets if t not in priority_set)
160
+ return _build_buttons(targets_dict, priority, "make")
161
+ except Exception:
162
+ return []
163
+
164
+
165
+ def _discover_deno_tasks(base: Path) -> list[dict]:
166
+ """Extract tasks from deno.json or deno.jsonc."""
167
+ deno_file = _find_file(base, ["deno.json", "deno.jsonc"])
168
+ if not deno_file:
169
+ return []
170
+
171
+ try:
172
+ content = deno_file.read_text(encoding="utf-8")
173
+ if deno_file.suffix == ".jsonc":
174
+ content = _strip_json_comments(content)
175
+
176
+ tasks = json.loads(content).get("tasks", {})
177
+ priority = ["build", "dev", "start", "test", "lint", "format", "check"]
178
+
179
+ return _build_buttons(tasks, priority, "deno task")
180
+ except Exception:
181
+ return []
182
+
183
+
184
+ def _strip_json_comments(content: str) -> str:
185
+ """Strip comments from JSON content (for .jsonc files)."""
186
+ result = []
187
+ i = 0
188
+ in_string = False
189
+ escape_next = False
190
+
191
+ while i < len(content):
192
+ char = content[i]
193
+
194
+ if escape_next:
195
+ result.append(char)
196
+ escape_next = False
197
+ i += 1
198
+ continue
199
+
200
+ if char == "\\" and in_string:
201
+ result.append(char)
202
+ escape_next = True
203
+ i += 1
204
+ continue
205
+
206
+ if char == '"' and not escape_next:
207
+ in_string = not in_string
208
+ result.append(char)
209
+ i += 1
210
+ continue
211
+
212
+ if not in_string:
213
+ # Single-line comment
214
+ if content[i : i + 2] == "//":
215
+ while i < len(content) and content[i] != "\n":
216
+ i += 1
217
+ continue
218
+ # Multi-line comment
219
+ if content[i : i + 2] == "/*":
220
+ i += 2
221
+ while i < len(content) - 1 and content[i : i + 2] != "*/":
222
+ i += 1
223
+ i += 2
224
+ continue
225
+
226
+ result.append(char)
227
+ i += 1
228
+
229
+ return "".join(result)
230
+
231
+
232
+ def _discover_just_recipes(base: Path) -> list[dict]:
233
+ """Extract recipes from justfile."""
234
+ justfile = _find_file(base, ["justfile", "Justfile", ".justfile"])
235
+ if not justfile:
236
+ return []
237
+
238
+ try:
239
+ content = justfile.read_text(encoding="utf-8")
240
+ # Match recipe definitions: "recipe:" or "recipe arg:" at start of line
241
+ # Exclude private recipes (starting with _) and recipes with @ prefix
242
+ pattern = r"^([a-zA-Z][a-zA-Z0-9_-]*)\s*(?:[^:]*)?:"
243
+ recipes = re.findall(pattern, content, re.MULTILINE)
244
+
245
+ # Convert list to dict for _build_buttons compatibility
246
+ recipes_dict = {r: True for r in recipes}
247
+ priority = ["build", "test", "run", "dev", "check", "lint", "fmt", "clean"]
248
+
249
+ return _build_buttons(recipes_dict, priority, "just")
250
+ except Exception:
251
+ return []
252
+
253
+
254
+ def _discover_taskfile_tasks(base: Path) -> list[dict]:
255
+ """Extract tasks from Taskfile.yml."""
256
+ taskfile = _find_file(base, ["Taskfile.yml", "Taskfile.yaml", "taskfile.yml", "taskfile.yaml"])
257
+ if not taskfile:
258
+ return []
259
+
260
+ try:
261
+ tasks = yaml.safe_load(taskfile.read_text(encoding="utf-8")).get("tasks", {})
262
+ priority = ["build", "test", "run", "dev", "lint", "fmt", "clean", "default"]
109
263
 
110
- return [{"label": name, "send": f"make {name}\r", "row": 2} for name in ordered[:6]]
264
+ return _build_buttons(tasks, priority, "task")
111
265
  except Exception:
112
266
  return []
@@ -2,9 +2,9 @@
2
2
 
3
3
  from dataclasses import dataclass
4
4
 
5
- # Default business rules
6
- DEFAULT_RATE = 100.0 # tokens per second
7
- DEFAULT_BURST = 500
5
+ # Defaults: 1KB/s sustained, 16KB burst (allows reasonable paste operations)
6
+ DEFAULT_RATE = 1000.0
7
+ DEFAULT_BURST = 16384
8
8
 
9
9
 
10
10
  @dataclass(frozen=True, slots=True)
@@ -61,11 +61,19 @@ class ShellDetector:
61
61
  def _get_platform_candidates(self) -> list[tuple[str, str, str, list[str]]]:
62
62
  """Get shell candidates for current platform.
63
63
 
64
+ On Windows, discovers shells from:
65
+ - Windows Terminal profiles (includes WSL distros)
66
+ - Hardcoded common shells (PowerShell, CMD, Git Bash)
67
+ - Visual Studio Developer shells
68
+
69
+ Note: WSL distros are detected from Windows Terminal profiles only.
70
+ We don't probe inside WSL for individual shells to avoid crossing
71
+ environment boundaries.
72
+
64
73
  Returns:
65
74
  List of (name, id, command, args) tuples.
66
75
  """
67
76
  if sys.platform == "win32":
68
- # Get Windows Terminal profiles first, then hardcoded defaults
69
77
  wt_profiles = self._get_windows_terminal_profiles()
70
78
  hardcoded = [
71
79
  ("PS 7", "pwsh", "pwsh.exe", ["-NoLogo"]),
@@ -74,15 +82,18 @@ class ShellDetector:
74
82
  ("WSL", "wsl", "wsl.exe", []),
75
83
  ("Git Bash", "gitbash", r"C:\Program Files\Git\bin\bash.exe", ["--login"]),
76
84
  ]
77
- # Merge WT profiles with hardcoded (dedupe by command)
78
85
  merged = self._merge_candidates(wt_profiles, hardcoded)
79
- # Add VS dev shells (no deduplication - they're unique due to args)
80
86
  vs_shells = self._get_visual_studio_shells()
81
87
  return merged + vs_shells
82
88
  return [
83
89
  ("Bash", "bash", "bash", ["--login"]),
84
90
  ("Zsh", "zsh", "zsh", ["--login"]),
85
91
  ("Fish", "fish", "fish", []),
92
+ ("Nu", "nu", "nu", []),
93
+ ("Ion", "ion", "ion", []),
94
+ ("Dash", "dash", "dash", []),
95
+ ("Ksh", "ksh", "ksh", ["--login"]),
96
+ ("Tcsh", "tcsh", "tcsh", []),
86
97
  ("Sh", "sh", "sh", []),
87
98
  ]
88
99
 
@@ -118,8 +129,20 @@ class ShellDetector:
118
129
  for profile in profile_list:
119
130
  name = profile.get("name", "")
120
131
  commandline = profile.get("commandline", "")
132
+ source = profile.get("source", "")
133
+
134
+ if not name:
135
+ continue
136
+
137
+ # Handle WSL distro profiles (they use source instead of commandline)
138
+ if source == "Windows.Terminal.Wsl" and not commandline:
139
+ # Use wsl.exe -d <distro> to launch the specific distro
140
+ shell_id = self._slugify(name)
141
+ short_name = self._abbreviate_name(name)
142
+ profiles.append((short_name, shell_id, "wsl.exe", ["-d", name]))
143
+ continue
121
144
 
122
- if not name or not commandline:
145
+ if not commandline:
123
146
  continue
124
147
 
125
148
  # Parse commandline into command and args
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ptn
3
- Version: 0.4.2
3
+ Version: 0.4.6
4
4
  Summary: Web-based terminal accessible from phone via Cloudflare Tunnel
5
5
  Project-URL: Homepage, https://github.com/lyehe/porterminal
6
6
  Project-URL: Repository, https://github.com/lyehe/porterminal
@@ -1,6 +1,6 @@
1
- porterminal/__init__.py,sha256=aftLcHuDFp0nI5MF-eNyahFyx3nyGxf8SERK59-G_0Y,12604
1
+ porterminal/__init__.py,sha256=DvabdkAk4SwWv8_n9noLr-ESlhd0M_INWRf_CkijOQQ,12916
2
2
  porterminal/__main__.py,sha256=XLo21rqmISrIZFiaHC58Trgq8E0gxH4Wb3driD4JA7c,137
3
- porterminal/_version.py,sha256=A45grTqzrHuDn1CT9K5GVUbY4_Q3OSTcXAl3zdHzcEI,704
3
+ porterminal/_version.py,sha256=18TAUheMEe2us-SdlWuaPoE-optSFboNqdVGdL_jZ0I,704
4
4
  porterminal/app.py,sha256=Q3w-61i6h2Atz4wirAgrT--xTH_qCp4Ig2zgnAIyJ9U,15220
5
5
  porterminal/asgi.py,sha256=P76H7k03T3GYBAmjWqLaCZXV-YIou6NMhfRySO8He1A,1276
6
6
  porterminal/composition.py,sha256=H1QJszJqGX4BXZE4cFk3I46yxybZPtNG35srqRi35vo,6799
@@ -18,9 +18,9 @@ porterminal/application/services/session_service.py,sha256=ng1B4GQOVBCjI6gqkb_cO
18
18
  porterminal/application/services/tab_service.py,sha256=0_S978dYhBW2-eaZN2wOlcZOY4V4iPt2MMNOyoZQxPs,8307
19
19
  porterminal/application/services/terminal_service.py,sha256=0zaQWgZ8Uz8JmDfTqww4p2rJfH6AIyG8dVmSH4JIp2I,22054
20
20
  porterminal/cli/__init__.py,sha256=A3y-QgKrT-vdAYV-xsZjeyMkiPymZaZzYUGQ-_3cXmQ,305
21
- porterminal/cli/args.py,sha256=ixXT4W3D6MjS3IemZNeEjyC0-Vfsf8kQGwHotpjJhXM,5970
21
+ porterminal/cli/args.py,sha256=_QlvJWyV_-wIrLN8vbAJlw9LtGcMditmLrE3YZV9PDM,7543
22
22
  porterminal/cli/display.py,sha256=PgLnRj9odMKYERMCLyrEhTrpYeYPNYC-a1nWcz_fFE0,5433
23
- porterminal/cli/script_discovery.py,sha256=jdDRBZ-0LbtC2F1oV_oQtrd00duYWH2QXFs1FakKxtc,3869
23
+ porterminal/cli/script_discovery.py,sha256=E70bAWP1gCypkaPtLEPXmzyrTWrLz1_6bTQ51vRwZBM,8777
24
24
  porterminal/domain/__init__.py,sha256=16WEj1SAKhIqkrQHqaan66pV3zsUn1aEz7oymN54xFQ,1428
25
25
  porterminal/domain/entities/__init__.py,sha256=dIQp6T0M-Bl_DJpSOGQGBD29Rl_FDR0XfVrDL08ck50,463
26
26
  porterminal/domain/entities/output_buffer.py,sha256=TqqgGk3NEM_htNbAhZQCJM_dek4ajhwViPaVE3XXCeI,4220
@@ -37,7 +37,7 @@ porterminal/domain/services/session_limits.py,sha256=EqW_tqr0Br3a2gzgl5Dg80dx1um
37
37
  porterminal/domain/services/tab_limits.py,sha256=nokX8gP9_41MNIdQ7Fri2JLAs4_Ch-fy3CgpKHTZnT8,1434
38
38
  porterminal/domain/values/__init__.py,sha256=SuUiW8EINXgbpaCuqmdTGnVrmP0x2FuoJz3HsUzB7M0,516
39
39
  porterminal/domain/values/environment_rules.py,sha256=ii3D3CP4v8V-4H13VadzE76Z8EyZdbx-h7CYnEvXmpw,3946
40
- porterminal/domain/values/rate_limit_config.py,sha256=BZb-Jbxav8kJ6ijADIrW_-JhDt8WuPIe8OIqLnQykXA,548
40
+ porterminal/domain/values/rate_limit_config.py,sha256=kngpLF87aFnHmHG0PWeA38C4Vq_QVxh7gjndZPkAyX0,582
41
41
  porterminal/domain/values/session_id.py,sha256=dYBSZdG6WWjp06mUbTZwjufb30A6lSuK-NKwqxAAavY,461
42
42
  porterminal/domain/values/shell_command.py,sha256=9U723nsdDCsW_M17qBp__cu3r9uuBVuur0kifaoiEF0,1102
43
43
  porterminal/domain/values/tab_id.py,sha256=zKZZ1lBQUmXZ3PQlH0jejUQ_AA_SVTW-Z7jCI7Tq37A,557
@@ -49,7 +49,7 @@ porterminal/infrastructure/cloudflared.py,sha256=E7VtcZv4U5HeO1t4JYAO-97JavlD811
49
49
  porterminal/infrastructure/network.py,sha256=XnYbEXKQA8BnrLFl9b4OTTixFG1pVFxfeVuhN-XPkfU,1275
50
50
  porterminal/infrastructure/server.py,sha256=n6lSZjd6m380QzvdAZAu7CbbRoTvTAL8Dp-mcMaNFmY,5803
51
51
  porterminal/infrastructure/config/__init__.py,sha256=kcoM8mQYa83rkxn8TUfh0nETKynrsLmVwD9ySIfvdPQ,139
52
- porterminal/infrastructure/config/shell_detector.py,sha256=CPL7b7EgUcFNmjDzwzA-HxYCQNmyOzVw2xNGfmiQv_Q,16569
52
+ porterminal/infrastructure/config/shell_detector.py,sha256=VcFQ7IJdTxvH0P-j3Zw_uXZDGe5DOlUkZ1vKcg1aQw8,17463
53
53
  porterminal/infrastructure/registry/__init__.py,sha256=reNbIYRr1alNps7zm_oENuS6QWNKiiO-WG09LT5MLmM,157
54
54
  porterminal/infrastructure/registry/user_connection_registry.py,sha256=u9KOSijHiRITnwnOBKwXuYOKbFguh2NbxmNMxIoo8vc,3398
55
55
  porterminal/infrastructure/repositories/__init__.py,sha256=UyF9lpobXgKb9Ti6ucC6fDcC9CkMAGDThggEG-Rf-Aw,250
@@ -67,8 +67,8 @@ porterminal/static/icon.svg,sha256=y7-MIl7F_wVQMLHWmmC7MrZHZK5ikLg1BR_0jbqnTkc,1
67
67
  porterminal/static/index.html,sha256=xLfeiS_g9ujuJooN0QZWgt38q2ub1UcHs_anNS4ONgU,6401
68
68
  porterminal/static/assets/app-DlWNJWFE.js,sha256=HxwLXBSp5CrSOMhaI6O0zHxBY3J8dQlW5IuC_L8LnoQ,486711
69
69
  porterminal/static/assets/app-xPAM7YhQ.css,sha256=dYk-DAJSmvxP8fn5ABN5Jy_djhlpREDFjcIGKpb2gjw,18949
70
- ptn-0.4.2.dist-info/METADATA,sha256=1Y7txZBGmjLeOs4MI_UR3FgWM0X_d_DBUj5Ln10mJtg,8124
71
- ptn-0.4.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
72
- ptn-0.4.2.dist-info/entry_points.txt,sha256=Ftj1zSu_7G0yD5mAtGN3RoewyIuoBOfP_noSISe73tU,41
73
- ptn-0.4.2.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
74
- ptn-0.4.2.dist-info/RECORD,,
70
+ ptn-0.4.6.dist-info/METADATA,sha256=A0IQdX6yCq99NpW5z1rmrOFx0ShVqi8VmI_tqzN3Dzg,8124
71
+ ptn-0.4.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
72
+ ptn-0.4.6.dist-info/entry_points.txt,sha256=Ftj1zSu_7G0yD5mAtGN3RoewyIuoBOfP_noSISe73tU,41
73
+ ptn-0.4.6.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
74
+ ptn-0.4.6.dist-info/RECORD,,
File without changes