openship 0.1.7 → 0.1.9

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.
package/dist/index.js CHANGED
@@ -102,7 +102,10 @@ var LANGUAGES = {
102
102
  },
103
103
  java: {
104
104
  name: "Java",
105
- buildImage: "eclipse-temurin:21-jdk-alpine",
105
+ // Maven image bundles both `mvn` and JDK 21, so the generated Dockerfile
106
+ // builds Maven projects out of the box; Gradle/Kotlin projects build via
107
+ // their `./gradlew` wrapper (needs only the JDK, which this image has).
108
+ buildImage: "maven:3.9-eclipse-temurin-21",
106
109
  runtimeImage: "eclipse-temurin:21-jre-alpine",
107
110
  packageManagers: ["maven", "gradle"],
108
111
  requiredTools: ["java", "javac"]
@@ -573,15 +576,18 @@ var STACKS = {
573
576
  }
574
577
  },
575
578
  // ── PHP ────────────────────────────────────────────────────────────────────
579
+ // PHP stacks run php-fpm behind nginx (docroot public/). The generated
580
+ // Dockerfile (docker-build-plan.ts PHP branch) installs nginx + writes the
581
+ // config template; this start command renders it for the injected $PORT and
582
+ // launches both processes. Runtime image inherits php:8.3-fpm from the language.
576
583
  laravel: {
577
584
  name: "Laravel",
578
585
  language: "php",
579
586
  category: "fullstack",
580
- runtimeImage: "php:8.3-apache",
581
587
  outputDirectory: "public",
582
588
  defaultPort: 8e3,
583
589
  defaultBuildCommand: "composer install --no-dev --optimize-autoloader",
584
- defaultStartCommand: "php artisan serve --host=0.0.0.0 --port=8000",
590
+ defaultStartCommand: "envsubst '$PORT' < /etc/nginx/app.conf.template > /etc/nginx/conf.d/default.conf && php-fpm -D && nginx -g 'daemon off;'",
585
591
  detection: {
586
592
  rootMarkers: ["artisan", "composer.json"],
587
593
  deps: ["laravel/framework"]
@@ -591,11 +597,10 @@ var STACKS = {
591
597
  name: "Symfony",
592
598
  language: "php",
593
599
  category: "fullstack",
594
- runtimeImage: "php:8.3-apache",
595
600
  outputDirectory: "public",
596
601
  defaultPort: 8e3,
597
602
  defaultBuildCommand: "composer install --no-dev --optimize-autoloader",
598
- defaultStartCommand: "php -S 0.0.0.0:8000 -t public",
603
+ defaultStartCommand: "envsubst '$PORT' < /etc/nginx/app.conf.template > /etc/nginx/conf.d/default.conf && php-fpm -D && nginx -g 'daemon off;'",
599
604
  detection: {
600
605
  rootMarkers: ["composer.json", "symfony.lock"],
601
606
  deps: ["symfony/framework-bundle"]
@@ -612,6 +617,9 @@ var STACKS = {
612
617
  defaultStartCommand: "java -jar target/*.jar",
613
618
  productionPaths: ["target"],
614
619
  defaultBuildStrategy: "local",
620
+ // Predominantly a Maven stack; bare-metal builds need `mvn` ensured. Gradle
621
+ // Spring Boot projects still build via their `./gradlew` wrapper (JDK-only).
622
+ requiredTools: ["java", "javac", "maven"],
615
623
  detection: {
616
624
  rootMarkers: ["pom.xml", "build.gradle", "build.gradle.kts"],
617
625
  deps: ["org.springframework.boot:spring-boot-starter-web", "spring-boot"],
@@ -632,6 +640,7 @@ var STACKS = {
632
640
  defaultStartCommand: "java -jar target/quarkus-app/quarkus-run.jar",
633
641
  productionPaths: ["target"],
634
642
  defaultBuildStrategy: "local",
643
+ requiredTools: ["java", "javac", "maven"],
635
644
  detection: {
636
645
  rootMarkers: ["pom.xml", "build.gradle", "build.gradle.kts"],
637
646
  deps: ["io.quarkus:quarkus-core", "quarkus"],
@@ -642,15 +651,44 @@ var STACKS = {
642
651
  }
643
652
  }
644
653
  },
654
+ // ── Kotlin (JVM, Gradle) ─────────────────────────────────────────────────
655
+ // Plain Kotlin/JVM services (Ktor, http4k, or a bare `main`). A Kotlin *Spring
656
+ // Boot* project still matches `springboot` first (its content pattern wins in
657
+ // the rule order), so this catches Kotlin projects that aren't Spring/Quarkus.
658
+ kotlin: {
659
+ name: "Kotlin",
660
+ language: "java",
661
+ category: "backend",
662
+ outputDirectory: "build/libs",
663
+ defaultPort: 8080,
664
+ defaultBuildCommand: "gradle build -x test",
665
+ defaultStartCommand: "java -jar build/libs/*.jar",
666
+ productionPaths: ["build/libs"],
667
+ defaultBuildStrategy: "local",
668
+ // Gradle-based; bare-metal builds need `gradle` ensured (or the `./gradlew`
669
+ // wrapper, which the detector prefers when present).
670
+ requiredTools: ["java", "javac", "gradle"],
671
+ detection: {
672
+ rootMarkers: ["build.gradle.kts", "build.gradle"],
673
+ contentPatterns: {
674
+ "build.gradle.kts": "kotlin\\s*\\(|org\\.jetbrains\\.kotlin",
675
+ "build.gradle": "org\\.jetbrains\\.kotlin|kotlin[- ]"
676
+ }
677
+ }
678
+ },
645
679
  // ── C# / .NET ──────────────────────────────────────────────────────────────
646
680
  dotnet: {
647
681
  name: ".NET",
648
682
  language: "csharp",
649
683
  category: "backend",
650
- outputDirectory: "bin/Release/net8.0/publish",
684
+ // Build runs `dotnet publish -c Release -o publish`, so the artifact is ./publish.
685
+ outputDirectory: "publish",
651
686
  defaultPort: 5e3,
652
687
  defaultBuildCommand: "dotnet publish -c Release -o publish",
653
- defaultStartCommand: "dotnet publish/app.dll",
688
+ // .NET reads ASPNETCORE_URLS (not $PORT) and defaults to :8080, so bind it
689
+ // explicitly to the injected port. The detector rewrites `app.dll` to the
690
+ // real assembly name (from the .csproj); `app` is the fallback.
691
+ defaultStartCommand: "ASPNETCORE_URLS=http://0.0.0.0:$PORT dotnet publish/app.dll",
654
692
  productionPaths: ["publish"],
655
693
  detection: {
656
694
  // .csproj/.fsproj/.sln are detected by suffix; rootMarkers is decorative
@@ -660,12 +698,14 @@ var STACKS = {
660
698
  blazor: {
661
699
  name: "Blazor",
662
700
  language: "csharp",
663
- category: "fullstack",
664
- outputDirectory: "bin/Release/net8.0/publish/wwwroot",
701
+ // Blazor WebAssembly compiles to a static bundle under publish/wwwroot —
702
+ // it's served as files, not a running server (Blazor Server folds into `dotnet`).
703
+ category: "static",
704
+ outputDirectory: "publish/wwwroot",
665
705
  defaultPort: 5e3,
666
706
  defaultBuildCommand: "dotnet publish -c Release -o publish",
667
- defaultStartCommand: "dotnet publish/app.dll",
668
- productionPaths: ["publish"],
707
+ defaultStartCommand: "",
708
+ productionPaths: ["publish/wwwroot"],
669
709
  detection: {
670
710
  deps: ["Microsoft.AspNetCore.Components.WebAssembly"]
671
711
  }
@@ -1541,11 +1581,39 @@ var goLanguageDetector = {
1541
1581
  function parseJavaManifest(_filename, _content) {
1542
1582
  return {};
1543
1583
  }
1584
+ function clampPort(raw) {
1585
+ const port = parseInt(raw, 10);
1586
+ return port > 0 && port <= 65535 ? port : null;
1587
+ }
1588
+ function detectJavaPort(context) {
1589
+ const fc = context.fileContents;
1590
+ if (!fc) return null;
1591
+ const props = fc["application.properties"];
1592
+ if (props) {
1593
+ const m = props.match(/^\s*server\.port\s*=\s*(\d{2,5})\s*$/m);
1594
+ if (m) return clampPort(m[1]);
1595
+ }
1596
+ for (const name of ["application.yml", "application.yaml"]) {
1597
+ const yml = fc[name];
1598
+ if (!yml) continue;
1599
+ const m = yml.match(/server:[\s\S]*?\bport:\s*["']?(\d{2,5})/) ?? yml.match(/^\s*port:\s*["']?(\d{2,5})/m);
1600
+ if (m) return clampPort(m[1]);
1601
+ }
1602
+ return null;
1603
+ }
1544
1604
  var javaLanguageDetector = {
1545
1605
  id: "java",
1546
- label: "Java",
1547
- manifestFiles: ["pom.xml", "build.gradle", "build.gradle.kts"],
1548
- parseManifest: parseJavaManifest
1606
+ label: "Java / Kotlin",
1607
+ manifestFiles: [
1608
+ "pom.xml",
1609
+ "build.gradle",
1610
+ "build.gradle.kts",
1611
+ "application.properties",
1612
+ "application.yml",
1613
+ "application.yaml"
1614
+ ],
1615
+ parseManifest: parseJavaManifest,
1616
+ detectPort: detectJavaPort
1549
1617
  };
1550
1618
 
1551
1619
  // ../../packages/core/src/languages/javascript.ts
@@ -2397,7 +2465,7 @@ var doctorCommand = new Command8("doctor").description("Diagnose the CLI setup (
2397
2465
 
2398
2466
  // src/commands/deploy.ts
2399
2467
  import { Command as Command9 } from "commander";
2400
- import { execFileSync as execFileSync2 } from "child_process";
2468
+ import { execFileSync as execFileSync3 } from "child_process";
2401
2469
  import ora2 from "ora";
2402
2470
 
2403
2471
  // src/lib/project-link.ts
@@ -2424,6 +2492,113 @@ function readProjectLink(from) {
2424
2492
  }
2425
2493
  }
2426
2494
 
2495
+ // src/lib/folder-deploy.ts
2496
+ import { execFileSync as execFileSync2 } from "child_process";
2497
+ import { readFileSync as readFileSync4, existsSync as existsSync6, rmSync } from "fs";
2498
+ import { tmpdir } from "os";
2499
+ import { join as join5, basename } from "path";
2500
+ function detectPackageManager(dir) {
2501
+ if (existsSync6(join5(dir, "bun.lockb")) || existsSync6(join5(dir, "bun.lock"))) return "bun";
2502
+ if (existsSync6(join5(dir, "pnpm-lock.yaml"))) return "pnpm";
2503
+ if (existsSync6(join5(dir, "yarn.lock"))) return "yarn";
2504
+ if (existsSync6(join5(dir, "package.json"))) return "npm";
2505
+ return void 0;
2506
+ }
2507
+ function detectStack(dir) {
2508
+ if (existsSync6(join5(dir, "go.mod"))) return "go";
2509
+ if (existsSync6(join5(dir, "Cargo.toml"))) return "rust";
2510
+ if (existsSync6(join5(dir, "requirements.txt")) || existsSync6(join5(dir, "pyproject.toml"))) return "python";
2511
+ if (existsSync6(join5(dir, "package.json"))) return "node";
2512
+ return void 0;
2513
+ }
2514
+ async function deployFolder(opts) {
2515
+ const { cwd } = opts;
2516
+ const step = opts.onStep ?? (() => {
2517
+ });
2518
+ const name = opts.name || basename(cwd) || "app";
2519
+ step("Creating upload session");
2520
+ const session = await apiRequest("/projects/folder/session", {
2521
+ method: "POST",
2522
+ body: JSON.stringify({ name, packageManager: detectPackageManager(cwd), stack: detectStack(cwd) })
2523
+ });
2524
+ if (!session.sessionId || !session.upload) {
2525
+ throw new Error(session.error || "Failed to open upload session");
2526
+ }
2527
+ step("Packaging folder");
2528
+ const tarball = join5(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
2529
+ execFileSync2(
2530
+ "tar",
2531
+ [
2532
+ "-czf",
2533
+ tarball,
2534
+ "--exclude=./node_modules",
2535
+ "--exclude=./.git",
2536
+ "--exclude=./dist",
2537
+ "--exclude=./.next",
2538
+ "--exclude=./.DS_Store",
2539
+ "-C",
2540
+ cwd,
2541
+ "."
2542
+ ],
2543
+ { stdio: "ignore" }
2544
+ );
2545
+ step("Uploading source");
2546
+ try {
2547
+ const body = readFileSync4(tarball);
2548
+ const up = session.upload;
2549
+ const method = up.method || "POST";
2550
+ const res = /^https?:\/\//i.test(up.url) ? await fetch(up.url, { method, headers: up.headers, body }) : await apiRaw(`/${up.url.replace(/^\/+/, "")}`, { method, headers: up.headers, body });
2551
+ if (!res.ok) throw new Error(`upload failed (HTTP ${res.status})`);
2552
+ } finally {
2553
+ try {
2554
+ rmSync(tarball, { force: true });
2555
+ } catch {
2556
+ }
2557
+ }
2558
+ step("Detecting build config");
2559
+ const scan = await apiRequest(`/projects/folder/scan/${session.sessionId}`, {
2560
+ method: "POST",
2561
+ body: "{}"
2562
+ });
2563
+ if (scan.success === false) throw new Error(scan.error || "Failed to scan uploaded source");
2564
+ step("Creating project");
2565
+ const hasBuild = Boolean(scan.buildCommand);
2566
+ const hasServer = Boolean(scan.startCommand);
2567
+ const ensured = await apiRequest("/projects/ensure", {
2568
+ method: "POST",
2569
+ body: JSON.stringify({
2570
+ projectId: opts.projectId,
2571
+ name: scan.name || name,
2572
+ gitProvider: "upload",
2573
+ framework: scan.stack,
2574
+ projectType: scan.projectType,
2575
+ packageManager: scan.packageManager,
2576
+ installCommand: scan.installCommand,
2577
+ buildCommand: scan.buildCommand,
2578
+ startCommand: scan.startCommand || void 0,
2579
+ outputDirectory: scan.outputDirectory,
2580
+ rootDirectory: scan.rootDirectory,
2581
+ buildImage: scan.buildImage,
2582
+ hasBuild,
2583
+ hasServer,
2584
+ productionMode: hasServer ? "standalone" : "static",
2585
+ ...hasServer && scan.port ? { port: scan.port } : {}
2586
+ })
2587
+ });
2588
+ if (!ensured.project_id) throw new Error(ensured.error || "Failed to create project");
2589
+ step("Deploying");
2590
+ const dep = await apiRequest("/deployments/build/access", {
2591
+ method: "POST",
2592
+ body: JSON.stringify({
2593
+ projectId: ensured.project_id,
2594
+ uploadSessionId: session.sessionId,
2595
+ ...opts.environment ? { environment: opts.environment } : {}
2596
+ })
2597
+ });
2598
+ if (!dep.deployment_id) throw new Error(dep.error || "Failed to start deployment");
2599
+ return { deploymentId: dep.deployment_id, projectId: dep.project_id };
2600
+ }
2601
+
2427
2602
  // src/lib/sse.ts
2428
2603
  async function* parseSSE(stream) {
2429
2604
  const reader = stream.getReader();
@@ -2545,51 +2720,78 @@ ${payload.error ?? "Stream error"}`);
2545
2720
  // src/commands/deploy.ts
2546
2721
  function git(args) {
2547
2722
  try {
2548
- return execFileSync2("git", args, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim() || void 0;
2723
+ return execFileSync3("git", args, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim() || void 0;
2549
2724
  } catch {
2550
2725
  return void 0;
2551
2726
  }
2552
2727
  }
2553
- var deployCommand = new Command9("deploy").description("Trigger a deployment for the current project").option("--project <id>", "Project ID (defaults to the linked project in .openship/project.json)").option("--branch <name>", "Git branch to deploy (defaults to the current branch)").option("--commit <sha>", "Specific commit SHA (defaults to the latest commit on the branch)").option("--env <environment>", "Target environment: production | preview", "production").option("--force-all", "Rebuild every enabled service (skip smart per-service routing)").option("--service-ids <ids>", "Comma-separated service IDs to deploy (smart routing)").option("--smart-route", "Rebuild only services changed since the active deploy").option("--refresh", "Re-apply current env to the active deploy (no git pull, no rebuild)").option("--watch", "Stream the deployment logs until it finishes").action(async (opts) => {
2728
+ var deployCommand = new Command9("deploy").description("Trigger a deployment for the current project").option("--project <id>", "Project ID (defaults to the linked project in .openship/project.json)").option("--branch <name>", "Git branch to deploy (defaults to the current branch)").option("--commit <sha>", "Specific commit SHA (defaults to the latest commit on the branch)").option("--env <environment>", "Target environment: production | preview", "production").option("--force-all", "Rebuild every enabled service (skip smart per-service routing)").option("--service-ids <ids>", "Comma-separated service IDs to deploy (smart routing)").option("--smart-route", "Rebuild only services changed since the active deploy").option("--refresh", "Re-apply current env to the active deploy (no git pull, no rebuild)").option("--name <name>", "Project name for a folder (non-git) deploy (defaults to the directory name)").option("--watch", "Stream the deployment logs until it finishes").action(async (opts) => {
2554
2729
  const link = readProjectLink();
2555
- const projectId = opts.project || link?.projectId;
2556
- if (!projectId) {
2557
- err("No project specified. Pass --project <id> or run `openship init` to link one.");
2558
- process.exit(1);
2559
- }
2560
2730
  const env = opts.env;
2561
2731
  if (env !== "production" && env !== "preview") {
2562
2732
  err(`Invalid --env "${env}". Must be "production" or "preview".`);
2563
2733
  process.exit(1);
2564
2734
  }
2565
- const branch = opts.branch || link?.branch || git(["rev-parse", "--abbrev-ref", "HEAD"]);
2566
- const serviceIds = opts.serviceIds ? opts.serviceIds.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
2567
- const body = {
2568
- projectId,
2569
- branch,
2570
- commitSha: opts.commit || void 0,
2571
- environment: env,
2572
- forceAll: opts.forceAll || void 0,
2573
- serviceIds,
2574
- smartRoute: opts.smartRoute || void 0,
2575
- refresh: opts.refresh || void 0
2576
- };
2577
- const spinner2 = isJsonMode() ? null : ora2("Triggering deployment").start();
2578
- let res;
2579
- try {
2580
- res = await apiRequest("/deployments", {
2581
- method: "POST",
2582
- body: JSON.stringify(body)
2583
- });
2584
- } catch (e) {
2585
- spinner2?.fail("Deployment failed to start");
2586
- err(e instanceof ApiError ? e.message : String(e));
2587
- process.exit(1);
2735
+ const inGitRepo = git(["rev-parse", "--is-inside-work-tree"]) === "true";
2736
+ const gitOnlyFlags = opts.commit || opts.serviceIds || opts.smartRoute || opts.refresh;
2737
+ let deploymentId;
2738
+ let payload;
2739
+ if (!inGitRepo && !gitOnlyFlags) {
2740
+ const spinner2 = isJsonMode() ? null : ora2("Deploying folder").start();
2741
+ try {
2742
+ const result2 = await deployFolder({
2743
+ cwd: process.cwd(),
2744
+ name: opts.name,
2745
+ projectId: opts.project || link?.projectId,
2746
+ environment: env,
2747
+ onStep: (m) => {
2748
+ if (spinner2) spinner2.text = m;
2749
+ }
2750
+ });
2751
+ deploymentId = result2.deploymentId;
2752
+ payload = { success: true, deployment_id: result2.deploymentId, project_id: result2.projectId };
2753
+ spinner2?.succeed(deploymentId ? `Deployment queued: ${deploymentId}` : "Deployment queued");
2754
+ } catch (e) {
2755
+ spinner2?.fail("Folder deploy failed");
2756
+ err(e instanceof ApiError ? e.message : String(e));
2757
+ process.exit(1);
2758
+ }
2759
+ } else {
2760
+ const projectId = opts.project || link?.projectId;
2761
+ if (!projectId) {
2762
+ err("No project specified. Pass --project <id> or run `openship init` to link one.");
2763
+ process.exit(1);
2764
+ }
2765
+ const branch = opts.branch || link?.branch || git(["rev-parse", "--abbrev-ref", "HEAD"]);
2766
+ const serviceIds = opts.serviceIds ? opts.serviceIds.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
2767
+ const body = {
2768
+ projectId,
2769
+ branch,
2770
+ commitSha: opts.commit || void 0,
2771
+ environment: env,
2772
+ forceAll: opts.forceAll || void 0,
2773
+ serviceIds,
2774
+ smartRoute: opts.smartRoute || void 0,
2775
+ refresh: opts.refresh || void 0
2776
+ };
2777
+ const spinner2 = isJsonMode() ? null : ora2("Triggering deployment").start();
2778
+ let res;
2779
+ try {
2780
+ res = await apiRequest("/deployments", {
2781
+ method: "POST",
2782
+ body: JSON.stringify(body)
2783
+ });
2784
+ } catch (e) {
2785
+ spinner2?.fail("Deployment failed to start");
2786
+ err(e instanceof ApiError ? e.message : String(e));
2787
+ process.exit(1);
2788
+ }
2789
+ deploymentId = res.data?.deployment_id;
2790
+ payload = res.data ?? res;
2791
+ spinner2?.succeed(deploymentId ? `Deployment queued: ${deploymentId}` : "Deployment queued");
2588
2792
  }
2589
- const deploymentId = res.data?.deployment_id;
2590
- spinner2?.succeed(deploymentId ? `Deployment queued: ${deploymentId}` : "Deployment queued");
2591
2793
  if (isJsonMode() && !opts.watch) {
2592
- printJson(res.data ?? res);
2794
+ printJson(payload ?? {});
2593
2795
  return;
2594
2796
  }
2595
2797
  if (!deploymentId) {
@@ -4240,7 +4442,7 @@ var serverCommand = server;
4240
4442
  // src/commands/system.ts
4241
4443
  import { Command as Command16 } from "commander";
4242
4444
  import ora5 from "ora";
4243
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
4445
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
4244
4446
  import { createInterface as createInterface6 } from "readline/promises";
4245
4447
  import { stdin as input5, stdout as output5 } from "process";
4246
4448
  async function guarded(fn) {
@@ -4566,7 +4768,7 @@ dataTransferCommand.command("import").description("Import an instance export fil
4566
4768
  }
4567
4769
  let file;
4568
4770
  try {
4569
- file = JSON.parse(readFileSync4(opts.file, "utf8"));
4771
+ file = JSON.parse(readFileSync5(opts.file, "utf8"));
4570
4772
  } catch {
4571
4773
  err(`
4572
4774
  Could not read or parse ${opts.file}.
@@ -4909,7 +5111,7 @@ var mailCommand = new Command17("mail").description("Self-hosted mail server (iR
4909
5111
  // src/commands/backup.ts
4910
5112
  import { Command as Command18 } from "commander";
4911
5113
  import ora7 from "ora";
4912
- import { readFileSync as readFileSync5 } from "fs";
5114
+ import { readFileSync as readFileSync6 } from "fs";
4913
5115
  async function guard3(fn) {
4914
5116
  try {
4915
5117
  await fn();
@@ -5202,7 +5404,7 @@ destinationCmd.command("create").description("Create a backup destination").requ
5202
5404
  let sftpPrivateKey = opts.sftpPrivateKey;
5203
5405
  if (opts.sftpPrivateKeyFile) {
5204
5406
  try {
5205
- sftpPrivateKey = readFileSync5(opts.sftpPrivateKeyFile, "utf8");
5407
+ sftpPrivateKey = readFileSync6(opts.sftpPrivateKeyFile, "utf8");
5206
5408
  } catch {
5207
5409
  throw new Error(`Cannot read key file: ${opts.sftpPrivateKeyFile}`);
5208
5410
  }
@@ -5392,21 +5594,21 @@ var apiCommand = new Command20("api").description("Make an authenticated request
5392
5594
 
5393
5595
  // src/commands/install.ts
5394
5596
  import { Command as Command21 } from "commander";
5395
- import { chmodSync as chmodSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
5597
+ import { chmodSync as chmodSync2, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
5396
5598
  import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
5397
5599
  import { homedir as homedir4 } from "os";
5398
- import { join as join6 } from "path";
5600
+ import { join as join7 } from "path";
5399
5601
  import ora9 from "ora";
5400
5602
 
5401
5603
  // src/lib/cache.ts
5402
5604
  import { createHash } from "crypto";
5403
5605
  import { createReadStream, createWriteStream, mkdirSync as mkdirSync4 } from "fs";
5404
5606
  import { homedir as homedir3 } from "os";
5405
- import { dirname as dirname3, join as join5 } from "path";
5406
- var CACHE_DIR = join5(homedir3(), ".openship", "cache");
5407
- var RELEASES_DIR = join5(CACHE_DIR, "releases");
5607
+ import { dirname as dirname3, join as join6 } from "path";
5608
+ var CACHE_DIR = join6(homedir3(), ".openship", "cache");
5609
+ var RELEASES_DIR = join6(CACHE_DIR, "releases");
5408
5610
  function releaseDir(tag) {
5409
- return join5(RELEASES_DIR, tag);
5611
+ return join6(RELEASES_DIR, tag);
5410
5612
  }
5411
5613
  function parseSha256(content) {
5412
5614
  const token = content.trim().split(/\s+/)[0]?.toLowerCase();
@@ -5491,7 +5693,7 @@ async function fetchSidecar(url) {
5491
5693
  return res.text();
5492
5694
  }
5493
5695
  function installDmg(dmg) {
5494
- const homeApps = join6(homedir4(), "Applications");
5696
+ const homeApps = join7(homedir4(), "Applications");
5495
5697
  let dest = homeApps;
5496
5698
  try {
5497
5699
  mkdirSync5(homeApps, { recursive: true });
@@ -5504,14 +5706,14 @@ function installDmg(dmg) {
5504
5706
  if (attach.status !== 0) throw new Error(`hdiutil attach failed: ${attach.stderr?.trim()}`);
5505
5707
  const mount = (attach.stdout.match(/\/Volumes\/[^\n]*/g) ?? []).pop()?.trim();
5506
5708
  if (!mount) throw new Error("Could not determine the mounted volume");
5507
- let target = join6(dest, "Openship.app");
5709
+ let target = join7(dest, "Openship.app");
5508
5710
  try {
5509
- const appInDmg = join6(mount, "Openship.app");
5510
- if (!existsSync6(appInDmg)) throw new Error("Openship.app not found in the disk image");
5711
+ const appInDmg = join7(mount, "Openship.app");
5712
+ if (!existsSync7(appInDmg)) throw new Error("Openship.app not found in the disk image");
5511
5713
  spawnSync2("rm", ["-rf", target]);
5512
5714
  let copy = spawnSync2("ditto", [appInDmg, target], { encoding: "utf8" });
5513
5715
  if (copy.status !== 0 && dest === homeApps) {
5514
- target = join6("/Applications", "Openship.app");
5716
+ target = join7("/Applications", "Openship.app");
5515
5717
  spawnSync2("rm", ["-rf", target]);
5516
5718
  copy = spawnSync2("ditto", [appInDmg, target], { encoding: "utf8" });
5517
5719
  }
@@ -5526,8 +5728,8 @@ function installAppImage(appImage) {
5526
5728
  return appImage;
5527
5729
  }
5528
5730
  function installZip(zip) {
5529
- const localAppData = process.env.LOCALAPPDATA || join6(homedir4(), "AppData", "Local");
5530
- const target = join6(localAppData, "Programs", "Openship");
5731
+ const localAppData = process.env.LOCALAPPDATA || join7(homedir4(), "AppData", "Local");
5732
+ const target = join7(localAppData, "Programs", "Openship");
5531
5733
  mkdirSync5(target, { recursive: true });
5532
5734
  const expand = spawnSync2(
5533
5735
  "powershell",
@@ -5555,8 +5757,8 @@ function launch(kind, target) {
5555
5757
  child.unref();
5556
5758
  return;
5557
5759
  }
5558
- const exe = join6(target, "Openship.exe");
5559
- const path2 = existsSync6(exe) ? exe : target;
5760
+ const exe = join7(target, "Openship.exe");
5761
+ const path2 = existsSync7(exe) ? exe : target;
5560
5762
  spawnSync2("cmd", ["/c", "start", "", path2]);
5561
5763
  }
5562
5764
  var installCommand = new Command21("install").description("Download and install the Openship desktop app for this OS").option("--version <tag>", "Release tag to install (e.g. v1.2.3)").option("--latest", "Install the latest release (default)").option("--force", "Re-download even if a verified copy is cached").option("--no-verify", "Skip SHA-256 verification (allowed when no sidecar exists)").option("--no-launch", "Install without launching the app").action(async (opts) => {
@@ -5582,13 +5784,13 @@ var installCommand = new Command21("install").description("Download and install
5582
5784
  process.exit(1);
5583
5785
  }
5584
5786
  const dir = releaseDir(tag);
5585
- const assetPath = join6(dir, asset.name);
5787
+ const assetPath = join7(dir, asset.name);
5586
5788
  const sidecarPath = `${assetPath}.sha256`;
5587
5789
  const assetUrl = `${RELEASES}/download/${tag}/${asset.name}`;
5588
5790
  const sidecarUrl = `${assetUrl}.sha256`;
5589
5791
  let downloaded = false;
5590
5792
  let sha;
5591
- const cachedUsable = !opts.force && existsSync6(assetPath) && (existsSync6(sidecarPath) || opts.verify === false);
5793
+ const cachedUsable = !opts.force && existsSync7(assetPath) && (existsSync7(sidecarPath) || opts.verify === false);
5592
5794
  if (cachedUsable) {
5593
5795
  info(` Using cached ${asset.name} (${tag}).`);
5594
5796
  } else {
@@ -5610,8 +5812,8 @@ var installCommand = new Command21("install").description("Download and install
5610
5812
  const s2 = spin4("Verifying checksum\u2026");
5611
5813
  try {
5612
5814
  let sidecarBody;
5613
- if (existsSync6(sidecarPath) && !downloaded) {
5614
- sidecarBody = readFileSync6(sidecarPath, "utf8");
5815
+ if (existsSync7(sidecarPath) && !downloaded) {
5816
+ sidecarBody = readFileSync7(sidecarPath, "utf8");
5615
5817
  } else {
5616
5818
  sidecarBody = await fetchSidecar(sidecarUrl);
5617
5819
  }
@@ -5675,17 +5877,17 @@ var installCommand = new Command21("install").description("Download and install
5675
5877
 
5676
5878
  // src/commands/cache.ts
5677
5879
  import { Command as Command22 } from "commander";
5678
- import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync7, rmSync, statSync } from "fs";
5679
- import { join as join7 } from "path";
5880
+ import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync8, rmSync as rmSync2, statSync } from "fs";
5881
+ import { join as join8 } from "path";
5680
5882
  function listAssets() {
5681
- if (!existsSync7(RELEASES_DIR)) return [];
5883
+ if (!existsSync8(RELEASES_DIR)) return [];
5682
5884
  const out = [];
5683
5885
  for (const tag of readdirSync(RELEASES_DIR)) {
5684
5886
  const dir = releaseDir(tag);
5685
5887
  if (!statSync(dir).isDirectory()) continue;
5686
5888
  for (const name of readdirSync(dir)) {
5687
5889
  if (name.endsWith(".sha256")) continue;
5688
- const path2 = join7(dir, name);
5890
+ const path2 = join8(dir, name);
5689
5891
  const st = statSync(path2);
5690
5892
  if (!st.isFile()) continue;
5691
5893
  out.push({
@@ -5693,7 +5895,7 @@ function listAssets() {
5693
5895
  name,
5694
5896
  path: path2,
5695
5897
  size: st.size,
5696
- hasSidecar: existsSync7(`${path2}.sha256`)
5898
+ hasSidecar: existsSync8(`${path2}.sha256`)
5697
5899
  });
5698
5900
  }
5699
5901
  }
@@ -5724,7 +5926,7 @@ var verifyCmd2 = new Command22("verify").description("Re-hash cached assets and
5724
5926
  results.push({ tag: a.tag, asset: a.name, result: "no-sidecar" });
5725
5927
  continue;
5726
5928
  }
5727
- const expected = parseSha256(readFileSync7(`${a.path}.sha256`, "utf8"));
5929
+ const expected = parseSha256(readFileSync8(`${a.path}.sha256`, "utf8"));
5728
5930
  const actual = await hashFile(a.path);
5729
5931
  const okMatch = expected !== null && expected === actual;
5730
5932
  if (!okMatch) bad += 1;
@@ -5740,12 +5942,12 @@ var verifyCmd2 = new Command22("verify").description("Re-hash cached assets and
5740
5942
  });
5741
5943
  var cleanCmd = new Command22("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
5742
5944
  const target = tag ? releaseDir(tag) : RELEASES_DIR;
5743
- if (!existsSync7(target)) {
5945
+ if (!existsSync8(target)) {
5744
5946
  if (isJsonMode()) printJson({ removed: false, path: target });
5745
5947
  else info(` Nothing to clean (${target}).`);
5746
5948
  return;
5747
5949
  }
5748
- rmSync(target, { recursive: true, force: true });
5950
+ rmSync2(target, { recursive: true, force: true });
5749
5951
  if (isJsonMode()) printJson({ removed: true, path: target });
5750
5952
  else ok(`
5751
5953
  Removed ${target}
@@ -5758,7 +5960,7 @@ var cacheCommand = new Command22("cache").description("Manage the local download
5758
5960
 
5759
5961
  // src/index.ts
5760
5962
  var program = new Command23();
5761
- program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.1.7").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
5963
+ program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.1.9").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
5762
5964
  if (thisCommand.opts().json) setJsonMode(true);
5763
5965
  });
5764
5966
  program.addCommand(upCommand);