@nmakarov/cli-toolkit 0.29.0 → 0.33.0

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
@@ -542,7 +542,7 @@ async function showScreen(config2) {
542
542
  onRender,
543
543
  parentData = {}
544
544
  } = config2;
545
- return new Promise((resolve2) => {
545
+ return new Promise((resolve3) => {
546
546
  let instance2;
547
547
  const keyBindings = [];
548
548
  const actions = {};
@@ -717,7 +717,7 @@ async function showScreen(config2) {
717
717
  };
718
718
  const cleanup = (result) => {
719
719
  if (instance2) instance2.unmount();
720
- setTimeout(() => resolve2(result), 50);
720
+ setTimeout(() => resolve3(result), 50);
721
721
  };
722
722
  instance2 = render(h3(Screen));
723
723
  });
@@ -2438,8 +2438,8 @@ var FileDatabase = class _FileDatabase {
2438
2438
  const items = await fs3.promises.readdir(destPath);
2439
2439
  const versions = items.filter((item) => {
2440
2440
  const itemPath = path3.join(destPath, item);
2441
- const stat = fs3.statSync(itemPath);
2442
- return stat.isDirectory() && isTimestampFolder(item);
2441
+ const stat2 = fs3.statSync(itemPath);
2442
+ return stat2.isDirectory() && isTimestampFolder(item);
2443
2443
  });
2444
2444
  return versions.sort();
2445
2445
  } catch (error) {
@@ -2504,8 +2504,8 @@ var FileDatabase = class _FileDatabase {
2504
2504
  }
2505
2505
  const versionFolders = items.filter((item) => {
2506
2506
  const itemPath = path3.join(tablePath, item);
2507
- const stat = fs3.statSync(itemPath);
2508
- return stat.isDirectory() && isTimestampFolder(item);
2507
+ const stat2 = fs3.statSync(itemPath);
2508
+ return stat2.isDirectory() && isTimestampFolder(item);
2509
2509
  });
2510
2510
  if (versionFolders.length > 0) {
2511
2511
  const latestVersion = versionFolders.sort().pop();
@@ -3972,6 +3972,265 @@ var S3 = class _S3 {
3972
3972
  }
3973
3973
  };
3974
3974
 
3975
+ // src/aws/index.js
3976
+ import {
3977
+ STSClient,
3978
+ GetCallerIdentityCommand
3979
+ } from "@aws-sdk/client-sts";
3980
+ import {
3981
+ EC2Client,
3982
+ DescribeRegionsCommand,
3983
+ DescribeAvailabilityZonesCommand,
3984
+ DescribeVpcsCommand,
3985
+ DescribeSubnetsCommand,
3986
+ DescribeSecurityGroupsCommand,
3987
+ DescribeKeyPairsCommand,
3988
+ DescribeImagesCommand
3989
+ } from "@aws-sdk/client-ec2";
3990
+ import {
3991
+ Route53Client,
3992
+ ListHostedZonesCommand
3993
+ } from "@aws-sdk/client-route-53";
3994
+ var DEFAULT_REGION = "us-east-1";
3995
+ var CANONICAL_OWNER_ID = "099720109477";
3996
+ var DEFAULT_UBUNTU_PATTERN = "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*";
3997
+ function nameTag(tags) {
3998
+ const t = (tags ?? []).find((x) => x.Key === "Name");
3999
+ return t?.Value ?? null;
4000
+ }
4001
+ var Aws = class _Aws {
4002
+ /**
4003
+ * Resolve credentials/region from params and return an Aws instance.
4004
+ * Does not call AWS until you invoke a method (clients are lazy).
4005
+ */
4006
+ static async init(context, options = {}) {
4007
+ const getOpt = async (k) => {
4008
+ try {
4009
+ const v = await context?.params?.get?.(k, "string");
4010
+ return v != null && String(v).trim() !== "" ? v : void 0;
4011
+ } catch {
4012
+ return void 0;
4013
+ }
4014
+ };
4015
+ const config2 = {
4016
+ region: options.region ?? await getOpt("awsRegion") ?? DEFAULT_REGION,
4017
+ accessKeyId: options.accessKeyId ?? await getOpt("awsAccessKeyId"),
4018
+ secretAccessKey: options.secretAccessKey ?? await getOpt("awsSecretAccessKey"),
4019
+ sessionToken: options.sessionToken ?? await getOpt("awsSessionToken")
4020
+ };
4021
+ const aws = new _Aws(context, config2);
4022
+ context?.logger?.debug?.(
4023
+ `[aws] region=${config2.region} credentials=${config2.accessKeyId ? "explicit" : "default-chain"}`
4024
+ );
4025
+ return aws;
4026
+ }
4027
+ constructor(context, config2) {
4028
+ this.logger = context?.logger ?? console;
4029
+ this.region = config2.region || DEFAULT_REGION;
4030
+ this._credentials = config2.accessKeyId && config2.secretAccessKey ? {
4031
+ accessKeyId: config2.accessKeyId,
4032
+ secretAccessKey: config2.secretAccessKey,
4033
+ ...config2.sessionToken ? { sessionToken: config2.sessionToken } : {}
4034
+ } : void 0;
4035
+ this.hasExplicitCredentials = !!this._credentials;
4036
+ process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= "true";
4037
+ this._clients = {};
4038
+ }
4039
+ // ── clients (lazy, optionally region-scoped) ──────────────────────────────
4040
+ _clientConfig(region) {
4041
+ const cfg = { region: region || this.region };
4042
+ if (this._credentials) cfg.credentials = this._credentials;
4043
+ return cfg;
4044
+ }
4045
+ _sts() {
4046
+ return this._clients.sts ??= new STSClient(this._clientConfig());
4047
+ }
4048
+ _ec2(region) {
4049
+ const key = `ec2:${region || this.region}`;
4050
+ return this._clients[key] ??= new EC2Client(this._clientConfig(region));
4051
+ }
4052
+ _route53() {
4053
+ return this._clients.route53 ??= new Route53Client(this._clientConfig());
4054
+ }
4055
+ getRegion() {
4056
+ return this.region;
4057
+ }
4058
+ // ── credentials ─────────────────────────────────────────────────────────────
4059
+ /**
4060
+ * Are any credentials available *before* hitting AWS? Returns
4061
+ * { ok, source } — explicit keys, or a resolvable default chain (profile/SSO/
4062
+ * instance role). { ok:false } means there's nothing to even try with.
4063
+ * Note: "ok" only means creds were *found*, not that AWS will accept them.
4064
+ */
4065
+ async checkCredentials() {
4066
+ if (this.hasExplicitCredentials) {
4067
+ return { ok: true, source: "explicit keys (env/.env/CLI)" };
4068
+ }
4069
+ try {
4070
+ const provider = this._sts().config.credentials;
4071
+ const resolved = typeof provider === "function" ? await provider() : provider;
4072
+ if (resolved?.accessKeyId) {
4073
+ return { ok: true, source: "default credential chain (profile/SSO/role)" };
4074
+ }
4075
+ } catch {
4076
+ }
4077
+ return { ok: false };
4078
+ }
4079
+ /** True for "bad/missing credentials" style errors (vs. real failures). */
4080
+ static isAuthError(err) {
4081
+ const name = err?.name || err?.Code || err?.__type || "";
4082
+ return [
4083
+ "CredentialsProviderError",
4084
+ "InvalidClientTokenId",
4085
+ "UnrecognizedClientException",
4086
+ "AuthFailure",
4087
+ "AccessDenied",
4088
+ "AccessDeniedException",
4089
+ "ExpiredToken",
4090
+ "ExpiredTokenException",
4091
+ "SignatureDoesNotMatch",
4092
+ "MissingAuthenticationToken"
4093
+ ].includes(name);
4094
+ }
4095
+ /** Short, precise instructions for getting AWS credentials into .env. */
4096
+ static credentialsHelp(region = DEFAULT_REGION) {
4097
+ return [
4098
+ "No usable AWS credentials were found (or AWS rejected them).",
4099
+ "",
4100
+ "Put a read-only access key in the project's .env:",
4101
+ "",
4102
+ " AWS_ACCESS_KEY_ID=AKIA...",
4103
+ " AWS_SECRET_ACCESS_KEY=...",
4104
+ ` AWS_REGION=${region} # optional (default ${DEFAULT_REGION})`,
4105
+ "",
4106
+ "Get a key from the AWS console (~2 min):",
4107
+ " 1. IAM \u2192 Users \u2192 create or pick a user (console sign-in not needed).",
4108
+ ' 2. Attach a policy \u2014 "ReadOnlyAccess" (AWS managed) is enough for discovery.',
4109
+ ' 3. The user \u2192 "Security credentials" \u2192 "Create access key" \u2192 "CLI".',
4110
+ " 4. Copy the Access key ID + Secret access key (the secret shows only once).",
4111
+ " 5. Paste both into .env, then re-run.",
4112
+ " Direct link: https://console.aws.amazon.com/iam/home#/users",
4113
+ "",
4114
+ "Prefer a named profile or an EC2 instance role? Re-run with AWS_PROFILE=<name>",
4115
+ "set (or on the instance) and credentials resolve automatically."
4116
+ ].join("\n");
4117
+ }
4118
+ // ── identity ──────────────────────────────────────────────────────────────
4119
+ /** { account, arn, userId } — confirm which account/identity the keys belong to. */
4120
+ async whoAmI() {
4121
+ const out = await this._sts().send(new GetCallerIdentityCommand({}));
4122
+ return { account: out.Account, arn: out.Arn, userId: out.UserId };
4123
+ }
4124
+ // ── regions / AZs ──────────────────────────────────────────────────────────
4125
+ /** Enabled region names, sorted. */
4126
+ async listRegions({ allRegions = false } = {}) {
4127
+ const out = await this._ec2().send(new DescribeRegionsCommand({ AllRegions: allRegions }));
4128
+ return (out.Regions ?? []).map((r) => r.RegionName).sort();
4129
+ }
4130
+ /** Availability zone names for a region (defaults to the instance region). */
4131
+ async listAvailabilityZones(region) {
4132
+ const out = await this._ec2(region).send(new DescribeAvailabilityZonesCommand({}));
4133
+ return (out.AvailabilityZones ?? []).filter((z) => z.State === "available").map((z) => z.ZoneName).sort();
4134
+ }
4135
+ // ── VPC / subnets / SGs / key pairs ────────────────────────────────────────
4136
+ /** [{ id, cidr, isDefault, name }] in a region. */
4137
+ async listVpcs(region) {
4138
+ const out = await this._ec2(region).send(new DescribeVpcsCommand({}));
4139
+ return (out.Vpcs ?? []).map((v) => ({
4140
+ id: v.VpcId,
4141
+ cidr: v.CidrBlock,
4142
+ isDefault: !!v.IsDefault,
4143
+ name: nameTag(v.Tags)
4144
+ }));
4145
+ }
4146
+ /** [{ id, vpcId, az, cidr, public, name }]; pass vpcId to scope. */
4147
+ async listSubnets({ vpcId, region } = {}) {
4148
+ const Filters = vpcId ? [{ Name: "vpc-id", Values: [vpcId] }] : void 0;
4149
+ const out = await this._ec2(region).send(new DescribeSubnetsCommand({ Filters }));
4150
+ return (out.Subnets ?? []).map((s) => ({
4151
+ id: s.SubnetId,
4152
+ vpcId: s.VpcId,
4153
+ az: s.AvailabilityZone,
4154
+ cidr: s.CidrBlock,
4155
+ public: !!s.MapPublicIpOnLaunch,
4156
+ name: nameTag(s.Tags)
4157
+ }));
4158
+ }
4159
+ /** [{ id, name, vpcId, description }]; pass vpcId to scope. */
4160
+ async listSecurityGroups({ vpcId, region } = {}) {
4161
+ const Filters = vpcId ? [{ Name: "vpc-id", Values: [vpcId] }] : void 0;
4162
+ const out = await this._ec2(region).send(new DescribeSecurityGroupsCommand({ Filters }));
4163
+ return (out.SecurityGroups ?? []).map((g) => ({
4164
+ id: g.GroupId,
4165
+ name: g.GroupName,
4166
+ vpcId: g.VpcId,
4167
+ description: g.Description
4168
+ }));
4169
+ }
4170
+ /** [{ name, fingerprint }] EC2 key pairs in a region. */
4171
+ async listKeyPairs(region) {
4172
+ const out = await this._ec2(region).send(new DescribeKeyPairsCommand({}));
4173
+ return (out.KeyPairs ?? []).map((k) => ({ name: k.KeyName, fingerprint: k.KeyFingerprint }));
4174
+ }
4175
+ // ── Route53 ────────────────────────────────────────────────────────────────
4176
+ /** [{ id, name, private, recordCount }] — id is the bare zone id (no /hostedzone/). */
4177
+ async listHostedZones() {
4178
+ const zones = [];
4179
+ let marker;
4180
+ do {
4181
+ const out = await this._route53().send(new ListHostedZonesCommand({ Marker: marker }));
4182
+ for (const z of out.HostedZones ?? []) {
4183
+ zones.push({
4184
+ id: (z.Id ?? "").replace("/hostedzone/", ""),
4185
+ name: (z.Name ?? "").replace(/\.$/, ""),
4186
+ // strip trailing dot
4187
+ private: !!z.Config?.PrivateZone,
4188
+ recordCount: z.ResourceRecordSetCount
4189
+ });
4190
+ }
4191
+ marker = out.IsTruncated ? out.NextMarker : void 0;
4192
+ } while (marker);
4193
+ return zones;
4194
+ }
4195
+ /**
4196
+ * Best hosted zone for a domain: exact match, else the longest zone name that
4197
+ * is a suffix of the domain (so "api.foo.com" matches the "foo.com" zone).
4198
+ * Returns the zone object or null.
4199
+ */
4200
+ async findHostedZoneForDomain(domain) {
4201
+ const target = String(domain ?? "").replace(/\.$/, "").toLowerCase();
4202
+ if (!target) return null;
4203
+ const zones = await this.listHostedZones();
4204
+ const exact = zones.find((z) => z.name.toLowerCase() === target);
4205
+ if (exact) return exact;
4206
+ return zones.filter((z) => target.endsWith(`.${z.name.toLowerCase()}`)).sort((a, b) => b.name.length - a.name.length)[0] ?? null;
4207
+ }
4208
+ // ── AMI lookup ──────────────────────────────────────────────────────────────
4209
+ /**
4210
+ * Latest Ubuntu AMI matching a name pattern in a region.
4211
+ * Returns { id, name, creationDate, architecture } or null.
4212
+ */
4213
+ async findLatestUbuntuAmi({ region, pattern = DEFAULT_UBUNTU_PATTERN, architecture = "x86_64" } = {}) {
4214
+ const out = await this._ec2(region).send(new DescribeImagesCommand({
4215
+ Owners: [CANONICAL_OWNER_ID],
4216
+ Filters: [
4217
+ { Name: "name", Values: [pattern] },
4218
+ { Name: "virtualization-type", Values: ["hvm"] },
4219
+ { Name: "state", Values: ["available"] },
4220
+ ...architecture ? [{ Name: "architecture", Values: [architecture] }] : []
4221
+ ]
4222
+ }));
4223
+ const newest = (out.Images ?? []).sort((a, b) => String(b.CreationDate).localeCompare(String(a.CreationDate)))[0];
4224
+ if (!newest) return null;
4225
+ return {
4226
+ id: newest.ImageId,
4227
+ name: newest.Name,
4228
+ creationDate: newest.CreationDate,
4229
+ architecture: newest.Architecture
4230
+ };
4231
+ }
4232
+ };
4233
+
3975
4234
  // src/logger/index.js
3976
4235
  import chalk from "chalk";
3977
4236
  import util from "util";
@@ -4344,12 +4603,1061 @@ function setupContext(opts = {}) {
4344
4603
  return setup(opts);
4345
4604
  }
4346
4605
 
4606
+ // src/deploy/service.js
4607
+ function deriveRepoDirName(repoUrl) {
4608
+ const tail = String(repoUrl ?? "").split("/").pop() ?? "repo";
4609
+ return tail.replace(/\.git$/, "") || "repo";
4610
+ }
4611
+ function defineService(service) {
4612
+ if (!service?.name) throw new Error("service manifest needs a `name`");
4613
+ if (!service.appsRoot) throw new Error(`service "${service.name}" needs an appsRoot`);
4614
+ if (!service.repoUrl) throw new Error(`service "${service.name}" needs a repoUrl`);
4615
+ if (!service.pm2?.script) throw new Error(`service "${service.name}" needs pm2.script`);
4616
+ const pm2 = {
4617
+ appName: service.name,
4618
+ args: "",
4619
+ ...service.pm2
4620
+ };
4621
+ const nginx = service.nginx ? { siteName: service.name, ...service.nginx } : null;
4622
+ return {
4623
+ repoDirName: deriveRepoDirName(service.repoUrl),
4624
+ repoSubdir: "",
4625
+ keepReleases: 3,
4626
+ testCommand: null,
4627
+ envScrubPatterns: [],
4628
+ legacyRepoEnv: null,
4629
+ buildInfoPath: "build-info.json",
4630
+ deployKey: null,
4631
+ requireEnv: false,
4632
+ ...service,
4633
+ pm2,
4634
+ nginx
4635
+ };
4636
+ }
4637
+
4638
+ // src/deploy/paths.js
4639
+ import { join as join2 } from "path";
4640
+ function servicePaths(service) {
4641
+ const root = service.appsRoot;
4642
+ const repoDirName = service.repoDirName ?? "repo";
4643
+ const repoSubdir = service.repoSubdir ?? "";
4644
+ const repo = join2(root, repoDirName);
4645
+ const repoRun = repoSubdir ? join2(repo, repoSubdir) : repo;
4646
+ return {
4647
+ root,
4648
+ repo,
4649
+ repoRun,
4650
+ repoEnv: join2(repoRun, ".env"),
4651
+ releases: join2(root, "releases"),
4652
+ shared: join2(root, "shared"),
4653
+ logs: join2(root, "logs"),
4654
+ current: join2(root, "current"),
4655
+ sharedEnv: join2(root, "shared", ".env"),
4656
+ ecosystem: join2(root, "shared", "ecosystem.config.cjs"),
4657
+ deployLog: join2(root, "logs", "deploy.log"),
4658
+ lockHashFile: join2(root, "shared", ".package-lock.sha256")
4659
+ };
4660
+ }
4661
+ function releaseStamp(date = /* @__PURE__ */ new Date()) {
4662
+ return date.toISOString().replace(/\.\d{3}Z$/, "Z");
4663
+ }
4664
+ function releaseDir(releasesRoot, stamp) {
4665
+ return join2(releasesRoot, stamp);
4666
+ }
4667
+
4668
+ // src/deploy/run.js
4669
+ import { spawn } from "child_process";
4670
+ function npmEnv(baseEnv = process.env) {
4671
+ const env = { ...baseEnv };
4672
+ delete env.NODE_ENV;
4673
+ return env;
4674
+ }
4675
+ function npmInstallEnv(baseEnv = process.env) {
4676
+ return {
4677
+ ...npmEnv(baseEnv),
4678
+ NPM_CONFIG_PRODUCTION: "false",
4679
+ npm_config_production: "false"
4680
+ };
4681
+ }
4682
+ function run(cmd, args, options = {}) {
4683
+ const { cwd, env, logger } = options;
4684
+ return new Promise((resolve3, reject) => {
4685
+ logger?.info?.(`$ ${cmd} ${args.join(" ")}${cwd ? ` (cwd=${cwd})` : ""}`);
4686
+ const child = spawn(cmd, args, {
4687
+ cwd,
4688
+ env: env ?? process.env,
4689
+ stdio: "inherit"
4690
+ });
4691
+ child.on("error", reject);
4692
+ child.on("close", (code) => {
4693
+ if (code === 0) resolve3();
4694
+ else reject(new Error(`${cmd} exited with code ${code}`));
4695
+ });
4696
+ });
4697
+ }
4698
+ function runShell(command, options = {}) {
4699
+ return run("bash", ["-lc", command], options);
4700
+ }
4701
+
4702
+ // src/deploy/log.js
4703
+ import { appendFile, mkdir } from "fs/promises";
4704
+ async function appendDeployLog(deployLogPath, message) {
4705
+ await mkdir(deployLogPath.replace(/\/[^/]+$/, ""), { recursive: true });
4706
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
4707
+ `;
4708
+ await appendFile(deployLogPath, line);
4709
+ }
4710
+
4711
+ // src/deploy/git.js
4712
+ import { access } from "fs/promises";
4713
+ async function pathExists(path5) {
4714
+ try {
4715
+ await access(path5);
4716
+ return true;
4717
+ } catch {
4718
+ return false;
4719
+ }
4720
+ }
4721
+ async function cloneRepo(service, options = {}) {
4722
+ const { dryRun = false, logger = console } = options;
4723
+ const paths = servicePaths(service);
4724
+ if (await pathExists(paths.repo)) {
4725
+ throw new Error(`Repo already exists at ${paths.repo} \u2014 use git pull instead`);
4726
+ }
4727
+ if (dryRun) {
4728
+ logger.info(`[dryRun] would git clone ${service.repoUrl} ${paths.repo}`);
4729
+ return;
4730
+ }
4731
+ await run("git", ["clone", service.repoUrl, paths.repo], { logger });
4732
+ logger.info(`cloned ${service.repoUrl} \u2192 ${paths.repo}`);
4733
+ }
4734
+ async function pullRepo(service, options = {}) {
4735
+ const { dryRun = false, logger = console } = options;
4736
+ const paths = servicePaths(service);
4737
+ if (!await pathExists(paths.repo)) {
4738
+ throw new Error(`Repo missing at ${paths.repo} \u2014 run provision first`);
4739
+ }
4740
+ if (dryRun) {
4741
+ logger.info(`[dryRun] would git -C ${paths.repo} pull --ff-only`);
4742
+ return;
4743
+ }
4744
+ await run("git", ["-C", paths.repo, "pull", "--ff-only"], { logger });
4745
+ logger.info(`pulled ${paths.repo}`);
4746
+ }
4747
+
4748
+ // src/deploy/release.js
4749
+ import { cp, mkdir as mkdir2, readlink, readdir, stat } from "fs/promises";
4750
+ import { join as join3, dirname as pathDirname, sep } from "path";
4751
+ var SKIP_TOP = /* @__PURE__ */ new Set(["node_modules", ".git"]);
4752
+ function shouldCopyEntry(srcPath) {
4753
+ const parts = srcPath.split(sep);
4754
+ if (parts.some((p) => p === "node_modules" || p === ".git")) return false;
4755
+ const top = parts.at(-1);
4756
+ if (parts.length === 1 && SKIP_TOP.has(top)) return false;
4757
+ return true;
4758
+ }
4759
+ async function createRelease(service, options = {}) {
4760
+ const { stamp = releaseStamp(), dryRun = false, logger = console } = options;
4761
+ const paths = servicePaths(service);
4762
+ const dest = releaseDir(paths.releases, stamp);
4763
+ if (dryRun) {
4764
+ logger.info(`[dryRun] would copy ${paths.repoRun} \u2192 ${dest}`);
4765
+ return { stamp, path: dest };
4766
+ }
4767
+ await mkdir2(paths.releases, { recursive: true });
4768
+ await cp(paths.repoRun, dest, {
4769
+ recursive: true,
4770
+ filter: (src) => shouldCopyEntry(src)
4771
+ });
4772
+ logger.info(`release ${stamp} created at ${dest}`);
4773
+ return { stamp, path: dest };
4774
+ }
4775
+ async function readCurrentRelease(paths) {
4776
+ try {
4777
+ const target = await readlink(paths.current);
4778
+ return target.startsWith("/") ? target : join3(pathDirname(paths.current), target);
4779
+ } catch {
4780
+ return null;
4781
+ }
4782
+ }
4783
+ async function listReleases(paths) {
4784
+ let names;
4785
+ try {
4786
+ names = await readdir(paths.releases);
4787
+ } catch {
4788
+ return [];
4789
+ }
4790
+ const entries = [];
4791
+ for (const name of names) {
4792
+ const full = releaseDir(paths.releases, name);
4793
+ const s = await stat(full);
4794
+ if (s.isDirectory()) entries.push({ name, path: full, mtime: s.mtime });
4795
+ }
4796
+ entries.sort((a, b) => b.name.localeCompare(a.name));
4797
+ return entries;
4798
+ }
4799
+
4800
+ // src/deploy/activate.js
4801
+ import { symlink, unlink } from "fs/promises";
4802
+ import { join as join4 } from "path";
4803
+ async function activateRelease(releasePath, paths, options = {}) {
4804
+ const { dryRun = false, logger = console } = options;
4805
+ if (dryRun) {
4806
+ logger.info(`[dryRun] would activate ${releasePath} \u2192 ${paths.current}`);
4807
+ return;
4808
+ }
4809
+ try {
4810
+ await unlink(paths.current);
4811
+ } catch {
4812
+ }
4813
+ await symlink(releasePath, paths.current);
4814
+ const envLink = join4(releasePath, ".env");
4815
+ try {
4816
+ await unlink(envLink);
4817
+ } catch {
4818
+ }
4819
+ await symlink(paths.sharedEnv, envLink);
4820
+ logger.info(`active release: ${releasePath}`);
4821
+ }
4822
+
4823
+ // src/deploy/deps.js
4824
+ import { createHash } from "crypto";
4825
+ import { access as access2, cp as cp2, mkdir as mkdir3, readFile, rm, writeFile } from "fs/promises";
4826
+ import { join as join5 } from "path";
4827
+ async function pathExists2(path5) {
4828
+ try {
4829
+ await access2(path5);
4830
+ return true;
4831
+ } catch {
4832
+ return false;
4833
+ }
4834
+ }
4835
+ async function lockfileHash(releasePath) {
4836
+ const lockPath = join5(releasePath, "package-lock.json");
4837
+ try {
4838
+ const buf = await readFile(lockPath);
4839
+ return createHash("sha256").update(buf).digest("hex");
4840
+ } catch {
4841
+ return null;
4842
+ }
4843
+ }
4844
+ async function readHashFile(path5) {
4845
+ try {
4846
+ return (await readFile(path5, "utf8")).trim();
4847
+ } catch {
4848
+ return null;
4849
+ }
4850
+ }
4851
+ async function npmInstall(releasePath, hasLock, logger) {
4852
+ await rm(join5(releasePath, "node_modules"), { recursive: true, force: true });
4853
+ const cmd = hasLock ? ["ci", "--include=dev"] : ["install", "--include=dev", "--no-audit", "--no-fund"];
4854
+ logger.info(`running npm ${cmd.join(" ")} in ${releasePath}`);
4855
+ await run("npm", cmd, { cwd: releasePath, logger, env: npmInstallEnv() });
4856
+ }
4857
+ async function installDeps(service, releasePath, paths, options = {}) {
4858
+ const { dryRun = false, logger = console } = options;
4859
+ if (dryRun) {
4860
+ logger.info(`[dryRun] would install deps in ${releasePath}`);
4861
+ return { hash: null, copied: false };
4862
+ }
4863
+ const hash = await lockfileHash(releasePath);
4864
+ const hasLock = hash !== null;
4865
+ const hashMarker = join5(releasePath, ".deploy-package-lock.sha256");
4866
+ const currentPath = await readCurrentRelease(paths);
4867
+ let copied = false;
4868
+ if (hasLock && currentPath && await pathExists2(join5(currentPath, "node_modules"))) {
4869
+ const currentHash = await readHashFile(join5(currentPath, ".deploy-package-lock.sha256"));
4870
+ if (currentHash === hash) {
4871
+ logger.info("lockfile unchanged \u2014 copying node_modules from current release");
4872
+ await cp2(join5(currentPath, "node_modules"), join5(releasePath, "node_modules"), {
4873
+ recursive: true,
4874
+ force: true
4875
+ });
4876
+ copied = true;
4877
+ }
4878
+ }
4879
+ if (!copied) {
4880
+ await npmInstall(releasePath, hasLock, logger);
4881
+ }
4882
+ if (hasLock) {
4883
+ await writeFile(hashMarker, `${hash}
4884
+ `, "utf8");
4885
+ await mkdir3(paths.shared, { recursive: true });
4886
+ await writeFile(paths.lockHashFile, `${hash}
4887
+ `, "utf8");
4888
+ }
4889
+ return { hash, copied };
4890
+ }
4891
+
4892
+ // src/deploy/test.js
4893
+ import { symlink as symlink2, unlink as unlink2 } from "fs/promises";
4894
+ import { join as join6 } from "path";
4895
+ async function runReleaseTests(service, releasePath, paths, options = {}) {
4896
+ const { dryRun = false, logger = console } = options;
4897
+ if (!service.testCommand) {
4898
+ logger.info("no testCommand configured \u2014 skipping tests");
4899
+ return;
4900
+ }
4901
+ if (dryRun) {
4902
+ logger.info(`[dryRun] would run "${service.testCommand}" in ${releasePath}`);
4903
+ return;
4904
+ }
4905
+ const envLink = join6(releasePath, ".env");
4906
+ try {
4907
+ await unlink2(envLink);
4908
+ } catch {
4909
+ }
4910
+ await symlink2(paths.sharedEnv, envLink);
4911
+ logger.info(`running "${service.testCommand}" in ${releasePath}`);
4912
+ await runShell(service.testCommand, { cwd: releasePath, logger, env: npmInstallEnv() });
4913
+ }
4914
+
4915
+ // src/deploy/prune.js
4916
+ import { rm as rm2, readlink as readlink2 } from "fs/promises";
4917
+ async function pruneReleases(service, paths, options = {}) {
4918
+ const { dryRun = false, logger = console } = options;
4919
+ const keep = service.keepReleases ?? 3;
4920
+ const releases = await listReleases(paths);
4921
+ let activeName = null;
4922
+ try {
4923
+ const target = await readlink2(paths.current);
4924
+ activeName = target.split("/").pop();
4925
+ } catch {
4926
+ }
4927
+ const keepSet = /* @__PURE__ */ new Set();
4928
+ for (const rel of releases) {
4929
+ if (keepSet.size < keep) keepSet.add(rel.name);
4930
+ }
4931
+ if (activeName) keepSet.add(activeName);
4932
+ const toRemove = releases.filter((rel) => !keepSet.has(rel.name));
4933
+ for (const rel of toRemove) {
4934
+ if (dryRun) {
4935
+ logger.info(`[dryRun] would rm -rf ${rel.path}`);
4936
+ } else {
4937
+ await rm2(rel.path, { recursive: true, force: true });
4938
+ logger.info(`pruned ${rel.path}`);
4939
+ }
4940
+ }
4941
+ return { removed: toRemove.map((r) => r.name) };
4942
+ }
4943
+
4944
+ // src/deploy/pm2.js
4945
+ async function reloadPm2(paths, options = {}) {
4946
+ const { dryRun = false, logger = console } = options;
4947
+ if (dryRun) {
4948
+ logger.info(`[dryRun] would pm2 startOrReload ${paths.ecosystem} --update-env`);
4949
+ return;
4950
+ }
4951
+ await runShell(`pm2 startOrReload "${paths.ecosystem}" --update-env`, { logger });
4952
+ logger.info("pm2 reloaded");
4953
+ }
4954
+
4955
+ // src/deploy/nginx.js
4956
+ import { writeFile as writeFile2 } from "fs/promises";
4957
+ import { tmpdir } from "os";
4958
+ import { join as join7 } from "path";
4959
+ async function hasTlsCert(certPath) {
4960
+ try {
4961
+ await runShell(`sudo test -f '${certPath}'`, {});
4962
+ return true;
4963
+ } catch {
4964
+ return false;
4965
+ }
4966
+ }
4967
+ function proxyBlock(port) {
4968
+ return ` location / {
4969
+ proxy_pass http://127.0.0.1:${port};
4970
+ proxy_http_version 1.1;
4971
+ proxy_set_header Host $host;
4972
+ proxy_set_header X-Real-IP $remote_addr;
4973
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
4974
+ proxy_set_header X-Forwarded-Proto $scheme;
4975
+ proxy_read_timeout 120s;
4976
+ }`;
4977
+ }
4978
+ function buildNginxConfig(service) {
4979
+ const { nginx, pm2 } = service;
4980
+ const certDir = `/etc/letsencrypt/live/${nginx.fqdn}`;
4981
+ return `# ${nginx.siteName} \u2014 managed by cli-toolkit deploy (proxy mode)
4982
+ # ${nginx.fqdn} \u2192 127.0.0.1:${pm2.port}
4983
+
4984
+ server {
4985
+ listen 80;
4986
+ listen [::]:80;
4987
+ server_name ${nginx.fqdn};
4988
+ location / { return 301 https://$host$request_uri; }
4989
+ }
4990
+
4991
+ server {
4992
+ listen 443 ssl;
4993
+ listen [::]:443 ssl;
4994
+ server_name ${nginx.fqdn};
4995
+
4996
+ ssl_certificate ${certDir}/fullchain.pem;
4997
+ ssl_certificate_key ${certDir}/privkey.pem;
4998
+ include /etc/letsencrypt/options-ssl-nginx.conf;
4999
+ ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
5000
+
5001
+ client_max_body_size 25m;
5002
+
5003
+ ${proxyBlock(pm2.port)}
5004
+ }
5005
+ `;
5006
+ }
5007
+ function buildNginxConfigHttpOnly(service) {
5008
+ const { nginx, pm2 } = service;
5009
+ return `# ${nginx.siteName} \u2014 managed by cli-toolkit deploy (HTTP proxy, no TLS cert yet)
5010
+
5011
+ server {
5012
+ listen 80;
5013
+ listen [::]:80;
5014
+ server_name ${nginx.fqdn};
5015
+
5016
+ client_max_body_size 25m;
5017
+
5018
+ ${proxyBlock(pm2.port)}
5019
+ }
5020
+ `;
5021
+ }
5022
+ async function enableNginxUpstream(service, options = {}) {
5023
+ const { dryRun = false, logger = console } = options;
5024
+ const { nginx } = service;
5025
+ if (!nginx) {
5026
+ logger.info("no nginx config on service \u2014 skipping nginx step");
5027
+ return { skipped: true };
5028
+ }
5029
+ const siteAvailable = `/etc/nginx/sites-available/${nginx.siteName}`;
5030
+ const siteEnabled = `/etc/nginx/sites-enabled/${nginx.siteName}`;
5031
+ const cert = `/etc/letsencrypt/live/${nginx.fqdn}/fullchain.pem`;
5032
+ if (dryRun) {
5033
+ logger.info(`[dryRun] would write ${siteAvailable} (proxy \u2192 127.0.0.1:${service.pm2.port}) and reload nginx`);
5034
+ return { hasCert: null };
5035
+ }
5036
+ const hasCert = await hasTlsCert(cert);
5037
+ const config2 = hasCert ? buildNginxConfig(service) : buildNginxConfigHttpOnly(service);
5038
+ const tmp = join7(tmpdir(), `${service.name}-nginx.conf`);
5039
+ await writeFile2(tmp, config2);
5040
+ await runShell(
5041
+ `sudo cp '${tmp}' '${siteAvailable}' && sudo ln -sf '${siteAvailable}' '${siteEnabled}' && sudo nginx -t && sudo systemctl reload nginx`,
5042
+ { logger }
5043
+ );
5044
+ logger.info(`nginx upstream enabled for ${nginx.fqdn} \u2192 127.0.0.1:${service.pm2.port} (tls=${hasCert})`);
5045
+ return { hasCert };
5046
+ }
5047
+
5048
+ // src/deploy/sync-env.js
5049
+ import { access as access3, mkdir as mkdir4, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
5050
+ function scrubEnvContent(content, patterns = []) {
5051
+ const regexes = patterns.map((p) => p instanceof RegExp ? p : new RegExp(p));
5052
+ if (regexes.length === 0) return content;
5053
+ return content.split("\n").filter((line) => !regexes.some((re) => re.test(line.trim()))).join("\n");
5054
+ }
5055
+ async function pathExists3(path5) {
5056
+ try {
5057
+ await access3(path5);
5058
+ return true;
5059
+ } catch {
5060
+ return false;
5061
+ }
5062
+ }
5063
+ async function syncEnv(service, options = {}) {
5064
+ const { dryRun = false, logger = console } = options;
5065
+ const paths = servicePaths(service);
5066
+ const patterns = service.envScrubPatterns ?? [];
5067
+ if (dryRun) {
5068
+ logger.info(`[dryRun] would sync ${paths.repoEnv} \u2192 ${paths.sharedEnv}`);
5069
+ return { source: paths.repoEnv, dest: paths.sharedEnv };
5070
+ }
5071
+ let source = paths.repoEnv;
5072
+ if (!await pathExists3(source) && service.legacyRepoEnv && await pathExists3(service.legacyRepoEnv)) {
5073
+ logger.info(`using legacy env: ${service.legacyRepoEnv}`);
5074
+ source = service.legacyRepoEnv;
5075
+ }
5076
+ await mkdir4(paths.shared, { recursive: true });
5077
+ if (!await pathExists3(source)) {
5078
+ if (service.requireEnv) {
5079
+ throw new Error(
5080
+ `No .env found at ${paths.repoEnv}` + (service.legacyRepoEnv ? ` or ${service.legacyRepoEnv}` : "") + " \u2014 place .env on the host or run the remote deploy from a laptop with a local .env (auto-scp)"
5081
+ );
5082
+ }
5083
+ if (!await pathExists3(paths.sharedEnv)) {
5084
+ await writeFile3(paths.sharedEnv, "", { mode: 384 });
5085
+ }
5086
+ logger.warn(`no .env found (source ${source}) \u2014 using empty ${paths.sharedEnv}`);
5087
+ return { source: null, dest: paths.sharedEnv };
5088
+ }
5089
+ const raw = await readFile2(source, "utf8");
5090
+ await writeFile3(paths.sharedEnv, scrubEnvContent(raw, patterns), { mode: 384 });
5091
+ logger.info(`synced ${source} \u2192 ${paths.sharedEnv}`);
5092
+ return { source, dest: paths.sharedEnv };
5093
+ }
5094
+
5095
+ // src/deploy/build-info.js
5096
+ import { readFile as readFile3, writeFile as writeFile4, mkdir as mkdir5 } from "fs/promises";
5097
+ import { join as join8, dirname as dirname2 } from "path";
5098
+ import { execFile } from "child_process";
5099
+ import { promisify } from "util";
5100
+ var execFileAsync = promisify(execFile);
5101
+ function bumpPatchVersion(version) {
5102
+ const parts = String(version).trim().split(".");
5103
+ const major = Number.parseInt(parts[0], 10) || 0;
5104
+ const minor = Number.parseInt(parts[1], 10) || 0;
5105
+ const patch = Number.parseInt(parts[2], 10) || 0;
5106
+ return `${major}.${minor}.${patch + 1}`;
5107
+ }
5108
+ async function readJson(path5) {
5109
+ try {
5110
+ return JSON.parse(await readFile3(path5, "utf8"));
5111
+ } catch {
5112
+ return null;
5113
+ }
5114
+ }
5115
+ async function gitShortCommit(repoPath) {
5116
+ try {
5117
+ const { stdout } = await execFileAsync("git", ["-C", repoPath, "rev-parse", "--short", "HEAD"], {
5118
+ encoding: "utf8"
5119
+ });
5120
+ return stdout.trim() || null;
5121
+ } catch {
5122
+ return null;
5123
+ }
5124
+ }
5125
+ async function resolveNextVersion(service, paths, pkgPath) {
5126
+ const pkg = await readJson(pkgPath) ?? {};
5127
+ const baseVersion = pkg.version || "0.1.0";
5128
+ const currentPath = await readCurrentRelease(paths);
5129
+ if (currentPath) {
5130
+ const active = await readJson(join8(currentPath, service.buildInfoPath));
5131
+ if (active?.version) return bumpPatchVersion(active.version);
5132
+ }
5133
+ return baseVersion;
5134
+ }
5135
+ async function readReleaseBuildInfo(service, releasePath) {
5136
+ return readJson(join8(releasePath, service.buildInfoPath));
5137
+ }
5138
+ async function writeReleaseBuildInfo(service, releasePath, { stamp, dryRun = false, logger = console }) {
5139
+ const paths = servicePaths(service);
5140
+ const pkgPath = join8(paths.repoRun, "package.json");
5141
+ const version = await resolveNextVersion(service, paths, pkgPath);
5142
+ const gitCommit = await gitShortCommit(paths.repo);
5143
+ const buildInfo = {
5144
+ version,
5145
+ release: stamp,
5146
+ deployedAt: (/* @__PURE__ */ new Date()).toISOString(),
5147
+ gitCommit,
5148
+ service: service.name
5149
+ };
5150
+ const dest = join8(releasePath, service.buildInfoPath);
5151
+ if (dryRun) {
5152
+ logger.info(`[dryRun] would write ${dest} (${JSON.stringify(buildInfo)})`);
5153
+ return buildInfo;
5154
+ }
5155
+ await mkdir5(dirname2(dest), { recursive: true });
5156
+ await writeFile4(dest, `${JSON.stringify(buildInfo, null, 2)}
5157
+ `, { mode: 420 });
5158
+ const gitSuffix = gitCommit ? ` git=${gitCommit}` : "";
5159
+ logger.info(`build info: v${version} release=${stamp}${gitSuffix}`);
5160
+ return buildInfo;
5161
+ }
5162
+
5163
+ // src/deploy/init-structure.js
5164
+ import { access as access4, appendFile as appendFile2, mkdir as mkdir6, writeFile as writeFile5 } from "fs/promises";
5165
+ import { dirname as dirname3, join as join9 } from "path";
5166
+ async function pathExists4(path5) {
5167
+ try {
5168
+ await access4(path5);
5169
+ return true;
5170
+ } catch {
5171
+ return false;
5172
+ }
5173
+ }
5174
+ async function requireDeployRoot(parentDir, serviceRoot) {
5175
+ if (await pathExists4(parentDir)) return;
5176
+ throw new Error(
5177
+ `Cannot create ${serviceRoot}: parent directory ${parentDir} does not exist.
5178
+ This is meant to run on the target host (where ${parentDir} exists). For a local dry run use --appsRoot=/tmp/<service>.`
5179
+ );
5180
+ }
5181
+ function buildEcosystemConfig(service, paths) {
5182
+ const { pm2 } = service;
5183
+ const outLog = join9(paths.logs, `${pm2.appName}.out.log`);
5184
+ const errLog = join9(paths.logs, `${pm2.appName}.err.log`);
5185
+ return `/**
5186
+ * pm2 ecosystem for ${service.name} \u2014 seeded by cli-toolkit deploy (init).
5187
+ *
5188
+ * cwd points at the \`current\` symlink (created on first deploy).
5189
+ * Tweak \`args\` here, then: pm2 reload ${paths.ecosystem} --update-env
5190
+ */
5191
+ module.exports = {
5192
+ apps: [
5193
+ {
5194
+ name: "${pm2.appName}",
5195
+ script: "${pm2.script}",
5196
+ cwd: "${paths.current}",
5197
+ args: "${pm2.args}",
5198
+ instances: 1,
5199
+ exec_mode: "fork",
5200
+ autorestart: true,
5201
+ min_uptime: "10s",
5202
+ max_restarts: 10,
5203
+ restart_delay: 2000,
5204
+ max_memory_restart: "1500M",
5205
+ out_file: "${outLog}",
5206
+ error_file: "${errLog}",
5207
+ merge_logs: true,
5208
+ time: true,
5209
+ env: {
5210
+ NODE_ENV: "production",
5211
+ },
5212
+ },
5213
+ ],
5214
+ };
5215
+ `;
5216
+ }
5217
+ async function initServiceStructure(service, options = {}) {
5218
+ const { dryRun = false, logger = console } = options;
5219
+ const paths = servicePaths(service);
5220
+ const dirs = [paths.releases, paths.shared, paths.logs];
5221
+ const created = [];
5222
+ const skipped = [];
5223
+ if (!dryRun) {
5224
+ await requireDeployRoot(dirname3(paths.root), paths.root);
5225
+ }
5226
+ for (const dir of dirs) {
5227
+ if (await pathExists4(dir)) {
5228
+ skipped.push(dir);
5229
+ continue;
5230
+ }
5231
+ if (!dryRun) await mkdir6(dir, { recursive: true });
5232
+ created.push(dir);
5233
+ }
5234
+ let ecosystemCreated = false;
5235
+ if (await pathExists4(paths.ecosystem)) {
5236
+ skipped.push(paths.ecosystem);
5237
+ } else {
5238
+ if (!dryRun) {
5239
+ await writeFile5(paths.ecosystem, buildEcosystemConfig(service, paths), { mode: 420 });
5240
+ }
5241
+ ecosystemCreated = true;
5242
+ created.push(paths.ecosystem);
5243
+ }
5244
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] init-structure service=${service.name} dryRun=${dryRun} created=${created.length} skipped=${skipped.length}
5245
+ `;
5246
+ if (!dryRun) {
5247
+ await mkdir6(paths.logs, { recursive: true });
5248
+ await appendFile2(paths.deployLog, line);
5249
+ }
5250
+ logger.info(`service=${service.name} appsRoot=${paths.root}`);
5251
+ logger.info(`created: ${created.length ? created.join(", ") : "(none)"}`);
5252
+ logger.info(`already present: ${skipped.length ? skipped.join(", ") : "(none)"}`);
5253
+ if (ecosystemCreated) logger.info(`seeded ${paths.ecosystem}`);
5254
+ return { paths, created, skipped, ecosystemCreated };
5255
+ }
5256
+
5257
+ // src/deploy/bootstrap-host.js
5258
+ import { execSync as execSync2 } from "child_process";
5259
+ import { access as access5, chmod, copyFile, mkdir as mkdir7, readFile as readFile4, writeFile as writeFile6 } from "fs/promises";
5260
+ import { homedir } from "os";
5261
+ import { basename as basename2, join as join10 } from "path";
5262
+ async function pathExists5(path5) {
5263
+ try {
5264
+ await access5(path5);
5265
+ return true;
5266
+ } catch {
5267
+ return false;
5268
+ }
5269
+ }
5270
+ function expandHome(path5) {
5271
+ return path5.startsWith("~/") ? join10(homedir(), path5.slice(2)) : path5;
5272
+ }
5273
+ async function ensurePm2Startup(options = {}) {
5274
+ const { user = "ubuntu", dryRun = false, logger = console } = options;
5275
+ if (dryRun) {
5276
+ logger.info("[dryRun] would configure pm2 startup systemd");
5277
+ return;
5278
+ }
5279
+ try {
5280
+ execSync2("pm2 ping", { stdio: "ignore" });
5281
+ } catch {
5282
+ }
5283
+ try {
5284
+ const out = execSync2(`pm2 startup systemd -u ${user} --hp /home/${user}`, { encoding: "utf8" });
5285
+ const sudoLine = out.split("\n").find((l) => l.trim().startsWith("sudo"));
5286
+ if (sudoLine) {
5287
+ execSync2(sudoLine.trim(), { stdio: "inherit" });
5288
+ logger.info("pm2 startup systemd configured");
5289
+ }
5290
+ } catch (err) {
5291
+ logger.warn(`pm2 startup skipped or already configured: ${err.message}`);
5292
+ }
5293
+ }
5294
+ async function installDeployKey(deployKeyPath, options = {}) {
5295
+ const { dryRun = false, logger = console } = options;
5296
+ const keyPath = expandHome(deployKeyPath);
5297
+ if (!await pathExists5(keyPath)) {
5298
+ logger.warn(`deploy key not found at ${keyPath} \u2014 skipping git ssh setup`);
5299
+ return;
5300
+ }
5301
+ const keyBase = basename2(keyPath);
5302
+ const sshDir = join10(homedir(), ".ssh");
5303
+ const destKey = join10(sshDir, keyBase);
5304
+ const configPath = join10(sshDir, "config");
5305
+ const block = `
5306
+ Host github.com
5307
+ HostName github.com
5308
+ User git
5309
+ IdentityFile ${destKey}
5310
+ IdentitiesOnly yes
5311
+ `;
5312
+ if (dryRun) {
5313
+ logger.info(`[dryRun] would install deploy key ${keyPath} \u2192 ${destKey}`);
5314
+ return;
5315
+ }
5316
+ await mkdir7(sshDir, { recursive: true, mode: 448 });
5317
+ await copyFile(keyPath, destKey);
5318
+ await chmod(destKey, 384);
5319
+ let config2 = "";
5320
+ if (await pathExists5(configPath)) config2 = await readFile4(configPath, "utf8");
5321
+ if (!config2.includes("Host github.com")) {
5322
+ await writeFile6(configPath, `${config2.trimEnd()}
5323
+ ${block}
5324
+ `, { mode: 384 });
5325
+ logger.info("updated ~/.ssh/config for github.com");
5326
+ }
5327
+ logger.info(`deploy key installed at ${destKey}`);
5328
+ }
5329
+ async function installLogrotate(service, options = {}) {
5330
+ const { dryRun = false, logger = console } = options;
5331
+ const conf = `/etc/logrotate.d/${service.name}`;
5332
+ const body = `${service.appsRoot}/logs/*.log {
5333
+ daily
5334
+ rotate 14
5335
+ compress
5336
+ delaycompress
5337
+ missingok
5338
+ notifempty
5339
+ copytruncate
5340
+ }
5341
+ `;
5342
+ if (dryRun) {
5343
+ logger.info(`[dryRun] would write ${conf}`);
5344
+ return;
5345
+ }
5346
+ const tmp = join10("/tmp", `${service.name}-logrotate.conf`);
5347
+ await writeFile6(tmp, body);
5348
+ await runShell(`sudo cp '${tmp}' '${conf}'`, { logger });
5349
+ logger.info(`logrotate config written: ${conf}`);
5350
+ }
5351
+ async function bootstrapHost(service, options = {}) {
5352
+ const { deployKey = service.deployKey, user = "ubuntu", dryRun = false, logger = console } = options;
5353
+ await ensurePm2Startup({ user, dryRun, logger });
5354
+ if (deployKey) await installDeployKey(deployKey, { dryRun, logger });
5355
+ await installLogrotate(service, { dryRun, logger });
5356
+ logger.info("bootstrap-host complete");
5357
+ }
5358
+
5359
+ // src/deploy/deploy-service.js
5360
+ async function deployService(service, options = {}) {
5361
+ const {
5362
+ dryRun = false,
5363
+ skipPull = false,
5364
+ skipTests = false,
5365
+ skipNginx = false,
5366
+ logger = console
5367
+ } = options;
5368
+ const paths = servicePaths(service);
5369
+ await initServiceStructure(service, { dryRun, logger });
5370
+ if (!skipPull) await pullRepo(service, { dryRun, logger });
5371
+ await syncEnv(service, { dryRun, logger });
5372
+ const { stamp, path: releasePath } = await createRelease(service, { dryRun, logger });
5373
+ await writeReleaseBuildInfo(service, releasePath, { stamp, dryRun, logger });
5374
+ await installDeps(service, releasePath, paths, { dryRun, logger });
5375
+ if (!skipTests) await runReleaseTests(service, releasePath, paths, { dryRun, logger });
5376
+ await activateRelease(releasePath, paths, { dryRun, logger });
5377
+ await pruneReleases(service, paths, { dryRun, logger });
5378
+ await reloadPm2(paths, { dryRun, logger });
5379
+ if (!skipNginx) await enableNginxUpstream(service, { dryRun, logger });
5380
+ const summary = `deploy complete stamp=${stamp} dryRun=${dryRun}`;
5381
+ logger.info(summary);
5382
+ if (!dryRun) await appendDeployLog(paths.deployLog, summary);
5383
+ return { stamp, releasePath };
5384
+ }
5385
+
5386
+ // src/deploy/provision-service.js
5387
+ import { access as access6 } from "fs/promises";
5388
+ async function pathExists6(path5) {
5389
+ try {
5390
+ await access6(path5);
5391
+ return true;
5392
+ } catch {
5393
+ return false;
5394
+ }
5395
+ }
5396
+ async function provisionService(service, options = {}) {
5397
+ const {
5398
+ dryRun = false,
5399
+ deploy = true,
5400
+ skipBootstrap = true,
5401
+ deployKey,
5402
+ logger = console
5403
+ } = options;
5404
+ if (!skipBootstrap) {
5405
+ await bootstrapHost(service, { deployKey, dryRun, logger });
5406
+ }
5407
+ await initServiceStructure(service, { dryRun, logger });
5408
+ const paths = servicePaths(service);
5409
+ if (await pathExists6(paths.repo)) {
5410
+ logger.info(`repo exists at ${paths.repo} \u2014 pulling`);
5411
+ await pullRepo(service, { dryRun, logger });
5412
+ } else {
5413
+ await cloneRepo(service, { dryRun, logger });
5414
+ }
5415
+ await syncEnv(service, { dryRun, logger });
5416
+ if (deploy) {
5417
+ await deployService(service, { dryRun, logger, skipPull: true });
5418
+ } else {
5419
+ logger.info("provision complete (deploy skipped)");
5420
+ }
5421
+ }
5422
+
5423
+ // src/deploy/rollback-service.js
5424
+ import { readlink as readlink3 } from "fs/promises";
5425
+ async function rollbackService(service, options = {}) {
5426
+ const { release: targetName, dryRun = false, logger = console } = options;
5427
+ const paths = servicePaths(service);
5428
+ const releases = await listReleases(paths);
5429
+ if (releases.length === 0) throw new Error("No releases to roll back to");
5430
+ let activeName = null;
5431
+ try {
5432
+ const target = await readlink3(paths.current);
5433
+ activeName = target.split("/").pop();
5434
+ } catch {
5435
+ throw new Error("No active release (current symlink missing)");
5436
+ }
5437
+ let rollbackTarget;
5438
+ if (targetName) {
5439
+ rollbackTarget = releases.find((r) => r.name === targetName);
5440
+ if (!rollbackTarget) throw new Error(`Release not found: ${targetName}`);
5441
+ } else {
5442
+ rollbackTarget = releases.find((r) => r.name !== activeName);
5443
+ if (!rollbackTarget) throw new Error("No previous release to roll back to");
5444
+ }
5445
+ if (rollbackTarget.name === activeName) throw new Error(`Already on release ${activeName}`);
5446
+ logger.info(`rollback ${activeName} \u2192 ${rollbackTarget.name}`);
5447
+ const buildInfo = await readReleaseBuildInfo(service, rollbackTarget.path);
5448
+ if (buildInfo?.version) {
5449
+ logger.info(`rollback target: v${buildInfo.version} release=${buildInfo.release ?? rollbackTarget.name}`);
5450
+ }
5451
+ await activateRelease(rollbackTarget.path, paths, { dryRun, logger });
5452
+ await reloadPm2(paths, { dryRun, logger });
5453
+ const summary = `rollback ${activeName} \u2192 ${rollbackTarget.name} dryRun=${dryRun}`;
5454
+ if (!dryRun) await appendDeployLog(paths.deployLog, summary);
5455
+ return { from: activeName, to: rollbackTarget.name, path: rollbackTarget.path };
5456
+ }
5457
+
5458
+ // src/deploy/ssh-remote.js
5459
+ import { access as access7, readFile as readFile5, writeFile as writeFile7 } from "fs/promises";
5460
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
5461
+ import { basename as basename3, dirname as dirname4, join as join11 } from "path";
5462
+ import { spawn as spawn2 } from "child_process";
5463
+ var REMOTE_CLI_REL = "node_modules/@nmakarov/cli-toolkit/scripts/deploy/cli.js";
5464
+ async function pathExists7(path5) {
5465
+ try {
5466
+ await access7(path5);
5467
+ return true;
5468
+ } catch {
5469
+ return false;
5470
+ }
5471
+ }
5472
+ function expandHome2(path5) {
5473
+ return path5.startsWith("~/") ? join11(homedir2(), path5.slice(2)) : path5;
5474
+ }
5475
+ function resolveLocalEnvPath(envFile) {
5476
+ if (envFile) {
5477
+ const expanded = expandHome2(envFile);
5478
+ return expanded.startsWith("/") ? expanded : join11(process.cwd(), expanded);
5479
+ }
5480
+ return join11(process.cwd(), ".env");
5481
+ }
5482
+ function parseGitHost(repoUrl) {
5483
+ const u = String(repoUrl ?? "");
5484
+ let m = u.match(/^[^@]+@([^:]+):/);
5485
+ if (m) return m[1];
5486
+ m = u.match(/^ssh:\/\/[^@]+@([^/:]+)/);
5487
+ if (m) return m[1];
5488
+ return null;
5489
+ }
5490
+ function shellQuote(value) {
5491
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
5492
+ }
5493
+ function sshRun(host, remoteCommand, options = {}) {
5494
+ const { logger } = options;
5495
+ return new Promise((resolve3, reject) => {
5496
+ logger?.info?.(`ssh ${host} ${remoteCommand.slice(0, 120)}${remoteCommand.length > 120 ? "\u2026" : ""}`);
5497
+ const child = spawn2("ssh", [host, remoteCommand], { stdio: "inherit" });
5498
+ child.on("error", reject);
5499
+ child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`ssh ${host} exited with code ${code}`)));
5500
+ });
5501
+ }
5502
+ function scp(localPath, remoteSpec) {
5503
+ return new Promise((resolve3, reject) => {
5504
+ const child = spawn2("scp", [localPath, remoteSpec], { stdio: "inherit" });
5505
+ child.on("error", reject);
5506
+ child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`scp exited with code ${code}`)));
5507
+ });
5508
+ }
5509
+ async function ensureDeployKeyOnRemote(host, deployKeyPath, options = {}) {
5510
+ const { logger = console } = options;
5511
+ if (!deployKeyPath) return false;
5512
+ const localPath = expandHome2(deployKeyPath);
5513
+ if (!await pathExists7(localPath)) return false;
5514
+ const keyBase = basename3(localPath);
5515
+ logger.info(`copying deploy key ${localPath} \u2192 ${host}:~/.ssh/${keyBase}`);
5516
+ await sshRun(host, "mkdir -p ~/.ssh && chmod 700 ~/.ssh", { logger });
5517
+ await scp(localPath, `${host}:.ssh/${keyBase}`, { logger });
5518
+ await sshRun(host, `chmod 600 ~/.ssh/${keyBase}`, { logger });
5519
+ return true;
5520
+ }
5521
+ async function prepareGitHost(host, options = {}) {
5522
+ const { logger = console, gitHost, keyBasename } = options;
5523
+ if (!gitHost) return;
5524
+ const configBlock = keyBasename ? `if ! grep -q 'Host ${gitHost}' ~/.ssh/config 2>/dev/null; then
5525
+ printf '%s\\n' '' 'Host ${gitHost}' ' HostName ${gitHost}' ' User git' ' IdentityFile ~/.ssh/${keyBasename}' ' IdentitiesOnly yes' >> ~/.ssh/config
5526
+ chmod 600 ~/.ssh/config
5527
+ echo "configured ~/.ssh/config for ${gitHost}"
5528
+ fi` : `:`;
5529
+ const script = `
5530
+ set -euo pipefail
5531
+ mkdir -p ~/.ssh
5532
+ chmod 700 ~/.ssh
5533
+ if ! grep -q '^${gitHost}' ~/.ssh/known_hosts 2>/dev/null; then
5534
+ ssh-keyscan -t ed25519,rsa ${gitHost} >> ~/.ssh/known_hosts 2>/dev/null
5535
+ echo "added ${gitHost} to known_hosts"
5536
+ fi
5537
+ ${configBlock}
5538
+ `.trim();
5539
+ await sshRun(host, script, { logger });
5540
+ }
5541
+ async function ensureRepoDependencies(host, service, options = {}) {
5542
+ const { logger = console } = options;
5543
+ const paths = servicePaths(service);
5544
+ const run2 = shellQuote(paths.repoRun);
5545
+ await sshRun(
5546
+ host,
5547
+ `cd ${run2} && if [ ! -d node_modules/@nmakarov/cli-toolkit ] || [ package-lock.json -nt node_modules/.package-lock.json ]; then npm ci; fi`,
5548
+ { logger }
5549
+ );
5550
+ }
5551
+ async function ensureEnvOnRemote(host, service, options = {}) {
5552
+ const { logger = console, envFile } = options;
5553
+ const paths = servicePaths(service);
5554
+ const localPath = resolveLocalEnvPath(envFile);
5555
+ if (!await pathExists7(localPath)) return false;
5556
+ logger.info(`copying .env ${localPath} \u2192 ${host}:${paths.repoEnv}`);
5557
+ await sshRun(host, `mkdir -p ${shellQuote(dirname4(paths.repoEnv))}`, { logger });
5558
+ const scrubbed = scrubEnvContent(await readFile5(localPath, "utf8"), service.envScrubPatterns ?? []);
5559
+ const tmp = join11(tmpdir2(), `deploy-env-${Date.now()}`);
5560
+ await writeFile7(tmp, scrubbed, { mode: 384 });
5561
+ await scp(tmp, `${host}:${paths.repoEnv}`, { logger });
5562
+ await sshRun(host, `chmod 600 ${shellQuote(paths.repoEnv)}`, { logger });
5563
+ return true;
5564
+ }
5565
+ async function ensureRemoteRepo(host, service, options = {}) {
5566
+ const { logger = console, deployKey = service.deployKey, envFile } = options;
5567
+ const paths = servicePaths(service);
5568
+ const repo = shellQuote(paths.repo);
5569
+ const repoUrl = shellQuote(service.repoUrl);
5570
+ const root = shellQuote(paths.root);
5571
+ const localKeyBase = deployKey ? basename3(expandHome2(deployKey)) : null;
5572
+ await ensureDeployKeyOnRemote(host, deployKey, { logger });
5573
+ await prepareGitHost(host, { logger, gitHost: parseGitHost(service.repoUrl), keyBasename: localKeyBase });
5574
+ await sshRun(
5575
+ host,
5576
+ `mkdir -p ${root} && if [ -d ${repo}/.git ]; then git -C ${repo} pull --ff-only; else git clone ${repoUrl} ${repo}; fi`,
5577
+ { logger }
5578
+ );
5579
+ await ensureRepoDependencies(host, service, { logger });
5580
+ await ensureEnvOnRemote(host, service, { logger, envFile });
5581
+ }
5582
+ async function runRemoteCli(host, service, command, args = [], options = {}) {
5583
+ const { logger = console, manifests, skipPull = false, deployKey = service.deployKey, envFile } = options;
5584
+ const paths = servicePaths(service);
5585
+ const run2 = shellQuote(paths.repoRun);
5586
+ const cli = shellQuote(REMOTE_CLI_REL);
5587
+ if (!skipPull) {
5588
+ await ensureRemoteRepo(host, service, { logger, deployKey, envFile });
5589
+ } else {
5590
+ await ensureRepoDependencies(host, service, { logger });
5591
+ await ensureEnvOnRemote(host, service, { logger, envFile });
5592
+ }
5593
+ const passthrough = [
5594
+ `--service=${service.name}`,
5595
+ ...manifests ? [`--manifests=${manifests}`] : [],
5596
+ ...args
5597
+ ].map(shellQuote).join(" ");
5598
+ await sshRun(host, `cd ${run2} && node ${cli} ${shellQuote(command)} ${passthrough}`.trim(), { logger });
5599
+ }
5600
+ async function runRemoteStatus(host, service, options = {}) {
5601
+ const { logger = console } = options;
5602
+ const paths = servicePaths(service);
5603
+ const port = service.pm2.port;
5604
+ const app = service.pm2.appName;
5605
+ const script = `
5606
+ echo "=== apps root ==="
5607
+ ls -la ${shellQuote(paths.root)} 2>/dev/null || echo "(missing)"
5608
+ echo ""
5609
+ echo "=== current ==="
5610
+ readlink ${shellQuote(paths.current)} 2>/dev/null || echo "(not set)"
5611
+ echo ""
5612
+ echo "=== releases ==="
5613
+ ls -1 ${shellQuote(paths.releases)} 2>/dev/null || echo "(none)"
5614
+ echo ""
5615
+ echo "=== pm2 ==="
5616
+ pm2 describe ${app} 2>/dev/null | head -20 || pm2 status ${app} 2>/dev/null || echo "(not running)"
5617
+ ${port ? `echo ""
5618
+ echo "=== app health (localhost) ==="
5619
+ curl -sf http://127.0.0.1:${port}/healthz 2>/dev/null || echo "(no /healthz on :${port})"` : ""}
5620
+ `.trim();
5621
+ await sshRun(host, script, { logger });
5622
+ }
5623
+
5624
+ // src/deploy/manifests.js
5625
+ import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
5626
+ import { pathToFileURL } from "url";
5627
+ async function loadServices({ manifests = "deploy/services.js", cwd = process.cwd() } = {}) {
5628
+ const abs = isAbsolute2(manifests) ? manifests : resolve2(cwd, manifests);
5629
+ let mod;
5630
+ try {
5631
+ mod = await import(pathToFileURL(abs).href);
5632
+ } catch (err) {
5633
+ throw new Error(`Could not load deploy manifests from ${abs}: ${err.message}`);
5634
+ }
5635
+ const raw = mod.services ?? mod.default;
5636
+ if (!raw) {
5637
+ throw new Error(`Manifests module ${abs} must export \`services\` (or default): a map or array of service manifests`);
5638
+ }
5639
+ const list = Array.isArray(raw) ? raw : Object.values(raw);
5640
+ const out = {};
5641
+ for (const entry of list) {
5642
+ const svc = defineService(entry);
5643
+ out[svc.name] = svc;
5644
+ }
5645
+ return out;
5646
+ }
5647
+ function resolveServiceFrom(serviceMap, name, { appsRoot } = {}) {
5648
+ const svc = serviceMap[name];
5649
+ if (!svc) {
5650
+ throw new Error(`Unknown service "${name}". Known: ${Object.keys(serviceMap).join(", ") || "(none)"}`);
5651
+ }
5652
+ return appsRoot ? { ...svc, appsRoot } : svc;
5653
+ }
5654
+
4347
5655
  // src/tasks/index.js
4348
5656
  import os3 from "os";
4349
5657
 
4350
5658
  // src/utils/core-utils.js
4351
5659
  function sleepMs(ms) {
4352
- return new Promise((resolve2) => setTimeout(resolve2, ms));
5660
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4353
5661
  }
4354
5662
  function toJsonColumn(value) {
4355
5663
  if (value === void 0 || value === null) return null;
@@ -5408,10 +6716,10 @@ var TaskSampleProcess = class extends AbstractTask {
5408
6716
  };
5409
6717
 
5410
6718
  // src/tasks/coreTasks/TaskShellCommand.js
5411
- import { spawn } from "child_process";
6719
+ import { spawn as spawn3 } from "child_process";
5412
6720
  function runShellCommand(command, cwd) {
5413
- return new Promise((resolve2, reject) => {
5414
- const child = spawn(command, {
6721
+ return new Promise((resolve3, reject) => {
6722
+ const child = spawn3(command, {
5415
6723
  shell: true,
5416
6724
  cwd: cwd || process.cwd(),
5417
6725
  stdio: ["ignore", "pipe", "pipe"]
@@ -5428,7 +6736,7 @@ function runShellCommand(command, cwd) {
5428
6736
  reject(error);
5429
6737
  });
5430
6738
  child.on("close", (exitCode, signal) => {
5431
- resolve2({
6739
+ resolve3({
5432
6740
  exitCode,
5433
6741
  output: output.trim(),
5434
6742
  stderr: stderr.trim(),
@@ -5900,7 +7208,7 @@ function mergeAllowedTasksWithServiceTasks(names) {
5900
7208
  }
5901
7209
 
5902
7210
  // src/tasks/taskScriptRunner.js
5903
- import { spawn as spawn2 } from "child_process";
7211
+ import { spawn as spawn4 } from "child_process";
5904
7212
  var MAX_PROGRESS_TEXT_LEN = 4e3;
5905
7213
  function toCliArgs(args = []) {
5906
7214
  return args.filter((a) => typeof a === "string" && a.length > 0);
@@ -5978,7 +7286,7 @@ function createSerializedQueue() {
5978
7286
  async function runNodeTaskScript(context, options) {
5979
7287
  const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
5980
7288
  const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
5981
- const child = spawn2(process.execPath, nodeArgs, {
7289
+ const child = spawn4(process.execPath, nodeArgs, {
5982
7290
  cwd: options.cwd || process.cwd(),
5983
7291
  stdio: ["ignore", "pipe", "pipe", "ipc"],
5984
7292
  env: {
@@ -6065,13 +7373,13 @@ async function runNodeTaskScript(context, options) {
6065
7373
  }
6066
7374
  forwardChildLogToParent(context, prefix, message);
6067
7375
  });
6068
- return await new Promise((resolve2, reject) => {
7376
+ return await new Promise((resolve3, reject) => {
6069
7377
  child.on("error", (error) => reject(error));
6070
7378
  child.on("close", (exitCode, signal) => {
6071
7379
  void (async () => {
6072
7380
  await flushTaskIpcLogs(context);
6073
7381
  await progressQueue.drain();
6074
- resolve2({
7382
+ resolve3({
6075
7383
  exitCode,
6076
7384
  signal,
6077
7385
  stdout: state.stdout.trim(),
@@ -6637,6 +7945,7 @@ var TasksManager = class _TasksManager {
6637
7945
  export {
6638
7946
  AbstractTask,
6639
7947
  Args,
7948
+ Aws,
6640
7949
  Box4 as Box,
6641
7950
  Db,
6642
7951
  Divider,
@@ -6650,6 +7959,7 @@ export {
6650
7959
  MultiColumnListComponent,
6651
7960
  MultiColumnListWithPreviewComponent,
6652
7961
  Params,
7962
+ REMOTE_CLI_REL,
6653
7963
  React2 as React,
6654
7964
  S3,
6655
7965
  SERVICE_TASK_NAMES,
@@ -6670,51 +7980,94 @@ export {
6670
7980
  TasksRegistry,
6671
7981
  Text5 as Text,
6672
7982
  TextBlock,
7983
+ activateRelease,
7984
+ appendDeployLog,
6673
7985
  appendTaskIpcLog,
7986
+ bootstrapHost,
6674
7987
  buildBreadcrumb,
6675
7988
  buildDetailBreadcrumb,
6676
7989
  buildFooter,
7990
+ bumpPatchVersion,
7991
+ cloneRepo,
6677
7992
  convertPattern,
7993
+ createRelease,
6678
7994
  defaultFileSynopsisFunction,
6679
7995
  defaultTasksRegistry,
6680
7996
  defaultVersionSynopsisFunction,
7997
+ defineService,
7998
+ deployService,
7999
+ deriveRepoDirName,
8000
+ enableNginxUpstream,
6681
8001
  enqueueStopTask,
6682
8002
  enqueueTask,
8003
+ ensureDeployKeyOnRemote,
8004
+ ensureEnvOnRemote,
8005
+ ensureRemoteRepo,
8006
+ ensureRepoDependencies,
6683
8007
  ensureTaskTables,
6684
8008
  flushTaskIpcLogs,
6685
8009
  getArgsInstance,
6686
8010
  createElement2 as h,
8011
+ initServiceStructure,
8012
+ installDeps,
6687
8013
  ipcFileLogsTableNameForSourceResource,
6688
8014
  joiEdateType,
6689
8015
  joiStringArrayType,
6690
8016
  listServicesRegistry as listAliveRunnerHeartbeats,
8017
+ listReleases,
6691
8018
  listServicesRegistry,
6692
8019
  listSources,
6693
8020
  listTables,
6694
8021
  load,
8022
+ loadServices,
6695
8023
  matchesParsedPattern,
6696
8024
  memo,
6697
8025
  mergeAllowedTasksWithServiceTasks,
6698
8026
  nextTimeMatch,
6699
8027
  normalizeAllowedTasks,
8028
+ npmEnv,
8029
+ npmInstallEnv,
6700
8030
  organizeFooterMessages,
8031
+ parseGitHost,
8032
+ prepareGitHost,
8033
+ provisionService,
8034
+ pruneReleases,
8035
+ pullRepo,
6701
8036
  queueToTableNames,
8037
+ readCurrentRelease,
8038
+ readReleaseBuildInfo,
6702
8039
  readTaskIpcLogsSnapshot,
6703
8040
  registerInServicesRegistry,
6704
8041
  registerInServicesRegistry as registerRunnerHeartbeat,
8042
+ releaseDir,
8043
+ releaseStamp,
8044
+ reloadPm2,
6705
8045
  resolveAsterisks,
6706
8046
  resolveIpcFileLogsDir,
8047
+ resolveNextVersion,
6707
8048
  resolveRanges,
8049
+ resolveServiceFrom,
6708
8050
  resolveSteps,
8051
+ rollbackService,
8052
+ run,
6709
8053
  runNodeTaskScript,
8054
+ runReleaseTests,
8055
+ runRemoteCli,
8056
+ runRemoteStatus,
8057
+ runShell,
6710
8058
  runTasksLoop,
8059
+ scrubEnvContent,
8060
+ servicePaths,
6711
8061
  setupContext,
8062
+ shellQuote,
6712
8063
  showListScreen,
6713
8064
  showMenuScreen,
6714
8065
  showMultiColumnListScreen,
6715
8066
  showMultiColumnListWithPreviewScreen,
6716
8067
  showScreen,
6717
8068
  showWordGridScreen,
8069
+ sshRun,
8070
+ syncEnv,
6718
8071
  taskHistoryInsertFromQueueRow,
6719
8072
  timeMatcher,
6720
8073
  touchServicesRegistry as touchRunnerHeartbeat,
@@ -6730,6 +8083,7 @@ export {
6730
8083
  useMemo,
6731
8084
  useRef2 as useRef,
6732
8085
  useState3 as useState,
6733
- waitForTaskResult
8086
+ waitForTaskResult,
8087
+ writeReleaseBuildInfo
6734
8088
  };
6735
8089
  //# sourceMappingURL=index.js.map