shipit-cli 0.7.1__py3-none-any.whl → 0.9.0__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.
@@ -12,7 +12,7 @@ echo "" > wp-content/upgrade/.keep
12
12
 
13
13
  echo "Installing WordPress core..."
14
14
 
15
- wp-cli core install \
15
+ wp core install \
16
16
  --url="$WASMER_APP_URL" \
17
17
  --title="$WP_SITE_TITLE" \
18
18
  --admin_user="$WP_ADMIN_USERNAME" \
@@ -21,13 +21,8 @@ wp-cli core install \
21
21
  --locale="$WP_LOCALE"
22
22
 
23
23
 
24
- if [ -z "$WASMER_FIRST_DEPLOYMENT" ]; then
25
- wp-cli core update-db
26
- fi
27
-
28
- echo "Installing theme..."
29
- wp-cli wasmer-aio-install install \
30
- --locale="$WP_LOCALE" \
31
- --theme=twentytwentyfive || true
24
+ if [ -z "$WP_UPDATE_DB" ]; then
25
+ wp core update-db
26
+ fi
32
27
 
33
28
  echo "Installation complete"
shipit/cli.py CHANGED
@@ -148,10 +148,12 @@ class Build:
148
148
 
149
149
  def write_stdout(line: str) -> None:
150
150
  sys.stdout.write(line) # print to console
151
+ sys.stdout.flush()
151
152
 
152
153
 
153
154
  def write_stderr(line: str) -> None:
154
155
  sys.stderr.write(line) # print to console
156
+ sys.stderr.flush()
155
157
 
156
158
 
157
159
  class MapperItem(TypedDict):
@@ -179,6 +181,16 @@ class Builder(Protocol):
179
181
 
180
182
 
181
183
  class DockerBuilder:
184
+ mise_mapper = {
185
+ "php": {
186
+ "source": "ubi:adwinying/php",
187
+ },
188
+ "composer": {
189
+ "source": "ubi:composer/composer",
190
+ "postinstall": """composer_dir=$(mise where ubi:composer/composer); ln -s "$composer_dir/composer.phar" /usr/local/bin/composer""",
191
+ },
192
+ }
193
+
182
194
  def __init__(self, src_dir: Path, docker_client: Optional[str] = None) -> None:
183
195
  self.src_dir = src_dir
184
196
  self.docker_file_contents = ""
@@ -293,7 +305,7 @@ class DockerBuilder:
293
305
  image_name,
294
306
  command,
295
307
  *(extra_args or []),
296
- _env=os.environ, # Pass the current environment variables to the Docker client
308
+ _env={"DOCKER_BUILDKIT": "1", **os.environ}, # Pass the current environment variables to the Docker client
297
309
  _out=write_stdout,
298
310
  _err=write_stderr,
299
311
  )
@@ -340,12 +352,17 @@ RUN chmod {oct(mode)[2:]} {path.absolute()}
340
352
  )
341
353
  self.docker_file_contents += f"RUN curl --proto '=https' --tlsv1.2 -sSfL https://get.static-web-server.net | sh\n"
342
354
  return
355
+
356
+ mapped_dependency = self.mise_mapper.get(dependency.name, {})
357
+ package_name = mapped_dependency.get("source", dependency.name)
343
358
  if dependency.version:
344
359
  self.docker_file_contents += (
345
- f"RUN mise use --global {dependency.name}@{dependency.version}\n"
360
+ f"RUN mise use --global {package_name}@{dependency.version}\n"
346
361
  )
347
362
  else:
348
- self.docker_file_contents += f"RUN mise use --global {dependency.name}\n"
363
+ self.docker_file_contents += f"RUN mise use --global {package_name}\n"
364
+ if mapped_dependency.get("postinstall"):
365
+ self.docker_file_contents += f"RUN {mapped_dependency.get('postinstall')}\n"
349
366
 
350
367
  def build(
351
368
  self, env: Dict[str, str], mounts: List[Mount], steps: List[Step]
@@ -353,14 +370,17 @@ RUN chmod {oct(mode)[2:]} {path.absolute()}
353
370
  base_path = self.docker_path
354
371
  shutil.rmtree(base_path, ignore_errors=True)
355
372
  base_path.mkdir(parents=True, exist_ok=True)
356
- self.docker_file_contents = "FROM debian:trixie-slim AS build\n"
373
+ self.docker_file_contents = "# syntax=docker/dockerfile:1.7-labs\n"
374
+ self.docker_file_contents += "FROM debian:trixie-slim AS build\n"
357
375
 
358
376
  self.docker_file_contents += """
359
377
  RUN apt-get update \\
360
378
  && apt-get -y --no-install-recommends install \\
361
- build-essential gcc make \\
362
- dpkg-dev pkg-config \\
379
+ build-essential gcc make autoconf libtool bison \\
380
+ dpkg-dev pkg-config re2c locate \\
363
381
  libmariadb-dev libmariadb-dev-compat libpq-dev \\
382
+ libvips-dev default-libmysqlclient-dev libmagickwand-dev \\
383
+ libicu-dev libxml2-dev libxslt-dev \\
364
384
  sudo curl ca-certificates \\
365
385
  && rm -rf /var/lib/apt/lists/*
366
386
 
@@ -407,7 +427,7 @@ RUN curl https://mise.run | sh
407
427
  # Read the file content and write it to the target file
408
428
  content_base64 = base64.b64encode(
409
429
  (ASSETS_PATH / step.source).read_bytes()
410
- )
430
+ ).decode("utf-8")
411
431
  self.docker_file_contents += (
412
432
  f"RUN echo '{content_base64}' | base64 -d > {step.target}\n"
413
433
  )
@@ -418,7 +438,11 @@ RUN curl https://mise.run | sh
418
438
  else:
419
439
  raise Exception(f"Asset {step.source} does not exist")
420
440
  else:
421
- self.docker_file_contents += f"COPY {step.source} {step.target}\n"
441
+ if step.ignore:
442
+ exclude = " \\\n" +" \\\n".join([f" --exclude={ignore}" for ignore in step.ignore]) + " \\\n "
443
+ else:
444
+ exclude = ""
445
+ self.docker_file_contents += f"COPY{exclude} {step.source} {step.target}\n"
422
446
  elif isinstance(step, EnvStep):
423
447
  env_vars = " ".join(
424
448
  [f"{key}={value}" for key, value in step.variables.items()]
@@ -446,12 +470,6 @@ Shipit
446
470
  def get_path(self) -> Path:
447
471
  return Path("/")
448
472
 
449
- def get_build_path(self) -> Path:
450
- return self.get_path() / "app"
451
-
452
- def get_serve_path(self) -> Path:
453
- return self.get_path() / "serve"
454
-
455
473
  def prepare(self, env: Dict[str, str], prepare: List[PrepareStep]) -> None:
456
474
  raise NotImplementedError
457
475
 
@@ -461,13 +479,12 @@ Shipit
461
479
  for dep in serve.deps:
462
480
  self.add_dependency(dep)
463
481
 
464
- build_path = self.get_build_path()
465
482
  for command in serve.commands:
466
483
  console.print(f"* {command}")
467
484
  command_path = serve_command_path / command
468
485
  self.create_file(
469
486
  command_path,
470
- f"#!/bin/bash\ncd {build_path}\n{serve.commands[command]}",
487
+ f"#!/bin/bash\ncd {serve.cwd}\n{serve.commands[command]}",
471
488
  mode=0o755,
472
489
  )
473
490
 
@@ -480,6 +497,7 @@ class LocalBuilder:
480
497
  def __init__(self, src_dir: Path) -> None:
481
498
  self.src_dir = src_dir
482
499
  self.local_path = self.src_dir / ".shipit" / "local"
500
+ self.serve_bin_path = self.local_path / "serve" / "bin"
483
501
  self.prepare_bash_script = self.local_path / "prepare" / "prepare.sh"
484
502
  self.build_path = self.local_path / "build"
485
503
  self.workdir = self.build_path
@@ -503,6 +521,8 @@ class LocalBuilder:
503
521
  elif isinstance(step, WorkdirStep):
504
522
  console.print(f"[bold]Working in {step.path}[/bold]")
505
523
  self.workdir = step.path
524
+ # We make sure the dir exists
525
+ step.path.mkdir(parents=True, exist_ok=True)
506
526
  elif isinstance(step, RunStep):
507
527
  extra = ""
508
528
  if step.inputs:
@@ -525,9 +545,10 @@ class LocalBuilder:
525
545
  exe = shutil.which(program, path=PATH)
526
546
  if not exe:
527
547
  raise Exception(f"Program is not installed: {program}")
528
- cmd = sh.Command(exe) # "grep"
548
+ cmd = sh.Command("bash") # "grep"
529
549
  result = cmd(
530
- *parts[1:],
550
+ "-c",
551
+ command_line,
531
552
  _env={**env, "PATH": PATH},
532
553
  _cwd=build_path,
533
554
  _out=write_stdout,
@@ -628,12 +649,6 @@ class LocalBuilder:
628
649
  def get_path(self) -> Path:
629
650
  return self.local_path
630
651
 
631
- def get_build_path(self) -> Path:
632
- return self.get_path() / "build"
633
-
634
- def get_serve_path(self) -> Path:
635
- return self.get_path() / "serve"
636
-
637
652
  def build_prepare(self, serve: Serve) -> None:
638
653
  self.prepare_bash_script.parent.mkdir(parents=True, exist_ok=True)
639
654
  commands: List[str] = []
@@ -676,14 +691,13 @@ class LocalBuilder:
676
691
  def build_serve(self, serve: Serve) -> None:
677
692
  # Remember serve configuration for run-time
678
693
  console.print("\n[bold]Building serve[/bold]")
679
- serve_command_path = self.get_serve_path() / "bin"
680
- serve_command_path.mkdir(parents=True, exist_ok=False)
694
+ self.serve_bin_path.mkdir(parents=True, exist_ok=False)
681
695
  path = self.get_path() / ".path"
682
696
  path_text = path.read_text()
683
697
  console.print(f"[bold]Serve Commands:[/bold]")
684
698
  for command in serve.commands:
685
699
  console.print(f"* {command}")
686
- command_path = serve_command_path / command
700
+ command_path = self.serve_bin_path / command
687
701
  env_vars = ""
688
702
  if serve.env:
689
703
  env_vars = " ".join([f"{k}={v}" for k, v in serve.env.items()])
@@ -707,8 +721,7 @@ class LocalBuilder:
707
721
 
708
722
  def run_serve_command(self, command: str) -> None:
709
723
  console.print(f"\n[bold]Running {command} command[/bold]")
710
- base_path = self.get_serve_path() / "bin"
711
- command_path = base_path / command
724
+ command_path = self.serve_bin_path / command
712
725
  sh.Command(str(command_path))(_out=write_stdout, _err=write_stderr)
713
726
 
714
727
 
@@ -808,9 +821,6 @@ class WasmerBuilder:
808
821
  ) -> None:
809
822
  return self.inner_builder.build(env, mounts, build)
810
823
 
811
- def get_build_path(self) -> Path:
812
- return Path("/app")
813
-
814
824
  def build_prepare(self, serve: Serve) -> None:
815
825
  print("Building prepare")
816
826
  prepare_dir = self.wasmer_dir_path / "prepare"
@@ -1704,7 +1714,7 @@ def main() -> None:
1704
1714
  app()
1705
1715
  except Exception as e:
1706
1716
  console.print(f"[bold red]{type(e).__name__}[/bold red]: {e}")
1707
- raise e
1717
+ # raise e
1708
1718
 
1709
1719
 
1710
1720
  if __name__ == "__main__":
shipit/generator.py CHANGED
@@ -109,7 +109,7 @@ def generate_shipit(path: Path, custom_commands: CustomCommands) -> str:
109
109
 
110
110
  build_steps_block = ",\n".join([f" {s}" for s in build_steps])
111
111
  deps_array = ", ".join(serve_dep_vars)
112
- commands_lines = ",\n".join([f' "{k}": {v}' for k, v in plan.commands.items()])
112
+ commands_lines = ",\n".join([f' "{k}": {v}.replace("$PORT", PORT)' for k, v in plan.commands.items()])
113
113
  env_lines = None
114
114
  if plan.env is not None:
115
115
  if len(plan.env) == 0:
@@ -119,31 +119,43 @@ def generate_shipit(path: Path, custom_commands: CustomCommands) -> str:
119
119
  mounts_block = None
120
120
  volumes_block = None
121
121
  attach_serve_names: list[str] = []
122
+
122
123
  if plan.mounts:
123
124
  mounts = list(filter(lambda m: m.attach_to_serve, plan.mounts))
124
125
  attach_serve_names = [m.name for m in mounts]
125
126
  mounts_block = ",\n".join([f" {m.name}" for m in mounts])
127
+
126
128
  if plan.volumes:
127
129
  volumes_block = ",\n".join(
128
130
  [f" {v.var_name or v.name}" for v in plan.volumes]
129
131
  )
130
132
 
131
133
  out: List[str] = []
134
+
132
135
  if dep_block:
133
136
  out.append(dep_block)
134
137
  out.append("")
138
+
135
139
  for m in plan.mounts:
136
140
  out.append(f"{m.name} = mount(\"{m.name}\")")
141
+ out.append("")
142
+
137
143
  if plan.volumes:
138
144
  for v in plan.volumes:
139
145
  out.append(f"{v.var_name or v.name} = volume(\"{v.name}\", {v.serve_path})")
146
+ out.append("")
147
+
140
148
  if plan.services:
141
149
  for s in plan.services:
142
150
  out.append(f"{s.name} = service(\n name=\"{s.name}\",\n provider=\"{s.provider}\"\n)")
151
+ out.append("")
152
+
153
+ out.append("PORT = getenv(\"PORT\") or\"8080\"")
143
154
 
144
155
  if plan.declarations:
145
156
  out.append(plan.declarations)
146
- out.append("")
157
+
158
+ out.append("")
147
159
  out.append("serve(")
148
160
  out.append(f' name="{plan.serve_name}",')
149
161
  out.append(f' provider="{plan.provider}",')
shipit/providers/hugo.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Dict, Optional
5
5
 
6
- from .base import DetectResult, DependencySpec, Provider, _exists, ServiceSpec, VolumeSpec, CustomCommands
6
+ from .base import DetectResult, DependencySpec, Provider, _exists, ServiceSpec, VolumeSpec, CustomCommands, MountSpec
7
7
  from .staticfile import StaticFileProvider
8
8
 
9
9
  class HugoProvider(StaticFileProvider):
@@ -43,10 +43,14 @@ class HugoProvider(StaticFileProvider):
43
43
 
44
44
  def build_steps(self) -> list[str]:
45
45
  return [
46
+ 'workdir(temp["build"])',
46
47
  'copy(".", ".", ignore=[".git"])',
47
48
  'run("hugo build --destination={}".format(app["build"]), group="build")',
48
49
  ]
49
-
50
+
51
+ def mounts(self) -> list[MountSpec]:
52
+ return [MountSpec("temp", attach_to_serve=False), *super().mounts()]
53
+
50
54
  def services(self) -> list[ServiceSpec]:
51
55
  return []
52
56
 
@@ -80,7 +80,7 @@ class LaravelProvider:
80
80
 
81
81
  def commands(self) -> Dict[str, str]:
82
82
  return {
83
- "start": '"php -S localhost:8080 -t public"',
83
+ "start": 'f"php -S localhost:{PORT} -t public"',
84
84
  "after_deploy": '"php artisan migrate"',
85
85
  }
86
86
 
@@ -30,6 +30,8 @@ class MkdocsProvider(StaticFileProvider):
30
30
  def detect(cls, path: Path, custom_commands: CustomCommands) -> Optional[DetectResult]:
31
31
  if _exists(path, "mkdocs.yml", "mkdocs.yaml"):
32
32
  return DetectResult(cls.name(), 85)
33
+ if custom_commands.build and custom_commands.build.startswith("mkdocs "):
34
+ return DetectResult(cls.name(), 85)
33
35
  return None
34
36
 
35
37
  def initialize(self) -> None:
@@ -1,37 +1,206 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
4
+ import yaml
3
5
  from pathlib import Path
4
- from typing import Dict, Optional
6
+ from typing import Dict, Optional, Any, Set
7
+ from enum import Enum
8
+ from semantic_version import Version, NpmSpec
9
+
5
10
 
6
11
  from .base import (
7
12
  DetectResult,
8
13
  DependencySpec,
9
14
  Provider,
10
15
  _exists,
11
- _has_dependency,
12
16
  MountSpec,
13
17
  ServiceSpec,
14
18
  VolumeSpec,
15
19
  CustomCommands,
16
20
  )
21
+ from .staticfile import StaticFileProvider
22
+
23
+
24
+ class PackageManager(Enum):
25
+ NPM = "npm"
26
+ PNPM = "pnpm"
27
+ YARN = "yarn"
28
+ BUN = "bun"
29
+
30
+ def as_dependency(self, path) -> DependencySpec:
31
+ dep_name = {
32
+ PackageManager.NPM: "npm",
33
+ PackageManager.PNPM: "pnpm",
34
+ PackageManager.YARN: "yarn",
35
+ PackageManager.BUN: "bun",
36
+ }[self]
37
+
38
+ default_version = None
39
+ if self == PackageManager.PNPM:
40
+ lockfile = path / self.lockfile()
41
+ lockfile_version = self.pnpm_lockfile_version(lockfile)
42
+ if lockfile_version:
43
+ if lockfile_version.startswith("5."):
44
+ default_version = "7"
45
+ elif lockfile_version.startswith("6."):
46
+ default_version = "8"
47
+
48
+ return DependencySpec(
49
+ dep_name,
50
+ env_var=f"SHIPIT_{dep_name.upper()}_VERSION",
51
+ default_version=default_version,
52
+ )
53
+
54
+ def lockfile(self) -> (str):
55
+ return {
56
+ PackageManager.NPM: "package-lock.json",
57
+ PackageManager.PNPM: "pnpm-lock.yaml",
58
+ PackageManager.YARN: "yarn.lock",
59
+ PackageManager.BUN: "bun.lockb",
60
+ }[self]
61
+
62
+ @classmethod
63
+ def pnpm_lockfile_version(cls, lockfile: Path) -> Optional[str]:
64
+ # Read line by line and return the lockfileVersion
65
+ with open(lockfile, "r") as f:
66
+ for line in f:
67
+ if "lockfileVersion" in line:
68
+ try:
69
+ config = yaml.safe_load(line)
70
+ version = config.get("lockfileVersion")
71
+ assert isinstance(version, (str, bytes))
72
+ return version
73
+ except:
74
+ pass
75
+ return None
76
+
77
+ def install_command(self, has_lockfile: bool = False) -> str:
78
+ return {
79
+ PackageManager.NPM: f"npm {'ci' if has_lockfile else 'install'}",
80
+ PackageManager.PNPM: f"pnpm install{' --frozen-lockfile' if has_lockfile else ''}",
81
+ PackageManager.YARN: f"yarn install{' --frozen-lockfile' if has_lockfile else ''}",
82
+ PackageManager.BUN: f"bun install{' --no-save' if has_lockfile else ''}",
83
+ }[self]
84
+
85
+ def run_command(self, command: str) -> str:
86
+ return {
87
+ PackageManager.NPM: f"npm run {command}",
88
+ PackageManager.PNPM: f"pnpm run {command}",
89
+ PackageManager.YARN: f"yarn run {command}",
90
+ PackageManager.BUN: f"bun run {command}",
91
+ }[self]
92
+
93
+ def run_execute_command(self, command: str) -> str:
94
+ return {
95
+ PackageManager.NPM: f"npx {command}",
96
+ PackageManager.PNPM: f"pnpx {command}",
97
+ PackageManager.YARN: f"ypx {command}",
98
+ PackageManager.BUN: f"bunx {command}",
99
+ }[self]
100
+
101
+
102
+ class StaticGenerator(Enum):
103
+ ASTRO = "astro"
104
+ VITE = "vite"
105
+ NEXT = "next"
106
+ GATSBY = "gatsby"
107
+ DOCUSAURUS = "docusaurus"
108
+ SVELTE = "svelte"
109
+ REMIX = "remix"
110
+ NUXT_OLD = "nuxt"
111
+ NUXT_V3 = "nuxt3"
17
112
 
18
113
 
19
- class NodeStaticProvider:
114
+ class NodeStaticProvider(StaticFileProvider):
115
+ package_manager: PackageManager
116
+ package_json: Optional[Dict[str, Any]]
117
+ extra_dependencies: Set[str]
118
+
20
119
  def __init__(self, path: Path, custom_commands: CustomCommands):
21
- self.path = path
22
- self.custom_commands = custom_commands
120
+ super().__init__(path, custom_commands)
121
+ if (path / "package-lock.json").exists():
122
+ self.package_manager = PackageManager.NPM
123
+ elif (path / "pnpm-lock.yaml").exists():
124
+ self.package_manager = PackageManager.PNPM
125
+ elif (path / "yarn.lock").exists():
126
+ self.package_manager = PackageManager.YARN
127
+ elif (path / "bun.lockb").exists():
128
+ self.package_manager = PackageManager.BUN
129
+ else:
130
+ self.package_manager = PackageManager.PNPM
131
+
132
+ self.package_json = self.parse_package_json(path)
133
+
134
+ if self.has_dependency(self.package_json, "gatsby"):
135
+ self.static_generator = StaticGenerator.GATSBY
136
+ elif self.has_dependency(self.package_json, "astro"):
137
+ self.static_generator = StaticGenerator.ASTRO
138
+ elif self.has_dependency(self.package_json, "@docusaurus/core"):
139
+ self.static_generator = StaticGenerator.DOCUSAURUS
140
+ elif self.has_dependency(self.package_json, "svelte"):
141
+ self.static_generator = StaticGenerator.SVELTE
142
+ elif self.has_dependency(self.package_json, "@remix-run/dev"):
143
+ self.static_generator = StaticGenerator.REMIX
144
+ elif self.has_dependency(self.package_json, "vite"):
145
+ self.static_generator = StaticGenerator.VITE
146
+ elif self.has_dependency(self.package_json, "next"):
147
+ self.static_generator = StaticGenerator.NEXT
148
+ elif self.has_dependency(self.package_json, "nuxt", "2") or self.has_dependency(
149
+ self.package_json, "nuxt", "1"
150
+ ):
151
+ self.static_generator = StaticGenerator.NUXT_OLD
152
+ elif self.has_dependency(self.package_json, "nuxt"):
153
+ self.static_generator = StaticGenerator.NUXT_V3
154
+
155
+ # if self.has_dependency(self.package_json, "sharp"):
156
+ # self.extra_dependencies.add("libvips")
157
+
158
+ @classmethod
159
+ def parse_package_json(cls, path: Path) -> Optional[Dict[str, Any]]:
160
+ package_json_path = path / "package.json"
161
+ if not package_json_path.exists():
162
+ return None
163
+ try:
164
+ package_json = json.loads(package_json_path.read_text())
165
+ assert isinstance(package_json, dict), (
166
+ "package.json must be a valid JSON object"
167
+ )
168
+ return package_json
169
+ except Exception:
170
+ return None
171
+
172
+ @classmethod
173
+ def has_dependency(
174
+ cls, package_json: Optional[Dict[str, Any]], dep: str, version: Optional[str] = None
175
+ ) -> bool:
176
+ if not package_json:
177
+ return False
178
+ for section in ("dependencies", "devDependencies", "peerDependencies"):
179
+ dep_section = package_json.get(section, {})
180
+ if dep in dep_section:
181
+ if version:
182
+ try:
183
+ constraint = NpmSpec(dep_section[dep])
184
+ return Version(version) in constraint
185
+ except Exception:
186
+ pass
187
+ else:
188
+ return True
189
+ return False
23
190
 
24
191
  @classmethod
25
192
  def name(cls) -> str:
26
193
  return "node-static"
27
194
 
28
195
  @classmethod
29
- def detect(cls, path: Path, custom_commands: CustomCommands) -> Optional[DetectResult]:
30
- pkg = path / "package.json"
31
- if not pkg.exists():
196
+ def detect(
197
+ cls, path: Path, custom_commands: CustomCommands
198
+ ) -> Optional[DetectResult]:
199
+ package_json = cls.parse_package_json(path)
200
+ if not package_json:
32
201
  return None
33
- static_generators = ["astro", "vite", "next", "nuxt"]
34
- if any(_has_dependency(pkg, dep) for dep in static_generators):
202
+ static_generators = ["astro", "vite", "next", "nuxt", "gatsby", "svelte", "@docusaurus/core", "@remix-run/dev"]
203
+ if any(cls.has_dependency(package_json, dep) for dep in static_generators):
35
204
  return DetectResult(cls.name(), 40)
36
205
  return None
37
206
 
@@ -45,6 +214,8 @@ class NodeStaticProvider:
45
214
  return "staticsite"
46
215
 
47
216
  def dependencies(self) -> list[DependencySpec]:
217
+ package_manager_dep = self.package_manager.as_dependency(self.path)
218
+ package_manager_dep.use_in_build = True
48
219
  return [
49
220
  DependencySpec(
50
221
  "node",
@@ -52,37 +223,82 @@ class NodeStaticProvider:
52
223
  default_version="22",
53
224
  use_in_build=True,
54
225
  ),
55
- DependencySpec("npm", use_in_build=True),
226
+ package_manager_dep,
56
227
  DependencySpec("static-web-server", use_in_serve=True),
57
228
  ]
58
229
 
59
230
  def declarations(self) -> Optional[str]:
60
231
  return None
61
232
 
233
+ def get_output_dir(self) -> str:
234
+ if self.static_generator == StaticGenerator.NEXT:
235
+ return "out"
236
+ elif self.static_generator in [StaticGenerator.ASTRO, StaticGenerator.VITE, StaticGenerator.NUXT_OLD, StaticGenerator.NUXT_V3]:
237
+ return "dist"
238
+ elif self.static_generator in [StaticGenerator.GATSBY, StaticGenerator.REMIX]:
239
+ return "public"
240
+ elif self.static_generator in [StaticGenerator.DOCUSAURUS, StaticGenerator.SVELTE]:
241
+ return "build"
242
+ else:
243
+ return "dist"
244
+
245
+ def get_build_command(self) -> bool:
246
+ if not self.package_json:
247
+ return False
248
+ build_command = self.package_json.get("scripts", {}).get("build")
249
+ if build_command:
250
+ return self.package_manager.run_command("build")
251
+ if self.static_generator == StaticGenerator.GATSBY:
252
+ return self.package_manager.run_execute_command("gatsby build")
253
+ if self.static_generator == StaticGenerator.ASTRO:
254
+ return self.package_manager.run_execute_command("astro build")
255
+ elif self.static_generator == StaticGenerator.REMIX:
256
+ return self.package_manager.run_execute_command("remix-ssg build")
257
+ elif self.static_generator == StaticGenerator.DOCUSAURUS:
258
+ return self.package_manager.run_execute_command("docusaurus build")
259
+ elif self.static_generator == StaticGenerator.SVELTE:
260
+ return self.package_manager.run_execute_command("svelte-kit build")
261
+ elif self.static_generator == StaticGenerator.VITE:
262
+ return self.package_manager.run_execute_command("vite build")
263
+ elif self.static_generator == StaticGenerator.NEXT:
264
+ return self.package_manager.run_execute_command("next export")
265
+ elif self.static_generator == StaticGenerator.NUXT_V3:
266
+ return self.package_manager.run_execute_command("nuxi generate")
267
+ elif self.static_generator == StaticGenerator.NUXT_OLD:
268
+ return self.package_manager.run_execute_command("nuxt generate")
269
+ return False
270
+
62
271
  def build_steps(self) -> list[str]:
63
- output_dir = "dist" if (self.path / "dist").exists() else "public"
272
+ output_dir = self.get_output_dir()
273
+ get_build_command = self.get_build_command()
274
+ lockfile = self.package_manager.lockfile()
275
+ has_lockfile = (self.path / lockfile).exists()
276
+ install_command = self.package_manager.install_command(has_lockfile=has_lockfile)
277
+ input_files = ["package.json"]
278
+ if has_lockfile:
279
+ input_files.append(lockfile)
280
+ inputs_install_files = ", ".join([f'"{file}"' for file in input_files])
281
+
64
282
  return [
65
- "run(\"npm install\", inputs=[\"package.json\", \"package-lock.json\"], group=\"install\")",
66
- "copy(\".\", \".\", ignore=[\"node_modules\", \".git\"])",
67
- f"run(\"npm run build\", outputs=[\"{output_dir}\"], group=\"build\")",
68
- f"run(\"cp -R {output_dir}/* {{}}/\".format(app[\"build\"]))",
283
+ 'workdir(temp["build"])',
284
+ # 'run("npx corepack enable", inputs=["package.json"], group="install")',
285
+ f'run("{install_command}", inputs=[{inputs_install_files}], group="install")',
286
+ 'copy(".", ".", ignore=["node_modules", ".git"])',
287
+ f'run("{get_build_command}", outputs=["{output_dir}"], group="build")',
288
+ f'run("cp -R {output_dir}/* {{}}/".format(app["build"]))',
69
289
  ]
70
290
 
71
291
  def prepare_steps(self) -> Optional[list[str]]:
72
292
  return None
73
293
 
74
- def commands(self) -> Dict[str, str]:
75
- output_dir = "dist" if (self.path / "dist").exists() else "public"
76
- return {"start": f'"static-web-server --root /app/{output_dir}"'}
77
-
78
294
  def mounts(self) -> list[MountSpec]:
79
- return [MountSpec("app")]
295
+ return [MountSpec("temp"), *super().mounts()]
80
296
 
81
297
  def volumes(self) -> list[VolumeSpec]:
82
298
  return []
83
299
 
84
300
  def env(self) -> Optional[Dict[str, str]]:
85
301
  return None
86
-
302
+
87
303
  def services(self) -> list[ServiceSpec]:
88
304
  return []
shipit/providers/php.py CHANGED
@@ -92,9 +92,9 @@ class PhpProvider:
92
92
 
93
93
  def base_commands(self) -> Dict[str, str]:
94
94
  if _exists(self.path, "public/index.php"):
95
- return {"start": '"php -S localhost:8080 -t public"'}
95
+ return {"start": 'f"php -S localhost:{PORT} -t public"'}
96
96
  elif _exists(self.path, "index.php"):
97
- return {"start": '"php -S localhost:8080 -t ."'}
97
+ return {"start": 'f"php -S localhost:{PORT} -t ."'}
98
98
 
99
99
  def mounts(self) -> list[MountSpec]:
100
100
  return [
@@ -284,7 +284,7 @@ class PythonProvider:
284
284
  if not self.only_build:
285
285
  steps = ['workdir(app["build"])']
286
286
  else:
287
- steps = []
287
+ steps = ['workdir(temp["build"])']
288
288
 
289
289
  extra_deps = ", ".join([f"{dep}" for dep in self.extra_dependencies])
290
290
  has_requirements = _exists(self.path, "requirements.txt")
@@ -317,7 +317,7 @@ class PythonProvider:
317
317
  ]
318
318
  if not self.only_build:
319
319
  steps += [
320
- 'run(f"uv pip compile pyproject.toml --python-version={python_version} --universal --extra-index-url {python_extra_index_url} --index-url=https://pypi.org/simple --emit-index-url -o cross-requirements.txt", outputs=["cross-requirements.txt"]) if cross_platform else None',
320
+ 'run(f"uv pip compile pyproject.toml --python-version={python_version} --universal --extra-index-url {python_extra_index_url} --index-url=https://pypi.org/simple --emit-index-url --no-deps -o cross-requirements.txt", outputs=["cross-requirements.txt"]) if cross_platform else None',
321
321
  f'run(f"uvx pip install -r cross-requirements.txt {extra_deps} --target {{python_cross_packages_path}} --platform {{cross_platform}} --only-binary=:all: --python-version={{python_version}} --compile") if cross_platform else None',
322
322
  'run("rm cross-requirements.txt") if cross_platform else None',
323
323
  ]
@@ -337,7 +337,7 @@ class PythonProvider:
337
337
  ]
338
338
  if not self.only_build:
339
339
  steps += [
340
- 'run(f"uv pip compile requirements.txt --python-version={python_version} --universal --extra-index-url {python_extra_index_url} --index-url=https://pypi.org/simple --emit-index-url -o cross-requirements.txt", inputs=["requirements.txt"], outputs=["cross-requirements.txt"]) if cross_platform else None',
340
+ 'run(f"uv pip compile requirements.txt --python-version={python_version} --universal --extra-index-url {python_extra_index_url} --index-url=https://pypi.org/simple --emit-index-url --no-deps -o cross-requirements.txt", inputs=["requirements.txt"], outputs=["cross-requirements.txt"]) if cross_platform else None',
341
341
  f'run(f"uvx pip install -r cross-requirements.txt {extra_deps} --target {{python_cross_packages_path}} --platform {{cross_platform}} --only-binary=:all: --python-version={{python_version}} --compile") if cross_platform else None',
342
342
  'run("rm cross-requirements.txt") if cross_platform else None',
343
343
  ]
@@ -396,20 +396,20 @@ class PythonProvider:
396
396
  if self.server == PythonServer.Daphne and self.asgi_application:
397
397
  asgi_application = format_app_import(self.asgi_application)
398
398
  start_cmd = (
399
- f'"python -m daphne {asgi_application} --bind 0.0.0.0 --port 8000"'
399
+ f'f"python -m daphne {asgi_application} --bind 0.0.0.0 --port {{PORT}}"'
400
400
  )
401
401
  elif self.server == PythonServer.Uvicorn:
402
402
  if self.asgi_application:
403
403
  asgi_application = format_app_import(self.asgi_application)
404
- start_cmd = f'"python -m uvicorn {asgi_application} --host 0.0.0.0 --port 8000"'
404
+ start_cmd = f'f"python -m uvicorn {asgi_application} --host 0.0.0.0 --port {{PORT}}"'
405
405
  elif self.wsgi_application:
406
406
  wsgi_application = format_app_import(self.wsgi_application)
407
- start_cmd = f'"python -m uvicorn {wsgi_application} --interface=wsgi --host 0.0.0.0 --port 8000"'
407
+ start_cmd = f'f"python -m uvicorn {wsgi_application} --interface=wsgi --host 0.0.0.0 --port {{PORT}}"'
408
408
  # elif self.server == PythonServer.Gunicorn:
409
- # start_cmd = f'"python -m gunicorn {self.wsgi_application} --bind 0.0.0.0 --port 8000"'
409
+ # start_cmd = f'"fpython -m gunicorn {self.wsgi_application} --bind 0.0.0.0 --port {{PORT}}"'
410
410
  if not start_cmd:
411
411
  # We run the default runserver command if no server is specified
412
- start_cmd = '"python manage.py runserver 0.0.0.0:8000"'
412
+ start_cmd = 'f"python manage.py runserver 0.0.0.0:{PORT}"'
413
413
  migrate_cmd = '"python manage.py migrate"'
414
414
  return {"start": start_cmd, "after_deploy": migrate_cmd}
415
415
 
@@ -423,21 +423,21 @@ class PythonProvider:
423
423
  python_path = file_to_python_path(main_file)
424
424
  path = f"{python_path}:app"
425
425
  if self.server == PythonServer.Uvicorn:
426
- start_cmd = f'"python -m uvicorn {path} --host 0.0.0.0 --port 8000"'
426
+ start_cmd = f'f"python -m uvicorn {path} --host 0.0.0.0 --port {{PORT}}"'
427
427
  elif self.server == PythonServer.Hypercorn:
428
- start_cmd = f'"python -m hypercorn {path} --bind 0.0.0.0:8000"'
428
+ start_cmd = f'f"python -m hypercorn {path} --bind 0.0.0.0:{{PORT}}"'
429
429
  else:
430
430
  start_cmd = '"python -c \'print(\\"No start command detected, please provide a start command manually\\")\'"'
431
431
  return {"start": start_cmd}
432
432
 
433
433
  elif self.framework == PythonFramework.Streamlit:
434
- start_cmd = f'"python -m streamlit run {main_file} --server.port 8000 --server.address 0.0.0.0 --server.headless true"'
434
+ start_cmd = f'f"python -m streamlit run {main_file} --server.port {{PORT}} --server.address 0.0.0.0 --server.headless true"'
435
435
 
436
436
  elif self.framework == PythonFramework.Flask:
437
437
  python_path = file_to_python_path(main_file)
438
438
  path = f"{python_path}:app"
439
- # start_cmd = f'"python -m flask --app {path} run --debug --host 0.0.0.0 --port 8000"'
440
- start_cmd = f'"python -m uvicorn {path} --interface=wsgi --host 0.0.0.0 --port 8000"'
439
+ # start_cmd = f'f"python -m flask --app {path} run --debug --host 0.0.0.0 --port {{PORT}}"'
440
+ start_cmd = f'f"python -m uvicorn {path} --interface=wsgi --host 0.0.0.0 --port {{PORT}}"'
441
441
 
442
442
  elif self.framework == PythonFramework.MCP:
443
443
  contents = (self.path / main_file).read_text()
@@ -449,7 +449,7 @@ class PythonProvider:
449
449
  elif self.framework == PythonFramework.FastHTML:
450
450
  python_path = file_to_python_path(main_file)
451
451
  path = f"{python_path}:app"
452
- start_cmd = f'"python -m uvicorn {path} --host 0.0.0.0 --port 8000"'
452
+ start_cmd = f'f"python -m uvicorn {path} --host 0.0.0.0 --port {{PORT}}"'
453
453
 
454
454
  else:
455
455
  start_cmd = f'"python {main_file}"'
@@ -459,6 +459,7 @@ class PythonProvider:
459
459
  def mounts(self) -> list[MountSpec]:
460
460
  if self.only_build:
461
461
  return [
462
+ MountSpec("temp", attach_to_serve=False),
462
463
  MountSpec("local_venv", attach_to_serve=False),
463
464
  ]
464
465
  return [
@@ -486,7 +487,7 @@ class PythonProvider:
486
487
  env_vars["STREAMLIT_SERVER_HEADLESS"] = '"true"'
487
488
  elif self.framework == PythonFramework.MCP:
488
489
  env_vars["FASTMCP_HOST"] = '"0.0.0.0"'
489
- env_vars["FASTMCP_PORT"] = '"8000"'
490
+ env_vars["FASTMCP_PORT"] = 'PORT'
490
491
  return env_vars
491
492
 
492
493
  def services(self) -> list[ServiceSpec]:
@@ -16,7 +16,7 @@ def providers() -> list[type[Provider]]:
16
16
  # Order matters: more specific providers first
17
17
  return [
18
18
  LaravelProvider,
19
- GatsbyProvider,
19
+ # GatsbyProvider,
20
20
  HugoProvider,
21
21
  MkdocsProvider,
22
22
  PythonProvider,
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
4
  from typing import Dict, Optional
5
+ import json
6
+ import yaml
5
7
 
6
8
  from .base import (
7
9
  DetectResult,
@@ -16,9 +18,19 @@ from .base import (
16
18
 
17
19
 
18
20
  class StaticFileProvider:
21
+ config: Optional[dict] = None
22
+ path: Path
23
+ custom_commands: CustomCommands
24
+
19
25
  def __init__(self, path: Path, custom_commands: CustomCommands):
20
26
  self.path = path
21
27
  self.custom_commands = custom_commands
28
+ if (self.path / "Staticfile").exists():
29
+ try:
30
+ self.config = yaml.safe_load((self.path / "Staticfile").read_text())
31
+ except yaml.YAMLError as e:
32
+ print(f"Error loading Staticfile: {e}")
33
+ pass
22
34
 
23
35
  @classmethod
24
36
  def name(cls) -> str:
@@ -58,7 +70,7 @@ class StaticFileProvider:
58
70
  def build_steps(self) -> list[str]:
59
71
  return [
60
72
  'workdir(app["build"])',
61
- 'copy(".", ".", ignore=[".git"])'
73
+ 'copy({}, ".", ignore=[".git"])'.format(json.dumps(self.config and self.config.get("root") or "."))
62
74
  ]
63
75
 
64
76
  def prepare_steps(self) -> Optional[list[str]]:
@@ -69,7 +81,7 @@ class StaticFileProvider:
69
81
 
70
82
  def commands(self) -> Dict[str, str]:
71
83
  return {
72
- "start": '"static-web-server --root={} --log-level=info".format(app["serve"])'
84
+ "start": '"static-web-server --root={} --log-level=info --port={}".format(app["serve"], PORT)'
73
85
  }
74
86
 
75
87
  def mounts(self) -> list[MountSpec]:
@@ -70,7 +70,7 @@ class WordPressProvider(PhpProvider):
70
70
 
71
71
  def commands(self) -> Dict[str, str]:
72
72
  return {
73
- "start": '"php -S localhost:8080 -t ."',
73
+ "start": 'f"php -S localhost:{PORT} -t ."',
74
74
  "wp": '"php {}/wp-cli.phar --allow-root --path={}".format(assets[\"serve\"], app[\"serve\"])',
75
75
  "after_deploy": '"bash {}/wordpress-install.sh".format(assets["serve"])',
76
76
  }
shipit/version.py CHANGED
@@ -1,5 +1,5 @@
1
1
  __all__ = ["version", "version_info"]
2
2
 
3
3
 
4
- version = "0.7.1"
5
- version_info = (0, 7, 1, "final", 0)
4
+ version = "0.9.0"
5
+ version_info = (0, 9, 0, "final", 0)
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shipit-cli
3
- Version: 0.7.1
4
- Summary: Add your description here
3
+ Version: 0.9.0
4
+ Summary: Shipit CLI is the best way to build, serve and deploy your projects anywhere.
5
5
  Project-URL: homepage, https://wasmer.io
6
6
  Project-URL: repository, https://github.com/wasmerio/shipit
7
7
  Project-URL: Changelog, https://github.com/wasmerio/shipit/changelog
@@ -10,6 +10,7 @@ Requires-Dist: dotenv>=0.9.9
10
10
  Requires-Dist: pyyaml>=6.0.2
11
11
  Requires-Dist: requests>=2.32.5
12
12
  Requires-Dist: rich>=14.1.0
13
+ Requires-Dist: semantic-version>=2.10.0
13
14
  Requires-Dist: sh>=2.2.2
14
15
  Requires-Dist: starlark-pyo3>=2025.1
15
16
  Requires-Dist: tomlkit>=0.13.3
@@ -0,0 +1,23 @@
1
+ shipit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ shipit/cli.py,sha256=NMZ6xSLq8MlV9yT3FLi9XZ5IBw2IMppoPna_vl9LK1Y,59170
3
+ shipit/generator.py,sha256=W4MynSFwId4PRWWrF0R3NsANye_Zv8TwXCUXai94pW0,6553
4
+ shipit/procfile.py,sha256=GlfdwzFUr0GWGKaaiXlLKNFInWaRNMy_wN14UEyU_5Q,2974
5
+ shipit/version.py,sha256=0hkQAsOavRu0fhDHnq4yN300QZjC88Y_Nr7gIReJ_9Y,95
6
+ shipit/assets/php/php.ini,sha256=f4irndAjB4GuuouEImRkNV22Q-yw1KqR-43jAMDw730,2531
7
+ shipit/assets/wordpress/install.sh,sha256=fJkVeGAw_dj-ywmQipAWRjaz57sayD6F0OFvZKMIKug,669
8
+ shipit/assets/wordpress/wp-config.php,sha256=IdGQoeg8E89JiqwxO2i8WnGMzmhNWgRz80X6lbU5GXc,4134
9
+ shipit/providers/base.py,sha256=-lraLhnXtc2SfYNIUiyry7xD2NL4q0n6voNjr6qfRis,2994
10
+ shipit/providers/gatsby.py,sha256=kzfS-z040GaJ0a9u2_6S6K-ykGSX2yPG17VpjUWBOBA,2393
11
+ shipit/providers/hugo.py,sha256=l3IZ14LGYc3wQ7Uk0iQMDNN9Syd1G4HPzm0ePCGFyzE,1808
12
+ shipit/providers/laravel.py,sha256=2rCuOi2kC5OYQfAG7C0NMhG7KEgzfudwUT_CvVFz4hM,3031
13
+ shipit/providers/mkdocs.py,sha256=mDJpT3rzYAr5Vw-yt5fCpV0QgBZyksn9MrkPd4nzROU,2200
14
+ shipit/providers/node_static.py,sha256=j5VKT_RblonIHTtpiUNai4e_izZUhbyLtPTsGeALpcY,11508
15
+ shipit/providers/php.py,sha256=BSOqS3UhABoe_ued8aXtyn0KdqWbOCq6VV2ehtoBlmk,3547
16
+ shipit/providers/python.py,sha256=kQpaaQi8ajHM_SFk2xI5lRIeOc6PXSKumb-Tv_5n6m0,20954
17
+ shipit/providers/registry.py,sha256=JCuQaYTvJcWK1nS-om9TIQgGW6pT5BuNLIRzChLLFWE,731
18
+ shipit/providers/staticfile.py,sha256=F4thEuihDW-h5DO-lotrjSdHYWzoXofQPpsAgWonvpY,2677
19
+ shipit/providers/wordpress.py,sha256=Wjc1fgFburewlf4hQ2ZmxTQko_SHQW-8jrDukzKx0Gs,3019
20
+ shipit_cli-0.9.0.dist-info/METADATA,sha256=ULUMj5u3OzMZA00AoJJOxA0iAsHIw5R-d8IODyR0hJ8,615
21
+ shipit_cli-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
+ shipit_cli-0.9.0.dist-info/entry_points.txt,sha256=7AE1NjSrHaSDfbfsRRO50KKnHFTbB0Imsccd1WynzAQ,72
23
+ shipit_cli-0.9.0.dist-info/RECORD,,
@@ -1,23 +0,0 @@
1
- shipit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- shipit/cli.py,sha256=1U65pviCsaH-fZO3hGYWoyO1WqXCfOxnGwPIomwc5Fg,58363
3
- shipit/generator.py,sha256=GFsuqZG83gn7yxphE4GHwZjEB3sToJn24yNDE-zrTtM,6400
4
- shipit/procfile.py,sha256=GlfdwzFUr0GWGKaaiXlLKNFInWaRNMy_wN14UEyU_5Q,2974
5
- shipit/version.py,sha256=-1rPwQ4VEhKG8LAXODaAwzxwpFY9hDgohd2s_aRySc4,95
6
- shipit/assets/php/php.ini,sha256=f4irndAjB4GuuouEImRkNV22Q-yw1KqR-43jAMDw730,2531
7
- shipit/assets/wordpress/install.sh,sha256=GOIwJX3qPp7VuZXjkKVFFY6Mo9k7VFWfobS6aZYitPg,817
8
- shipit/assets/wordpress/wp-config.php,sha256=IdGQoeg8E89JiqwxO2i8WnGMzmhNWgRz80X6lbU5GXc,4134
9
- shipit/providers/base.py,sha256=-lraLhnXtc2SfYNIUiyry7xD2NL4q0n6voNjr6qfRis,2994
10
- shipit/providers/gatsby.py,sha256=kzfS-z040GaJ0a9u2_6S6K-ykGSX2yPG17VpjUWBOBA,2393
11
- shipit/providers/hugo.py,sha256=BvSYTDTVxHz6FW2mA4sqIsNayFRo5BK6Z4tWFRRimmY,1644
12
- shipit/providers/laravel.py,sha256=yhOnaaXZ2US8EO5ZD5pBTerSOBi9IWBBIqeQrbW7YoM,3028
13
- shipit/providers/mkdocs.py,sha256=pWTeIUEnSyB653s7Cw_OSllgiSGcDymRTEHgV5ChDLM,2070
14
- shipit/providers/node_static.py,sha256=QVUTTZbvUGtj2yT9WTbGmuM30rJzejnp76crIqSGA7o,2564
15
- shipit/providers/php.py,sha256=JIWl5CdVEdf9DAr9O9rR8NucucYCj_M5PbJRw_SnmYU,3541
16
- shipit/providers/python.py,sha256=n0748miZ5RpQyQUCmkq3_G2Nnub8SaogZoJTNNFalZc,20801
17
- shipit/providers/registry.py,sha256=lHUViVuPJf1OIZD8I_NTX4LP7E3Uo5-MmLfarmAA_cM,729
18
- shipit/providers/staticfile.py,sha256=O1D0cXa_cFZH4OXRFLjBG3e-LbjMDo7ZBC2D3CvrPo4,2218
19
- shipit/providers/wordpress.py,sha256=wMIcBpICqcEpbOgJCufrnM-r76AVI7_xC-NUlh9XMmE,3016
20
- shipit_cli-0.7.1.dist-info/METADATA,sha256=uW1Lt7b6mOl7H5k3CPpaPPWNNr-0DHY6p5dxE4Z9B6g,523
21
- shipit_cli-0.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- shipit_cli-0.7.1.dist-info/entry_points.txt,sha256=7AE1NjSrHaSDfbfsRRO50KKnHFTbB0Imsccd1WynzAQ,72
23
- shipit_cli-0.7.1.dist-info/RECORD,,