episoda 0.2.32 → 0.2.34
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/daemon/daemon-process.js +346 -178
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +671 -68
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
1556
|
+
await fs15.access(rebaseMergePath);
|
|
1557
1557
|
inRebase = true;
|
|
1558
1558
|
} catch {
|
|
1559
1559
|
try {
|
|
1560
|
-
await
|
|
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
|
|
1614
|
+
const fs15 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1615
1615
|
try {
|
|
1616
|
-
await
|
|
1616
|
+
await fs15.access(command.path);
|
|
1617
1617
|
return {
|
|
1618
1618
|
success: false,
|
|
1619
1619
|
error: "WORKTREE_EXISTS",
|
|
@@ -1628,6 +1628,9 @@ var require_git_executor = __commonJS({
|
|
|
1628
1628
|
const args = ["worktree", "add"];
|
|
1629
1629
|
if (command.create) {
|
|
1630
1630
|
args.push("-b", command.branch, command.path);
|
|
1631
|
+
if (command.startPoint) {
|
|
1632
|
+
args.push(command.startPoint);
|
|
1633
|
+
}
|
|
1631
1634
|
} else {
|
|
1632
1635
|
args.push(command.path, command.branch);
|
|
1633
1636
|
}
|
|
@@ -1664,9 +1667,9 @@ var require_git_executor = __commonJS({
|
|
|
1664
1667
|
*/
|
|
1665
1668
|
async executeWorktreeRemove(command, cwd, options) {
|
|
1666
1669
|
try {
|
|
1667
|
-
const
|
|
1670
|
+
const fs15 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1668
1671
|
try {
|
|
1669
|
-
await
|
|
1672
|
+
await fs15.access(command.path);
|
|
1670
1673
|
} catch {
|
|
1671
1674
|
return {
|
|
1672
1675
|
success: false,
|
|
@@ -1819,10 +1822,10 @@ var require_git_executor = __commonJS({
|
|
|
1819
1822
|
*/
|
|
1820
1823
|
async executeCloneBare(command, options) {
|
|
1821
1824
|
try {
|
|
1822
|
-
const
|
|
1823
|
-
const
|
|
1825
|
+
const fs15 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1826
|
+
const path17 = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1824
1827
|
try {
|
|
1825
|
-
await
|
|
1828
|
+
await fs15.access(command.path);
|
|
1826
1829
|
return {
|
|
1827
1830
|
success: false,
|
|
1828
1831
|
error: "BRANCH_ALREADY_EXISTS",
|
|
@@ -1831,9 +1834,9 @@ var require_git_executor = __commonJS({
|
|
|
1831
1834
|
};
|
|
1832
1835
|
} catch {
|
|
1833
1836
|
}
|
|
1834
|
-
const parentDir =
|
|
1837
|
+
const parentDir = path17.dirname(command.path);
|
|
1835
1838
|
try {
|
|
1836
|
-
await
|
|
1839
|
+
await fs15.mkdir(parentDir, { recursive: true });
|
|
1837
1840
|
} catch {
|
|
1838
1841
|
}
|
|
1839
1842
|
const { stdout, stderr } = await execAsync(
|
|
@@ -1876,22 +1879,22 @@ var require_git_executor = __commonJS({
|
|
|
1876
1879
|
*/
|
|
1877
1880
|
async executeProjectInfo(cwd, options) {
|
|
1878
1881
|
try {
|
|
1879
|
-
const
|
|
1880
|
-
const
|
|
1882
|
+
const fs15 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1883
|
+
const path17 = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1881
1884
|
let currentPath = cwd;
|
|
1882
1885
|
let projectPath = cwd;
|
|
1883
1886
|
let bareRepoPath;
|
|
1884
1887
|
for (let i = 0; i < 10; i++) {
|
|
1885
|
-
const bareDir =
|
|
1886
|
-
const episodaDir =
|
|
1888
|
+
const bareDir = path17.join(currentPath, ".bare");
|
|
1889
|
+
const episodaDir = path17.join(currentPath, ".episoda");
|
|
1887
1890
|
try {
|
|
1888
|
-
await
|
|
1889
|
-
await
|
|
1891
|
+
await fs15.access(bareDir);
|
|
1892
|
+
await fs15.access(episodaDir);
|
|
1890
1893
|
projectPath = currentPath;
|
|
1891
1894
|
bareRepoPath = bareDir;
|
|
1892
1895
|
break;
|
|
1893
1896
|
} catch {
|
|
1894
|
-
const parentPath =
|
|
1897
|
+
const parentPath = path17.dirname(currentPath);
|
|
1895
1898
|
if (parentPath === currentPath) {
|
|
1896
1899
|
break;
|
|
1897
1900
|
}
|
|
@@ -2104,7 +2107,7 @@ var require_websocket_client = __commonJS({
|
|
|
2104
2107
|
clearTimeout(this.reconnectTimeout);
|
|
2105
2108
|
this.reconnectTimeout = void 0;
|
|
2106
2109
|
}
|
|
2107
|
-
return new Promise((
|
|
2110
|
+
return new Promise((resolve5, reject) => {
|
|
2108
2111
|
const connectionTimeout = setTimeout(() => {
|
|
2109
2112
|
if (this.ws) {
|
|
2110
2113
|
this.ws.terminate();
|
|
@@ -2131,7 +2134,7 @@ var require_websocket_client = __commonJS({
|
|
|
2131
2134
|
daemonPid: this.daemonPid
|
|
2132
2135
|
});
|
|
2133
2136
|
this.startHeartbeat();
|
|
2134
|
-
|
|
2137
|
+
resolve5();
|
|
2135
2138
|
});
|
|
2136
2139
|
this.ws.on("pong", () => {
|
|
2137
2140
|
if (this.heartbeatTimeoutTimer) {
|
|
@@ -2247,13 +2250,13 @@ var require_websocket_client = __commonJS({
|
|
|
2247
2250
|
if (!this.ws || !this.isConnected) {
|
|
2248
2251
|
throw new Error("WebSocket not connected");
|
|
2249
2252
|
}
|
|
2250
|
-
return new Promise((
|
|
2253
|
+
return new Promise((resolve5, reject) => {
|
|
2251
2254
|
this.ws.send(JSON.stringify(message), (error) => {
|
|
2252
2255
|
if (error) {
|
|
2253
2256
|
console.error("[EpisodaClient] Failed to send message:", error);
|
|
2254
2257
|
reject(error);
|
|
2255
2258
|
} else {
|
|
2256
|
-
|
|
2259
|
+
resolve5();
|
|
2257
2260
|
}
|
|
2258
2261
|
});
|
|
2259
2262
|
});
|
|
@@ -2482,34 +2485,34 @@ var require_auth = __commonJS({
|
|
|
2482
2485
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
2483
2486
|
exports2.getConfigDir = getConfigDir5;
|
|
2484
2487
|
exports2.getConfigPath = getConfigPath4;
|
|
2485
|
-
exports2.loadConfig =
|
|
2488
|
+
exports2.loadConfig = loadConfig9;
|
|
2486
2489
|
exports2.saveConfig = saveConfig3;
|
|
2487
2490
|
exports2.validateToken = validateToken;
|
|
2488
|
-
var
|
|
2489
|
-
var
|
|
2490
|
-
var
|
|
2491
|
+
var fs15 = __importStar(require("fs"));
|
|
2492
|
+
var path17 = __importStar(require("path"));
|
|
2493
|
+
var os4 = __importStar(require("os"));
|
|
2491
2494
|
var child_process_1 = require("child_process");
|
|
2492
2495
|
var DEFAULT_CONFIG_FILE = "config.json";
|
|
2493
2496
|
function getConfigDir5() {
|
|
2494
|
-
return process.env.EPISODA_CONFIG_DIR ||
|
|
2497
|
+
return process.env.EPISODA_CONFIG_DIR || path17.join(os4.homedir(), ".episoda");
|
|
2495
2498
|
}
|
|
2496
2499
|
function getConfigPath4(configPath) {
|
|
2497
2500
|
if (configPath) {
|
|
2498
2501
|
return configPath;
|
|
2499
2502
|
}
|
|
2500
|
-
return
|
|
2503
|
+
return path17.join(getConfigDir5(), DEFAULT_CONFIG_FILE);
|
|
2501
2504
|
}
|
|
2502
2505
|
function ensureConfigDir(configPath) {
|
|
2503
|
-
const dir =
|
|
2504
|
-
const isNew = !
|
|
2506
|
+
const dir = path17.dirname(configPath);
|
|
2507
|
+
const isNew = !fs15.existsSync(dir);
|
|
2505
2508
|
if (isNew) {
|
|
2506
|
-
|
|
2509
|
+
fs15.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2507
2510
|
}
|
|
2508
2511
|
if (process.platform === "darwin") {
|
|
2509
|
-
const nosyncPath =
|
|
2510
|
-
if (isNew || !
|
|
2512
|
+
const nosyncPath = path17.join(dir, ".nosync");
|
|
2513
|
+
if (isNew || !fs15.existsSync(nosyncPath)) {
|
|
2511
2514
|
try {
|
|
2512
|
-
|
|
2515
|
+
fs15.writeFileSync(nosyncPath, "", { mode: 384 });
|
|
2513
2516
|
(0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
|
|
2514
2517
|
stdio: "ignore",
|
|
2515
2518
|
timeout: 5e3
|
|
@@ -2519,13 +2522,13 @@ var require_auth = __commonJS({
|
|
|
2519
2522
|
}
|
|
2520
2523
|
}
|
|
2521
2524
|
}
|
|
2522
|
-
async function
|
|
2525
|
+
async function loadConfig9(configPath) {
|
|
2523
2526
|
const fullPath = getConfigPath4(configPath);
|
|
2524
|
-
if (!
|
|
2527
|
+
if (!fs15.existsSync(fullPath)) {
|
|
2525
2528
|
return null;
|
|
2526
2529
|
}
|
|
2527
2530
|
try {
|
|
2528
|
-
const content =
|
|
2531
|
+
const content = fs15.readFileSync(fullPath, "utf8");
|
|
2529
2532
|
const config = JSON.parse(content);
|
|
2530
2533
|
return config;
|
|
2531
2534
|
} catch (error) {
|
|
@@ -2538,7 +2541,7 @@ var require_auth = __commonJS({
|
|
|
2538
2541
|
ensureConfigDir(fullPath);
|
|
2539
2542
|
try {
|
|
2540
2543
|
const content = JSON.stringify(config, null, 2);
|
|
2541
|
-
|
|
2544
|
+
fs15.writeFileSync(fullPath, content, { mode: 384 });
|
|
2542
2545
|
} catch (error) {
|
|
2543
2546
|
throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
|
|
2544
2547
|
}
|
|
@@ -2649,7 +2652,7 @@ var require_dist = __commonJS({
|
|
|
2649
2652
|
|
|
2650
2653
|
// src/index.ts
|
|
2651
2654
|
var import_commander = require("commander");
|
|
2652
|
-
var
|
|
2655
|
+
var import_core18 = __toESM(require_dist());
|
|
2653
2656
|
|
|
2654
2657
|
// src/commands/dev.ts
|
|
2655
2658
|
var import_core4 = __toESM(require_dist());
|
|
@@ -2986,7 +2989,7 @@ async function startDaemon() {
|
|
|
2986
2989
|
const pid = child.pid;
|
|
2987
2990
|
const pidPath = getPidFilePath();
|
|
2988
2991
|
fs2.writeFileSync(pidPath, pid.toString(), "utf-8");
|
|
2989
|
-
await new Promise((
|
|
2992
|
+
await new Promise((resolve5) => setTimeout(resolve5, 500));
|
|
2990
2993
|
const runningPid = isDaemonRunning();
|
|
2991
2994
|
if (!runningPid) {
|
|
2992
2995
|
throw new Error("Daemon failed to start");
|
|
@@ -3008,7 +3011,7 @@ async function stopDaemon(timeout = 5e3) {
|
|
|
3008
3011
|
while (Date.now() - startTime < timeout) {
|
|
3009
3012
|
try {
|
|
3010
3013
|
process.kill(pid, 0);
|
|
3011
|
-
await new Promise((
|
|
3014
|
+
await new Promise((resolve5) => setTimeout(resolve5, 100));
|
|
3012
3015
|
} catch (error) {
|
|
3013
3016
|
const pidPath2 = getPidFilePath();
|
|
3014
3017
|
if (fs2.existsSync(pidPath2)) {
|
|
@@ -3038,7 +3041,7 @@ var import_core2 = __toESM(require_dist());
|
|
|
3038
3041
|
var getSocketPath = () => path3.join((0, import_core2.getConfigDir)(), "daemon.sock");
|
|
3039
3042
|
var DEFAULT_TIMEOUT = 15e3;
|
|
3040
3043
|
async function sendCommand(command, params, timeout = DEFAULT_TIMEOUT) {
|
|
3041
|
-
return new Promise((
|
|
3044
|
+
return new Promise((resolve5, reject) => {
|
|
3042
3045
|
const socket = net.createConnection(getSocketPath());
|
|
3043
3046
|
const requestId = crypto.randomUUID();
|
|
3044
3047
|
let buffer = "";
|
|
@@ -3069,7 +3072,7 @@ async function sendCommand(command, params, timeout = DEFAULT_TIMEOUT) {
|
|
|
3069
3072
|
clearTimeout(timeoutHandle);
|
|
3070
3073
|
socket.end();
|
|
3071
3074
|
if (response.success) {
|
|
3072
|
-
|
|
3075
|
+
resolve5(response.data);
|
|
3073
3076
|
} else {
|
|
3074
3077
|
reject(new Error(response.error || "Command failed"));
|
|
3075
3078
|
}
|
|
@@ -3132,18 +3135,18 @@ var fs5 = __toESM(require("fs"));
|
|
|
3132
3135
|
// src/utils/port-check.ts
|
|
3133
3136
|
var net2 = __toESM(require("net"));
|
|
3134
3137
|
async function isPortInUse(port) {
|
|
3135
|
-
return new Promise((
|
|
3138
|
+
return new Promise((resolve5) => {
|
|
3136
3139
|
const server = net2.createServer();
|
|
3137
3140
|
server.once("error", (err) => {
|
|
3138
3141
|
if (err.code === "EADDRINUSE") {
|
|
3139
|
-
|
|
3142
|
+
resolve5(true);
|
|
3140
3143
|
} else {
|
|
3141
|
-
|
|
3144
|
+
resolve5(false);
|
|
3142
3145
|
}
|
|
3143
3146
|
});
|
|
3144
3147
|
server.once("listening", () => {
|
|
3145
3148
|
server.close();
|
|
3146
|
-
|
|
3149
|
+
resolve5(false);
|
|
3147
3150
|
});
|
|
3148
3151
|
server.listen(port);
|
|
3149
3152
|
});
|
|
@@ -3262,14 +3265,19 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
3262
3265
|
action: "fetch",
|
|
3263
3266
|
remote: "origin"
|
|
3264
3267
|
}, { cwd: this.bareRepoPath });
|
|
3265
|
-
if (!fetchResult.success) {
|
|
3266
|
-
console.
|
|
3268
|
+
if (!fetchResult.success && createBranch) {
|
|
3269
|
+
console.error("[worktree-manager] Failed to fetch from origin:", fetchResult.output);
|
|
3270
|
+
return {
|
|
3271
|
+
success: false,
|
|
3272
|
+
error: "Failed to fetch latest refs from origin. Cannot create worktree with stale refs."
|
|
3273
|
+
};
|
|
3267
3274
|
}
|
|
3268
3275
|
const result = await this.gitExecutor.execute({
|
|
3269
3276
|
action: "worktree_add",
|
|
3270
3277
|
path: worktreePath,
|
|
3271
3278
|
branch: branchName,
|
|
3272
|
-
create: createBranch
|
|
3279
|
+
create: createBranch,
|
|
3280
|
+
startPoint: createBranch ? "origin/main" : void 0
|
|
3273
3281
|
}, { cwd: this.bareRepoPath });
|
|
3274
3282
|
if (!result.success) {
|
|
3275
3283
|
return {
|
|
@@ -3510,7 +3518,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
3510
3518
|
const lockContent = fs3.readFileSync(lockPath, "utf-8").trim();
|
|
3511
3519
|
const lockPid = parseInt(lockContent, 10);
|
|
3512
3520
|
if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
|
|
3513
|
-
await new Promise((
|
|
3521
|
+
await new Promise((resolve5) => setTimeout(resolve5, retryInterval));
|
|
3514
3522
|
continue;
|
|
3515
3523
|
}
|
|
3516
3524
|
} catch {
|
|
@@ -3524,7 +3532,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
3524
3532
|
} catch {
|
|
3525
3533
|
continue;
|
|
3526
3534
|
}
|
|
3527
|
-
await new Promise((
|
|
3535
|
+
await new Promise((resolve5) => setTimeout(resolve5, retryInterval));
|
|
3528
3536
|
continue;
|
|
3529
3537
|
}
|
|
3530
3538
|
throw err;
|
|
@@ -3791,7 +3799,7 @@ async function fetchProjectPath(config, projectId) {
|
|
|
3791
3799
|
return null;
|
|
3792
3800
|
}
|
|
3793
3801
|
}
|
|
3794
|
-
async function syncProjectPath(config, projectId,
|
|
3802
|
+
async function syncProjectPath(config, projectId, path17) {
|
|
3795
3803
|
if (!config.device_id || !config.access_token) {
|
|
3796
3804
|
return false;
|
|
3797
3805
|
}
|
|
@@ -3804,7 +3812,7 @@ async function syncProjectPath(config, projectId, path15) {
|
|
|
3804
3812
|
"Authorization": `Bearer ${config.access_token}`,
|
|
3805
3813
|
"Content-Type": "application/json"
|
|
3806
3814
|
},
|
|
3807
|
-
body: JSON.stringify({ path:
|
|
3815
|
+
body: JSON.stringify({ path: path17 })
|
|
3808
3816
|
});
|
|
3809
3817
|
if (!response.ok) {
|
|
3810
3818
|
console.debug(`[MachineSettings] syncProjectPath failed: ${response.status} ${response.statusText}`);
|
|
@@ -3895,7 +3903,7 @@ async function devCommand(options = {}) {
|
|
|
3895
3903
|
const killedCount = killAllEpisodaProcesses();
|
|
3896
3904
|
if (killedCount > 0) {
|
|
3897
3905
|
status.info(`Cleaned up ${killedCount} stale process${killedCount > 1 ? "es" : ""}`);
|
|
3898
|
-
await new Promise((
|
|
3906
|
+
await new Promise((resolve5) => setTimeout(resolve5, 2e3));
|
|
3899
3907
|
}
|
|
3900
3908
|
}
|
|
3901
3909
|
let projectPath;
|
|
@@ -3955,7 +3963,7 @@ async function devCommand(options = {}) {
|
|
|
3955
3963
|
for (let retry = 0; retry < CONNECTION_MAX_RETRIES && !connected; retry++) {
|
|
3956
3964
|
if (retry > 0) {
|
|
3957
3965
|
status.info(`Retrying connection (attempt ${retry + 1}/${CONNECTION_MAX_RETRIES})...`);
|
|
3958
|
-
await new Promise((
|
|
3966
|
+
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
3959
3967
|
}
|
|
3960
3968
|
try {
|
|
3961
3969
|
const result = await addProject(config.project_id, projectPath);
|
|
@@ -4578,10 +4586,10 @@ async function initiateDeviceFlow(apiUrl, machineId) {
|
|
|
4578
4586
|
return await response.json();
|
|
4579
4587
|
}
|
|
4580
4588
|
async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
|
|
4581
|
-
return new Promise((
|
|
4589
|
+
return new Promise((resolve5) => {
|
|
4582
4590
|
const timeout = setTimeout(() => {
|
|
4583
4591
|
status.error("Authorization timed out");
|
|
4584
|
-
|
|
4592
|
+
resolve5(false);
|
|
4585
4593
|
}, expiresIn * 1e3);
|
|
4586
4594
|
const url = `${apiUrl}/api/oauth/authorize-stream?device_code=${deviceCode}`;
|
|
4587
4595
|
const curlProcess = (0, import_child_process5.spawn)("curl", ["-N", url]);
|
|
@@ -4607,26 +4615,26 @@ async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
|
|
|
4607
4615
|
if (eventType === "authorized") {
|
|
4608
4616
|
clearTimeout(timeout);
|
|
4609
4617
|
curlProcess.kill();
|
|
4610
|
-
|
|
4618
|
+
resolve5(true);
|
|
4611
4619
|
return;
|
|
4612
4620
|
} else if (eventType === "denied") {
|
|
4613
4621
|
clearTimeout(timeout);
|
|
4614
4622
|
curlProcess.kill();
|
|
4615
4623
|
status.error("Authorization denied by user");
|
|
4616
|
-
|
|
4624
|
+
resolve5(false);
|
|
4617
4625
|
return;
|
|
4618
4626
|
} else if (eventType === "expired") {
|
|
4619
4627
|
clearTimeout(timeout);
|
|
4620
4628
|
curlProcess.kill();
|
|
4621
4629
|
status.error("Authorization code expired");
|
|
4622
|
-
|
|
4630
|
+
resolve5(false);
|
|
4623
4631
|
return;
|
|
4624
4632
|
} else if (eventType === "error") {
|
|
4625
4633
|
const errorData = JSON.parse(data);
|
|
4626
4634
|
clearTimeout(timeout);
|
|
4627
4635
|
curlProcess.kill();
|
|
4628
4636
|
status.error(`Authorization error: ${errorData.error_description || errorData.error || "Unknown error"}`);
|
|
4629
|
-
|
|
4637
|
+
resolve5(false);
|
|
4630
4638
|
return;
|
|
4631
4639
|
}
|
|
4632
4640
|
} catch (error) {
|
|
@@ -4636,13 +4644,13 @@ async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
|
|
|
4636
4644
|
curlProcess.on("error", (error) => {
|
|
4637
4645
|
clearTimeout(timeout);
|
|
4638
4646
|
status.error(`Failed to monitor authorization: ${error.message}`);
|
|
4639
|
-
|
|
4647
|
+
resolve5(false);
|
|
4640
4648
|
});
|
|
4641
4649
|
curlProcess.on("close", (code) => {
|
|
4642
4650
|
clearTimeout(timeout);
|
|
4643
4651
|
if (code !== 0 && code !== null) {
|
|
4644
4652
|
status.error(`Authorization monitoring failed with code ${code}`);
|
|
4645
|
-
|
|
4653
|
+
resolve5(false);
|
|
4646
4654
|
}
|
|
4647
4655
|
});
|
|
4648
4656
|
});
|
|
@@ -5398,7 +5406,7 @@ async function fetchWithRetry(url, options, maxRetries = 3) {
|
|
|
5398
5406
|
lastError = error instanceof Error ? error : new Error("Network error");
|
|
5399
5407
|
}
|
|
5400
5408
|
if (attempt < maxRetries - 1) {
|
|
5401
|
-
await new Promise((
|
|
5409
|
+
await new Promise((resolve5) => setTimeout(resolve5, Math.pow(2, attempt) * 1e3));
|
|
5402
5410
|
}
|
|
5403
5411
|
}
|
|
5404
5412
|
throw lastError || new Error("Request failed after retries");
|
|
@@ -5981,7 +5989,7 @@ async function updateCommand(options = {}) {
|
|
|
5981
5989
|
}
|
|
5982
5990
|
if (options.restart && daemonWasRunning) {
|
|
5983
5991
|
status.info("Restarting daemon...");
|
|
5984
|
-
await new Promise((
|
|
5992
|
+
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
5985
5993
|
if (startDaemon2()) {
|
|
5986
5994
|
status.success("\u2713 Daemon restarted");
|
|
5987
5995
|
status.info("");
|
|
@@ -6002,8 +6010,534 @@ async function updateCommand(options = {}) {
|
|
|
6002
6010
|
}
|
|
6003
6011
|
}
|
|
6004
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
|
+
|
|
6005
6539
|
// src/index.ts
|
|
6006
|
-
import_commander.program.name("episoda").description("Episoda CLI - local development with git worktree isolation").version(
|
|
6540
|
+
import_commander.program.name("episoda").description("Episoda CLI - local development with git worktree isolation").version(import_core18.VERSION);
|
|
6007
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) => {
|
|
6008
6542
|
try {
|
|
6009
6543
|
await authCommand(options);
|
|
@@ -6159,5 +6693,74 @@ function formatUptime(seconds) {
|
|
|
6159
6693
|
const mins = Math.floor(seconds % 3600 / 60);
|
|
6160
6694
|
return `${hours}h ${mins}m`;
|
|
6161
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
|
+
});
|
|
6162
6765
|
import_commander.program.parse();
|
|
6163
6766
|
//# sourceMappingURL=index.js.map
|