@kithinji/pod 1.0.22 → 1.0.23

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/main.js CHANGED
@@ -4266,7 +4266,7 @@ build
4266
4266
  `;
4267
4267
  }
4268
4268
  function genEnv() {
4269
- return `host=localhost
4269
+ return `HOST=localhost
4270
4270
  `;
4271
4271
  }
4272
4272
  function genIndexHtml(name) {
@@ -4546,100 +4546,123 @@ async function createDeployfile(cwd, projectName) {
4546
4546
  const deployFile = `name: ${projectName}
4547
4547
  version: 1.0.0
4548
4548
 
4549
+ vars:
4550
+ deploy_path: &deploy_path "/home/ubuntu/${projectName}"
4551
+ backup_path: &backup_path "/home/ubuntu/backups"
4552
+ user: &user "ubuntu"
4553
+
4554
+ shared_operations:
4555
+ install_docker: &install_docker
4556
+ type: ensure
4557
+ ensure:
4558
+ docker:
4559
+ version: "28.5.2"
4560
+ addUserToGroup: true
4561
+
4562
+ stop_containers: &stop_containers
4563
+ type: action
4564
+ action:
4565
+ command: docker compose down --remove-orphans 2>/dev/null || true
4566
+
4567
+ pull_images: &pull_images
4568
+ type: action
4569
+ action:
4570
+ command: docker compose pull --quiet
4571
+
4572
+ build_and_start: &build_and_start
4573
+ type: action
4574
+ action:
4575
+ command: docker compose up -d --build --remove-orphans --wait
4576
+
4577
+ cleanup_docker: &cleanup_docker
4578
+ type: action
4579
+ action:
4580
+ command: docker system prune -f --volumes --filter "until=168h"
4581
+
4549
4582
  targets:
4583
+ localhost:
4584
+ type: local
4585
+ operations:
4586
+ #- name: "Environment Setup"
4587
+ # <<: *install_docker
4588
+ - name: "Refresh Stack"
4589
+ <<: *build_and_start
4590
+
4550
4591
  ec2:
4592
+ type: ssh
4551
4593
  host: ec2-xx-xx-xxx-xxx.xx-xxxx-x.compute.amazonaws.com
4552
- user: ubuntu
4594
+ user: *user
4553
4595
  keyPath: ~/xxxx.pem
4554
4596
  port: 22
4555
- deployPath: /home/\${user}/app
4597
+ deployPath: *deploy_path
4556
4598
 
4557
4599
  operations:
4558
- - name: "Setup swap space"
4600
+ - name: "Provision Directories and Swap"
4559
4601
  type: ensure
4560
4602
  ensure:
4561
4603
  swap:
4562
4604
  size: 4G
4563
4605
 
4564
4606
  - name: "Install Docker"
4565
- type: ensure
4566
- ensure:
4567
- docker:
4568
- version: "28.5.2"
4569
- addUserToGroup: true
4607
+ <<: *install_docker
4570
4608
 
4571
4609
  - name: "Create application directories"
4572
4610
  type: ensure
4573
4611
  ensure:
4574
4612
  directory:
4575
- path: \${deployPath}
4576
- owner: \${user}
4613
+ path: *deploy_path
4614
+ owner: *user
4577
4615
 
4578
4616
  - name: "Create backup directory"
4579
4617
  type: ensure
4580
4618
  ensure:
4581
4619
  directory:
4582
- path: /home/\${user}/backups
4583
- owner: \${user}
4584
-
4585
- - name: "Stop running containers"
4586
- type: action
4587
- action:
4588
- command: cd \${deployPath} && docker compose down 2>/dev/null || true
4620
+ path: *backup_path
4621
+ owner: *user
4589
4622
 
4590
- - name: "Sync application files"
4623
+ - name: "Sync Source Files"
4591
4624
  type: action
4592
4625
  action:
4593
4626
  rsync:
4594
4627
  source: ./
4595
- destination: \${deployPath}/
4628
+ destination: *deploy_path
4629
+ delete: true
4596
4630
  exclude:
4597
- - node_modules/
4598
4631
  - .git/
4599
- - "*.log"
4632
+ - node_modules/
4600
4633
  - .env.local
4601
- - dist/
4602
- - public/
4634
+ - "*.log"
4603
4635
 
4604
- - name: "Pull Docker images"
4636
+ - name: "Navigate to Deploy Path"
4605
4637
  type: action
4606
4638
  action:
4607
- command: cd \${deployPath} && docker compose pull
4639
+ command: cd *deploy_path
4608
4640
 
4609
- - name: "Build and start containers"
4641
+ - name: "Create Pre-deployment Backup"
4610
4642
  type: action
4611
4643
  action:
4612
- command: cd \${deployPath} && docker compose up -d --build --remove-orphans
4644
+ command: tar -czf *backup_path/backup-$(date +%Y%m%d-%H%M%S).tar.gz .
4613
4645
 
4614
- - name: "Wait for services to start"
4615
- type: action
4616
- action:
4617
- command: sleep 10
4646
+ - name: "Pull Latest Images"
4647
+ <<: *pull_images
4618
4648
 
4619
- - name: "Show container status"
4620
- type: action
4621
- action:
4622
- command: cd \${deployPath} && docker compose ps
4649
+ - name: "Stop Existing Stack"
4650
+ <<: *stop_containers
4623
4651
 
4624
- - name: "Show recent logs"
4625
- type: action
4626
- action:
4627
- command: cd \${deployPath} && docker compose logs --tail=30
4652
+ - name: "Build and Launch"
4653
+ <<: *build_and_start
4628
4654
 
4629
- - name: "Cleanup old backups"
4630
- type: action
4631
- action:
4632
- command: find /home/\${user}/backups -name "backup-*.tar.gz" -mtime +7 -delete
4655
+ - name: "Verify Health Status"
4656
+ type: verify
4657
+ verify:
4658
+ command: ! "[ $(docker compose ps --format json | grep -qv 'running\\|healthy') ]"
4633
4659
 
4634
- - name: "Cleanup Docker resources"
4660
+ - name: "Maintenance: Cleanup"
4635
4661
  type: action
4636
4662
  action:
4637
- command: docker system prune -f --volumes
4638
-
4639
- - name: "Verify containers are running"
4640
- type: verify
4641
- verify:
4642
- command: cd \${deployPath} && docker compose ps | grep -q "Up"
4663
+ command: |
4664
+ find *backup_path -name "backup-*.tar.gz" -mtime +7 -delete
4665
+ docker image prune -af --filter "until=24h"
4643
4666
  `;
4644
4667
  const deployFilePath = path12.join(cwd, "pod.deploy.yml");
4645
4668
  await fs9.writeFile(deployFilePath, deployFile);
@@ -4944,6 +4967,9 @@ import path13 from "path";
4944
4967
  import os from "os";
4945
4968
  import { NodeSSH } from "node-ssh";
4946
4969
  import chalk from "chalk";
4970
+ import { exec } from "child_process";
4971
+ import { promisify } from "util";
4972
+ var execAsync = promisify(exec);
4947
4973
  function interpolate(str, context2) {
4948
4974
  if (!str) return "";
4949
4975
  return str.replace(/\${([^}]+)}/g, (match, key) => {
@@ -5142,19 +5168,42 @@ if [ "${addToGroup}" = "true" ]; then
5142
5168
  fi
5143
5169
  `
5144
5170
  };
5145
- var RemoteShell = class {
5146
- constructor(ssh) {
5147
- this.ssh = ssh;
5171
+ var SSHStrategy = class {
5172
+ constructor(target) {
5173
+ this.ssh = new NodeSSH();
5174
+ this.target = target;
5175
+ this.currentDir = target.deployPath || ".";
5176
+ }
5177
+ async connect() {
5178
+ await this.ssh.connect({
5179
+ host: this.target.host,
5180
+ username: this.target.user,
5181
+ privateKeyPath: this.target.keyPath,
5182
+ port: this.target.port || 22
5183
+ });
5148
5184
  }
5149
- async uploadContent(remotePath, content) {
5150
- const localTmp = path13.join(os.tmpdir(), `pod_tmp_${Date.now()}`);
5151
- fs10.writeFileSync(localTmp, content);
5152
- try {
5153
- await this.ssh.execCommand(`mkdir -p $(dirname ${remotePath})`);
5154
- await this.ssh.putFile(localTmp, remotePath);
5155
- } finally {
5156
- if (fs10.existsSync(localTmp)) fs10.unlinkSync(localTmp);
5185
+ async disconnect() {
5186
+ this.ssh.dispose();
5187
+ }
5188
+ async runCommand(cmd, silent = false) {
5189
+ const trimmed = cmd.trim();
5190
+ if (trimmed.startsWith("cd ")) {
5191
+ const newPath = trimmed.replace("cd ", "").trim();
5192
+ this.currentDir = path13.posix.resolve(this.currentDir, newPath);
5193
+ if (!silent) console.log(chalk.gray(` [SSH Path: ${this.currentDir}]`));
5194
+ return { stdout: "", stderr: "", code: 0 };
5157
5195
  }
5196
+ const result = await this.ssh.execCommand(trimmed, {
5197
+ cwd: this.currentDir
5198
+ });
5199
+ if (result.code !== 0 && result.code !== null) {
5200
+ throw new Error(`Execution failed: ${trimmed}
5201
+ STDERR: ${result.stderr}`);
5202
+ }
5203
+ if (!silent && result.stdout) {
5204
+ result.stdout.split("\n").filter((l) => l.startsWith("LOG:")).forEach((l) => console.log(chalk.gray(` ${l.replace("LOG: ", "")}`)));
5205
+ }
5206
+ return result;
5158
5207
  }
5159
5208
  async runScript(name, content, context2) {
5160
5209
  const interpolated = interpolate(content, context2);
@@ -5162,22 +5211,20 @@ var RemoteShell = class {
5162
5211
  await this.uploadContent(remotePath, interpolated);
5163
5212
  try {
5164
5213
  await this.ssh.execCommand(`chmod +x ${remotePath}`);
5165
- return await this.run(remotePath, context2);
5214
+ await this.runCommand(remotePath);
5166
5215
  } finally {
5167
5216
  await this.ssh.execCommand(`rm -f ${remotePath}`);
5168
5217
  }
5169
5218
  }
5170
- async run(cmd, context2, silent = false) {
5171
- const interpolated = interpolate(cmd, context2);
5172
- const result = await this.ssh.execCommand(interpolated);
5173
- if (result.code !== 0 && result.code !== null) {
5174
- throw new Error(`Execution failed: ${cmd}
5175
- STDERR: ${result.stderr}`);
5176
- }
5177
- if (!silent && result.stdout) {
5178
- result.stdout.split("\n").filter((l) => l.startsWith("LOG:")).forEach((l) => console.log(chalk.gray(` ${l.replace("LOG: ", "")}`)));
5219
+ async uploadContent(remotePath, content) {
5220
+ const localTmp = path13.join(os.tmpdir(), `pod_tmp_${Date.now()}`);
5221
+ fs10.writeFileSync(localTmp, content);
5222
+ try {
5223
+ await this.ssh.execCommand(`mkdir -p $(dirname ${remotePath})`);
5224
+ await this.ssh.putFile(localTmp, remotePath);
5225
+ } finally {
5226
+ if (fs10.existsSync(localTmp)) fs10.unlinkSync(localTmp);
5179
5227
  }
5180
- return result;
5181
5228
  }
5182
5229
  async readJson(remotePath) {
5183
5230
  const res = await this.ssh.execCommand(`cat ${remotePath}`);
@@ -5187,211 +5234,354 @@ STDERR: ${result.stderr}`);
5187
5234
  return null;
5188
5235
  }
5189
5236
  }
5237
+ async syncDirectory(source, destination, exclude) {
5238
+ const putOptions = { recursive: true, concurrency: 10 };
5239
+ if (exclude?.length) {
5240
+ putOptions.validate = (filePath) => {
5241
+ const relative = path13.relative(source, filePath);
5242
+ if (relative === "") return true;
5243
+ return !exclude.some((pattern) => {
5244
+ if (pattern.endsWith("/")) {
5245
+ const dir = pattern.slice(0, -1);
5246
+ const segment = "/" + dir + "/";
5247
+ return relative === dir || relative.startsWith(dir + "/") || relative.includes(segment);
5248
+ }
5249
+ if (pattern.startsWith("*.")) {
5250
+ return relative.endsWith(pattern.slice(1));
5251
+ }
5252
+ return relative === pattern;
5253
+ });
5254
+ };
5255
+ }
5256
+ console.log(chalk.gray(` Syncing ${source} \u2192 ${destination}`));
5257
+ await this.ssh.putDirectory(source, destination, putOptions);
5258
+ }
5190
5259
  };
5191
- async function deploy(targetName, options) {
5192
- const cwd = process.cwd();
5193
- const rawConfig = yaml2.load(
5194
- fs10.readFileSync(path13.join(cwd, "pod.deploy.yml"), "utf8")
5195
- );
5196
- const rawTarget = rawConfig.targets?.[targetName];
5197
- if (!rawTarget) throw new Error(`Target ${targetName} not found.`);
5198
- console.log(
5199
- chalk.blue.bold(
5200
- `
5201
- \u{1F680} Pod Deploy: ${rawConfig.name} v${rawConfig.version} \u2192 ${targetName}
5202
- `
5203
- )
5204
- );
5205
- let target = deepInterpolate(rawTarget, {
5206
- ...rawConfig,
5207
- ...rawTarget
5208
- });
5209
- target = resolveLocalPaths(target, cwd);
5210
- const ssh = new NodeSSH();
5211
- const shell = new RemoteShell(ssh);
5212
- try {
5213
- await ssh.connect({
5214
- host: target.host,
5215
- username: target.user,
5216
- privateKeyPath: target.keyPath,
5217
- port: target.port || 22
5218
- });
5219
- const lockPath = path13.posix.join(target.deployPath, "pod-lock.json");
5220
- let lock = await shell.readJson(lockPath) || {
5221
- ensures: {},
5222
- once_actions: []
5223
- };
5224
- if (lock.deployment_version !== rawConfig.version) {
5225
- console.log(chalk.magenta(`\u2192 Version change: ${rawConfig.version}`));
5226
- lock.deployment_version = rawConfig.version;
5227
- lock.once_actions = [];
5228
- await shell.uploadContent(lockPath, JSON.stringify(lock, null, 2));
5260
+ var LocalStrategy = class {
5261
+ constructor(target, cwd) {
5262
+ this.target = target;
5263
+ this.currentDir = cwd;
5264
+ }
5265
+ async connect() {
5266
+ }
5267
+ async disconnect() {
5268
+ }
5269
+ async runCommand(cmd, silent = false) {
5270
+ const trimmed = cmd.trim();
5271
+ if (trimmed.startsWith("cd ")) {
5272
+ const newPath = trimmed.replace("cd ", "").trim();
5273
+ this.currentDir = path13.resolve(this.currentDir, newPath);
5274
+ if (!silent) console.log(chalk.gray(` [Local Path: ${this.currentDir}]`));
5275
+ return { stdout: "", stderr: "", code: 0 };
5229
5276
  }
5230
- for (const op of target.operations) {
5231
- try {
5232
- if (op.type === "ensure") {
5233
- await handleEnsure(op, shell, target, lock, lockPath, options);
5234
- } else if (op.type === "action") {
5235
- await handleAction(op, shell, target, lock, lockPath);
5236
- } else if (op.type === "verify") {
5237
- await handleVerify(op, shell, target);
5277
+ try {
5278
+ const { stdout, stderr } = await execAsync(trimmed, {
5279
+ cwd: this.currentDir
5280
+ });
5281
+ if (!silent && stdout) {
5282
+ stdout.split("\n").filter((l) => l.startsWith("LOG:")).forEach(
5283
+ (l) => console.log(chalk.gray(` ${l.replace("LOG: ", "")}`))
5284
+ );
5285
+ }
5286
+ return { stdout, stderr, code: 0 };
5287
+ } catch (err) {
5288
+ throw new Error(
5289
+ `Execution failed: ${trimmed}
5290
+ STDERR: ${err.stderr || err.message}`
5291
+ );
5292
+ }
5293
+ }
5294
+ async runScript(name, content, context2) {
5295
+ const interpolated = interpolate(content, context2);
5296
+ const scriptPath = path13.join(
5297
+ os.tmpdir(),
5298
+ `pod_script_${name}_${Date.now()}.sh`
5299
+ );
5300
+ fs10.writeFileSync(scriptPath, interpolated);
5301
+ fs10.chmodSync(scriptPath, "755");
5302
+ try {
5303
+ await this.runCommand(scriptPath);
5304
+ } finally {
5305
+ if (fs10.existsSync(scriptPath)) fs10.unlinkSync(scriptPath);
5306
+ }
5307
+ }
5308
+ async uploadContent(localPath, content) {
5309
+ const dir = path13.dirname(localPath);
5310
+ if (!fs10.existsSync(dir)) {
5311
+ fs10.mkdirSync(dir, { recursive: true });
5312
+ }
5313
+ fs10.writeFileSync(localPath, content);
5314
+ }
5315
+ async readJson(localPath) {
5316
+ try {
5317
+ if (!fs10.existsSync(localPath)) return null;
5318
+ const content = fs10.readFileSync(localPath, "utf8");
5319
+ return JSON.parse(content);
5320
+ } catch {
5321
+ return null;
5322
+ }
5323
+ }
5324
+ async syncDirectory(source, destination, exclude) {
5325
+ console.log(chalk.gray(` Copying ${source} \u2192 ${destination}`));
5326
+ if (!fs10.existsSync(destination)) {
5327
+ fs10.mkdirSync(destination, { recursive: true });
5328
+ }
5329
+ const shouldExclude = (relativePath) => {
5330
+ if (!exclude?.length) return false;
5331
+ return exclude.some((pattern) => {
5332
+ if (pattern.endsWith("/")) {
5333
+ const dir = pattern.slice(0, -1);
5334
+ const segment = "/" + dir + "/";
5335
+ return relativePath === dir || relativePath.startsWith(dir + "/") || relativePath.includes(segment);
5336
+ }
5337
+ if (pattern.startsWith("*.")) {
5338
+ return relativePath.endsWith(pattern.slice(1));
5339
+ }
5340
+ return relativePath === pattern;
5341
+ });
5342
+ };
5343
+ const copyRecursive = (src, dest) => {
5344
+ const entries = fs10.readdirSync(src, { withFileTypes: true });
5345
+ for (const entry of entries) {
5346
+ const srcPath = path13.join(src, entry.name);
5347
+ const destPath = path13.join(dest, entry.name);
5348
+ const relativePath = path13.relative(source, srcPath);
5349
+ if (shouldExclude(relativePath)) continue;
5350
+ if (entry.isDirectory()) {
5351
+ if (!fs10.existsSync(destPath)) {
5352
+ fs10.mkdirSync(destPath, { recursive: true });
5353
+ }
5354
+ copyRecursive(srcPath, destPath);
5238
5355
  } else {
5239
- throw new Error(`Unknown operation type: ${op.type}`);
5356
+ fs10.copyFileSync(srcPath, destPath);
5240
5357
  }
5241
- } catch (err) {
5242
- throw new Error(`Failed at operation "${op.name}": ${err.message}`);
5243
5358
  }
5359
+ };
5360
+ copyRecursive(source, destination);
5361
+ }
5362
+ };
5363
+ var StrategyFactory = class {
5364
+ static create(target, cwd) {
5365
+ const targetType = target.type || (target.host ? "ssh" : "local");
5366
+ switch (targetType) {
5367
+ case "ssh":
5368
+ return new SSHStrategy(target);
5369
+ case "local":
5370
+ return new LocalStrategy(target, cwd);
5371
+ default:
5372
+ throw new Error(`Unknown target type: ${targetType}`);
5244
5373
  }
5245
- console.log(chalk.green.bold(`
5246
- \u2705 Deployment successful!
5247
- `));
5248
- } catch (err) {
5249
- console.error(chalk.red.bold(`
5250
- \u274C Deployment Failed: ${err.message}`));
5251
- throw err;
5252
- } finally {
5253
- ssh.dispose();
5254
5374
  }
5255
- }
5256
- async function handleEnsure(op, shell, target, lock, lockPath, options) {
5257
- if (!op.ensure) {
5258
- throw new Error(`Ensure operation "${op.name}" missing ensure config`);
5375
+ };
5376
+ var OperationHandler = class {
5377
+ constructor(strategy, target, lock, lockPath) {
5378
+ this.strategy = strategy;
5379
+ this.target = target;
5380
+ this.lock = lock;
5381
+ this.lockPath = lockPath;
5382
+ }
5383
+ async handleEnsure(op, options) {
5384
+ if (!op.ensure) {
5385
+ throw new Error(`Ensure operation "${op.name}" missing ensure config`);
5386
+ }
5387
+ if (op.ensure.swap) {
5388
+ await this.ensureSwap(op, options);
5389
+ }
5390
+ if (op.ensure.docker) {
5391
+ await this.ensureDocker(op, options);
5392
+ }
5393
+ if (op.ensure.directory) {
5394
+ await this.ensureDirectory(op, options);
5395
+ }
5259
5396
  }
5260
- if (op.ensure.swap) {
5397
+ async ensureSwap(op, options) {
5261
5398
  const key = "swap";
5262
- const locked = lock.ensures[key];
5399
+ const locked = this.lock.ensures[key];
5263
5400
  const currentConfig = op.ensure.swap;
5264
5401
  const configChanged = JSON.stringify(locked?.config) !== JSON.stringify(currentConfig);
5265
5402
  if (options?.forceEnsure || !locked || locked.version !== currentConfig.size || configChanged) {
5266
5403
  console.log(chalk.yellow(`\u2192 Ensuring: ${op.name}`));
5267
5404
  const script = SCRIPTS.SWAP(currentConfig.size);
5268
- await shell.runScript(key, script, target);
5269
- lock.ensures[key] = {
5405
+ await this.strategy.runScript(key, script, this.target);
5406
+ this.lock.ensures[key] = {
5270
5407
  version: currentConfig.size,
5271
5408
  config: currentConfig
5272
5409
  };
5273
- await shell.uploadContent(lockPath, JSON.stringify(lock, null, 2));
5410
+ await this.saveLock();
5274
5411
  } else {
5275
5412
  console.log(chalk.gray(`\u2713 ${op.name} (already satisfied)`));
5276
5413
  }
5277
5414
  }
5278
- if (op.ensure.docker) {
5415
+ async ensureDocker(op, options) {
5279
5416
  const key = "docker";
5280
- const locked = lock.ensures[key];
5417
+ const locked = this.lock.ensures[key];
5281
5418
  const currentConfig = op.ensure.docker;
5282
5419
  const configChanged = JSON.stringify(locked?.config) !== JSON.stringify(currentConfig);
5283
5420
  if (options?.forceEnsure || !locked || locked.version !== currentConfig.version || configChanged) {
5284
5421
  console.log(chalk.yellow(`\u2192 Ensuring: ${op.name}`));
5285
5422
  const script = SCRIPTS.DOCKER(
5286
5423
  currentConfig.version,
5287
- target.user,
5424
+ this.target.user || os.userInfo().username,
5288
5425
  !!currentConfig.addUserToGroup
5289
5426
  );
5290
- await shell.runScript(key, script, target);
5291
- lock.ensures[key] = {
5427
+ await this.strategy.runScript(key, script, this.target);
5428
+ this.lock.ensures[key] = {
5292
5429
  version: currentConfig.version,
5293
5430
  config: currentConfig
5294
5431
  };
5295
- await shell.uploadContent(lockPath, JSON.stringify(lock, null, 2));
5432
+ await this.saveLock();
5296
5433
  } else {
5297
5434
  console.log(chalk.gray(`\u2713 ${op.name} (already satisfied)`));
5298
5435
  }
5299
5436
  }
5300
- if (op.ensure.directory) {
5437
+ async ensureDirectory(op, options) {
5301
5438
  const key = `directory_${op.ensure.directory.path}`;
5302
- const locked = lock.ensures[key];
5439
+ const locked = this.lock.ensures[key];
5303
5440
  const currentConfig = op.ensure.directory;
5304
5441
  const configChanged = JSON.stringify(locked?.config) !== JSON.stringify(currentConfig);
5305
5442
  if (options?.forceEnsure || !locked || configChanged) {
5306
5443
  console.log(chalk.yellow(`\u2192 Ensuring: ${op.name}`));
5307
- const dirPath = interpolate(currentConfig.path, target);
5308
- const owner = currentConfig.owner ? interpolate(currentConfig.owner, target) : target.user;
5309
- await shell.run(`mkdir -p ${dirPath}`, target, true);
5310
- await shell.run(
5311
- `sudo chown -R ${owner}:${owner} ${dirPath}`,
5312
- target,
5313
- true
5314
- );
5315
- lock.ensures[key] = {
5444
+ const dirPath = interpolate(currentConfig.path, this.target);
5445
+ const owner = currentConfig.owner ? interpolate(currentConfig.owner, this.target) : this.target.user || os.userInfo().username;
5446
+ await this.strategy.runCommand(`mkdir -p ${dirPath}`, true);
5447
+ if (this.target.user) {
5448
+ await this.strategy.runCommand(
5449
+ `sudo chown -R ${owner}:${owner} ${dirPath}`,
5450
+ true
5451
+ );
5452
+ }
5453
+ this.lock.ensures[key] = {
5316
5454
  version: dirPath,
5317
5455
  config: currentConfig
5318
5456
  };
5319
- await shell.uploadContent(lockPath, JSON.stringify(lock, null, 2));
5457
+ await this.saveLock();
5320
5458
  } else {
5321
5459
  console.log(chalk.gray(`\u2713 ${op.name} (already satisfied)`));
5322
5460
  }
5323
5461
  }
5324
- }
5325
- async function handleAction(op, shell, target, lock, lockPath) {
5326
- if (!op.action) {
5327
- throw new Error(`Action operation "${op.name}" missing action config`);
5328
- }
5329
- const when = op.when || "always";
5330
- if (when === "never") {
5331
- console.log(chalk.gray(`\u2298 ${op.name} (disabled)`));
5332
- return;
5333
- }
5334
- const actionId = `action_${op.name}`;
5335
- if (when === "once" && lock.once_actions.includes(actionId)) {
5336
- console.log(chalk.gray(`\u2713 ${op.name} (already completed)`));
5337
- return;
5338
- }
5339
- console.log(chalk.cyan(`\u2192 Running: ${op.name}`));
5340
- if (op.action.rsync) {
5341
- const src = op.action.rsync.source;
5342
- const dest = interpolate(op.action.rsync.destination || ".", target);
5343
- const putOptions = { recursive: true, concurrency: 10 };
5344
- if (op.action.rsync.exclude?.length) {
5345
- const excludePatterns = op.action.rsync.exclude;
5346
- putOptions.validate = (filePath) => {
5347
- const relative = path13.relative(src, filePath);
5348
- if (relative === "") return true;
5349
- return !excludePatterns.some((pattern) => {
5350
- if (pattern.endsWith("/")) {
5351
- const dir = pattern.slice(0, -1);
5352
- const segment = "/" + dir + "/";
5353
- return relative === dir || relative.startsWith(dir + "/") || relative.includes(segment);
5354
- }
5355
- if (pattern.startsWith("*.")) {
5356
- return relative.endsWith(pattern.slice(1));
5357
- }
5358
- return relative === pattern;
5359
- });
5360
- };
5462
+ async handleAction(op) {
5463
+ if (!op.action) {
5464
+ throw new Error(`Action operation "${op.name}" missing action config`);
5465
+ }
5466
+ const when = op.when || "always";
5467
+ if (when === "never") {
5468
+ console.log(chalk.gray(`\u2298 ${op.name} (disabled)`));
5469
+ return;
5470
+ }
5471
+ const actionId = `action_${op.name}`;
5472
+ if (when === "once" && this.lock.once_actions.includes(actionId)) {
5473
+ console.log(chalk.gray(`\u2713 ${op.name} (already completed)`));
5474
+ return;
5475
+ }
5476
+ console.log(chalk.cyan(`\u2192 Running: ${op.name}`));
5477
+ if (op.action.rsync) {
5478
+ const src = op.action.rsync.source;
5479
+ const dest = interpolate(op.action.rsync.destination || ".", this.target);
5480
+ await this.strategy.syncDirectory(src, dest, op.action.rsync.exclude);
5481
+ }
5482
+ if (op.action.command) {
5483
+ const cmd = interpolate(op.action.command, this.target);
5484
+ await this.strategy.runCommand(cmd);
5485
+ }
5486
+ if (when === "once") {
5487
+ this.lock.once_actions.push(actionId);
5488
+ await this.saveLock();
5361
5489
  }
5362
- console.log(chalk.gray(` Syncing ${src} \u2192 ${dest}`));
5363
- await shell.ssh.putDirectory(src, dest, putOptions);
5364
- }
5365
- if (op.action.command) {
5366
- await shell.run(op.action.command, target);
5367
- }
5368
- if (when === "once") {
5369
- lock.once_actions.push(actionId);
5370
- await shell.uploadContent(lockPath, JSON.stringify(lock, null, 2));
5371
5490
  }
5372
- }
5373
- async function handleVerify(op, shell, target) {
5374
- if (!op.verify) {
5375
- throw new Error(`Verify operation "${op.name}" missing verify config`);
5491
+ async handleVerify(op) {
5492
+ if (!op.verify) {
5493
+ throw new Error(`Verify operation "${op.name}" missing verify config`);
5494
+ }
5495
+ console.log(chalk.cyan(`\u2192 Verifying: ${op.name}`));
5496
+ if (op.verify.http) {
5497
+ const url = interpolate(op.verify.http.url, this.target);
5498
+ const timeout = op.verify.http.timeout || "30s";
5499
+ await this.strategy.runCommand(
5500
+ `curl -f --max-time ${timeout} ${url}`,
5501
+ true
5502
+ );
5503
+ }
5504
+ if (op.verify.command) {
5505
+ const cmd = interpolate(op.verify.command, this.target);
5506
+ await this.strategy.runCommand(cmd, true);
5507
+ }
5376
5508
  }
5377
- console.log(chalk.cyan(`\u2192 Verifying: ${op.name}`));
5378
- if (op.verify.http) {
5379
- const url = interpolate(op.verify.http.url, target);
5380
- const timeout = op.verify.http.timeout || "30s";
5381
- await shell.run(`curl -f --max-time ${timeout} ${url}`, target, true);
5509
+ async saveLock() {
5510
+ await this.strategy.uploadContent(
5511
+ this.lockPath,
5512
+ JSON.stringify(this.lock, null, 2)
5513
+ );
5382
5514
  }
5383
- if (op.verify.command) {
5384
- await shell.run(op.verify.command, target, true);
5515
+ };
5516
+ async function deploy(targetName, options) {
5517
+ const cwd = process.cwd();
5518
+ const rawConfig = yaml2.load(
5519
+ fs10.readFileSync(path13.join(cwd, "pod.deploy.yml"), "utf8"),
5520
+ { schema: yaml2.DEFAULT_SCHEMA }
5521
+ );
5522
+ const rawTarget = rawConfig.targets?.[targetName];
5523
+ if (!rawTarget) throw new Error(`Target ${targetName} not found.`);
5524
+ console.log(
5525
+ chalk.blue.bold(
5526
+ `
5527
+ Pod Deploy: ${rawConfig.name} v${rawConfig.version} \u2192 ${targetName}
5528
+ `
5529
+ )
5530
+ );
5531
+ let target = deepInterpolate(rawTarget, {
5532
+ ...rawConfig,
5533
+ ...rawTarget
5534
+ });
5535
+ target = resolveLocalPaths(target, cwd);
5536
+ const strategy = StrategyFactory.create(target, cwd);
5537
+ try {
5538
+ await strategy.connect();
5539
+ const lockPath = target.deployPath ? path13.posix.join(target.deployPath, "pod-lock.json") : path13.join(cwd, "pod-lock.json");
5540
+ let lock = await strategy.readJson(lockPath) || {
5541
+ ensures: {},
5542
+ once_actions: []
5543
+ };
5544
+ if (lock.deployment_version !== rawConfig.version) {
5545
+ console.log(chalk.magenta(`\u2192 Version change: ${rawConfig.version}`));
5546
+ lock.deployment_version = rawConfig.version;
5547
+ lock.once_actions = [];
5548
+ await strategy.uploadContent(lockPath, JSON.stringify(lock, null, 2));
5549
+ }
5550
+ const handler = new OperationHandler(strategy, target, lock, lockPath);
5551
+ for (const op of target.operations) {
5552
+ try {
5553
+ if (op.type === "ensure") {
5554
+ await handler.handleEnsure(op, options);
5555
+ } else if (op.type === "action") {
5556
+ await handler.handleAction(op);
5557
+ } else if (op.type === "verify") {
5558
+ await handler.handleVerify(op);
5559
+ } else {
5560
+ throw new Error(`Unknown operation type: ${op.type}`);
5561
+ }
5562
+ } catch (err) {
5563
+ throw new Error(`Failed at operation "${op.name}": ${err.message}`);
5564
+ }
5565
+ }
5566
+ console.log(chalk.green.bold(`
5567
+ Deployment successful!
5568
+ `));
5569
+ } catch (err) {
5570
+ console.error(chalk.red.bold(`
5571
+ Deployment Failed: ${err.message}`));
5572
+ throw err;
5573
+ } finally {
5574
+ await strategy.disconnect();
5385
5575
  }
5386
5576
  }
5387
5577
 
5388
5578
  // src/main.ts
5389
5579
  import chalk2 from "chalk";
5390
5580
  var program = new Command();
5391
- program.name("pod").description("Pod cli tool").version("1.0.22");
5581
+ program.name("pod").description("Pod cli tool").version("1.0.23");
5392
5582
  program.command("new <name>").description("Start a new Orca Project").action(async (name) => {
5393
5583
  await addNew(name);
5394
- const appDir = path14.resolve(process.cwd(), name);
5584
+ const appDir = path14.resolve(process.cwd());
5395
5585
  const shell = process.platform === "win32" ? process.env.ComSpec || "cmd.exe" : "/bin/sh";
5396
5586
  console.log("Installing dependencies...");
5397
5587
  execSync("npm install", { stdio: "inherit", cwd: appDir, shell });