@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.cjs CHANGED
@@ -561,7 +561,7 @@ async function showScreen(config2) {
561
561
  onRender,
562
562
  parentData = {}
563
563
  } = config2;
564
- return new Promise((resolve2) => {
564
+ return new Promise((resolve3) => {
565
565
  let instance2;
566
566
  const keyBindings = [];
567
567
  const actions = {};
@@ -736,7 +736,7 @@ async function showScreen(config2) {
736
736
  };
737
737
  const cleanup = (result) => {
738
738
  if (instance2) instance2.unmount();
739
- setTimeout(() => resolve2(result), 50);
739
+ setTimeout(() => resolve3(result), 50);
740
740
  };
741
741
  instance2 = (0, import_ink3.render)((0, import_react3.createElement)(Screen));
742
742
  });
@@ -1072,6 +1072,7 @@ var src_exports = {};
1072
1072
  __export(src_exports, {
1073
1073
  AbstractTask: () => AbstractTask,
1074
1074
  Args: () => Args,
1075
+ Aws: () => Aws,
1075
1076
  Box: () => import_ink5.Box,
1076
1077
  Db: () => Db,
1077
1078
  Divider: () => Divider,
@@ -1085,6 +1086,7 @@ __export(src_exports, {
1085
1086
  MultiColumnListComponent: () => MultiColumnListComponent,
1086
1087
  MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
1087
1088
  Params: () => Params,
1089
+ REMOTE_CLI_REL: () => REMOTE_CLI_REL,
1088
1090
  React: () => import_react5.default,
1089
1091
  S3: () => S3,
1090
1092
  SERVICE_TASK_NAMES: () => SERVICE_TASK_NAMES,
@@ -1105,51 +1107,94 @@ __export(src_exports, {
1105
1107
  TasksRegistry: () => TasksRegistry,
1106
1108
  Text: () => import_ink5.Text,
1107
1109
  TextBlock: () => TextBlock,
1110
+ activateRelease: () => activateRelease,
1111
+ appendDeployLog: () => appendDeployLog,
1108
1112
  appendTaskIpcLog: () => appendTaskIpcLog,
1113
+ bootstrapHost: () => bootstrapHost,
1109
1114
  buildBreadcrumb: () => buildBreadcrumb,
1110
1115
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1111
1116
  buildFooter: () => buildFooter,
1117
+ bumpPatchVersion: () => bumpPatchVersion,
1118
+ cloneRepo: () => cloneRepo,
1112
1119
  convertPattern: () => convertPattern,
1120
+ createRelease: () => createRelease,
1113
1121
  defaultFileSynopsisFunction: () => defaultFileSynopsisFunction,
1114
1122
  defaultTasksRegistry: () => defaultTasksRegistry,
1115
1123
  defaultVersionSynopsisFunction: () => defaultVersionSynopsisFunction,
1124
+ defineService: () => defineService,
1125
+ deployService: () => deployService,
1126
+ deriveRepoDirName: () => deriveRepoDirName,
1127
+ enableNginxUpstream: () => enableNginxUpstream,
1116
1128
  enqueueStopTask: () => enqueueStopTask,
1117
1129
  enqueueTask: () => enqueueTask,
1130
+ ensureDeployKeyOnRemote: () => ensureDeployKeyOnRemote,
1131
+ ensureEnvOnRemote: () => ensureEnvOnRemote,
1132
+ ensureRemoteRepo: () => ensureRemoteRepo,
1133
+ ensureRepoDependencies: () => ensureRepoDependencies,
1118
1134
  ensureTaskTables: () => ensureTaskTables,
1119
1135
  flushTaskIpcLogs: () => flushTaskIpcLogs,
1120
1136
  getArgsInstance: () => getArgsInstance,
1121
1137
  h: () => import_react5.createElement,
1138
+ initServiceStructure: () => initServiceStructure,
1139
+ installDeps: () => installDeps,
1122
1140
  ipcFileLogsTableNameForSourceResource: () => ipcFileLogsTableNameForSourceResource,
1123
1141
  joiEdateType: () => joiEdateType,
1124
1142
  joiStringArrayType: () => joiStringArrayType,
1125
1143
  listAliveRunnerHeartbeats: () => listServicesRegistry,
1144
+ listReleases: () => listReleases,
1126
1145
  listServicesRegistry: () => listServicesRegistry,
1127
1146
  listSources: () => listSources,
1128
1147
  listTables: () => listTables,
1129
1148
  load: () => load,
1149
+ loadServices: () => loadServices,
1130
1150
  matchesParsedPattern: () => matchesParsedPattern,
1131
1151
  memo: () => import_react5.memo,
1132
1152
  mergeAllowedTasksWithServiceTasks: () => mergeAllowedTasksWithServiceTasks,
1133
1153
  nextTimeMatch: () => nextTimeMatch,
1134
1154
  normalizeAllowedTasks: () => normalizeAllowedTasks,
1155
+ npmEnv: () => npmEnv,
1156
+ npmInstallEnv: () => npmInstallEnv,
1135
1157
  organizeFooterMessages: () => organizeFooterMessages,
1158
+ parseGitHost: () => parseGitHost,
1159
+ prepareGitHost: () => prepareGitHost,
1160
+ provisionService: () => provisionService,
1161
+ pruneReleases: () => pruneReleases,
1162
+ pullRepo: () => pullRepo,
1136
1163
  queueToTableNames: () => queueToTableNames,
1164
+ readCurrentRelease: () => readCurrentRelease,
1165
+ readReleaseBuildInfo: () => readReleaseBuildInfo,
1137
1166
  readTaskIpcLogsSnapshot: () => readTaskIpcLogsSnapshot,
1138
1167
  registerInServicesRegistry: () => registerInServicesRegistry,
1139
1168
  registerRunnerHeartbeat: () => registerInServicesRegistry,
1169
+ releaseDir: () => releaseDir,
1170
+ releaseStamp: () => releaseStamp,
1171
+ reloadPm2: () => reloadPm2,
1140
1172
  resolveAsterisks: () => resolveAsterisks,
1141
1173
  resolveIpcFileLogsDir: () => resolveIpcFileLogsDir,
1174
+ resolveNextVersion: () => resolveNextVersion,
1142
1175
  resolveRanges: () => resolveRanges,
1176
+ resolveServiceFrom: () => resolveServiceFrom,
1143
1177
  resolveSteps: () => resolveSteps,
1178
+ rollbackService: () => rollbackService,
1179
+ run: () => run,
1144
1180
  runNodeTaskScript: () => runNodeTaskScript,
1181
+ runReleaseTests: () => runReleaseTests,
1182
+ runRemoteCli: () => runRemoteCli,
1183
+ runRemoteStatus: () => runRemoteStatus,
1184
+ runShell: () => runShell,
1145
1185
  runTasksLoop: () => runTasksLoop,
1186
+ scrubEnvContent: () => scrubEnvContent,
1187
+ servicePaths: () => servicePaths,
1146
1188
  setupContext: () => setupContext,
1189
+ shellQuote: () => shellQuote,
1147
1190
  showListScreen: () => showListScreen,
1148
1191
  showMenuScreen: () => showMenuScreen,
1149
1192
  showMultiColumnListScreen: () => showMultiColumnListScreen,
1150
1193
  showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
1151
1194
  showScreen: () => showScreen,
1152
1195
  showWordGridScreen: () => showWordGridScreen,
1196
+ sshRun: () => sshRun,
1197
+ syncEnv: () => syncEnv,
1153
1198
  taskHistoryInsertFromQueueRow: () => taskHistoryInsertFromQueueRow,
1154
1199
  timeMatcher: () => timeMatcher,
1155
1200
  touchRunnerHeartbeat: () => touchServicesRegistry,
@@ -1165,7 +1210,8 @@ __export(src_exports, {
1165
1210
  useMemo: () => import_react5.useMemo,
1166
1211
  useRef: () => import_react5.useRef,
1167
1212
  useState: () => import_react5.useState,
1168
- waitForTaskResult: () => waitForTaskResult
1213
+ waitForTaskResult: () => waitForTaskResult,
1214
+ writeReleaseBuildInfo: () => writeReleaseBuildInfo
1169
1215
  });
1170
1216
  module.exports = __toCommonJS(src_exports);
1171
1217
 
@@ -2559,8 +2605,8 @@ var FileDatabase = class _FileDatabase {
2559
2605
  const items = await import_fs4.default.promises.readdir(destPath);
2560
2606
  const versions = items.filter((item) => {
2561
2607
  const itemPath = import_path4.default.join(destPath, item);
2562
- const stat = import_fs4.default.statSync(itemPath);
2563
- return stat.isDirectory() && isTimestampFolder(item);
2608
+ const stat2 = import_fs4.default.statSync(itemPath);
2609
+ return stat2.isDirectory() && isTimestampFolder(item);
2564
2610
  });
2565
2611
  return versions.sort();
2566
2612
  } catch (error) {
@@ -2625,8 +2671,8 @@ var FileDatabase = class _FileDatabase {
2625
2671
  }
2626
2672
  const versionFolders = items.filter((item) => {
2627
2673
  const itemPath = import_path4.default.join(tablePath, item);
2628
- const stat = import_fs4.default.statSync(itemPath);
2629
- return stat.isDirectory() && isTimestampFolder(item);
2674
+ const stat2 = import_fs4.default.statSync(itemPath);
2675
+ return stat2.isDirectory() && isTimestampFolder(item);
2630
2676
  });
2631
2677
  if (versionFolders.length > 0) {
2632
2678
  const latestVersion = versionFolders.sort().pop();
@@ -4082,6 +4128,250 @@ var S3 = class _S3 {
4082
4128
  }
4083
4129
  };
4084
4130
 
4131
+ // src/aws/index.js
4132
+ var import_client_sts = require("@aws-sdk/client-sts");
4133
+ var import_client_ec2 = require("@aws-sdk/client-ec2");
4134
+ var import_client_route_53 = require("@aws-sdk/client-route-53");
4135
+ var DEFAULT_REGION = "us-east-1";
4136
+ var CANONICAL_OWNER_ID = "099720109477";
4137
+ var DEFAULT_UBUNTU_PATTERN = "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*";
4138
+ function nameTag(tags) {
4139
+ const t = (tags ?? []).find((x) => x.Key === "Name");
4140
+ return t?.Value ?? null;
4141
+ }
4142
+ var Aws = class _Aws {
4143
+ /**
4144
+ * Resolve credentials/region from params and return an Aws instance.
4145
+ * Does not call AWS until you invoke a method (clients are lazy).
4146
+ */
4147
+ static async init(context, options = {}) {
4148
+ const getOpt = async (k) => {
4149
+ try {
4150
+ const v = await context?.params?.get?.(k, "string");
4151
+ return v != null && String(v).trim() !== "" ? v : void 0;
4152
+ } catch {
4153
+ return void 0;
4154
+ }
4155
+ };
4156
+ const config2 = {
4157
+ region: options.region ?? await getOpt("awsRegion") ?? DEFAULT_REGION,
4158
+ accessKeyId: options.accessKeyId ?? await getOpt("awsAccessKeyId"),
4159
+ secretAccessKey: options.secretAccessKey ?? await getOpt("awsSecretAccessKey"),
4160
+ sessionToken: options.sessionToken ?? await getOpt("awsSessionToken")
4161
+ };
4162
+ const aws = new _Aws(context, config2);
4163
+ context?.logger?.debug?.(
4164
+ `[aws] region=${config2.region} credentials=${config2.accessKeyId ? "explicit" : "default-chain"}`
4165
+ );
4166
+ return aws;
4167
+ }
4168
+ constructor(context, config2) {
4169
+ this.logger = context?.logger ?? console;
4170
+ this.region = config2.region || DEFAULT_REGION;
4171
+ this._credentials = config2.accessKeyId && config2.secretAccessKey ? {
4172
+ accessKeyId: config2.accessKeyId,
4173
+ secretAccessKey: config2.secretAccessKey,
4174
+ ...config2.sessionToken ? { sessionToken: config2.sessionToken } : {}
4175
+ } : void 0;
4176
+ this.hasExplicitCredentials = !!this._credentials;
4177
+ process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= "true";
4178
+ this._clients = {};
4179
+ }
4180
+ // ── clients (lazy, optionally region-scoped) ──────────────────────────────
4181
+ _clientConfig(region) {
4182
+ const cfg = { region: region || this.region };
4183
+ if (this._credentials) cfg.credentials = this._credentials;
4184
+ return cfg;
4185
+ }
4186
+ _sts() {
4187
+ return this._clients.sts ??= new import_client_sts.STSClient(this._clientConfig());
4188
+ }
4189
+ _ec2(region) {
4190
+ const key = `ec2:${region || this.region}`;
4191
+ return this._clients[key] ??= new import_client_ec2.EC2Client(this._clientConfig(region));
4192
+ }
4193
+ _route53() {
4194
+ return this._clients.route53 ??= new import_client_route_53.Route53Client(this._clientConfig());
4195
+ }
4196
+ getRegion() {
4197
+ return this.region;
4198
+ }
4199
+ // ── credentials ─────────────────────────────────────────────────────────────
4200
+ /**
4201
+ * Are any credentials available *before* hitting AWS? Returns
4202
+ * { ok, source } — explicit keys, or a resolvable default chain (profile/SSO/
4203
+ * instance role). { ok:false } means there's nothing to even try with.
4204
+ * Note: "ok" only means creds were *found*, not that AWS will accept them.
4205
+ */
4206
+ async checkCredentials() {
4207
+ if (this.hasExplicitCredentials) {
4208
+ return { ok: true, source: "explicit keys (env/.env/CLI)" };
4209
+ }
4210
+ try {
4211
+ const provider = this._sts().config.credentials;
4212
+ const resolved = typeof provider === "function" ? await provider() : provider;
4213
+ if (resolved?.accessKeyId) {
4214
+ return { ok: true, source: "default credential chain (profile/SSO/role)" };
4215
+ }
4216
+ } catch {
4217
+ }
4218
+ return { ok: false };
4219
+ }
4220
+ /** True for "bad/missing credentials" style errors (vs. real failures). */
4221
+ static isAuthError(err) {
4222
+ const name = err?.name || err?.Code || err?.__type || "";
4223
+ return [
4224
+ "CredentialsProviderError",
4225
+ "InvalidClientTokenId",
4226
+ "UnrecognizedClientException",
4227
+ "AuthFailure",
4228
+ "AccessDenied",
4229
+ "AccessDeniedException",
4230
+ "ExpiredToken",
4231
+ "ExpiredTokenException",
4232
+ "SignatureDoesNotMatch",
4233
+ "MissingAuthenticationToken"
4234
+ ].includes(name);
4235
+ }
4236
+ /** Short, precise instructions for getting AWS credentials into .env. */
4237
+ static credentialsHelp(region = DEFAULT_REGION) {
4238
+ return [
4239
+ "No usable AWS credentials were found (or AWS rejected them).",
4240
+ "",
4241
+ "Put a read-only access key in the project's .env:",
4242
+ "",
4243
+ " AWS_ACCESS_KEY_ID=AKIA...",
4244
+ " AWS_SECRET_ACCESS_KEY=...",
4245
+ ` AWS_REGION=${region} # optional (default ${DEFAULT_REGION})`,
4246
+ "",
4247
+ "Get a key from the AWS console (~2 min):",
4248
+ " 1. IAM \u2192 Users \u2192 create or pick a user (console sign-in not needed).",
4249
+ ' 2. Attach a policy \u2014 "ReadOnlyAccess" (AWS managed) is enough for discovery.',
4250
+ ' 3. The user \u2192 "Security credentials" \u2192 "Create access key" \u2192 "CLI".',
4251
+ " 4. Copy the Access key ID + Secret access key (the secret shows only once).",
4252
+ " 5. Paste both into .env, then re-run.",
4253
+ " Direct link: https://console.aws.amazon.com/iam/home#/users",
4254
+ "",
4255
+ "Prefer a named profile or an EC2 instance role? Re-run with AWS_PROFILE=<name>",
4256
+ "set (or on the instance) and credentials resolve automatically."
4257
+ ].join("\n");
4258
+ }
4259
+ // ── identity ──────────────────────────────────────────────────────────────
4260
+ /** { account, arn, userId } — confirm which account/identity the keys belong to. */
4261
+ async whoAmI() {
4262
+ const out = await this._sts().send(new import_client_sts.GetCallerIdentityCommand({}));
4263
+ return { account: out.Account, arn: out.Arn, userId: out.UserId };
4264
+ }
4265
+ // ── regions / AZs ──────────────────────────────────────────────────────────
4266
+ /** Enabled region names, sorted. */
4267
+ async listRegions({ allRegions = false } = {}) {
4268
+ const out = await this._ec2().send(new import_client_ec2.DescribeRegionsCommand({ AllRegions: allRegions }));
4269
+ return (out.Regions ?? []).map((r) => r.RegionName).sort();
4270
+ }
4271
+ /** Availability zone names for a region (defaults to the instance region). */
4272
+ async listAvailabilityZones(region) {
4273
+ const out = await this._ec2(region).send(new import_client_ec2.DescribeAvailabilityZonesCommand({}));
4274
+ return (out.AvailabilityZones ?? []).filter((z) => z.State === "available").map((z) => z.ZoneName).sort();
4275
+ }
4276
+ // ── VPC / subnets / SGs / key pairs ────────────────────────────────────────
4277
+ /** [{ id, cidr, isDefault, name }] in a region. */
4278
+ async listVpcs(region) {
4279
+ const out = await this._ec2(region).send(new import_client_ec2.DescribeVpcsCommand({}));
4280
+ return (out.Vpcs ?? []).map((v) => ({
4281
+ id: v.VpcId,
4282
+ cidr: v.CidrBlock,
4283
+ isDefault: !!v.IsDefault,
4284
+ name: nameTag(v.Tags)
4285
+ }));
4286
+ }
4287
+ /** [{ id, vpcId, az, cidr, public, name }]; pass vpcId to scope. */
4288
+ async listSubnets({ vpcId, region } = {}) {
4289
+ const Filters = vpcId ? [{ Name: "vpc-id", Values: [vpcId] }] : void 0;
4290
+ const out = await this._ec2(region).send(new import_client_ec2.DescribeSubnetsCommand({ Filters }));
4291
+ return (out.Subnets ?? []).map((s) => ({
4292
+ id: s.SubnetId,
4293
+ vpcId: s.VpcId,
4294
+ az: s.AvailabilityZone,
4295
+ cidr: s.CidrBlock,
4296
+ public: !!s.MapPublicIpOnLaunch,
4297
+ name: nameTag(s.Tags)
4298
+ }));
4299
+ }
4300
+ /** [{ id, name, vpcId, description }]; pass vpcId to scope. */
4301
+ async listSecurityGroups({ vpcId, region } = {}) {
4302
+ const Filters = vpcId ? [{ Name: "vpc-id", Values: [vpcId] }] : void 0;
4303
+ const out = await this._ec2(region).send(new import_client_ec2.DescribeSecurityGroupsCommand({ Filters }));
4304
+ return (out.SecurityGroups ?? []).map((g) => ({
4305
+ id: g.GroupId,
4306
+ name: g.GroupName,
4307
+ vpcId: g.VpcId,
4308
+ description: g.Description
4309
+ }));
4310
+ }
4311
+ /** [{ name, fingerprint }] EC2 key pairs in a region. */
4312
+ async listKeyPairs(region) {
4313
+ const out = await this._ec2(region).send(new import_client_ec2.DescribeKeyPairsCommand({}));
4314
+ return (out.KeyPairs ?? []).map((k) => ({ name: k.KeyName, fingerprint: k.KeyFingerprint }));
4315
+ }
4316
+ // ── Route53 ────────────────────────────────────────────────────────────────
4317
+ /** [{ id, name, private, recordCount }] — id is the bare zone id (no /hostedzone/). */
4318
+ async listHostedZones() {
4319
+ const zones = [];
4320
+ let marker;
4321
+ do {
4322
+ const out = await this._route53().send(new import_client_route_53.ListHostedZonesCommand({ Marker: marker }));
4323
+ for (const z of out.HostedZones ?? []) {
4324
+ zones.push({
4325
+ id: (z.Id ?? "").replace("/hostedzone/", ""),
4326
+ name: (z.Name ?? "").replace(/\.$/, ""),
4327
+ // strip trailing dot
4328
+ private: !!z.Config?.PrivateZone,
4329
+ recordCount: z.ResourceRecordSetCount
4330
+ });
4331
+ }
4332
+ marker = out.IsTruncated ? out.NextMarker : void 0;
4333
+ } while (marker);
4334
+ return zones;
4335
+ }
4336
+ /**
4337
+ * Best hosted zone for a domain: exact match, else the longest zone name that
4338
+ * is a suffix of the domain (so "api.foo.com" matches the "foo.com" zone).
4339
+ * Returns the zone object or null.
4340
+ */
4341
+ async findHostedZoneForDomain(domain) {
4342
+ const target = String(domain ?? "").replace(/\.$/, "").toLowerCase();
4343
+ if (!target) return null;
4344
+ const zones = await this.listHostedZones();
4345
+ const exact = zones.find((z) => z.name.toLowerCase() === target);
4346
+ if (exact) return exact;
4347
+ return zones.filter((z) => target.endsWith(`.${z.name.toLowerCase()}`)).sort((a, b) => b.name.length - a.name.length)[0] ?? null;
4348
+ }
4349
+ // ── AMI lookup ──────────────────────────────────────────────────────────────
4350
+ /**
4351
+ * Latest Ubuntu AMI matching a name pattern in a region.
4352
+ * Returns { id, name, creationDate, architecture } or null.
4353
+ */
4354
+ async findLatestUbuntuAmi({ region, pattern = DEFAULT_UBUNTU_PATTERN, architecture = "x86_64" } = {}) {
4355
+ const out = await this._ec2(region).send(new import_client_ec2.DescribeImagesCommand({
4356
+ Owners: [CANONICAL_OWNER_ID],
4357
+ Filters: [
4358
+ { Name: "name", Values: [pattern] },
4359
+ { Name: "virtualization-type", Values: ["hvm"] },
4360
+ { Name: "state", Values: ["available"] },
4361
+ ...architecture ? [{ Name: "architecture", Values: [architecture] }] : []
4362
+ ]
4363
+ }));
4364
+ const newest = (out.Images ?? []).sort((a, b) => String(b.CreationDate).localeCompare(String(a.CreationDate)))[0];
4365
+ if (!newest) return null;
4366
+ return {
4367
+ id: newest.ImageId,
4368
+ name: newest.Name,
4369
+ creationDate: newest.CreationDate,
4370
+ architecture: newest.Architecture
4371
+ };
4372
+ }
4373
+ };
4374
+
4085
4375
  // src/logger/index.js
4086
4376
  var import_chalk = __toESM(require("chalk"), 1);
4087
4377
  var import_util = __toESM(require("util"), 1);
@@ -4454,12 +4744,1061 @@ function setupContext(opts = {}) {
4454
4744
  return setup(opts);
4455
4745
  }
4456
4746
 
4747
+ // src/deploy/service.js
4748
+ function deriveRepoDirName(repoUrl) {
4749
+ const tail = String(repoUrl ?? "").split("/").pop() ?? "repo";
4750
+ return tail.replace(/\.git$/, "") || "repo";
4751
+ }
4752
+ function defineService(service) {
4753
+ if (!service?.name) throw new Error("service manifest needs a `name`");
4754
+ if (!service.appsRoot) throw new Error(`service "${service.name}" needs an appsRoot`);
4755
+ if (!service.repoUrl) throw new Error(`service "${service.name}" needs a repoUrl`);
4756
+ if (!service.pm2?.script) throw new Error(`service "${service.name}" needs pm2.script`);
4757
+ const pm2 = {
4758
+ appName: service.name,
4759
+ args: "",
4760
+ ...service.pm2
4761
+ };
4762
+ const nginx = service.nginx ? { siteName: service.name, ...service.nginx } : null;
4763
+ return {
4764
+ repoDirName: deriveRepoDirName(service.repoUrl),
4765
+ repoSubdir: "",
4766
+ keepReleases: 3,
4767
+ testCommand: null,
4768
+ envScrubPatterns: [],
4769
+ legacyRepoEnv: null,
4770
+ buildInfoPath: "build-info.json",
4771
+ deployKey: null,
4772
+ requireEnv: false,
4773
+ ...service,
4774
+ pm2,
4775
+ nginx
4776
+ };
4777
+ }
4778
+
4779
+ // src/deploy/paths.js
4780
+ var import_node_path = require("path");
4781
+ function servicePaths(service) {
4782
+ const root = service.appsRoot;
4783
+ const repoDirName = service.repoDirName ?? "repo";
4784
+ const repoSubdir = service.repoSubdir ?? "";
4785
+ const repo = (0, import_node_path.join)(root, repoDirName);
4786
+ const repoRun = repoSubdir ? (0, import_node_path.join)(repo, repoSubdir) : repo;
4787
+ return {
4788
+ root,
4789
+ repo,
4790
+ repoRun,
4791
+ repoEnv: (0, import_node_path.join)(repoRun, ".env"),
4792
+ releases: (0, import_node_path.join)(root, "releases"),
4793
+ shared: (0, import_node_path.join)(root, "shared"),
4794
+ logs: (0, import_node_path.join)(root, "logs"),
4795
+ current: (0, import_node_path.join)(root, "current"),
4796
+ sharedEnv: (0, import_node_path.join)(root, "shared", ".env"),
4797
+ ecosystem: (0, import_node_path.join)(root, "shared", "ecosystem.config.cjs"),
4798
+ deployLog: (0, import_node_path.join)(root, "logs", "deploy.log"),
4799
+ lockHashFile: (0, import_node_path.join)(root, "shared", ".package-lock.sha256")
4800
+ };
4801
+ }
4802
+ function releaseStamp(date = /* @__PURE__ */ new Date()) {
4803
+ return date.toISOString().replace(/\.\d{3}Z$/, "Z");
4804
+ }
4805
+ function releaseDir(releasesRoot, stamp) {
4806
+ return (0, import_node_path.join)(releasesRoot, stamp);
4807
+ }
4808
+
4809
+ // src/deploy/run.js
4810
+ var import_node_child_process = require("child_process");
4811
+ function npmEnv(baseEnv = process.env) {
4812
+ const env = { ...baseEnv };
4813
+ delete env.NODE_ENV;
4814
+ return env;
4815
+ }
4816
+ function npmInstallEnv(baseEnv = process.env) {
4817
+ return {
4818
+ ...npmEnv(baseEnv),
4819
+ NPM_CONFIG_PRODUCTION: "false",
4820
+ npm_config_production: "false"
4821
+ };
4822
+ }
4823
+ function run(cmd, args, options = {}) {
4824
+ const { cwd, env, logger } = options;
4825
+ return new Promise((resolve3, reject) => {
4826
+ logger?.info?.(`$ ${cmd} ${args.join(" ")}${cwd ? ` (cwd=${cwd})` : ""}`);
4827
+ const child = (0, import_node_child_process.spawn)(cmd, args, {
4828
+ cwd,
4829
+ env: env ?? process.env,
4830
+ stdio: "inherit"
4831
+ });
4832
+ child.on("error", reject);
4833
+ child.on("close", (code) => {
4834
+ if (code === 0) resolve3();
4835
+ else reject(new Error(`${cmd} exited with code ${code}`));
4836
+ });
4837
+ });
4838
+ }
4839
+ function runShell(command, options = {}) {
4840
+ return run("bash", ["-lc", command], options);
4841
+ }
4842
+
4843
+ // src/deploy/log.js
4844
+ var import_promises = require("fs/promises");
4845
+ async function appendDeployLog(deployLogPath, message) {
4846
+ await (0, import_promises.mkdir)(deployLogPath.replace(/\/[^/]+$/, ""), { recursive: true });
4847
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
4848
+ `;
4849
+ await (0, import_promises.appendFile)(deployLogPath, line);
4850
+ }
4851
+
4852
+ // src/deploy/git.js
4853
+ var import_promises2 = require("fs/promises");
4854
+ async function pathExists(path5) {
4855
+ try {
4856
+ await (0, import_promises2.access)(path5);
4857
+ return true;
4858
+ } catch {
4859
+ return false;
4860
+ }
4861
+ }
4862
+ async function cloneRepo(service, options = {}) {
4863
+ const { dryRun = false, logger = console } = options;
4864
+ const paths = servicePaths(service);
4865
+ if (await pathExists(paths.repo)) {
4866
+ throw new Error(`Repo already exists at ${paths.repo} \u2014 use git pull instead`);
4867
+ }
4868
+ if (dryRun) {
4869
+ logger.info(`[dryRun] would git clone ${service.repoUrl} ${paths.repo}`);
4870
+ return;
4871
+ }
4872
+ await run("git", ["clone", service.repoUrl, paths.repo], { logger });
4873
+ logger.info(`cloned ${service.repoUrl} \u2192 ${paths.repo}`);
4874
+ }
4875
+ async function pullRepo(service, options = {}) {
4876
+ const { dryRun = false, logger = console } = options;
4877
+ const paths = servicePaths(service);
4878
+ if (!await pathExists(paths.repo)) {
4879
+ throw new Error(`Repo missing at ${paths.repo} \u2014 run provision first`);
4880
+ }
4881
+ if (dryRun) {
4882
+ logger.info(`[dryRun] would git -C ${paths.repo} pull --ff-only`);
4883
+ return;
4884
+ }
4885
+ await run("git", ["-C", paths.repo, "pull", "--ff-only"], { logger });
4886
+ logger.info(`pulled ${paths.repo}`);
4887
+ }
4888
+
4889
+ // src/deploy/release.js
4890
+ var import_promises3 = require("fs/promises");
4891
+ var import_node_path2 = require("path");
4892
+ var SKIP_TOP = /* @__PURE__ */ new Set(["node_modules", ".git"]);
4893
+ function shouldCopyEntry(srcPath) {
4894
+ const parts = srcPath.split(import_node_path2.sep);
4895
+ if (parts.some((p) => p === "node_modules" || p === ".git")) return false;
4896
+ const top = parts.at(-1);
4897
+ if (parts.length === 1 && SKIP_TOP.has(top)) return false;
4898
+ return true;
4899
+ }
4900
+ async function createRelease(service, options = {}) {
4901
+ const { stamp = releaseStamp(), dryRun = false, logger = console } = options;
4902
+ const paths = servicePaths(service);
4903
+ const dest = releaseDir(paths.releases, stamp);
4904
+ if (dryRun) {
4905
+ logger.info(`[dryRun] would copy ${paths.repoRun} \u2192 ${dest}`);
4906
+ return { stamp, path: dest };
4907
+ }
4908
+ await (0, import_promises3.mkdir)(paths.releases, { recursive: true });
4909
+ await (0, import_promises3.cp)(paths.repoRun, dest, {
4910
+ recursive: true,
4911
+ filter: (src) => shouldCopyEntry(src)
4912
+ });
4913
+ logger.info(`release ${stamp} created at ${dest}`);
4914
+ return { stamp, path: dest };
4915
+ }
4916
+ async function readCurrentRelease(paths) {
4917
+ try {
4918
+ const target = await (0, import_promises3.readlink)(paths.current);
4919
+ return target.startsWith("/") ? target : (0, import_node_path2.join)((0, import_node_path2.dirname)(paths.current), target);
4920
+ } catch {
4921
+ return null;
4922
+ }
4923
+ }
4924
+ async function listReleases(paths) {
4925
+ let names;
4926
+ try {
4927
+ names = await (0, import_promises3.readdir)(paths.releases);
4928
+ } catch {
4929
+ return [];
4930
+ }
4931
+ const entries = [];
4932
+ for (const name of names) {
4933
+ const full = releaseDir(paths.releases, name);
4934
+ const s = await (0, import_promises3.stat)(full);
4935
+ if (s.isDirectory()) entries.push({ name, path: full, mtime: s.mtime });
4936
+ }
4937
+ entries.sort((a, b) => b.name.localeCompare(a.name));
4938
+ return entries;
4939
+ }
4940
+
4941
+ // src/deploy/activate.js
4942
+ var import_promises4 = require("fs/promises");
4943
+ var import_node_path3 = require("path");
4944
+ async function activateRelease(releasePath, paths, options = {}) {
4945
+ const { dryRun = false, logger = console } = options;
4946
+ if (dryRun) {
4947
+ logger.info(`[dryRun] would activate ${releasePath} \u2192 ${paths.current}`);
4948
+ return;
4949
+ }
4950
+ try {
4951
+ await (0, import_promises4.unlink)(paths.current);
4952
+ } catch {
4953
+ }
4954
+ await (0, import_promises4.symlink)(releasePath, paths.current);
4955
+ const envLink = (0, import_node_path3.join)(releasePath, ".env");
4956
+ try {
4957
+ await (0, import_promises4.unlink)(envLink);
4958
+ } catch {
4959
+ }
4960
+ await (0, import_promises4.symlink)(paths.sharedEnv, envLink);
4961
+ logger.info(`active release: ${releasePath}`);
4962
+ }
4963
+
4964
+ // src/deploy/deps.js
4965
+ var import_node_crypto = require("crypto");
4966
+ var import_promises5 = require("fs/promises");
4967
+ var import_node_path4 = require("path");
4968
+ async function pathExists2(path5) {
4969
+ try {
4970
+ await (0, import_promises5.access)(path5);
4971
+ return true;
4972
+ } catch {
4973
+ return false;
4974
+ }
4975
+ }
4976
+ async function lockfileHash(releasePath) {
4977
+ const lockPath = (0, import_node_path4.join)(releasePath, "package-lock.json");
4978
+ try {
4979
+ const buf = await (0, import_promises5.readFile)(lockPath);
4980
+ return (0, import_node_crypto.createHash)("sha256").update(buf).digest("hex");
4981
+ } catch {
4982
+ return null;
4983
+ }
4984
+ }
4985
+ async function readHashFile(path5) {
4986
+ try {
4987
+ return (await (0, import_promises5.readFile)(path5, "utf8")).trim();
4988
+ } catch {
4989
+ return null;
4990
+ }
4991
+ }
4992
+ async function npmInstall(releasePath, hasLock, logger) {
4993
+ await (0, import_promises5.rm)((0, import_node_path4.join)(releasePath, "node_modules"), { recursive: true, force: true });
4994
+ const cmd = hasLock ? ["ci", "--include=dev"] : ["install", "--include=dev", "--no-audit", "--no-fund"];
4995
+ logger.info(`running npm ${cmd.join(" ")} in ${releasePath}`);
4996
+ await run("npm", cmd, { cwd: releasePath, logger, env: npmInstallEnv() });
4997
+ }
4998
+ async function installDeps(service, releasePath, paths, options = {}) {
4999
+ const { dryRun = false, logger = console } = options;
5000
+ if (dryRun) {
5001
+ logger.info(`[dryRun] would install deps in ${releasePath}`);
5002
+ return { hash: null, copied: false };
5003
+ }
5004
+ const hash = await lockfileHash(releasePath);
5005
+ const hasLock = hash !== null;
5006
+ const hashMarker = (0, import_node_path4.join)(releasePath, ".deploy-package-lock.sha256");
5007
+ const currentPath = await readCurrentRelease(paths);
5008
+ let copied = false;
5009
+ if (hasLock && currentPath && await pathExists2((0, import_node_path4.join)(currentPath, "node_modules"))) {
5010
+ const currentHash = await readHashFile((0, import_node_path4.join)(currentPath, ".deploy-package-lock.sha256"));
5011
+ if (currentHash === hash) {
5012
+ logger.info("lockfile unchanged \u2014 copying node_modules from current release");
5013
+ await (0, import_promises5.cp)((0, import_node_path4.join)(currentPath, "node_modules"), (0, import_node_path4.join)(releasePath, "node_modules"), {
5014
+ recursive: true,
5015
+ force: true
5016
+ });
5017
+ copied = true;
5018
+ }
5019
+ }
5020
+ if (!copied) {
5021
+ await npmInstall(releasePath, hasLock, logger);
5022
+ }
5023
+ if (hasLock) {
5024
+ await (0, import_promises5.writeFile)(hashMarker, `${hash}
5025
+ `, "utf8");
5026
+ await (0, import_promises5.mkdir)(paths.shared, { recursive: true });
5027
+ await (0, import_promises5.writeFile)(paths.lockHashFile, `${hash}
5028
+ `, "utf8");
5029
+ }
5030
+ return { hash, copied };
5031
+ }
5032
+
5033
+ // src/deploy/test.js
5034
+ var import_promises6 = require("fs/promises");
5035
+ var import_node_path5 = require("path");
5036
+ async function runReleaseTests(service, releasePath, paths, options = {}) {
5037
+ const { dryRun = false, logger = console } = options;
5038
+ if (!service.testCommand) {
5039
+ logger.info("no testCommand configured \u2014 skipping tests");
5040
+ return;
5041
+ }
5042
+ if (dryRun) {
5043
+ logger.info(`[dryRun] would run "${service.testCommand}" in ${releasePath}`);
5044
+ return;
5045
+ }
5046
+ const envLink = (0, import_node_path5.join)(releasePath, ".env");
5047
+ try {
5048
+ await (0, import_promises6.unlink)(envLink);
5049
+ } catch {
5050
+ }
5051
+ await (0, import_promises6.symlink)(paths.sharedEnv, envLink);
5052
+ logger.info(`running "${service.testCommand}" in ${releasePath}`);
5053
+ await runShell(service.testCommand, { cwd: releasePath, logger, env: npmInstallEnv() });
5054
+ }
5055
+
5056
+ // src/deploy/prune.js
5057
+ var import_promises7 = require("fs/promises");
5058
+ async function pruneReleases(service, paths, options = {}) {
5059
+ const { dryRun = false, logger = console } = options;
5060
+ const keep = service.keepReleases ?? 3;
5061
+ const releases = await listReleases(paths);
5062
+ let activeName = null;
5063
+ try {
5064
+ const target = await (0, import_promises7.readlink)(paths.current);
5065
+ activeName = target.split("/").pop();
5066
+ } catch {
5067
+ }
5068
+ const keepSet = /* @__PURE__ */ new Set();
5069
+ for (const rel of releases) {
5070
+ if (keepSet.size < keep) keepSet.add(rel.name);
5071
+ }
5072
+ if (activeName) keepSet.add(activeName);
5073
+ const toRemove = releases.filter((rel) => !keepSet.has(rel.name));
5074
+ for (const rel of toRemove) {
5075
+ if (dryRun) {
5076
+ logger.info(`[dryRun] would rm -rf ${rel.path}`);
5077
+ } else {
5078
+ await (0, import_promises7.rm)(rel.path, { recursive: true, force: true });
5079
+ logger.info(`pruned ${rel.path}`);
5080
+ }
5081
+ }
5082
+ return { removed: toRemove.map((r) => r.name) };
5083
+ }
5084
+
5085
+ // src/deploy/pm2.js
5086
+ async function reloadPm2(paths, options = {}) {
5087
+ const { dryRun = false, logger = console } = options;
5088
+ if (dryRun) {
5089
+ logger.info(`[dryRun] would pm2 startOrReload ${paths.ecosystem} --update-env`);
5090
+ return;
5091
+ }
5092
+ await runShell(`pm2 startOrReload "${paths.ecosystem}" --update-env`, { logger });
5093
+ logger.info("pm2 reloaded");
5094
+ }
5095
+
5096
+ // src/deploy/nginx.js
5097
+ var import_promises8 = require("fs/promises");
5098
+ var import_node_os = require("os");
5099
+ var import_node_path6 = require("path");
5100
+ async function hasTlsCert(certPath) {
5101
+ try {
5102
+ await runShell(`sudo test -f '${certPath}'`, {});
5103
+ return true;
5104
+ } catch {
5105
+ return false;
5106
+ }
5107
+ }
5108
+ function proxyBlock(port) {
5109
+ return ` location / {
5110
+ proxy_pass http://127.0.0.1:${port};
5111
+ proxy_http_version 1.1;
5112
+ proxy_set_header Host $host;
5113
+ proxy_set_header X-Real-IP $remote_addr;
5114
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
5115
+ proxy_set_header X-Forwarded-Proto $scheme;
5116
+ proxy_read_timeout 120s;
5117
+ }`;
5118
+ }
5119
+ function buildNginxConfig(service) {
5120
+ const { nginx, pm2 } = service;
5121
+ const certDir = `/etc/letsencrypt/live/${nginx.fqdn}`;
5122
+ return `# ${nginx.siteName} \u2014 managed by cli-toolkit deploy (proxy mode)
5123
+ # ${nginx.fqdn} \u2192 127.0.0.1:${pm2.port}
5124
+
5125
+ server {
5126
+ listen 80;
5127
+ listen [::]:80;
5128
+ server_name ${nginx.fqdn};
5129
+ location / { return 301 https://$host$request_uri; }
5130
+ }
5131
+
5132
+ server {
5133
+ listen 443 ssl;
5134
+ listen [::]:443 ssl;
5135
+ server_name ${nginx.fqdn};
5136
+
5137
+ ssl_certificate ${certDir}/fullchain.pem;
5138
+ ssl_certificate_key ${certDir}/privkey.pem;
5139
+ include /etc/letsencrypt/options-ssl-nginx.conf;
5140
+ ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
5141
+
5142
+ client_max_body_size 25m;
5143
+
5144
+ ${proxyBlock(pm2.port)}
5145
+ }
5146
+ `;
5147
+ }
5148
+ function buildNginxConfigHttpOnly(service) {
5149
+ const { nginx, pm2 } = service;
5150
+ return `# ${nginx.siteName} \u2014 managed by cli-toolkit deploy (HTTP proxy, no TLS cert yet)
5151
+
5152
+ server {
5153
+ listen 80;
5154
+ listen [::]:80;
5155
+ server_name ${nginx.fqdn};
5156
+
5157
+ client_max_body_size 25m;
5158
+
5159
+ ${proxyBlock(pm2.port)}
5160
+ }
5161
+ `;
5162
+ }
5163
+ async function enableNginxUpstream(service, options = {}) {
5164
+ const { dryRun = false, logger = console } = options;
5165
+ const { nginx } = service;
5166
+ if (!nginx) {
5167
+ logger.info("no nginx config on service \u2014 skipping nginx step");
5168
+ return { skipped: true };
5169
+ }
5170
+ const siteAvailable = `/etc/nginx/sites-available/${nginx.siteName}`;
5171
+ const siteEnabled = `/etc/nginx/sites-enabled/${nginx.siteName}`;
5172
+ const cert = `/etc/letsencrypt/live/${nginx.fqdn}/fullchain.pem`;
5173
+ if (dryRun) {
5174
+ logger.info(`[dryRun] would write ${siteAvailable} (proxy \u2192 127.0.0.1:${service.pm2.port}) and reload nginx`);
5175
+ return { hasCert: null };
5176
+ }
5177
+ const hasCert = await hasTlsCert(cert);
5178
+ const config2 = hasCert ? buildNginxConfig(service) : buildNginxConfigHttpOnly(service);
5179
+ const tmp = (0, import_node_path6.join)((0, import_node_os.tmpdir)(), `${service.name}-nginx.conf`);
5180
+ await (0, import_promises8.writeFile)(tmp, config2);
5181
+ await runShell(
5182
+ `sudo cp '${tmp}' '${siteAvailable}' && sudo ln -sf '${siteAvailable}' '${siteEnabled}' && sudo nginx -t && sudo systemctl reload nginx`,
5183
+ { logger }
5184
+ );
5185
+ logger.info(`nginx upstream enabled for ${nginx.fqdn} \u2192 127.0.0.1:${service.pm2.port} (tls=${hasCert})`);
5186
+ return { hasCert };
5187
+ }
5188
+
5189
+ // src/deploy/sync-env.js
5190
+ var import_promises9 = require("fs/promises");
5191
+ function scrubEnvContent(content, patterns = []) {
5192
+ const regexes = patterns.map((p) => p instanceof RegExp ? p : new RegExp(p));
5193
+ if (regexes.length === 0) return content;
5194
+ return content.split("\n").filter((line) => !regexes.some((re) => re.test(line.trim()))).join("\n");
5195
+ }
5196
+ async function pathExists3(path5) {
5197
+ try {
5198
+ await (0, import_promises9.access)(path5);
5199
+ return true;
5200
+ } catch {
5201
+ return false;
5202
+ }
5203
+ }
5204
+ async function syncEnv(service, options = {}) {
5205
+ const { dryRun = false, logger = console } = options;
5206
+ const paths = servicePaths(service);
5207
+ const patterns = service.envScrubPatterns ?? [];
5208
+ if (dryRun) {
5209
+ logger.info(`[dryRun] would sync ${paths.repoEnv} \u2192 ${paths.sharedEnv}`);
5210
+ return { source: paths.repoEnv, dest: paths.sharedEnv };
5211
+ }
5212
+ let source = paths.repoEnv;
5213
+ if (!await pathExists3(source) && service.legacyRepoEnv && await pathExists3(service.legacyRepoEnv)) {
5214
+ logger.info(`using legacy env: ${service.legacyRepoEnv}`);
5215
+ source = service.legacyRepoEnv;
5216
+ }
5217
+ await (0, import_promises9.mkdir)(paths.shared, { recursive: true });
5218
+ if (!await pathExists3(source)) {
5219
+ if (service.requireEnv) {
5220
+ throw new Error(
5221
+ `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)"
5222
+ );
5223
+ }
5224
+ if (!await pathExists3(paths.sharedEnv)) {
5225
+ await (0, import_promises9.writeFile)(paths.sharedEnv, "", { mode: 384 });
5226
+ }
5227
+ logger.warn(`no .env found (source ${source}) \u2014 using empty ${paths.sharedEnv}`);
5228
+ return { source: null, dest: paths.sharedEnv };
5229
+ }
5230
+ const raw = await (0, import_promises9.readFile)(source, "utf8");
5231
+ await (0, import_promises9.writeFile)(paths.sharedEnv, scrubEnvContent(raw, patterns), { mode: 384 });
5232
+ logger.info(`synced ${source} \u2192 ${paths.sharedEnv}`);
5233
+ return { source, dest: paths.sharedEnv };
5234
+ }
5235
+
5236
+ // src/deploy/build-info.js
5237
+ var import_promises10 = require("fs/promises");
5238
+ var import_node_path7 = require("path");
5239
+ var import_node_child_process2 = require("child_process");
5240
+ var import_node_util = require("util");
5241
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
5242
+ function bumpPatchVersion(version) {
5243
+ const parts = String(version).trim().split(".");
5244
+ const major = Number.parseInt(parts[0], 10) || 0;
5245
+ const minor = Number.parseInt(parts[1], 10) || 0;
5246
+ const patch = Number.parseInt(parts[2], 10) || 0;
5247
+ return `${major}.${minor}.${patch + 1}`;
5248
+ }
5249
+ async function readJson(path5) {
5250
+ try {
5251
+ return JSON.parse(await (0, import_promises10.readFile)(path5, "utf8"));
5252
+ } catch {
5253
+ return null;
5254
+ }
5255
+ }
5256
+ async function gitShortCommit(repoPath) {
5257
+ try {
5258
+ const { stdout } = await execFileAsync("git", ["-C", repoPath, "rev-parse", "--short", "HEAD"], {
5259
+ encoding: "utf8"
5260
+ });
5261
+ return stdout.trim() || null;
5262
+ } catch {
5263
+ return null;
5264
+ }
5265
+ }
5266
+ async function resolveNextVersion(service, paths, pkgPath) {
5267
+ const pkg = await readJson(pkgPath) ?? {};
5268
+ const baseVersion = pkg.version || "0.1.0";
5269
+ const currentPath = await readCurrentRelease(paths);
5270
+ if (currentPath) {
5271
+ const active = await readJson((0, import_node_path7.join)(currentPath, service.buildInfoPath));
5272
+ if (active?.version) return bumpPatchVersion(active.version);
5273
+ }
5274
+ return baseVersion;
5275
+ }
5276
+ async function readReleaseBuildInfo(service, releasePath) {
5277
+ return readJson((0, import_node_path7.join)(releasePath, service.buildInfoPath));
5278
+ }
5279
+ async function writeReleaseBuildInfo(service, releasePath, { stamp, dryRun = false, logger = console }) {
5280
+ const paths = servicePaths(service);
5281
+ const pkgPath = (0, import_node_path7.join)(paths.repoRun, "package.json");
5282
+ const version = await resolveNextVersion(service, paths, pkgPath);
5283
+ const gitCommit = await gitShortCommit(paths.repo);
5284
+ const buildInfo = {
5285
+ version,
5286
+ release: stamp,
5287
+ deployedAt: (/* @__PURE__ */ new Date()).toISOString(),
5288
+ gitCommit,
5289
+ service: service.name
5290
+ };
5291
+ const dest = (0, import_node_path7.join)(releasePath, service.buildInfoPath);
5292
+ if (dryRun) {
5293
+ logger.info(`[dryRun] would write ${dest} (${JSON.stringify(buildInfo)})`);
5294
+ return buildInfo;
5295
+ }
5296
+ await (0, import_promises10.mkdir)((0, import_node_path7.dirname)(dest), { recursive: true });
5297
+ await (0, import_promises10.writeFile)(dest, `${JSON.stringify(buildInfo, null, 2)}
5298
+ `, { mode: 420 });
5299
+ const gitSuffix = gitCommit ? ` git=${gitCommit}` : "";
5300
+ logger.info(`build info: v${version} release=${stamp}${gitSuffix}`);
5301
+ return buildInfo;
5302
+ }
5303
+
5304
+ // src/deploy/init-structure.js
5305
+ var import_promises11 = require("fs/promises");
5306
+ var import_node_path8 = require("path");
5307
+ async function pathExists4(path5) {
5308
+ try {
5309
+ await (0, import_promises11.access)(path5);
5310
+ return true;
5311
+ } catch {
5312
+ return false;
5313
+ }
5314
+ }
5315
+ async function requireDeployRoot(parentDir, serviceRoot) {
5316
+ if (await pathExists4(parentDir)) return;
5317
+ throw new Error(
5318
+ `Cannot create ${serviceRoot}: parent directory ${parentDir} does not exist.
5319
+ This is meant to run on the target host (where ${parentDir} exists). For a local dry run use --appsRoot=/tmp/<service>.`
5320
+ );
5321
+ }
5322
+ function buildEcosystemConfig(service, paths) {
5323
+ const { pm2 } = service;
5324
+ const outLog = (0, import_node_path8.join)(paths.logs, `${pm2.appName}.out.log`);
5325
+ const errLog = (0, import_node_path8.join)(paths.logs, `${pm2.appName}.err.log`);
5326
+ return `/**
5327
+ * pm2 ecosystem for ${service.name} \u2014 seeded by cli-toolkit deploy (init).
5328
+ *
5329
+ * cwd points at the \`current\` symlink (created on first deploy).
5330
+ * Tweak \`args\` here, then: pm2 reload ${paths.ecosystem} --update-env
5331
+ */
5332
+ module.exports = {
5333
+ apps: [
5334
+ {
5335
+ name: "${pm2.appName}",
5336
+ script: "${pm2.script}",
5337
+ cwd: "${paths.current}",
5338
+ args: "${pm2.args}",
5339
+ instances: 1,
5340
+ exec_mode: "fork",
5341
+ autorestart: true,
5342
+ min_uptime: "10s",
5343
+ max_restarts: 10,
5344
+ restart_delay: 2000,
5345
+ max_memory_restart: "1500M",
5346
+ out_file: "${outLog}",
5347
+ error_file: "${errLog}",
5348
+ merge_logs: true,
5349
+ time: true,
5350
+ env: {
5351
+ NODE_ENV: "production",
5352
+ },
5353
+ },
5354
+ ],
5355
+ };
5356
+ `;
5357
+ }
5358
+ async function initServiceStructure(service, options = {}) {
5359
+ const { dryRun = false, logger = console } = options;
5360
+ const paths = servicePaths(service);
5361
+ const dirs = [paths.releases, paths.shared, paths.logs];
5362
+ const created = [];
5363
+ const skipped = [];
5364
+ if (!dryRun) {
5365
+ await requireDeployRoot((0, import_node_path8.dirname)(paths.root), paths.root);
5366
+ }
5367
+ for (const dir of dirs) {
5368
+ if (await pathExists4(dir)) {
5369
+ skipped.push(dir);
5370
+ continue;
5371
+ }
5372
+ if (!dryRun) await (0, import_promises11.mkdir)(dir, { recursive: true });
5373
+ created.push(dir);
5374
+ }
5375
+ let ecosystemCreated = false;
5376
+ if (await pathExists4(paths.ecosystem)) {
5377
+ skipped.push(paths.ecosystem);
5378
+ } else {
5379
+ if (!dryRun) {
5380
+ await (0, import_promises11.writeFile)(paths.ecosystem, buildEcosystemConfig(service, paths), { mode: 420 });
5381
+ }
5382
+ ecosystemCreated = true;
5383
+ created.push(paths.ecosystem);
5384
+ }
5385
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] init-structure service=${service.name} dryRun=${dryRun} created=${created.length} skipped=${skipped.length}
5386
+ `;
5387
+ if (!dryRun) {
5388
+ await (0, import_promises11.mkdir)(paths.logs, { recursive: true });
5389
+ await (0, import_promises11.appendFile)(paths.deployLog, line);
5390
+ }
5391
+ logger.info(`service=${service.name} appsRoot=${paths.root}`);
5392
+ logger.info(`created: ${created.length ? created.join(", ") : "(none)"}`);
5393
+ logger.info(`already present: ${skipped.length ? skipped.join(", ") : "(none)"}`);
5394
+ if (ecosystemCreated) logger.info(`seeded ${paths.ecosystem}`);
5395
+ return { paths, created, skipped, ecosystemCreated };
5396
+ }
5397
+
5398
+ // src/deploy/bootstrap-host.js
5399
+ var import_node_child_process3 = require("child_process");
5400
+ var import_promises12 = require("fs/promises");
5401
+ var import_node_os2 = require("os");
5402
+ var import_node_path9 = require("path");
5403
+ async function pathExists5(path5) {
5404
+ try {
5405
+ await (0, import_promises12.access)(path5);
5406
+ return true;
5407
+ } catch {
5408
+ return false;
5409
+ }
5410
+ }
5411
+ function expandHome(path5) {
5412
+ return path5.startsWith("~/") ? (0, import_node_path9.join)((0, import_node_os2.homedir)(), path5.slice(2)) : path5;
5413
+ }
5414
+ async function ensurePm2Startup(options = {}) {
5415
+ const { user = "ubuntu", dryRun = false, logger = console } = options;
5416
+ if (dryRun) {
5417
+ logger.info("[dryRun] would configure pm2 startup systemd");
5418
+ return;
5419
+ }
5420
+ try {
5421
+ (0, import_node_child_process3.execSync)("pm2 ping", { stdio: "ignore" });
5422
+ } catch {
5423
+ }
5424
+ try {
5425
+ const out = (0, import_node_child_process3.execSync)(`pm2 startup systemd -u ${user} --hp /home/${user}`, { encoding: "utf8" });
5426
+ const sudoLine = out.split("\n").find((l) => l.trim().startsWith("sudo"));
5427
+ if (sudoLine) {
5428
+ (0, import_node_child_process3.execSync)(sudoLine.trim(), { stdio: "inherit" });
5429
+ logger.info("pm2 startup systemd configured");
5430
+ }
5431
+ } catch (err) {
5432
+ logger.warn(`pm2 startup skipped or already configured: ${err.message}`);
5433
+ }
5434
+ }
5435
+ async function installDeployKey(deployKeyPath, options = {}) {
5436
+ const { dryRun = false, logger = console } = options;
5437
+ const keyPath = expandHome(deployKeyPath);
5438
+ if (!await pathExists5(keyPath)) {
5439
+ logger.warn(`deploy key not found at ${keyPath} \u2014 skipping git ssh setup`);
5440
+ return;
5441
+ }
5442
+ const keyBase = (0, import_node_path9.basename)(keyPath);
5443
+ const sshDir = (0, import_node_path9.join)((0, import_node_os2.homedir)(), ".ssh");
5444
+ const destKey = (0, import_node_path9.join)(sshDir, keyBase);
5445
+ const configPath = (0, import_node_path9.join)(sshDir, "config");
5446
+ const block = `
5447
+ Host github.com
5448
+ HostName github.com
5449
+ User git
5450
+ IdentityFile ${destKey}
5451
+ IdentitiesOnly yes
5452
+ `;
5453
+ if (dryRun) {
5454
+ logger.info(`[dryRun] would install deploy key ${keyPath} \u2192 ${destKey}`);
5455
+ return;
5456
+ }
5457
+ await (0, import_promises12.mkdir)(sshDir, { recursive: true, mode: 448 });
5458
+ await (0, import_promises12.copyFile)(keyPath, destKey);
5459
+ await (0, import_promises12.chmod)(destKey, 384);
5460
+ let config2 = "";
5461
+ if (await pathExists5(configPath)) config2 = await (0, import_promises12.readFile)(configPath, "utf8");
5462
+ if (!config2.includes("Host github.com")) {
5463
+ await (0, import_promises12.writeFile)(configPath, `${config2.trimEnd()}
5464
+ ${block}
5465
+ `, { mode: 384 });
5466
+ logger.info("updated ~/.ssh/config for github.com");
5467
+ }
5468
+ logger.info(`deploy key installed at ${destKey}`);
5469
+ }
5470
+ async function installLogrotate(service, options = {}) {
5471
+ const { dryRun = false, logger = console } = options;
5472
+ const conf = `/etc/logrotate.d/${service.name}`;
5473
+ const body = `${service.appsRoot}/logs/*.log {
5474
+ daily
5475
+ rotate 14
5476
+ compress
5477
+ delaycompress
5478
+ missingok
5479
+ notifempty
5480
+ copytruncate
5481
+ }
5482
+ `;
5483
+ if (dryRun) {
5484
+ logger.info(`[dryRun] would write ${conf}`);
5485
+ return;
5486
+ }
5487
+ const tmp = (0, import_node_path9.join)("/tmp", `${service.name}-logrotate.conf`);
5488
+ await (0, import_promises12.writeFile)(tmp, body);
5489
+ await runShell(`sudo cp '${tmp}' '${conf}'`, { logger });
5490
+ logger.info(`logrotate config written: ${conf}`);
5491
+ }
5492
+ async function bootstrapHost(service, options = {}) {
5493
+ const { deployKey = service.deployKey, user = "ubuntu", dryRun = false, logger = console } = options;
5494
+ await ensurePm2Startup({ user, dryRun, logger });
5495
+ if (deployKey) await installDeployKey(deployKey, { dryRun, logger });
5496
+ await installLogrotate(service, { dryRun, logger });
5497
+ logger.info("bootstrap-host complete");
5498
+ }
5499
+
5500
+ // src/deploy/deploy-service.js
5501
+ async function deployService(service, options = {}) {
5502
+ const {
5503
+ dryRun = false,
5504
+ skipPull = false,
5505
+ skipTests = false,
5506
+ skipNginx = false,
5507
+ logger = console
5508
+ } = options;
5509
+ const paths = servicePaths(service);
5510
+ await initServiceStructure(service, { dryRun, logger });
5511
+ if (!skipPull) await pullRepo(service, { dryRun, logger });
5512
+ await syncEnv(service, { dryRun, logger });
5513
+ const { stamp, path: releasePath } = await createRelease(service, { dryRun, logger });
5514
+ await writeReleaseBuildInfo(service, releasePath, { stamp, dryRun, logger });
5515
+ await installDeps(service, releasePath, paths, { dryRun, logger });
5516
+ if (!skipTests) await runReleaseTests(service, releasePath, paths, { dryRun, logger });
5517
+ await activateRelease(releasePath, paths, { dryRun, logger });
5518
+ await pruneReleases(service, paths, { dryRun, logger });
5519
+ await reloadPm2(paths, { dryRun, logger });
5520
+ if (!skipNginx) await enableNginxUpstream(service, { dryRun, logger });
5521
+ const summary = `deploy complete stamp=${stamp} dryRun=${dryRun}`;
5522
+ logger.info(summary);
5523
+ if (!dryRun) await appendDeployLog(paths.deployLog, summary);
5524
+ return { stamp, releasePath };
5525
+ }
5526
+
5527
+ // src/deploy/provision-service.js
5528
+ var import_promises13 = require("fs/promises");
5529
+ async function pathExists6(path5) {
5530
+ try {
5531
+ await (0, import_promises13.access)(path5);
5532
+ return true;
5533
+ } catch {
5534
+ return false;
5535
+ }
5536
+ }
5537
+ async function provisionService(service, options = {}) {
5538
+ const {
5539
+ dryRun = false,
5540
+ deploy = true,
5541
+ skipBootstrap = true,
5542
+ deployKey,
5543
+ logger = console
5544
+ } = options;
5545
+ if (!skipBootstrap) {
5546
+ await bootstrapHost(service, { deployKey, dryRun, logger });
5547
+ }
5548
+ await initServiceStructure(service, { dryRun, logger });
5549
+ const paths = servicePaths(service);
5550
+ if (await pathExists6(paths.repo)) {
5551
+ logger.info(`repo exists at ${paths.repo} \u2014 pulling`);
5552
+ await pullRepo(service, { dryRun, logger });
5553
+ } else {
5554
+ await cloneRepo(service, { dryRun, logger });
5555
+ }
5556
+ await syncEnv(service, { dryRun, logger });
5557
+ if (deploy) {
5558
+ await deployService(service, { dryRun, logger, skipPull: true });
5559
+ } else {
5560
+ logger.info("provision complete (deploy skipped)");
5561
+ }
5562
+ }
5563
+
5564
+ // src/deploy/rollback-service.js
5565
+ var import_promises14 = require("fs/promises");
5566
+ async function rollbackService(service, options = {}) {
5567
+ const { release: targetName, dryRun = false, logger = console } = options;
5568
+ const paths = servicePaths(service);
5569
+ const releases = await listReleases(paths);
5570
+ if (releases.length === 0) throw new Error("No releases to roll back to");
5571
+ let activeName = null;
5572
+ try {
5573
+ const target = await (0, import_promises14.readlink)(paths.current);
5574
+ activeName = target.split("/").pop();
5575
+ } catch {
5576
+ throw new Error("No active release (current symlink missing)");
5577
+ }
5578
+ let rollbackTarget;
5579
+ if (targetName) {
5580
+ rollbackTarget = releases.find((r) => r.name === targetName);
5581
+ if (!rollbackTarget) throw new Error(`Release not found: ${targetName}`);
5582
+ } else {
5583
+ rollbackTarget = releases.find((r) => r.name !== activeName);
5584
+ if (!rollbackTarget) throw new Error("No previous release to roll back to");
5585
+ }
5586
+ if (rollbackTarget.name === activeName) throw new Error(`Already on release ${activeName}`);
5587
+ logger.info(`rollback ${activeName} \u2192 ${rollbackTarget.name}`);
5588
+ const buildInfo = await readReleaseBuildInfo(service, rollbackTarget.path);
5589
+ if (buildInfo?.version) {
5590
+ logger.info(`rollback target: v${buildInfo.version} release=${buildInfo.release ?? rollbackTarget.name}`);
5591
+ }
5592
+ await activateRelease(rollbackTarget.path, paths, { dryRun, logger });
5593
+ await reloadPm2(paths, { dryRun, logger });
5594
+ const summary = `rollback ${activeName} \u2192 ${rollbackTarget.name} dryRun=${dryRun}`;
5595
+ if (!dryRun) await appendDeployLog(paths.deployLog, summary);
5596
+ return { from: activeName, to: rollbackTarget.name, path: rollbackTarget.path };
5597
+ }
5598
+
5599
+ // src/deploy/ssh-remote.js
5600
+ var import_promises15 = require("fs/promises");
5601
+ var import_node_os3 = require("os");
5602
+ var import_node_path10 = require("path");
5603
+ var import_node_child_process4 = require("child_process");
5604
+ var REMOTE_CLI_REL = "node_modules/@nmakarov/cli-toolkit/scripts/deploy/cli.js";
5605
+ async function pathExists7(path5) {
5606
+ try {
5607
+ await (0, import_promises15.access)(path5);
5608
+ return true;
5609
+ } catch {
5610
+ return false;
5611
+ }
5612
+ }
5613
+ function expandHome2(path5) {
5614
+ return path5.startsWith("~/") ? (0, import_node_path10.join)((0, import_node_os3.homedir)(), path5.slice(2)) : path5;
5615
+ }
5616
+ function resolveLocalEnvPath(envFile) {
5617
+ if (envFile) {
5618
+ const expanded = expandHome2(envFile);
5619
+ return expanded.startsWith("/") ? expanded : (0, import_node_path10.join)(process.cwd(), expanded);
5620
+ }
5621
+ return (0, import_node_path10.join)(process.cwd(), ".env");
5622
+ }
5623
+ function parseGitHost(repoUrl) {
5624
+ const u = String(repoUrl ?? "");
5625
+ let m = u.match(/^[^@]+@([^:]+):/);
5626
+ if (m) return m[1];
5627
+ m = u.match(/^ssh:\/\/[^@]+@([^/:]+)/);
5628
+ if (m) return m[1];
5629
+ return null;
5630
+ }
5631
+ function shellQuote(value) {
5632
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
5633
+ }
5634
+ function sshRun(host, remoteCommand, options = {}) {
5635
+ const { logger } = options;
5636
+ return new Promise((resolve3, reject) => {
5637
+ logger?.info?.(`ssh ${host} ${remoteCommand.slice(0, 120)}${remoteCommand.length > 120 ? "\u2026" : ""}`);
5638
+ const child = (0, import_node_child_process4.spawn)("ssh", [host, remoteCommand], { stdio: "inherit" });
5639
+ child.on("error", reject);
5640
+ child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`ssh ${host} exited with code ${code}`)));
5641
+ });
5642
+ }
5643
+ function scp(localPath, remoteSpec) {
5644
+ return new Promise((resolve3, reject) => {
5645
+ const child = (0, import_node_child_process4.spawn)("scp", [localPath, remoteSpec], { stdio: "inherit" });
5646
+ child.on("error", reject);
5647
+ child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`scp exited with code ${code}`)));
5648
+ });
5649
+ }
5650
+ async function ensureDeployKeyOnRemote(host, deployKeyPath, options = {}) {
5651
+ const { logger = console } = options;
5652
+ if (!deployKeyPath) return false;
5653
+ const localPath = expandHome2(deployKeyPath);
5654
+ if (!await pathExists7(localPath)) return false;
5655
+ const keyBase = (0, import_node_path10.basename)(localPath);
5656
+ logger.info(`copying deploy key ${localPath} \u2192 ${host}:~/.ssh/${keyBase}`);
5657
+ await sshRun(host, "mkdir -p ~/.ssh && chmod 700 ~/.ssh", { logger });
5658
+ await scp(localPath, `${host}:.ssh/${keyBase}`, { logger });
5659
+ await sshRun(host, `chmod 600 ~/.ssh/${keyBase}`, { logger });
5660
+ return true;
5661
+ }
5662
+ async function prepareGitHost(host, options = {}) {
5663
+ const { logger = console, gitHost, keyBasename } = options;
5664
+ if (!gitHost) return;
5665
+ const configBlock = keyBasename ? `if ! grep -q 'Host ${gitHost}' ~/.ssh/config 2>/dev/null; then
5666
+ printf '%s\\n' '' 'Host ${gitHost}' ' HostName ${gitHost}' ' User git' ' IdentityFile ~/.ssh/${keyBasename}' ' IdentitiesOnly yes' >> ~/.ssh/config
5667
+ chmod 600 ~/.ssh/config
5668
+ echo "configured ~/.ssh/config for ${gitHost}"
5669
+ fi` : `:`;
5670
+ const script = `
5671
+ set -euo pipefail
5672
+ mkdir -p ~/.ssh
5673
+ chmod 700 ~/.ssh
5674
+ if ! grep -q '^${gitHost}' ~/.ssh/known_hosts 2>/dev/null; then
5675
+ ssh-keyscan -t ed25519,rsa ${gitHost} >> ~/.ssh/known_hosts 2>/dev/null
5676
+ echo "added ${gitHost} to known_hosts"
5677
+ fi
5678
+ ${configBlock}
5679
+ `.trim();
5680
+ await sshRun(host, script, { logger });
5681
+ }
5682
+ async function ensureRepoDependencies(host, service, options = {}) {
5683
+ const { logger = console } = options;
5684
+ const paths = servicePaths(service);
5685
+ const run2 = shellQuote(paths.repoRun);
5686
+ await sshRun(
5687
+ host,
5688
+ `cd ${run2} && if [ ! -d node_modules/@nmakarov/cli-toolkit ] || [ package-lock.json -nt node_modules/.package-lock.json ]; then npm ci; fi`,
5689
+ { logger }
5690
+ );
5691
+ }
5692
+ async function ensureEnvOnRemote(host, service, options = {}) {
5693
+ const { logger = console, envFile } = options;
5694
+ const paths = servicePaths(service);
5695
+ const localPath = resolveLocalEnvPath(envFile);
5696
+ if (!await pathExists7(localPath)) return false;
5697
+ logger.info(`copying .env ${localPath} \u2192 ${host}:${paths.repoEnv}`);
5698
+ await sshRun(host, `mkdir -p ${shellQuote((0, import_node_path10.dirname)(paths.repoEnv))}`, { logger });
5699
+ const scrubbed = scrubEnvContent(await (0, import_promises15.readFile)(localPath, "utf8"), service.envScrubPatterns ?? []);
5700
+ const tmp = (0, import_node_path10.join)((0, import_node_os3.tmpdir)(), `deploy-env-${Date.now()}`);
5701
+ await (0, import_promises15.writeFile)(tmp, scrubbed, { mode: 384 });
5702
+ await scp(tmp, `${host}:${paths.repoEnv}`, { logger });
5703
+ await sshRun(host, `chmod 600 ${shellQuote(paths.repoEnv)}`, { logger });
5704
+ return true;
5705
+ }
5706
+ async function ensureRemoteRepo(host, service, options = {}) {
5707
+ const { logger = console, deployKey = service.deployKey, envFile } = options;
5708
+ const paths = servicePaths(service);
5709
+ const repo = shellQuote(paths.repo);
5710
+ const repoUrl = shellQuote(service.repoUrl);
5711
+ const root = shellQuote(paths.root);
5712
+ const localKeyBase = deployKey ? (0, import_node_path10.basename)(expandHome2(deployKey)) : null;
5713
+ await ensureDeployKeyOnRemote(host, deployKey, { logger });
5714
+ await prepareGitHost(host, { logger, gitHost: parseGitHost(service.repoUrl), keyBasename: localKeyBase });
5715
+ await sshRun(
5716
+ host,
5717
+ `mkdir -p ${root} && if [ -d ${repo}/.git ]; then git -C ${repo} pull --ff-only; else git clone ${repoUrl} ${repo}; fi`,
5718
+ { logger }
5719
+ );
5720
+ await ensureRepoDependencies(host, service, { logger });
5721
+ await ensureEnvOnRemote(host, service, { logger, envFile });
5722
+ }
5723
+ async function runRemoteCli(host, service, command, args = [], options = {}) {
5724
+ const { logger = console, manifests, skipPull = false, deployKey = service.deployKey, envFile } = options;
5725
+ const paths = servicePaths(service);
5726
+ const run2 = shellQuote(paths.repoRun);
5727
+ const cli = shellQuote(REMOTE_CLI_REL);
5728
+ if (!skipPull) {
5729
+ await ensureRemoteRepo(host, service, { logger, deployKey, envFile });
5730
+ } else {
5731
+ await ensureRepoDependencies(host, service, { logger });
5732
+ await ensureEnvOnRemote(host, service, { logger, envFile });
5733
+ }
5734
+ const passthrough = [
5735
+ `--service=${service.name}`,
5736
+ ...manifests ? [`--manifests=${manifests}`] : [],
5737
+ ...args
5738
+ ].map(shellQuote).join(" ");
5739
+ await sshRun(host, `cd ${run2} && node ${cli} ${shellQuote(command)} ${passthrough}`.trim(), { logger });
5740
+ }
5741
+ async function runRemoteStatus(host, service, options = {}) {
5742
+ const { logger = console } = options;
5743
+ const paths = servicePaths(service);
5744
+ const port = service.pm2.port;
5745
+ const app = service.pm2.appName;
5746
+ const script = `
5747
+ echo "=== apps root ==="
5748
+ ls -la ${shellQuote(paths.root)} 2>/dev/null || echo "(missing)"
5749
+ echo ""
5750
+ echo "=== current ==="
5751
+ readlink ${shellQuote(paths.current)} 2>/dev/null || echo "(not set)"
5752
+ echo ""
5753
+ echo "=== releases ==="
5754
+ ls -1 ${shellQuote(paths.releases)} 2>/dev/null || echo "(none)"
5755
+ echo ""
5756
+ echo "=== pm2 ==="
5757
+ pm2 describe ${app} 2>/dev/null | head -20 || pm2 status ${app} 2>/dev/null || echo "(not running)"
5758
+ ${port ? `echo ""
5759
+ echo "=== app health (localhost) ==="
5760
+ curl -sf http://127.0.0.1:${port}/healthz 2>/dev/null || echo "(no /healthz on :${port})"` : ""}
5761
+ `.trim();
5762
+ await sshRun(host, script, { logger });
5763
+ }
5764
+
5765
+ // src/deploy/manifests.js
5766
+ var import_node_path11 = require("path");
5767
+ var import_node_url = require("url");
5768
+ async function loadServices({ manifests = "deploy/services.js", cwd = process.cwd() } = {}) {
5769
+ const abs = (0, import_node_path11.isAbsolute)(manifests) ? manifests : (0, import_node_path11.resolve)(cwd, manifests);
5770
+ let mod;
5771
+ try {
5772
+ mod = await import((0, import_node_url.pathToFileURL)(abs).href);
5773
+ } catch (err) {
5774
+ throw new Error(`Could not load deploy manifests from ${abs}: ${err.message}`);
5775
+ }
5776
+ const raw = mod.services ?? mod.default;
5777
+ if (!raw) {
5778
+ throw new Error(`Manifests module ${abs} must export \`services\` (or default): a map or array of service manifests`);
5779
+ }
5780
+ const list = Array.isArray(raw) ? raw : Object.values(raw);
5781
+ const out = {};
5782
+ for (const entry of list) {
5783
+ const svc = defineService(entry);
5784
+ out[svc.name] = svc;
5785
+ }
5786
+ return out;
5787
+ }
5788
+ function resolveServiceFrom(serviceMap, name, { appsRoot } = {}) {
5789
+ const svc = serviceMap[name];
5790
+ if (!svc) {
5791
+ throw new Error(`Unknown service "${name}". Known: ${Object.keys(serviceMap).join(", ") || "(none)"}`);
5792
+ }
5793
+ return appsRoot ? { ...svc, appsRoot } : svc;
5794
+ }
5795
+
4457
5796
  // src/tasks/index.js
4458
- var import_node_os3 = __toESM(require("os"), 1);
5797
+ var import_node_os6 = __toESM(require("os"), 1);
4459
5798
 
4460
5799
  // src/utils/core-utils.js
4461
5800
  function sleepMs(ms) {
4462
- return new Promise((resolve2) => setTimeout(resolve2, ms));
5801
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4463
5802
  }
4464
5803
  function toJsonColumn(value) {
4465
5804
  if (value === void 0 || value === null) return null;
@@ -4467,10 +5806,10 @@ function toJsonColumn(value) {
4467
5806
  }
4468
5807
 
4469
5808
  // src/tasks/servicesRegistry.js
4470
- var import_node_os = __toESM(require("os"), 1);
5809
+ var import_node_os4 = __toESM(require("os"), 1);
4471
5810
 
4472
5811
  // src/tasks/taskUtils.js
4473
- var import_node_crypto = require("crypto");
5812
+ var import_node_crypto2 = require("crypto");
4474
5813
 
4475
5814
  // src/tasks/time-matcher.js
4476
5815
  var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
@@ -4652,7 +5991,7 @@ async function enqueueTask(context, options) {
4652
5991
  const db = getDb(context);
4653
5992
  const queueName = options.queueName ?? "tasks";
4654
5993
  const { tasksTable } = queueToTableNames(queueName);
4655
- const id = (0, import_node_crypto.randomUUID)();
5994
+ const id = (0, import_node_crypto2.randomUUID)();
4656
5995
  const name = options.name ?? options.task;
4657
5996
  if (!name) {
4658
5997
  throw new Error("enqueueTask: name (or task) is required");
@@ -4786,7 +6125,7 @@ async function registerInServicesRegistry(context, options) {
4786
6125
  if (!serviceGroup) {
4787
6126
  throw new Error("registerInServicesRegistry: serviceGroup is required");
4788
6127
  }
4789
- const serverName = import_node_os.default.hostname();
6128
+ const serverName = import_node_os4.default.hostname();
4790
6129
  const pid = typeof process.pid === "number" ? process.pid : null;
4791
6130
  const meta = buildMetadata(options);
4792
6131
  const groupBase = sanitizeNamePart(serviceGroup);
@@ -4899,7 +6238,7 @@ async function registerInServicesRegistry(context, options) {
4899
6238
  }
4900
6239
  async function touchServicesRegistry(context, registration) {
4901
6240
  const db = getDb2(context);
4902
- const serverName = import_node_os.default.hostname();
6241
+ const serverName = import_node_os4.default.hostname();
4903
6242
  const pid = typeof process.pid === "number" ? process.pid : null;
4904
6243
  await db(registration.registryTable).where({ id: registration.rowId }).update({
4905
6244
  last_seen_at: db.fn.now(),
@@ -4936,7 +6275,7 @@ async function listServicesRegistry(context, options = { queueName: "tasks" }) {
4936
6275
  }
4937
6276
 
4938
6277
  // src/tasks/taskLogs.js
4939
- var import_node_path = __toESM(require("path"), 1);
6278
+ var import_node_path12 = __toESM(require("path"), 1);
4940
6279
  function getLogsState(context) {
4941
6280
  const holder = context;
4942
6281
  if (holder.__tasksLogsState) return holder.__tasksLogsState;
@@ -5044,7 +6383,7 @@ function resolveIpcFileLogsDir(context, target) {
5044
6383
  const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
5045
6384
  const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
5046
6385
  const segments = target.tableName.split("/").filter(Boolean);
5047
- return import_node_path.default.resolve(basePath, namespace, ...segments);
6386
+ return import_node_path12.default.resolve(basePath, namespace, ...segments);
5048
6387
  }
5049
6388
  function getLogsStateForTarget(context, target) {
5050
6389
  const holder = context;
@@ -5518,10 +6857,10 @@ var TaskSampleProcess = class extends AbstractTask {
5518
6857
  };
5519
6858
 
5520
6859
  // src/tasks/coreTasks/TaskShellCommand.js
5521
- var import_node_child_process = require("child_process");
6860
+ var import_node_child_process5 = require("child_process");
5522
6861
  function runShellCommand(command, cwd) {
5523
- return new Promise((resolve2, reject) => {
5524
- const child = (0, import_node_child_process.spawn)(command, {
6862
+ return new Promise((resolve3, reject) => {
6863
+ const child = (0, import_node_child_process5.spawn)(command, {
5525
6864
  shell: true,
5526
6865
  cwd: cwd || process.cwd(),
5527
6866
  stdio: ["ignore", "pipe", "pipe"]
@@ -5538,7 +6877,7 @@ function runShellCommand(command, cwd) {
5538
6877
  reject(error);
5539
6878
  });
5540
6879
  child.on("close", (exitCode, signal) => {
5541
- resolve2({
6880
+ resolve3({
5542
6881
  exitCode,
5543
6882
  output: output.trim(),
5544
6883
  stderr: stderr.trim(),
@@ -5617,8 +6956,8 @@ var TaskShellCommand = class extends AbstractTask {
5617
6956
  };
5618
6957
 
5619
6958
  // src/tasks/coreTasks/TaskSystemInfo.js
5620
- var import_node_os2 = __toESM(require("os"), 1);
5621
- var import_promises = __toESM(require("fs/promises"), 1);
6959
+ var import_node_os5 = __toESM(require("os"), 1);
6960
+ var import_promises16 = __toESM(require("fs/promises"), 1);
5622
6961
  function toGb(valueBytes) {
5623
6962
  return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
5624
6963
  }
@@ -5626,7 +6965,7 @@ function toMb(valueBytes) {
5626
6965
  return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
5627
6966
  }
5628
6967
  async function getDiskStats() {
5629
- const stats = await import_promises.default.statfs("/");
6968
+ const stats = await import_promises16.default.statfs("/");
5630
6969
  const total = Number(stats.bsize) * Number(stats.blocks);
5631
6970
  const free = Number(stats.bsize) * Number(stats.bavail);
5632
6971
  const used = total - free;
@@ -5648,10 +6987,10 @@ var TaskSystemInfo = class extends AbstractTask {
5648
6987
  */
5649
6988
  async run() {
5650
6989
  try {
5651
- const totalMemory = import_node_os2.default.totalmem();
5652
- const freeMemory = import_node_os2.default.freemem();
6990
+ const totalMemory = import_node_os5.default.totalmem();
6991
+ const freeMemory = import_node_os5.default.freemem();
5653
6992
  const usedMemory = totalMemory - freeMemory;
5654
- const cpus = import_node_os2.default.cpus();
6993
+ const cpus = import_node_os5.default.cpus();
5655
6994
  const cpuUtilization = cpus.map((cpu) => {
5656
6995
  const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
5657
6996
  const usage = (total - cpu.times.idle) / total * 100;
@@ -5677,10 +7016,10 @@ var TaskSystemInfo = class extends AbstractTask {
5677
7016
  utilization: cpuUtilization
5678
7017
  },
5679
7018
  runtime: {
5680
- platform: import_node_os2.default.platform(),
5681
- arch: import_node_os2.default.arch(),
5682
- uptimeSec: import_node_os2.default.uptime(),
5683
- hostname: import_node_os2.default.hostname()
7019
+ platform: import_node_os5.default.platform(),
7020
+ arch: import_node_os5.default.arch(),
7021
+ uptimeSec: import_node_os5.default.uptime(),
7022
+ hostname: import_node_os5.default.hostname()
5684
7023
  }
5685
7024
  };
5686
7025
  this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
@@ -6010,7 +7349,7 @@ function mergeAllowedTasksWithServiceTasks(names) {
6010
7349
  }
6011
7350
 
6012
7351
  // src/tasks/taskScriptRunner.js
6013
- var import_node_child_process2 = require("child_process");
7352
+ var import_node_child_process6 = require("child_process");
6014
7353
  var MAX_PROGRESS_TEXT_LEN = 4e3;
6015
7354
  function toCliArgs(args = []) {
6016
7355
  return args.filter((a) => typeof a === "string" && a.length > 0);
@@ -6088,7 +7427,7 @@ function createSerializedQueue() {
6088
7427
  async function runNodeTaskScript(context, options) {
6089
7428
  const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
6090
7429
  const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
6091
- const child = (0, import_node_child_process2.spawn)(process.execPath, nodeArgs, {
7430
+ const child = (0, import_node_child_process6.spawn)(process.execPath, nodeArgs, {
6092
7431
  cwd: options.cwd || process.cwd(),
6093
7432
  stdio: ["ignore", "pipe", "pipe", "ipc"],
6094
7433
  env: {
@@ -6175,13 +7514,13 @@ async function runNodeTaskScript(context, options) {
6175
7514
  }
6176
7515
  forwardChildLogToParent(context, prefix, message);
6177
7516
  });
6178
- return await new Promise((resolve2, reject) => {
7517
+ return await new Promise((resolve3, reject) => {
6179
7518
  child.on("error", (error) => reject(error));
6180
7519
  child.on("close", (exitCode, signal) => {
6181
7520
  void (async () => {
6182
7521
  await flushTaskIpcLogs(context);
6183
7522
  await progressQueue.drain();
6184
- resolve2({
7523
+ resolve3({
6185
7524
  exitCode,
6186
7525
  signal,
6187
7526
  stdout: state.stdout.trim(),
@@ -6459,7 +7798,7 @@ async function runTasksLoop(context, options) {
6459
7798
  });
6460
7799
  runnerIdentity = {
6461
7800
  service_name: registryReg.serviceName,
6462
- server_name: import_node_os3.default.hostname(),
7801
+ server_name: import_node_os6.default.hostname(),
6463
7802
  instance_number: registryReg.instanceNumber
6464
7803
  };
6465
7804
  registryInterval = setInterval(() => {
@@ -6748,6 +8087,7 @@ var TasksManager = class _TasksManager {
6748
8087
  0 && (module.exports = {
6749
8088
  AbstractTask,
6750
8089
  Args,
8090
+ Aws,
6751
8091
  Box,
6752
8092
  Db,
6753
8093
  Divider,
@@ -6761,6 +8101,7 @@ var TasksManager = class _TasksManager {
6761
8101
  MultiColumnListComponent,
6762
8102
  MultiColumnListWithPreviewComponent,
6763
8103
  Params,
8104
+ REMOTE_CLI_REL,
6764
8105
  React,
6765
8106
  S3,
6766
8107
  SERVICE_TASK_NAMES,
@@ -6781,51 +8122,94 @@ var TasksManager = class _TasksManager {
6781
8122
  TasksRegistry,
6782
8123
  Text,
6783
8124
  TextBlock,
8125
+ activateRelease,
8126
+ appendDeployLog,
6784
8127
  appendTaskIpcLog,
8128
+ bootstrapHost,
6785
8129
  buildBreadcrumb,
6786
8130
  buildDetailBreadcrumb,
6787
8131
  buildFooter,
8132
+ bumpPatchVersion,
8133
+ cloneRepo,
6788
8134
  convertPattern,
8135
+ createRelease,
6789
8136
  defaultFileSynopsisFunction,
6790
8137
  defaultTasksRegistry,
6791
8138
  defaultVersionSynopsisFunction,
8139
+ defineService,
8140
+ deployService,
8141
+ deriveRepoDirName,
8142
+ enableNginxUpstream,
6792
8143
  enqueueStopTask,
6793
8144
  enqueueTask,
8145
+ ensureDeployKeyOnRemote,
8146
+ ensureEnvOnRemote,
8147
+ ensureRemoteRepo,
8148
+ ensureRepoDependencies,
6794
8149
  ensureTaskTables,
6795
8150
  flushTaskIpcLogs,
6796
8151
  getArgsInstance,
6797
8152
  h,
8153
+ initServiceStructure,
8154
+ installDeps,
6798
8155
  ipcFileLogsTableNameForSourceResource,
6799
8156
  joiEdateType,
6800
8157
  joiStringArrayType,
6801
8158
  listAliveRunnerHeartbeats,
8159
+ listReleases,
6802
8160
  listServicesRegistry,
6803
8161
  listSources,
6804
8162
  listTables,
6805
8163
  load,
8164
+ loadServices,
6806
8165
  matchesParsedPattern,
6807
8166
  memo,
6808
8167
  mergeAllowedTasksWithServiceTasks,
6809
8168
  nextTimeMatch,
6810
8169
  normalizeAllowedTasks,
8170
+ npmEnv,
8171
+ npmInstallEnv,
6811
8172
  organizeFooterMessages,
8173
+ parseGitHost,
8174
+ prepareGitHost,
8175
+ provisionService,
8176
+ pruneReleases,
8177
+ pullRepo,
6812
8178
  queueToTableNames,
8179
+ readCurrentRelease,
8180
+ readReleaseBuildInfo,
6813
8181
  readTaskIpcLogsSnapshot,
6814
8182
  registerInServicesRegistry,
6815
8183
  registerRunnerHeartbeat,
8184
+ releaseDir,
8185
+ releaseStamp,
8186
+ reloadPm2,
6816
8187
  resolveAsterisks,
6817
8188
  resolveIpcFileLogsDir,
8189
+ resolveNextVersion,
6818
8190
  resolveRanges,
8191
+ resolveServiceFrom,
6819
8192
  resolveSteps,
8193
+ rollbackService,
8194
+ run,
6820
8195
  runNodeTaskScript,
8196
+ runReleaseTests,
8197
+ runRemoteCli,
8198
+ runRemoteStatus,
8199
+ runShell,
6821
8200
  runTasksLoop,
8201
+ scrubEnvContent,
8202
+ servicePaths,
6822
8203
  setupContext,
8204
+ shellQuote,
6823
8205
  showListScreen,
6824
8206
  showMenuScreen,
6825
8207
  showMultiColumnListScreen,
6826
8208
  showMultiColumnListWithPreviewScreen,
6827
8209
  showScreen,
6828
8210
  showWordGridScreen,
8211
+ sshRun,
8212
+ syncEnv,
6829
8213
  taskHistoryInsertFromQueueRow,
6830
8214
  timeMatcher,
6831
8215
  touchRunnerHeartbeat,
@@ -6841,6 +8225,7 @@ var TasksManager = class _TasksManager {
6841
8225
  useMemo,
6842
8226
  useRef,
6843
8227
  useState,
6844
- waitForTaskResult
8228
+ waitForTaskResult,
8229
+ writeReleaseBuildInfo
6845
8230
  });
6846
8231
  //# sourceMappingURL=index.cjs.map