@askexenow/exe-os 0.9.82 → 0.9.84

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.
@@ -19,12 +19,13 @@ function isMainModule(importMetaUrl) {
19
19
  }
20
20
 
21
21
  // src/lib/stack-update.ts
22
- import { execFileSync } from "child_process";
23
- import { createVerify, verify as verifySignature } from "crypto";
24
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
22
+ import { execFileSync, spawnSync } from "child_process";
23
+ import { createVerify, randomBytes, verify as verifySignature } from "crypto";
24
+ import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
25
25
  import http from "http";
26
26
  import https from "https";
27
27
  import path3 from "path";
28
+ import { fileURLToPath as fileURLToPath2 } from "url";
28
29
 
29
30
  // src/lib/license.ts
30
31
  import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
@@ -382,10 +383,139 @@ Re-run with --allow-breaking ${missing.map((c) => c.id).join(",")}`
382
383
  );
383
384
  }
384
385
  }
386
+ function commandSucceeds(cmd, args = []) {
387
+ const res = spawnSync(cmd, args, { stdio: "ignore" });
388
+ return res.status === 0;
389
+ }
390
+ function shellSucceeds(command) {
391
+ const res = spawnSync("sh", ["-lc", command], { stdio: "ignore" });
392
+ return res.status === 0;
393
+ }
394
+ function resolvePackageRoot() {
395
+ const here = path3.dirname(fileURLToPath2(import.meta.url));
396
+ const candidates = [
397
+ path3.resolve(here, "..", ".."),
398
+ path3.resolve(here, ".."),
399
+ process.cwd()
400
+ ];
401
+ for (const c of candidates) {
402
+ if (existsSync4(path3.join(c, "package.json")) && existsSync4(path3.join(c, "deploy", "compose", "docker-compose.yml"))) return c;
403
+ }
404
+ return process.cwd();
405
+ }
406
+ function copyTemplateIfMissing(srcRel, dest, created) {
407
+ if (existsSync4(dest)) return;
408
+ const src = path3.join(resolvePackageRoot(), srcRel);
409
+ if (!existsSync4(src)) throw new Error(`Missing packaged stack template: ${srcRel}. Reinstall/update exe-os and retry.`);
410
+ mkdirSync3(path3.dirname(dest), { recursive: true });
411
+ copyFileSync(src, dest);
412
+ created.push(dest);
413
+ }
414
+ function installDockerUbuntu(exec) {
415
+ if (process.platform !== "linux") throw new Error("Docker auto-install is only supported on Linux. Install Docker manually, then retry.");
416
+ if (!existsSync4("/etc/os-release")) throw new Error("Cannot detect Linux distro; install Docker manually, then retry.");
417
+ const osRelease = readFileSync3("/etc/os-release", "utf8");
418
+ if (!/ID=(ubuntu|debian)|ID_LIKE=.*debian/.test(osRelease)) {
419
+ throw new Error("Docker auto-install currently supports Ubuntu/Debian only. Install Docker manually, then retry.");
420
+ }
421
+ const script = [
422
+ "set -e",
423
+ "sudo apt-get update",
424
+ "sudo apt-get install -y ca-certificates curl gnupg",
425
+ "sudo install -m 0755 -d /etc/apt/keyrings",
426
+ "if [ ! -f /etc/apt/keyrings/docker.gpg ]; then curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg; sudo chmod a+r /etc/apt/keyrings/docker.gpg; fi",
427
+ ". /etc/os-release",
428
+ 'CODENAME="${VERSION_CODENAME:-bookworm}"',
429
+ 'if [ "${ID:-}" = "debian" ]; then DOCKER_DISTRO=debian; else DOCKER_DISTRO=ubuntu; fi',
430
+ `printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/%s %s stable\\n' "$(dpkg --print-architecture)" "$DOCKER_DISTRO" "$CODENAME" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null`,
431
+ "sudo apt-get update",
432
+ "sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin",
433
+ "sudo systemctl enable --now docker",
434
+ "sudo docker version >/dev/null",
435
+ "sudo docker compose version >/dev/null"
436
+ ].join("\n");
437
+ exec("sh", ["-lc", script]);
438
+ }
439
+ function randomSecret(bytes = 32) {
440
+ return randomBytes(bytes).toString("base64url");
441
+ }
442
+ function hydrateEnv(raw, opts) {
443
+ let next = raw;
444
+ const license = opts.licenseKey || process.env.EXE_LICENSE_KEY || loadLicense() || "";
445
+ const domain = opts.domain || process.env.EXE_STACK_DOMAIN || process.env.CUSTOMER_DOMAIN || "";
446
+ const replacements = {};
447
+ const env = parseEnv(raw);
448
+ for (const [key, value] of env.entries()) {
449
+ if (!/CHANGEME/.test(value)) continue;
450
+ if (key === "EXE_LICENSE_KEY" && license) replacements[key] = license;
451
+ else if (key.endsWith("_PASSWORD")) replacements[key] = randomSecret(24);
452
+ else if (key.endsWith("_SECRET") || key.endsWith("_TOKEN") || key.endsWith("_KEY") || key.endsWith("_SALT")) replacements[key] = value.replace(/CHANGEME[A-Z0-9_]*/g, randomSecret(32));
453
+ else if (key === "EXED_MCP_TOKEN") replacements[key] = randomSecret(32);
454
+ }
455
+ if (domain) next = next.replaceAll("CHANGEME_DOMAIN", domain);
456
+ next = patchEnv(next, replacements);
457
+ const remaining = [...parseEnv(next).entries()].filter(([, value]) => /CHANGEME/.test(value)).map(([key, value]) => `${key}=${value}`);
458
+ return { raw: next, hadPlaceholders: /CHANGEME/.test(raw), remaining: [...new Set(remaining)] };
459
+ }
460
+ function bootstrapStackHost(options) {
461
+ const exec = options.exec ?? defaultExec;
462
+ const createdFiles = [];
463
+ const actions = [];
464
+ let dockerInstalled = commandSucceeds("docker", ["version"]);
465
+ let dockerComposeInstalled = shellSucceeds("docker compose version");
466
+ if ((!dockerInstalled || !dockerComposeInstalled) && options.installDocker) {
467
+ actions.push("install_docker");
468
+ installDockerUbuntu(exec);
469
+ dockerInstalled = commandSucceeds("docker", ["version"]);
470
+ dockerComposeInstalled = shellSucceeds("docker compose version");
471
+ }
472
+ copyTemplateIfMissing("deploy/compose/docker-compose.yml", options.composeFile, createdFiles);
473
+ copyTemplateIfMissing("deploy/compose/.env.customer.example", options.envFile, createdFiles);
474
+ const brandingDest = path3.join(path3.dirname(options.envFile), "branding.json");
475
+ copyTemplateIfMissing("deploy/compose/gateway.json", path3.join(path3.dirname(options.envFile), "gateway.json"), createdFiles);
476
+ if (!existsSync4(brandingDest)) writeFileSync2(brandingDest, JSON.stringify({ brandName: "Hygo", productName: "Hygo OS" }, null, 2) + "\n", { mode: 384 });
477
+ let envHadPlaceholders = false;
478
+ let envRemainingPlaceholders = [];
479
+ if (existsSync4(options.envFile)) {
480
+ const envRaw = readFileSync3(options.envFile, "utf8");
481
+ const hydrated = hydrateEnv(envRaw, options);
482
+ envHadPlaceholders = hydrated.hadPlaceholders;
483
+ envRemainingPlaceholders = hydrated.remaining;
484
+ if (hydrated.raw !== envRaw) {
485
+ writeFileSync2(options.envFile, hydrated.raw, { mode: 384 });
486
+ actions.push("hydrate_env_secrets");
487
+ }
488
+ }
489
+ return {
490
+ dockerInstalled,
491
+ dockerComposeInstalled,
492
+ composeFileExists: existsSync4(options.composeFile),
493
+ envFileExists: existsSync4(options.envFile),
494
+ envHadPlaceholders,
495
+ envRemainingPlaceholders,
496
+ licensePresent: !!(options.licenseKey || process.env.EXE_LICENSE_KEY || loadLicense()),
497
+ createdFiles,
498
+ actions
499
+ };
500
+ }
501
+ function assertHostReadyForApply(report) {
502
+ const blockers = [];
503
+ if (!report.dockerInstalled) blockers.push("Docker is not installed/running. Re-run with --yes to auto-install on Ubuntu/Debian, or install Docker manually.");
504
+ if (!report.dockerComposeInstalled) blockers.push("Docker Compose plugin is missing. Re-run with --yes to auto-install on Ubuntu/Debian, or install docker-compose-plugin manually.");
505
+ if (!report.composeFileExists) blockers.push("docker-compose.yml is missing and could not be created.");
506
+ if (!report.envFileExists) blockers.push(".env is missing and could not be created.");
507
+ if (!report.licensePresent) blockers.push("Exe OS license key is missing. Run `exe-os setup`, `exe-os --activate <key>`, or pass --license-key.");
508
+ const hardPlaceholders = report.envRemainingPlaceholders.filter((p) => !/WHATSAPP|API_ROUTER|MONITOR_AGENT/.test(p));
509
+ if (hardPlaceholders.length > 0) blockers.push(`Required .env placeholders remain: ${hardPlaceholders.join(", ")}`);
510
+ if (blockers.length > 0) throw new Error(`Stack host is not ready:
511
+ - ${blockers.join("\n- ")}`);
512
+ }
385
513
  async function runStackUpdate(options) {
386
514
  const exec = options.exec ?? defaultExec;
387
515
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
388
516
  if (options.rollback) return rollbackStackUpdate(options);
517
+ const report = bootstrapStackHost({ ...options, installDocker: options.installDocker ?? !!options.yes });
518
+ if (!options.dryRun) assertHostReadyForApply(report);
389
519
  const manifest = await loadStackManifest(options.manifestRef, options.fetchText, options.manifestPublicKey, options.manifestAuthToken);
390
520
  const envRaw = readFileSync3(options.envFile, "utf8");
391
521
  const plan = createStackUpdatePlan(manifest, envRaw, options.targetVersion);
@@ -553,12 +683,16 @@ async function defaultPostJson(url, body, authToken) {
553
683
  function defaultStackPaths() {
554
684
  const cwdCompose = path3.resolve("docker-compose.yml");
555
685
  const cwdEnv = path3.resolve(".env");
686
+ const packagedManifest = path3.join(resolvePackageRoot(), "deploy", "stack-manifests", "v0.9.json");
687
+ const manifestRef = process.env.EXE_STACK_MANIFEST || (existsSync4(packagedManifest) ? packagedManifest : "https://update.askexe.com/stack-manifest.json");
556
688
  return {
557
689
  composeFile: process.env.EXE_STACK_COMPOSE_FILE || (existsSync4(cwdCompose) ? cwdCompose : "/opt/exe-stack/docker-compose.yml"),
558
690
  envFile: process.env.EXE_STACK_ENV_FILE || (existsSync4(cwdEnv) ? cwdEnv : "/opt/exe-stack/.env"),
559
- manifestRef: process.env.EXE_STACK_MANIFEST || "https://update.askexe.com/stack-manifest.json",
560
- auditUrl: process.env.EXE_STACK_AUDIT_URL || "https://update.askexe.com/v1/deploy-audits",
561
- imageCredentialsUrl: process.env.EXE_STACK_IMAGE_CREDENTIALS_URL || "https://update.askexe.com/v1/image-credentials",
691
+ manifestRef,
692
+ // Only call update.askexe.com if explicitly configured or if a remote manifest was requested.
693
+ // Packaged manifests keep cold-start installs unblocked even before update-service entitlements are provisioned.
694
+ auditUrl: process.env.EXE_STACK_AUDIT_URL || (/^https?:\/\//.test(manifestRef) ? "https://update.askexe.com/v1/deploy-audits" : void 0),
695
+ imageCredentialsUrl: process.env.EXE_STACK_IMAGE_CREDENTIALS_URL || (/^https?:\/\//.test(manifestRef) ? "https://update.askexe.com/v1/image-credentials" : void 0),
562
696
  manifestAuthToken: process.env.EXE_STACK_UPDATE_TOKEN || process.env.EXE_LICENSE_KEY || loadLicense() || void 0,
563
697
  manifestPublicKey: loadDefaultPublicKey()
564
698
  };
@@ -589,7 +723,8 @@ function parseArgs(args) {
589
723
  rollback: false,
590
724
  deploymentPersona: process.env.EXE_STACK_DEPLOYMENT_PERSONA === "askexe-control-plane" ? "askexe-control-plane" : "customer",
591
725
  yes: false,
592
- allowedBreakingChangeIds: []
726
+ allowedBreakingChangeIds: [],
727
+ noBootstrap: false
593
728
  };
594
729
  for (let i = 0; i < args.length; i++) {
595
730
  const arg = args[i];
@@ -600,8 +735,8 @@ function parseArgs(args) {
600
735
  else if (arg.startsWith("--target=")) opts.targetVersion = arg.split("=")[1];
601
736
  else if (arg === "--compose-file") opts.composeFile = next();
602
737
  else if (arg.startsWith("--compose-file=")) opts.composeFile = arg.split("=").slice(1).join("=");
603
- else if (arg === "--env-file") opts.envFile = next();
604
- else if (arg.startsWith("--env-file=")) opts.envFile = arg.split("=").slice(1).join("=");
738
+ else if (arg === "--env-file" || arg === "--stack-env-file") opts.envFile = next();
739
+ else if (arg.startsWith("--env-file=") || arg.startsWith("--stack-env-file=")) opts.envFile = arg.split("=").slice(1).join("=");
605
740
  else if (arg === "--lock-file") opts.lockFile = next();
606
741
  else if (arg === "--public-key") opts.manifestPublicKey = readFileSync4(next(), "utf8");
607
742
  else if (arg.startsWith("--public-key=")) opts.manifestPublicKey = readFileSync4(arg.split("=").slice(1).join("="), "utf8");
@@ -630,6 +765,9 @@ function parseArgs(args) {
630
765
  else if (arg.startsWith("--break-glass=")) opts.breakGlassReason = arg.split("=").slice(1).join("=");
631
766
  else if (arg === "--break-glass-audit-file") opts.breakGlassAuditFile = next();
632
767
  else if (arg.startsWith("--break-glass-audit-file=")) opts.breakGlassAuditFile = arg.split("=").slice(1).join("=");
768
+ else if (arg === "--domain") opts.domain = next();
769
+ else if (arg.startsWith("--domain=")) opts.domain = arg.split("=").slice(1).join("=");
770
+ else if (arg === "--no-bootstrap") opts.noBootstrap = true;
633
771
  else if (arg === "--dry-run") opts.dryRun = true;
634
772
  else if (arg === "--check") opts.check = true;
635
773
  else if (arg === "--yes" || arg === "-y") opts.yes = true;
@@ -654,10 +792,13 @@ Options:
654
792
  --manifest <ref> Stack manifest JSON path or URL (default: update.askexe.com)
655
793
  --target <version> Stack version to install (default: manifest.latest)
656
794
  --compose-file <path> docker-compose.yml path (default: ./docker-compose.yml or /opt/exe-stack/docker-compose.yml)
657
- --env-file <path> .env path (default: ./.env or /opt/exe-stack/.env)
795
+ --stack-env-file <path> .env path (default: ./.env or /opt/exe-stack/.env)
796
+ --env-file <path> Alias; prefer --stack-env-file because Node 22 reserves --env-file
658
797
  --lock-file <path> Lock file path (default: beside .env)
659
798
  --check Print available changes only
660
799
  --dry-run Plan only; do not run Docker
800
+ --domain <domain> Fill CHANGEME_DOMAIN in new stack .env
801
+ --no-bootstrap Do not create templates/hydrate .env/check host prereqs
661
802
  --public-key <path> PEM public key for signed manifest verification
662
803
  --auth-token <token> Bearer token for private update manifest/audit API
663
804
  --auth-token-env <name> Read bearer token from an environment variable
@@ -695,6 +836,18 @@ function printBreaking(changes) {
695
836
  if (c.expectedDowntimeMinutes) console.log(` Expected downtime: ${c.expectedDowntimeMinutes} minutes`);
696
837
  }
697
838
  }
839
+ function printHostReport(report) {
840
+ console.log("Host preflight:");
841
+ console.log(` Docker: ${report.dockerInstalled ? "\u2705" : "\u274C"}`);
842
+ console.log(` Docker Compose: ${report.dockerComposeInstalled ? "\u2705" : "\u274C"}`);
843
+ console.log(` Compose file: ${report.composeFileExists ? "\u2705" : "\u274C"}`);
844
+ console.log(` Env file: ${report.envFileExists ? "\u2705" : "\u274C"}`);
845
+ console.log(` License: ${report.licensePresent ? "\u2705" : "\u274C"}`);
846
+ if (report.createdFiles.length > 0) console.log(` Created: ${report.createdFiles.join(", ")}`);
847
+ if (report.actions.length > 0) console.log(` Actions: ${report.actions.join(", ")}`);
848
+ if (report.envRemainingPlaceholders.length > 0) console.log(` Remaining placeholders: ${report.envRemainingPlaceholders.join(", ")}`);
849
+ console.log("");
850
+ }
698
851
  async function main(args = process.argv.slice(2)) {
699
852
  const opts = parseArgs(args);
700
853
  if (opts.rollback) {
@@ -706,6 +859,12 @@ async function main(args = process.argv.slice(2)) {
706
859
  console.log(`\u2705 Stack rollback attempted using backup: ${result2.backupEnvFile ?? "latest backup"}`);
707
860
  return;
708
861
  }
862
+ let hostReport;
863
+ if (!opts.noBootstrap) {
864
+ hostReport = bootstrapStackHost({ ...opts, installDocker: opts.yes, domain: opts.domain });
865
+ printHostReport(hostReport);
866
+ if (!opts.check && !opts.dryRun) assertHostReadyForApply(hostReport);
867
+ }
709
868
  const manifest = await loadStackManifest(opts.manifestRef, void 0, opts.manifestPublicKey, opts.manifestAuthToken);
710
869
  const envRaw = readFileSync4(opts.envFile, "utf8");
711
870
  const plan = createStackUpdatePlan(manifest, envRaw, opts.targetVersion);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askexenow/exe-os",
3
- "version": "0.9.82",
3
+ "version": "0.9.84",
4
4
  "description": "AI employee operating system — persistent memory, task management, and multi-agent coordination for Claude Code.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -57,7 +57,8 @@
57
57
  "stack.release.json",
58
58
  "stack.release.schema.json",
59
59
  "package.json",
60
- "LICENSE"
60
+ "LICENSE",
61
+ "deploy/compose"
61
62
  ],
62
63
  "exports": {
63
64
  ".": "./dist/index.js",
@@ -4,8 +4,8 @@
4
4
  "repo": "AskExe/exe-os",
5
5
  "service": "exed",
6
6
  "packageName": "@askexenow/exe-os",
7
- "version": "0.9.5",
8
- "image": "ghcr.io/askexe/exed:v0.9.5",
7
+ "version": "0.9.7",
8
+ "image": "ghcr.io/askexe/exed:v0.9.7",
9
9
  "imageEnv": "EXED_IMAGE_TAG",
10
10
  "stackParticipation": {
11
11
  "required": true,
@@ -42,9 +42,9 @@
42
42
  "breakingChanges": [],
43
43
  "dataSovereignty": "Customer-local memory/tasks/behaviors stay in SQLCipher/local storage. Updates must not overwrite roster, identity, behavior, or local memory files.",
44
44
  "releaseLine": "v0.9 private/customer pilot; v1.0 is public-beta stable.",
45
- "highGhostStack": "0.9.5",
45
+ "highGhostStack": "0.9.7",
46
46
  "deploymentScope": "customer"
47
47
  },
48
48
  "deploymentScope": "customer",
49
- "highGhostStack": "0.9.5"
49
+ "highGhostStack": "0.9.7"
50
50
  }