episoda 0.2.33 → 0.2.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1549,15 +1549,15 @@ var require_git_executor = __commonJS({
1549
1549
  try {
1550
1550
  const { stdout: gitDir } = await execAsync("git rev-parse --git-dir", { cwd, timeout: 5e3 });
1551
1551
  const gitDirPath = gitDir.trim();
1552
- const fs13 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1552
+ const fs15 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1553
1553
  const rebaseMergePath = `${gitDirPath}/rebase-merge`;
1554
1554
  const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
1555
1555
  try {
1556
- await fs13.access(rebaseMergePath);
1556
+ await fs15.access(rebaseMergePath);
1557
1557
  inRebase = true;
1558
1558
  } catch {
1559
1559
  try {
1560
- await fs13.access(rebaseApplyPath);
1560
+ await fs15.access(rebaseApplyPath);
1561
1561
  inRebase = true;
1562
1562
  } catch {
1563
1563
  inRebase = false;
@@ -1611,9 +1611,9 @@ var require_git_executor = __commonJS({
1611
1611
  error: validation.error || "UNKNOWN_ERROR"
1612
1612
  };
1613
1613
  }
1614
- const fs13 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1614
+ const fs15 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1615
1615
  try {
1616
- await fs13.access(command.path);
1616
+ await fs15.access(command.path);
1617
1617
  return {
1618
1618
  success: false,
1619
1619
  error: "WORKTREE_EXISTS",
@@ -1667,9 +1667,9 @@ var require_git_executor = __commonJS({
1667
1667
  */
1668
1668
  async executeWorktreeRemove(command, cwd, options) {
1669
1669
  try {
1670
- const fs13 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1670
+ const fs15 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1671
1671
  try {
1672
- await fs13.access(command.path);
1672
+ await fs15.access(command.path);
1673
1673
  } catch {
1674
1674
  return {
1675
1675
  success: false,
@@ -1822,10 +1822,10 @@ var require_git_executor = __commonJS({
1822
1822
  */
1823
1823
  async executeCloneBare(command, options) {
1824
1824
  try {
1825
- const fs13 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1826
- const path15 = await Promise.resolve().then(() => __importStar(require("path")));
1825
+ const fs15 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1826
+ const path17 = await Promise.resolve().then(() => __importStar(require("path")));
1827
1827
  try {
1828
- await fs13.access(command.path);
1828
+ await fs15.access(command.path);
1829
1829
  return {
1830
1830
  success: false,
1831
1831
  error: "BRANCH_ALREADY_EXISTS",
@@ -1834,9 +1834,9 @@ var require_git_executor = __commonJS({
1834
1834
  };
1835
1835
  } catch {
1836
1836
  }
1837
- const parentDir = path15.dirname(command.path);
1837
+ const parentDir = path17.dirname(command.path);
1838
1838
  try {
1839
- await fs13.mkdir(parentDir, { recursive: true });
1839
+ await fs15.mkdir(parentDir, { recursive: true });
1840
1840
  } catch {
1841
1841
  }
1842
1842
  const { stdout, stderr } = await execAsync(
@@ -1879,22 +1879,22 @@ var require_git_executor = __commonJS({
1879
1879
  */
1880
1880
  async executeProjectInfo(cwd, options) {
1881
1881
  try {
1882
- const fs13 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1883
- const path15 = await Promise.resolve().then(() => __importStar(require("path")));
1882
+ const fs15 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1883
+ const path17 = await Promise.resolve().then(() => __importStar(require("path")));
1884
1884
  let currentPath = cwd;
1885
1885
  let projectPath = cwd;
1886
1886
  let bareRepoPath;
1887
1887
  for (let i = 0; i < 10; i++) {
1888
- const bareDir = path15.join(currentPath, ".bare");
1889
- const episodaDir = path15.join(currentPath, ".episoda");
1888
+ const bareDir = path17.join(currentPath, ".bare");
1889
+ const episodaDir = path17.join(currentPath, ".episoda");
1890
1890
  try {
1891
- await fs13.access(bareDir);
1892
- await fs13.access(episodaDir);
1891
+ await fs15.access(bareDir);
1892
+ await fs15.access(episodaDir);
1893
1893
  projectPath = currentPath;
1894
1894
  bareRepoPath = bareDir;
1895
1895
  break;
1896
1896
  } catch {
1897
- const parentPath = path15.dirname(currentPath);
1897
+ const parentPath = path17.dirname(currentPath);
1898
1898
  if (parentPath === currentPath) {
1899
1899
  break;
1900
1900
  }
@@ -2107,7 +2107,7 @@ var require_websocket_client = __commonJS({
2107
2107
  clearTimeout(this.reconnectTimeout);
2108
2108
  this.reconnectTimeout = void 0;
2109
2109
  }
2110
- return new Promise((resolve4, reject) => {
2110
+ return new Promise((resolve5, reject) => {
2111
2111
  const connectionTimeout = setTimeout(() => {
2112
2112
  if (this.ws) {
2113
2113
  this.ws.terminate();
@@ -2134,7 +2134,7 @@ var require_websocket_client = __commonJS({
2134
2134
  daemonPid: this.daemonPid
2135
2135
  });
2136
2136
  this.startHeartbeat();
2137
- resolve4();
2137
+ resolve5();
2138
2138
  });
2139
2139
  this.ws.on("pong", () => {
2140
2140
  if (this.heartbeatTimeoutTimer) {
@@ -2250,13 +2250,13 @@ var require_websocket_client = __commonJS({
2250
2250
  if (!this.ws || !this.isConnected) {
2251
2251
  throw new Error("WebSocket not connected");
2252
2252
  }
2253
- return new Promise((resolve4, reject) => {
2253
+ return new Promise((resolve5, reject) => {
2254
2254
  this.ws.send(JSON.stringify(message), (error) => {
2255
2255
  if (error) {
2256
2256
  console.error("[EpisodaClient] Failed to send message:", error);
2257
2257
  reject(error);
2258
2258
  } else {
2259
- resolve4();
2259
+ resolve5();
2260
2260
  }
2261
2261
  });
2262
2262
  });
@@ -2485,34 +2485,34 @@ var require_auth = __commonJS({
2485
2485
  Object.defineProperty(exports2, "__esModule", { value: true });
2486
2486
  exports2.getConfigDir = getConfigDir5;
2487
2487
  exports2.getConfigPath = getConfigPath4;
2488
- exports2.loadConfig = loadConfig6;
2488
+ exports2.loadConfig = loadConfig9;
2489
2489
  exports2.saveConfig = saveConfig3;
2490
2490
  exports2.validateToken = validateToken;
2491
- var fs13 = __importStar(require("fs"));
2492
- var path15 = __importStar(require("path"));
2493
- var os3 = __importStar(require("os"));
2491
+ var fs15 = __importStar(require("fs"));
2492
+ var path17 = __importStar(require("path"));
2493
+ var os4 = __importStar(require("os"));
2494
2494
  var child_process_1 = require("child_process");
2495
2495
  var DEFAULT_CONFIG_FILE = "config.json";
2496
2496
  function getConfigDir5() {
2497
- return process.env.EPISODA_CONFIG_DIR || path15.join(os3.homedir(), ".episoda");
2497
+ return process.env.EPISODA_CONFIG_DIR || path17.join(os4.homedir(), ".episoda");
2498
2498
  }
2499
2499
  function getConfigPath4(configPath) {
2500
2500
  if (configPath) {
2501
2501
  return configPath;
2502
2502
  }
2503
- return path15.join(getConfigDir5(), DEFAULT_CONFIG_FILE);
2503
+ return path17.join(getConfigDir5(), DEFAULT_CONFIG_FILE);
2504
2504
  }
2505
2505
  function ensureConfigDir(configPath) {
2506
- const dir = path15.dirname(configPath);
2507
- const isNew = !fs13.existsSync(dir);
2506
+ const dir = path17.dirname(configPath);
2507
+ const isNew = !fs15.existsSync(dir);
2508
2508
  if (isNew) {
2509
- fs13.mkdirSync(dir, { recursive: true, mode: 448 });
2509
+ fs15.mkdirSync(dir, { recursive: true, mode: 448 });
2510
2510
  }
2511
2511
  if (process.platform === "darwin") {
2512
- const nosyncPath = path15.join(dir, ".nosync");
2513
- if (isNew || !fs13.existsSync(nosyncPath)) {
2512
+ const nosyncPath = path17.join(dir, ".nosync");
2513
+ if (isNew || !fs15.existsSync(nosyncPath)) {
2514
2514
  try {
2515
- fs13.writeFileSync(nosyncPath, "", { mode: 384 });
2515
+ fs15.writeFileSync(nosyncPath, "", { mode: 384 });
2516
2516
  (0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
2517
2517
  stdio: "ignore",
2518
2518
  timeout: 5e3
@@ -2522,13 +2522,13 @@ var require_auth = __commonJS({
2522
2522
  }
2523
2523
  }
2524
2524
  }
2525
- async function loadConfig6(configPath) {
2525
+ async function loadConfig9(configPath) {
2526
2526
  const fullPath = getConfigPath4(configPath);
2527
- if (!fs13.existsSync(fullPath)) {
2527
+ if (!fs15.existsSync(fullPath)) {
2528
2528
  return null;
2529
2529
  }
2530
2530
  try {
2531
- const content = fs13.readFileSync(fullPath, "utf8");
2531
+ const content = fs15.readFileSync(fullPath, "utf8");
2532
2532
  const config = JSON.parse(content);
2533
2533
  return config;
2534
2534
  } catch (error) {
@@ -2541,7 +2541,7 @@ var require_auth = __commonJS({
2541
2541
  ensureConfigDir(fullPath);
2542
2542
  try {
2543
2543
  const content = JSON.stringify(config, null, 2);
2544
- fs13.writeFileSync(fullPath, content, { mode: 384 });
2544
+ fs15.writeFileSync(fullPath, content, { mode: 384 });
2545
2545
  } catch (error) {
2546
2546
  throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
2547
2547
  }
@@ -2652,7 +2652,7 @@ var require_dist = __commonJS({
2652
2652
 
2653
2653
  // src/index.ts
2654
2654
  var import_commander = require("commander");
2655
- var import_core15 = __toESM(require_dist());
2655
+ var import_core18 = __toESM(require_dist());
2656
2656
 
2657
2657
  // src/commands/dev.ts
2658
2658
  var import_core4 = __toESM(require_dist());
@@ -2989,7 +2989,7 @@ async function startDaemon() {
2989
2989
  const pid = child.pid;
2990
2990
  const pidPath = getPidFilePath();
2991
2991
  fs2.writeFileSync(pidPath, pid.toString(), "utf-8");
2992
- await new Promise((resolve4) => setTimeout(resolve4, 500));
2992
+ await new Promise((resolve5) => setTimeout(resolve5, 500));
2993
2993
  const runningPid = isDaemonRunning();
2994
2994
  if (!runningPid) {
2995
2995
  throw new Error("Daemon failed to start");
@@ -3011,7 +3011,7 @@ async function stopDaemon(timeout = 5e3) {
3011
3011
  while (Date.now() - startTime < timeout) {
3012
3012
  try {
3013
3013
  process.kill(pid, 0);
3014
- await new Promise((resolve4) => setTimeout(resolve4, 100));
3014
+ await new Promise((resolve5) => setTimeout(resolve5, 100));
3015
3015
  } catch (error) {
3016
3016
  const pidPath2 = getPidFilePath();
3017
3017
  if (fs2.existsSync(pidPath2)) {
@@ -3041,7 +3041,7 @@ var import_core2 = __toESM(require_dist());
3041
3041
  var getSocketPath = () => path3.join((0, import_core2.getConfigDir)(), "daemon.sock");
3042
3042
  var DEFAULT_TIMEOUT = 15e3;
3043
3043
  async function sendCommand(command, params, timeout = DEFAULT_TIMEOUT) {
3044
- return new Promise((resolve4, reject) => {
3044
+ return new Promise((resolve5, reject) => {
3045
3045
  const socket = net.createConnection(getSocketPath());
3046
3046
  const requestId = crypto.randomUUID();
3047
3047
  let buffer = "";
@@ -3072,7 +3072,7 @@ async function sendCommand(command, params, timeout = DEFAULT_TIMEOUT) {
3072
3072
  clearTimeout(timeoutHandle);
3073
3073
  socket.end();
3074
3074
  if (response.success) {
3075
- resolve4(response.data);
3075
+ resolve5(response.data);
3076
3076
  } else {
3077
3077
  reject(new Error(response.error || "Command failed"));
3078
3078
  }
@@ -3135,18 +3135,18 @@ var fs5 = __toESM(require("fs"));
3135
3135
  // src/utils/port-check.ts
3136
3136
  var net2 = __toESM(require("net"));
3137
3137
  async function isPortInUse(port) {
3138
- return new Promise((resolve4) => {
3138
+ return new Promise((resolve5) => {
3139
3139
  const server = net2.createServer();
3140
3140
  server.once("error", (err) => {
3141
3141
  if (err.code === "EADDRINUSE") {
3142
- resolve4(true);
3142
+ resolve5(true);
3143
3143
  } else {
3144
- resolve4(false);
3144
+ resolve5(false);
3145
3145
  }
3146
3146
  });
3147
3147
  server.once("listening", () => {
3148
3148
  server.close();
3149
- resolve4(false);
3149
+ resolve5(false);
3150
3150
  });
3151
3151
  server.listen(port);
3152
3152
  });
@@ -3518,7 +3518,7 @@ var WorktreeManager = class _WorktreeManager {
3518
3518
  const lockContent = fs3.readFileSync(lockPath, "utf-8").trim();
3519
3519
  const lockPid = parseInt(lockContent, 10);
3520
3520
  if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
3521
- await new Promise((resolve4) => setTimeout(resolve4, retryInterval));
3521
+ await new Promise((resolve5) => setTimeout(resolve5, retryInterval));
3522
3522
  continue;
3523
3523
  }
3524
3524
  } catch {
@@ -3532,7 +3532,7 @@ var WorktreeManager = class _WorktreeManager {
3532
3532
  } catch {
3533
3533
  continue;
3534
3534
  }
3535
- await new Promise((resolve4) => setTimeout(resolve4, retryInterval));
3535
+ await new Promise((resolve5) => setTimeout(resolve5, retryInterval));
3536
3536
  continue;
3537
3537
  }
3538
3538
  throw err;
@@ -3799,7 +3799,7 @@ async function fetchProjectPath(config, projectId) {
3799
3799
  return null;
3800
3800
  }
3801
3801
  }
3802
- async function syncProjectPath(config, projectId, path15) {
3802
+ async function syncProjectPath(config, projectId, path17) {
3803
3803
  if (!config.device_id || !config.access_token) {
3804
3804
  return false;
3805
3805
  }
@@ -3812,7 +3812,7 @@ async function syncProjectPath(config, projectId, path15) {
3812
3812
  "Authorization": `Bearer ${config.access_token}`,
3813
3813
  "Content-Type": "application/json"
3814
3814
  },
3815
- body: JSON.stringify({ path: path15 })
3815
+ body: JSON.stringify({ path: path17 })
3816
3816
  });
3817
3817
  if (!response.ok) {
3818
3818
  console.debug(`[MachineSettings] syncProjectPath failed: ${response.status} ${response.statusText}`);
@@ -3903,7 +3903,7 @@ async function devCommand(options = {}) {
3903
3903
  const killedCount = killAllEpisodaProcesses();
3904
3904
  if (killedCount > 0) {
3905
3905
  status.info(`Cleaned up ${killedCount} stale process${killedCount > 1 ? "es" : ""}`);
3906
- await new Promise((resolve4) => setTimeout(resolve4, 2e3));
3906
+ await new Promise((resolve5) => setTimeout(resolve5, 2e3));
3907
3907
  }
3908
3908
  }
3909
3909
  let projectPath;
@@ -3963,7 +3963,7 @@ async function devCommand(options = {}) {
3963
3963
  for (let retry = 0; retry < CONNECTION_MAX_RETRIES && !connected; retry++) {
3964
3964
  if (retry > 0) {
3965
3965
  status.info(`Retrying connection (attempt ${retry + 1}/${CONNECTION_MAX_RETRIES})...`);
3966
- await new Promise((resolve4) => setTimeout(resolve4, 1e3));
3966
+ await new Promise((resolve5) => setTimeout(resolve5, 1e3));
3967
3967
  }
3968
3968
  try {
3969
3969
  const result = await addProject(config.project_id, projectPath);
@@ -4586,10 +4586,10 @@ async function initiateDeviceFlow(apiUrl, machineId) {
4586
4586
  return await response.json();
4587
4587
  }
4588
4588
  async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
4589
- return new Promise((resolve4) => {
4589
+ return new Promise((resolve5) => {
4590
4590
  const timeout = setTimeout(() => {
4591
4591
  status.error("Authorization timed out");
4592
- resolve4(false);
4592
+ resolve5(false);
4593
4593
  }, expiresIn * 1e3);
4594
4594
  const url = `${apiUrl}/api/oauth/authorize-stream?device_code=${deviceCode}`;
4595
4595
  const curlProcess = (0, import_child_process5.spawn)("curl", ["-N", url]);
@@ -4615,26 +4615,26 @@ async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
4615
4615
  if (eventType === "authorized") {
4616
4616
  clearTimeout(timeout);
4617
4617
  curlProcess.kill();
4618
- resolve4(true);
4618
+ resolve5(true);
4619
4619
  return;
4620
4620
  } else if (eventType === "denied") {
4621
4621
  clearTimeout(timeout);
4622
4622
  curlProcess.kill();
4623
4623
  status.error("Authorization denied by user");
4624
- resolve4(false);
4624
+ resolve5(false);
4625
4625
  return;
4626
4626
  } else if (eventType === "expired") {
4627
4627
  clearTimeout(timeout);
4628
4628
  curlProcess.kill();
4629
4629
  status.error("Authorization code expired");
4630
- resolve4(false);
4630
+ resolve5(false);
4631
4631
  return;
4632
4632
  } else if (eventType === "error") {
4633
4633
  const errorData = JSON.parse(data);
4634
4634
  clearTimeout(timeout);
4635
4635
  curlProcess.kill();
4636
4636
  status.error(`Authorization error: ${errorData.error_description || errorData.error || "Unknown error"}`);
4637
- resolve4(false);
4637
+ resolve5(false);
4638
4638
  return;
4639
4639
  }
4640
4640
  } catch (error) {
@@ -4644,13 +4644,13 @@ async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
4644
4644
  curlProcess.on("error", (error) => {
4645
4645
  clearTimeout(timeout);
4646
4646
  status.error(`Failed to monitor authorization: ${error.message}`);
4647
- resolve4(false);
4647
+ resolve5(false);
4648
4648
  });
4649
4649
  curlProcess.on("close", (code) => {
4650
4650
  clearTimeout(timeout);
4651
4651
  if (code !== 0 && code !== null) {
4652
4652
  status.error(`Authorization monitoring failed with code ${code}`);
4653
- resolve4(false);
4653
+ resolve5(false);
4654
4654
  }
4655
4655
  });
4656
4656
  });
@@ -5406,7 +5406,7 @@ async function fetchWithRetry(url, options, maxRetries = 3) {
5406
5406
  lastError = error instanceof Error ? error : new Error("Network error");
5407
5407
  }
5408
5408
  if (attempt < maxRetries - 1) {
5409
- await new Promise((resolve4) => setTimeout(resolve4, Math.pow(2, attempt) * 1e3));
5409
+ await new Promise((resolve5) => setTimeout(resolve5, Math.pow(2, attempt) * 1e3));
5410
5410
  }
5411
5411
  }
5412
5412
  throw lastError || new Error("Request failed after retries");
@@ -5989,7 +5989,7 @@ async function updateCommand(options = {}) {
5989
5989
  }
5990
5990
  if (options.restart && daemonWasRunning) {
5991
5991
  status.info("Restarting daemon...");
5992
- await new Promise((resolve4) => setTimeout(resolve4, 1e3));
5992
+ await new Promise((resolve5) => setTimeout(resolve5, 1e3));
5993
5993
  if (startDaemon2()) {
5994
5994
  status.success("\u2713 Daemon restarted");
5995
5995
  status.info("");
@@ -6010,8 +6010,534 @@ async function updateCommand(options = {}) {
6010
6010
  }
6011
6011
  }
6012
6012
 
6013
+ // src/commands/run.ts
6014
+ var import_child_process9 = require("child_process");
6015
+ var import_core15 = __toESM(require_dist());
6016
+
6017
+ // src/utils/env-cache.ts
6018
+ var fs13 = __toESM(require("fs"));
6019
+ var path15 = __toESM(require("path"));
6020
+ var os3 = __toESM(require("os"));
6021
+ var DEFAULT_CACHE_TTL = 60;
6022
+ var CACHE_DIR = path15.join(os3.homedir(), ".episoda", "cache");
6023
+ function getCacheFilePath(projectId) {
6024
+ return path15.join(CACHE_DIR, `env-vars-${projectId}.json`);
6025
+ }
6026
+ function ensureCacheDir() {
6027
+ if (!fs13.existsSync(CACHE_DIR)) {
6028
+ fs13.mkdirSync(CACHE_DIR, { recursive: true, mode: 448 });
6029
+ }
6030
+ }
6031
+ function readCache(projectId) {
6032
+ try {
6033
+ const cacheFile = getCacheFilePath(projectId);
6034
+ if (!fs13.existsSync(cacheFile)) {
6035
+ return null;
6036
+ }
6037
+ const content = fs13.readFileSync(cacheFile, "utf-8");
6038
+ const data = JSON.parse(content);
6039
+ if (!data.vars || typeof data.vars !== "object" || !data.fetchedAt) {
6040
+ return null;
6041
+ }
6042
+ return data;
6043
+ } catch {
6044
+ return null;
6045
+ }
6046
+ }
6047
+ function writeCache(projectId, vars) {
6048
+ try {
6049
+ ensureCacheDir();
6050
+ const cacheFile = getCacheFilePath(projectId);
6051
+ const data = {
6052
+ vars,
6053
+ fetchedAt: Date.now(),
6054
+ projectId
6055
+ };
6056
+ fs13.writeFileSync(cacheFile, JSON.stringify(data, null, 2), { mode: 384 });
6057
+ } catch (error) {
6058
+ console.warn("[env-cache] Failed to write cache:", error instanceof Error ? error.message : error);
6059
+ }
6060
+ }
6061
+ function isCacheValid(cache, ttlSeconds) {
6062
+ const ageMs = Date.now() - cache.fetchedAt;
6063
+ return ageMs < ttlSeconds * 1e3;
6064
+ }
6065
+ async function fetchEnvVarsWithCache(apiUrl, accessToken, options = {}) {
6066
+ const {
6067
+ noCache = false,
6068
+ cacheTtl = DEFAULT_CACHE_TTL,
6069
+ offline = false,
6070
+ projectId = "default"
6071
+ } = options;
6072
+ if (offline) {
6073
+ const cache = readCache(projectId);
6074
+ if (cache && Object.keys(cache.vars).length > 0) {
6075
+ return {
6076
+ envVars: cache.vars,
6077
+ fromCache: true,
6078
+ cacheAge: Date.now() - cache.fetchedAt
6079
+ };
6080
+ }
6081
+ throw new Error(
6082
+ "Offline mode requires cached env vars, but no cache found.\nRun without --offline first to populate the cache."
6083
+ );
6084
+ }
6085
+ if (!noCache) {
6086
+ const cache = readCache(projectId);
6087
+ if (cache && isCacheValid(cache, cacheTtl)) {
6088
+ return {
6089
+ envVars: cache.vars,
6090
+ fromCache: true,
6091
+ cacheAge: Date.now() - cache.fetchedAt
6092
+ };
6093
+ }
6094
+ }
6095
+ try {
6096
+ const envVars = await fetchEnvVars(apiUrl, accessToken);
6097
+ if (Object.keys(envVars).length > 0) {
6098
+ writeCache(projectId, envVars);
6099
+ }
6100
+ return {
6101
+ envVars,
6102
+ fromCache: false
6103
+ };
6104
+ } catch (error) {
6105
+ const cache = readCache(projectId);
6106
+ if (cache && Object.keys(cache.vars).length > 0) {
6107
+ const cacheAge = Date.now() - cache.fetchedAt;
6108
+ console.warn(
6109
+ `[env-cache] Failed to fetch env vars, using stale cache (${Math.round(cacheAge / 1e3)}s old)`
6110
+ );
6111
+ return {
6112
+ envVars: cache.vars,
6113
+ fromCache: true,
6114
+ cacheAge
6115
+ };
6116
+ }
6117
+ throw new Error(
6118
+ `Failed to fetch environment variables: ${error instanceof Error ? error.message : error}
6119
+ No cached values available as fallback.`
6120
+ );
6121
+ }
6122
+ }
6123
+
6124
+ // src/commands/run.ts
6125
+ async function runCommand(args, options = {}) {
6126
+ if (args.length === 0) {
6127
+ throw new Error(
6128
+ "No command specified.\n\nUsage:\n episoda run <command> [args...]\n episoda run -- <command> [args...]\n\nExamples:\n episoda run npm run dev\n episoda run -- npm run dev --port 3001\n episoda run python manage.py runserver"
6129
+ );
6130
+ }
6131
+ const config = await (0, import_core15.loadConfig)();
6132
+ if (!config || !config.access_token) {
6133
+ throw new Error(
6134
+ "Not authenticated. Please run `episoda auth` first."
6135
+ );
6136
+ }
6137
+ const apiUrl = config.api_url || "https://episoda.dev";
6138
+ const cacheOptions = {
6139
+ noCache: options.noCache,
6140
+ cacheTtl: options.cacheTtl,
6141
+ offline: options.offline,
6142
+ projectId: config.project_id
6143
+ };
6144
+ if (options.verbose) {
6145
+ status.info("Fetching environment variables...");
6146
+ }
6147
+ const { envVars, fromCache, cacheAge } = await fetchEnvVarsWithCache(
6148
+ apiUrl,
6149
+ config.access_token,
6150
+ cacheOptions
6151
+ );
6152
+ const varCount = Object.keys(envVars).length;
6153
+ if (varCount === 0) {
6154
+ status.warning("No environment variables found. Command will run with system env only.");
6155
+ } else if (options.verbose) {
6156
+ if (fromCache) {
6157
+ status.info(`Using ${varCount} cached env vars (${Math.round(cacheAge / 1e3)}s old)`);
6158
+ } else {
6159
+ status.info(`Fetched ${varCount} env vars from server`);
6160
+ }
6161
+ status.info(`Variables: ${Object.keys(envVars).join(", ")}`);
6162
+ }
6163
+ const mergedEnv = {
6164
+ ...process.env,
6165
+ ...envVars
6166
+ };
6167
+ const [command, ...commandArgs] = args;
6168
+ if (options.verbose) {
6169
+ status.info(`Running: ${command} ${commandArgs.join(" ")}`);
6170
+ status.info("");
6171
+ }
6172
+ const child = (0, import_child_process9.spawn)(command, commandArgs, {
6173
+ stdio: "inherit",
6174
+ env: mergedEnv,
6175
+ shell: process.platform === "win32"
6176
+ // Use shell on Windows for .cmd/.bat files
6177
+ });
6178
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
6179
+ const signalHandlers = /* @__PURE__ */ new Map();
6180
+ for (const signal of signals) {
6181
+ const handler = () => {
6182
+ if (child.pid) {
6183
+ child.kill(signal);
6184
+ }
6185
+ };
6186
+ signalHandlers.set(signal, handler);
6187
+ process.on(signal, handler);
6188
+ }
6189
+ child.on("error", (error) => {
6190
+ for (const [signal, handler] of signalHandlers) {
6191
+ process.removeListener(signal, handler);
6192
+ }
6193
+ if (error.code === "ENOENT") {
6194
+ status.error(`Command not found: ${command}`);
6195
+ process.exit(127);
6196
+ } else {
6197
+ status.error(`Failed to start command: ${error.message}`);
6198
+ process.exit(1);
6199
+ }
6200
+ });
6201
+ child.on("exit", (code, signal) => {
6202
+ for (const [sig, handler] of signalHandlers) {
6203
+ process.removeListener(sig, handler);
6204
+ }
6205
+ if (signal) {
6206
+ process.exit(128 + (signalToNumber(signal) || 0));
6207
+ } else {
6208
+ process.exit(code ?? 1);
6209
+ }
6210
+ });
6211
+ }
6212
+ function signalToNumber(signal) {
6213
+ const signalMap = {
6214
+ SIGHUP: 1,
6215
+ SIGINT: 2,
6216
+ SIGQUIT: 3,
6217
+ SIGTERM: 15
6218
+ };
6219
+ return signalMap[signal];
6220
+ }
6221
+
6222
+ // src/commands/shell.ts
6223
+ var import_child_process10 = require("child_process");
6224
+ var import_core16 = __toESM(require_dist());
6225
+ async function shellCommand(shell, options = {}) {
6226
+ const config = await (0, import_core16.loadConfig)();
6227
+ if (!config || !config.access_token) {
6228
+ throw new Error(
6229
+ "Not authenticated. Please run `episoda auth` first."
6230
+ );
6231
+ }
6232
+ const apiUrl = config.api_url || "https://episoda.dev";
6233
+ const cacheOptions = {
6234
+ noCache: options.noCache,
6235
+ cacheTtl: options.cacheTtl,
6236
+ offline: options.offline,
6237
+ projectId: config.project_id
6238
+ };
6239
+ status.info("Fetching environment variables...");
6240
+ const { envVars, fromCache, cacheAge } = await fetchEnvVarsWithCache(
6241
+ apiUrl,
6242
+ config.access_token,
6243
+ cacheOptions
6244
+ );
6245
+ const varCount = Object.keys(envVars).length;
6246
+ if (varCount === 0) {
6247
+ status.warning("No environment variables found.");
6248
+ } else if (fromCache) {
6249
+ status.info(`Using ${varCount} cached env vars (${Math.round(cacheAge / 1e3)}s old)`);
6250
+ } else {
6251
+ status.info(`Fetched ${varCount} env vars from server`);
6252
+ }
6253
+ if (options.print) {
6254
+ printExports(envVars);
6255
+ return;
6256
+ }
6257
+ const targetShell = shell || process.env.SHELL || "/bin/bash";
6258
+ status.info(`Opening ${targetShell} with ${varCount} environment variables...`);
6259
+ status.info('Type "exit" to return to your normal shell.');
6260
+ status.info("");
6261
+ const mergedEnv = {
6262
+ ...process.env,
6263
+ ...envVars,
6264
+ // Set a marker so users know they're in an episoda shell
6265
+ EPISODA_SHELL: "1"
6266
+ };
6267
+ const child = (0, import_child_process10.spawn)(targetShell, [], {
6268
+ stdio: "inherit",
6269
+ env: mergedEnv
6270
+ });
6271
+ child.on("error", (error) => {
6272
+ if (error.code === "ENOENT") {
6273
+ status.error(`Shell not found: ${targetShell}`);
6274
+ process.exit(127);
6275
+ } else {
6276
+ status.error(`Failed to start shell: ${error.message}`);
6277
+ process.exit(1);
6278
+ }
6279
+ });
6280
+ child.on("exit", (code) => {
6281
+ process.exit(code ?? 0);
6282
+ });
6283
+ }
6284
+ function printExports(envVars) {
6285
+ for (const [key, value] of Object.entries(envVars)) {
6286
+ const escapedValue = value.replace(/'/g, "'\\''");
6287
+ console.log(`export ${key}='${escapedValue}'`);
6288
+ }
6289
+ }
6290
+
6291
+ // src/commands/env.ts
6292
+ var fs14 = __toESM(require("fs"));
6293
+ var path16 = __toESM(require("path"));
6294
+ var readline = __toESM(require("readline"));
6295
+ var import_core17 = __toESM(require_dist());
6296
+ async function envListCommand(options = {}) {
6297
+ const config = await (0, import_core17.loadConfig)();
6298
+ if (!config || !config.access_token) {
6299
+ throw new Error("Not authenticated. Please run `episoda auth` first.");
6300
+ }
6301
+ const apiUrl = config.api_url || "https://episoda.dev";
6302
+ const url = `${apiUrl}/api/projects/${config.project_id}/env-vars`;
6303
+ const response = await fetch(url, {
6304
+ headers: {
6305
+ "Authorization": `Bearer ${config.access_token}`,
6306
+ "Content-Type": "application/json"
6307
+ }
6308
+ });
6309
+ if (!response.ok) {
6310
+ throw new Error(`Failed to list env vars: ${response.status} ${response.statusText}`);
6311
+ }
6312
+ const data = await response.json();
6313
+ if (!data.success || !data.env_vars) {
6314
+ throw new Error("Failed to parse env vars response");
6315
+ }
6316
+ if (data.env_vars.length === 0) {
6317
+ status.info("No environment variables configured.");
6318
+ status.info("Add one with: episoda env set KEY=value");
6319
+ return;
6320
+ }
6321
+ console.log("\nEnvironment Variables:");
6322
+ console.log("\u2500".repeat(60));
6323
+ for (const envVar of data.env_vars) {
6324
+ const envBadge = envVar.environment === "all" ? "" : ` [${envVar.environment}]`;
6325
+ const sealed = envVar.is_sealed ? " (sealed)" : "";
6326
+ if (options.showValues) {
6327
+ const preview = envVar.value_preview || "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
6328
+ console.log(` ${envVar.key}${envBadge}${sealed} = ${preview}`);
6329
+ } else {
6330
+ console.log(` ${envVar.key}${envBadge}${sealed}`);
6331
+ }
6332
+ }
6333
+ console.log("");
6334
+ console.log(`Total: ${data.env_vars.length} variable(s)`);
6335
+ if (!options.showValues) {
6336
+ console.log("\nTip: Use --show-values to see value previews");
6337
+ }
6338
+ }
6339
+ async function envSetCommand(keyValue, options = {}) {
6340
+ const config = await (0, import_core17.loadConfig)();
6341
+ if (!config || !config.access_token) {
6342
+ throw new Error("Not authenticated. Please run `episoda auth` first.");
6343
+ }
6344
+ const apiUrl = config.api_url || "https://episoda.dev";
6345
+ let key;
6346
+ let value;
6347
+ const eqIndex = keyValue.indexOf("=");
6348
+ if (eqIndex === -1) {
6349
+ key = keyValue;
6350
+ value = await promptForValue(key);
6351
+ } else {
6352
+ key = keyValue.slice(0, eqIndex);
6353
+ value = keyValue.slice(eqIndex + 1);
6354
+ }
6355
+ if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
6356
+ throw new Error(
6357
+ `Invalid key format: ${key}
6358
+ Keys must be uppercase with underscores (e.g., API_KEY, DATABASE_URL)`
6359
+ );
6360
+ }
6361
+ const url = `${apiUrl}/api/projects/${config.project_id}/env-vars`;
6362
+ const response = await fetch(url, {
6363
+ method: "POST",
6364
+ headers: {
6365
+ "Authorization": `Bearer ${config.access_token}`,
6366
+ "Content-Type": "application/json"
6367
+ },
6368
+ body: JSON.stringify({
6369
+ key,
6370
+ value,
6371
+ environment: options.environment || "dev"
6372
+ })
6373
+ });
6374
+ const data = await response.json();
6375
+ if (!response.ok) {
6376
+ if (response.status === 409) {
6377
+ status.info(`${key} already exists, updating...`);
6378
+ await updateEnvVar(apiUrl, config.access_token, config.project_id, key, value);
6379
+ return;
6380
+ }
6381
+ throw new Error(data.error?.message || `Failed to set env var: ${response.status}`);
6382
+ }
6383
+ status.success(`Set ${key} successfully`);
6384
+ }
6385
+ async function updateEnvVar(apiUrl, accessToken, projectId, key, value) {
6386
+ const listUrl = `${apiUrl}/api/projects/${projectId}/env-vars`;
6387
+ const listResponse = await fetch(listUrl, {
6388
+ headers: {
6389
+ "Authorization": `Bearer ${accessToken}`,
6390
+ "Content-Type": "application/json"
6391
+ }
6392
+ });
6393
+ if (!listResponse.ok) {
6394
+ throw new Error("Failed to find existing env var for update");
6395
+ }
6396
+ const listData = await listResponse.json();
6397
+ const existingVar = listData.env_vars?.find((v) => v.key === key);
6398
+ if (!existingVar) {
6399
+ throw new Error(`Env var ${key} not found for update`);
6400
+ }
6401
+ const updateUrl = `${apiUrl}/api/projects/${projectId}/env-vars/${existingVar.id}`;
6402
+ const updateResponse = await fetch(updateUrl, {
6403
+ method: "PATCH",
6404
+ headers: {
6405
+ "Authorization": `Bearer ${accessToken}`,
6406
+ "Content-Type": "application/json"
6407
+ },
6408
+ body: JSON.stringify({ value })
6409
+ });
6410
+ if (!updateResponse.ok) {
6411
+ throw new Error(`Failed to update env var: ${updateResponse.status}`);
6412
+ }
6413
+ status.success(`Updated ${key} successfully`);
6414
+ }
6415
+ async function envRemoveCommand(key) {
6416
+ const config = await (0, import_core17.loadConfig)();
6417
+ if (!config || !config.access_token) {
6418
+ throw new Error("Not authenticated. Please run `episoda auth` first.");
6419
+ }
6420
+ const apiUrl = config.api_url || "https://episoda.dev";
6421
+ const listUrl = `${apiUrl}/api/projects/${config.project_id}/env-vars`;
6422
+ const listResponse = await fetch(listUrl, {
6423
+ headers: {
6424
+ "Authorization": `Bearer ${config.access_token}`,
6425
+ "Content-Type": "application/json"
6426
+ }
6427
+ });
6428
+ if (!listResponse.ok) {
6429
+ throw new Error("Failed to list env vars");
6430
+ }
6431
+ const listData = await listResponse.json();
6432
+ const existingVar = listData.env_vars?.find((v) => v.key === key);
6433
+ if (!existingVar) {
6434
+ throw new Error(`Env var ${key} not found`);
6435
+ }
6436
+ const deleteUrl = `${apiUrl}/api/projects/${config.project_id}/env-vars/${existingVar.id}`;
6437
+ const deleteResponse = await fetch(deleteUrl, {
6438
+ method: "DELETE",
6439
+ headers: {
6440
+ "Authorization": `Bearer ${config.access_token}`
6441
+ }
6442
+ });
6443
+ if (!deleteResponse.ok) {
6444
+ throw new Error(`Failed to remove env var: ${deleteResponse.status}`);
6445
+ }
6446
+ status.success(`Removed ${key}`);
6447
+ }
6448
+ async function envPullCommand(options = {}) {
6449
+ const config = await (0, import_core17.loadConfig)();
6450
+ if (!config || !config.access_token) {
6451
+ throw new Error("Not authenticated. Please run `episoda auth` first.");
6452
+ }
6453
+ const apiUrl = config.api_url || "https://episoda.dev";
6454
+ status.info("Fetching environment variables...");
6455
+ const envVars = await fetchEnvVars(apiUrl, config.access_token);
6456
+ if (Object.keys(envVars).length === 0) {
6457
+ status.warning("No environment variables found.");
6458
+ return;
6459
+ }
6460
+ const header = [
6461
+ "# AUTO-GENERATED by episoda env pull",
6462
+ "# Do not edit - changes will be overwritten",
6463
+ `# Source of truth: ${apiUrl}/settings/env-vars`,
6464
+ `# Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
6465
+ "#",
6466
+ "# To update: episoda env set KEY=value",
6467
+ "# To refresh: episoda env pull",
6468
+ ""
6469
+ ].join("\n");
6470
+ const envContent = Object.entries(envVars).map(([key, value]) => {
6471
+ if (/[\s'"#$`\\]/.test(value) || value.includes("\n")) {
6472
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
6473
+ return `${key}="${escaped}"`;
6474
+ }
6475
+ return `${key}=${value}`;
6476
+ }).join("\n") + "\n";
6477
+ const fullContent = header + envContent;
6478
+ if (options.stdout) {
6479
+ console.log(fullContent);
6480
+ return;
6481
+ }
6482
+ const filename = options.file || ".env";
6483
+ const filepath = path16.resolve(process.cwd(), filename);
6484
+ fs14.writeFileSync(filepath, fullContent, { mode: 384 });
6485
+ status.success(`Wrote ${Object.keys(envVars).length} env vars to ${filename}`);
6486
+ }
6487
+ async function promptForValue(key) {
6488
+ if (!process.stdin.isTTY) {
6489
+ return new Promise((resolve5, reject) => {
6490
+ const rl = readline.createInterface({
6491
+ input: process.stdin,
6492
+ output: process.stdout
6493
+ });
6494
+ rl.question(`Enter value for ${key}: `, (answer) => {
6495
+ rl.close();
6496
+ resolve5(answer);
6497
+ });
6498
+ rl.on("close", () => {
6499
+ reject(new Error("Input closed"));
6500
+ });
6501
+ });
6502
+ }
6503
+ return new Promise((resolve5, reject) => {
6504
+ const rl = readline.createInterface({
6505
+ input: process.stdin,
6506
+ output: process.stdout
6507
+ });
6508
+ let value = "";
6509
+ let resolved = false;
6510
+ const cleanup = () => {
6511
+ if (process.stdin.isTTY) {
6512
+ process.stdin.setRawMode(false);
6513
+ }
6514
+ process.stdin.removeListener("data", dataHandler);
6515
+ rl.close();
6516
+ };
6517
+ const dataHandler = (char) => {
6518
+ const str = char.toString();
6519
+ if (str === "\n" || str === "\r" || str === "\r\n") {
6520
+ resolved = true;
6521
+ cleanup();
6522
+ console.log("");
6523
+ resolve5(value);
6524
+ } else if (str === "") {
6525
+ cleanup();
6526
+ reject(new Error("Cancelled"));
6527
+ } else if (str === "\x7F" || str === "\b") {
6528
+ value = value.slice(0, -1);
6529
+ } else {
6530
+ value += str;
6531
+ }
6532
+ };
6533
+ process.stdin.setRawMode(true);
6534
+ process.stdout.write(`Enter value for ${key}: `);
6535
+ process.stdin.on("data", dataHandler);
6536
+ });
6537
+ }
6538
+
6013
6539
  // src/index.ts
6014
- import_commander.program.name("episoda").description("Episoda CLI - local development with git worktree isolation").version(import_core15.VERSION);
6540
+ import_commander.program.name("episoda").description("Episoda CLI - local development with git worktree isolation").version(import_core18.VERSION);
6015
6541
  import_commander.program.command("auth").description("Authenticate to Episoda via OAuth and configure CLI").option("--api-url <url>", "API URL (default: https://episoda.dev)").action(async (options) => {
6016
6542
  try {
6017
6543
  await authCommand(options);
@@ -6167,5 +6693,74 @@ function formatUptime(seconds) {
6167
6693
  const mins = Math.floor(seconds % 3600 / 60);
6168
6694
  return `${hours}h ${mins}m`;
6169
6695
  }
6696
+ import_commander.program.command("run").description("Run a command with environment variables injected from the database").argument("<command...>", "Command to run with injected env vars").option("--no-cache", "Force fresh fetch from server").option("--cache-ttl <seconds>", "Cache TTL in seconds (default: 60)", parseInt).option("--offline", "Use cached values only, no server call").option("--verbose", "Show fetched variable names").allowUnknownOption().action(async (commandArgs, options) => {
6697
+ try {
6698
+ const args = process.argv.slice(process.argv.indexOf("run") + 1);
6699
+ const separatorIndex = args.indexOf("--");
6700
+ let finalArgs;
6701
+ if (separatorIndex >= 0) {
6702
+ finalArgs = args.slice(separatorIndex + 1);
6703
+ } else {
6704
+ finalArgs = commandArgs.filter(
6705
+ (arg) => !arg.startsWith("--no-cache") && !arg.startsWith("--cache-ttl") && !arg.startsWith("--offline") && !arg.startsWith("--verbose")
6706
+ );
6707
+ }
6708
+ await runCommand(finalArgs, {
6709
+ noCache: options.cache === false,
6710
+ cacheTtl: options.cacheTtl,
6711
+ offline: options.offline,
6712
+ verbose: options.verbose
6713
+ });
6714
+ } catch (error) {
6715
+ status.error(`Run failed: ${error instanceof Error ? error.message : String(error)}`);
6716
+ process.exit(1);
6717
+ }
6718
+ });
6719
+ import_commander.program.command("shell").description("Open an interactive shell with environment variables injected").argument("[shell]", "Shell to use (default: $SHELL)").option("--print", "Print export statements instead of opening shell").option("--no-cache", "Force fresh fetch from server").option("--cache-ttl <seconds>", "Cache TTL in seconds (default: 60)", parseInt).option("--offline", "Use cached values only").action(async (shell, options) => {
6720
+ try {
6721
+ await shellCommand(shell, {
6722
+ print: options.print,
6723
+ noCache: options.cache === false,
6724
+ cacheTtl: options.cacheTtl,
6725
+ offline: options.offline
6726
+ });
6727
+ } catch (error) {
6728
+ status.error(`Shell failed: ${error instanceof Error ? error.message : String(error)}`);
6729
+ process.exit(1);
6730
+ }
6731
+ });
6732
+ var envCmd = import_commander.program.command("env").description("Manage environment variables");
6733
+ envCmd.command("list").description("List environment variables").option("--show-values", "Show value previews (masked)").action(async (options) => {
6734
+ try {
6735
+ await envListCommand({ showValues: options.showValues });
6736
+ } catch (error) {
6737
+ status.error(`Env list failed: ${error instanceof Error ? error.message : String(error)}`);
6738
+ process.exit(1);
6739
+ }
6740
+ });
6741
+ envCmd.command("set").description("Set an environment variable").argument("<key=value>", "KEY=value pair (omit value to prompt)").option("-e, --environment <env>", "Environment: all, dev, preview, prod (default: dev)").action(async (keyValue, options) => {
6742
+ try {
6743
+ await envSetCommand(keyValue, { environment: options.environment });
6744
+ } catch (error) {
6745
+ status.error(`Env set failed: ${error instanceof Error ? error.message : String(error)}`);
6746
+ process.exit(1);
6747
+ }
6748
+ });
6749
+ envCmd.command("remove").description("Remove an environment variable").argument("<key>", "Variable key to remove").action(async (key) => {
6750
+ try {
6751
+ await envRemoveCommand(key);
6752
+ } catch (error) {
6753
+ status.error(`Env remove failed: ${error instanceof Error ? error.message : String(error)}`);
6754
+ process.exit(1);
6755
+ }
6756
+ });
6757
+ envCmd.command("pull").description("Generate .env file from database (explicit file generation)").option("-f, --file <filename>", "Output filename (default: .env)").option("--stdout", "Print to stdout instead of file").action(async (options) => {
6758
+ try {
6759
+ await envPullCommand({ file: options.file, stdout: options.stdout });
6760
+ } catch (error) {
6761
+ status.error(`Env pull failed: ${error instanceof Error ? error.message : String(error)}`);
6762
+ process.exit(1);
6763
+ }
6764
+ });
6170
6765
  import_commander.program.parse();
6171
6766
  //# sourceMappingURL=index.js.map