diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5995f7a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run tests + run: bun test + + build: + name: Build + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Build + run: bun run build + + - name: Check build output + run: | + if [ ! -f "dist/index.js" ]; then + echo "Build failed: dist/index.js not found" + exit 1 + fi + echo "✅ Build successful" diff --git a/.gitignore b/.gitignore index 3b24e4a..727f2a1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules dist .env *.log +test-temp-* diff --git a/package.json b/package.json index 091c6b3..3e6ead5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "scripts": { "flux": "bun src/index.ts", "build": "tsup", - "start": "node dist/index.js" + "start": "node dist/index.js", + "test": "bun test", + "test:watch": "bun test --watch" }, "dependencies": { "tsx": "^4.19.0" diff --git a/src/__tests__/README.md b/src/__tests__/README.md new file mode 100644 index 0000000..4467ab5 --- /dev/null +++ b/src/__tests__/README.md @@ -0,0 +1,92 @@ +# Flux Tests + +This directory contains tests for the Flux CLI. + +## Running Tests + +```bash +# Run all tests +bun test + +# Run tests in watch mode (re-runs on file changes) +bun test --watch + +# Run specific test file +bun test src/__tests__/agent-loader.test.ts +``` + +## Test Structure + +- `__tests__/` - Test files (*.test.ts) +- `__tests__/fixtures/` - Test fixtures and sample agent files + +## Coverage + +### agent-loader.test.ts + +Tests for agent discovery, validation, and loading functionality: + +**findAgentFile()** +- ✅ Returns null when no agent file exists +- ✅ Finds agent.ts in current directory +- ✅ Finds agent.js in current directory +- ✅ Prefers agent.ts over agent.js when both exist + +**validateAgentFile()** +- ✅ Validates correct TypeScript agent files +- ✅ Validates correct JavaScript agent files +- ✅ Rejects agents without default export +- ✅ Rejects agents without invoke method +- ✅ Rejects agents where invoke is not a function +- ✅ Handles invalid syntax gracefully +- ✅ Handles non-existent files +- ✅ Handles empty files + +**loadAgent()** +- ✅ Loads valid TypeScript agents +- ✅ Loads valid JavaScript agents +- ✅ Returns invokable agent +- ✅ Handles TypeScript features (interfaces, types) + +**Integration Tests** +- ✅ Complete workflow: find → validate → load → invoke +- ✅ Handles workflow with invalid agent + +## Test Fixtures + +Test fixtures are located in `fixtures/` directory: + +- `valid-agent.ts` - Valid TypeScript agent for testing +- `valid-agent.js` - Valid JavaScript agent for testing +- `no-default-export.ts` - Agent missing default export +- `no-invoke-method.ts` - Agent missing invoke method +- `invoke-not-function.ts` - Agent where invoke is not a function +- `invalid-syntax.ts` - Agent with syntax errors + +## Writing New Tests + +When adding new test files: + +1. Name files with `.test.ts` extension +2. Import from `bun:test`: `import { describe, it, expect } from "bun:test"` +3. Use `beforeEach`/`afterEach` for setup/cleanup +4. Group related tests in `describe` blocks +5. Make test descriptions clear and specific + +Example: + +```typescript +import { describe, it, expect } from "bun:test"; + +describe("myModule", () => { + it("should do something specific", () => { + expect(true).toBe(true); + }); +}); +``` + +## Notes + +- Tests use Bun's built-in test runner (no additional dependencies needed) +- Temporary test directories are automatically cleaned up +- The `tsx` warning messages during tests are expected and don't affect test results diff --git a/src/__tests__/agent-loader.test.ts b/src/__tests__/agent-loader.test.ts new file mode 100644 index 0000000..4972488 --- /dev/null +++ b/src/__tests__/agent-loader.test.ts @@ -0,0 +1,261 @@ +import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import * as fs from "fs"; +import * as path from "path"; +import { findAgentFile, validateAgentFile, loadAgent } from "../agent-loader"; + +describe("agent-loader", () => { + let originalCwd: string; + let testDir: string; + + beforeEach(() => { + // Save original working directory + originalCwd = process.cwd(); + + // Create a temporary test directory + testDir = path.join(process.cwd(), "test-temp-" + Date.now()); + fs.mkdirSync(testDir, { recursive: true }); + + // Change to test directory + process.chdir(testDir); + }); + + afterEach(() => { + // Restore original working directory + process.chdir(originalCwd); + + // Clean up test directory + if (fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true, force: true }); + } + }); + + describe("findAgentFile", () => { + it("should return null when no agent file exists", () => { + const result = findAgentFile(); + expect(result).toBeNull(); + }); + + it("should find agent.ts in current directory", () => { + // Create agent.ts + const agentPath = path.join(testDir, "agent.ts"); + fs.writeFileSync(agentPath, "export default { invoke: async () => 'test' };"); + + const result = findAgentFile(); + expect(result).toBe(agentPath); + }); + + it("should find agent.js in current directory", () => { + // Create agent.js + const agentPath = path.join(testDir, "agent.js"); + fs.writeFileSync(agentPath, "export default { invoke: async () => 'test' };"); + + const result = findAgentFile(); + expect(result).toBe(agentPath); + }); + + it("should prefer agent.ts over agent.js when both exist", () => { + // Create both files + const tsPath = path.join(testDir, "agent.ts"); + const jsPath = path.join(testDir, "agent.js"); + fs.writeFileSync(tsPath, "export default { invoke: async () => 'ts' };"); + fs.writeFileSync(jsPath, "export default { invoke: async () => 'js' };"); + + const result = findAgentFile(); + expect(result).toBe(tsPath); + }); + }); + + describe("validateAgentFile", () => { + it("should validate a correct TypeScript agent file", async () => { + const fixtureDir = path.join(originalCwd, "src", "__tests__", "fixtures"); + const agentPath = path.join(fixtureDir, "valid-agent.ts"); + + const result = await validateAgentFile(agentPath); + + expect(result.valid).toBe(true); + expect(result.error).toBeUndefined(); + }); + + it("should validate a correct JavaScript agent file", async () => { + const fixtureDir = path.join(originalCwd, "src", "__tests__", "fixtures"); + const agentPath = path.join(fixtureDir, "valid-agent.js"); + + const result = await validateAgentFile(agentPath); + + expect(result.valid).toBe(true); + expect(result.error).toBeUndefined(); + }); + + it("should reject agent without default export", async () => { + const fixtureDir = path.join(originalCwd, "src", "__tests__", "fixtures"); + const agentPath = path.join(fixtureDir, "no-default-export.ts"); + + const result = await validateAgentFile(agentPath); + + expect(result.valid).toBe(false); + expect(result.error).toBe("No default export found. Use `export default agent`"); + }); + + it("should reject agent without invoke method", async () => { + const fixtureDir = path.join(originalCwd, "src", "__tests__", "fixtures"); + const agentPath = path.join(fixtureDir, "no-invoke-method.ts"); + + const result = await validateAgentFile(agentPath); + + expect(result.valid).toBe(false); + expect(result.error).toBe("Agent must have an `invoke` method"); + }); + + it("should reject agent where invoke is not a function", async () => { + const fixtureDir = path.join(originalCwd, "src", "__tests__", "fixtures"); + const agentPath = path.join(fixtureDir, "invoke-not-function.ts"); + + const result = await validateAgentFile(agentPath); + + expect(result.valid).toBe(false); + expect(result.error).toBe("Agent must have an `invoke` method"); + }); + + it("should handle invalid syntax gracefully", async () => { + const fixtureDir = path.join(originalCwd, "src", "__tests__", "fixtures"); + const agentPath = path.join(fixtureDir, "invalid-syntax.ts"); + + const result = await validateAgentFile(agentPath); + + expect(result.valid).toBe(false); + expect(result.error).toMatch(/Failed to load agent:/); + }); + + it("should handle non-existent file", async () => { + const agentPath = path.join(testDir, "non-existent.ts"); + + const result = await validateAgentFile(agentPath); + + expect(result.valid).toBe(false); + expect(result.error).toMatch(/Failed to load agent:/); + }); + + it("should handle empty file", async () => { + const agentPath = path.join(testDir, "empty.ts"); + fs.writeFileSync(agentPath, ""); + + const result = await validateAgentFile(agentPath); + + expect(result.valid).toBe(false); + expect(result.error).toBe("No default export found. Use `export default agent`"); + }); + }); + + describe("loadAgent", () => { + it("should successfully load a valid TypeScript agent", async () => { + const fixtureDir = path.join(originalCwd, "src", "__tests__", "fixtures"); + const agentPath = path.join(fixtureDir, "valid-agent.ts"); + + const agent = await loadAgent(agentPath); + + expect(agent).toBeDefined(); + expect(typeof agent.invoke).toBe("function"); + }); + + it("should successfully load a valid JavaScript agent", async () => { + const fixtureDir = path.join(originalCwd, "src", "__tests__", "fixtures"); + const agentPath = path.join(fixtureDir, "valid-agent.js"); + + const agent = await loadAgent(agentPath); + + expect(agent).toBeDefined(); + expect(typeof agent.invoke).toBe("function"); + }); + + it("should load agent that can be invoked", async () => { + const fixtureDir = path.join(originalCwd, "src", "__tests__", "fixtures"); + const agentPath = path.join(fixtureDir, "valid-agent.ts"); + + const agent = await loadAgent(agentPath); + const response = await agent.invoke({ + message: "Hello", + userPhoneNumber: "+1234567890" + }); + + expect(response).toBe("Echo: Hello from +1234567890"); + }); + + it("should handle TypeScript features in agent", async () => { + // Create an agent with TypeScript features + const agentPath = path.join(testDir, "ts-features.ts"); + fs.writeFileSync(agentPath, ` + interface InvokeInput { + message: string; + userPhoneNumber: string; + } + + export default { + async invoke({ message, userPhoneNumber }: InvokeInput): Promise { + const greeting: string = "Hello"; + return \`\${greeting}: \${message}\`; + } + }; + `); + + const agent = await loadAgent(agentPath); + const response = await agent.invoke({ + message: "World", + userPhoneNumber: "+1234567890" + }); + + expect(response).toBe("Hello: World"); + }); + }); + + describe("Integration tests", () => { + it("should find, validate, and load an agent in workflow", async () => { + // Create a valid agent in test directory + const agentContent = ` + export default { + async invoke({ message }: { message: string }) { + return \`You said: \${message}\`; + } + }; + `; + fs.writeFileSync(path.join(testDir, "agent.ts"), agentContent); + + // Step 1: Find the agent + const foundPath = findAgentFile(); + expect(foundPath).not.toBeNull(); + + // Step 2: Validate the agent + const validation = await validateAgentFile(foundPath!); + expect(validation.valid).toBe(true); + + // Step 3: Load the agent + const agent = await loadAgent(foundPath!); + expect(agent).toBeDefined(); + + // Step 4: Use the agent + const response = await agent.invoke({ message: "test" }); + expect(response).toBe("You said: test"); + }); + + it("should handle workflow with invalid agent", async () => { + // Create an invalid agent (missing invoke method) + const invalidContent = ` + export default { + // Missing invoke method - has 'run' instead + async run({ message }: { message: string }) { + return message; + } + }; + `; + fs.writeFileSync(path.join(testDir, "agent.ts"), invalidContent); + + // Find the agent + const foundPath = findAgentFile(); + expect(foundPath).not.toBeNull(); + + // Validate should fail (no invoke method) + const validation = await validateAgentFile(foundPath!); + expect(validation.valid).toBe(false); + expect(validation.error).toContain("invoke"); + }); + }); +}); diff --git a/src/__tests__/fixtures/invalid-syntax.ts b/src/__tests__/fixtures/invalid-syntax.ts new file mode 100644 index 0000000..994b5eb --- /dev/null +++ b/src/__tests__/fixtures/invalid-syntax.ts @@ -0,0 +1,5 @@ +// Invalid TypeScript syntax +export default { + async invoke({ message }: { message: string }) { + return `Echo: ${message} + } // Missing closing brace diff --git a/src/__tests__/fixtures/invoke-not-function.ts b/src/__tests__/fixtures/invoke-not-function.ts new file mode 100644 index 0000000..85bf29b --- /dev/null +++ b/src/__tests__/fixtures/invoke-not-function.ts @@ -0,0 +1,4 @@ +// Agent where invoke is not a function +export default { + invoke: "not a function" +}; diff --git a/src/__tests__/fixtures/no-default-export.ts b/src/__tests__/fixtures/no-default-export.ts new file mode 100644 index 0000000..4d09640 --- /dev/null +++ b/src/__tests__/fixtures/no-default-export.ts @@ -0,0 +1,6 @@ +// Agent without default export +export const agent = { + async invoke({ message }: { message: string }) { + return `Echo: ${message}`; + } +}; diff --git a/src/__tests__/fixtures/no-invoke-method.ts b/src/__tests__/fixtures/no-invoke-method.ts new file mode 100644 index 0000000..de005b3 --- /dev/null +++ b/src/__tests__/fixtures/no-invoke-method.ts @@ -0,0 +1,6 @@ +// Agent without invoke method +export default { + async process({ message }: { message: string }) { + return `Echo: ${message}`; + } +}; diff --git a/src/__tests__/fixtures/valid-agent.js b/src/__tests__/fixtures/valid-agent.js new file mode 100644 index 0000000..c826932 --- /dev/null +++ b/src/__tests__/fixtures/valid-agent.js @@ -0,0 +1,6 @@ +// Valid JavaScript agent for testing +export default { + async invoke({ message, userPhoneNumber }) { + return `Echo: ${message} from ${userPhoneNumber}`; + } +}; diff --git a/src/__tests__/fixtures/valid-agent.ts b/src/__tests__/fixtures/valid-agent.ts new file mode 100644 index 0000000..3b34165 --- /dev/null +++ b/src/__tests__/fixtures/valid-agent.ts @@ -0,0 +1,6 @@ +// Valid TypeScript agent for testing +export default { + async invoke({ message, userPhoneNumber }: { message: string; userPhoneNumber: string }) { + return `Echo: ${message} from ${userPhoneNumber}`; + } +}; diff --git a/src/__tests__/status.test.ts b/src/__tests__/status.test.ts new file mode 100644 index 0000000..b2b081d --- /dev/null +++ b/src/__tests__/status.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"; +import { checkStatus, loadCredentials } from "../auth"; +import * as fs from "fs"; +import * as path from "path"; + +describe("flux status command", () => { + let originalHome: string; + let testConfigDir: string; + let testConfigFile: string; + + beforeEach(() => { + // Save original HOME + originalHome = process.env.HOME || ""; + + // Create temporary config directory + testConfigDir = path.join(process.cwd(), "test-config-" + Date.now()); + const fluxDir = path.join(testConfigDir, ".flux"); + fs.mkdirSync(fluxDir, { recursive: true }); + testConfigFile = path.join(fluxDir, "credentials.json"); + + // Override HOME to use test directory + process.env.HOME = testConfigDir; + }); + + afterEach(() => { + // Restore original HOME + process.env.HOME = originalHome; + + // Clean up test directory + if (fs.existsSync(testConfigDir)) { + fs.rmSync(testConfigDir, { recursive: true, force: true }); + } + }); + + describe("checkStatus", () => { + it("should return not logged in when no credentials exist", async () => { + const status = await checkStatus(); + + expect(status.loggedIn).toBe(false); + expect(status.phone).toBeUndefined(); + expect(status.tokenValid).toBeUndefined(); + expect(status.serverReachable).toBeUndefined(); + }); + + it("should include server address in response", async () => { + const status = await checkStatus(); + + expect(status.serverAddress).toBeDefined(); + expect(status.serverAddress).toContain("photon.codes"); + }); + + it("should return logged in status when credentials exist", async () => { + // Create mock credentials + const credentials = { + token: "mock-token-123", + phone: "+1234567890", + authenticatedAt: new Date().toISOString(), + }; + fs.writeFileSync(testConfigFile, JSON.stringify(credentials, null, 2)); + + // Note: This will try to reach the server and likely fail, + // but it should still show as logged in + const status = await checkStatus(); + + expect(status.loggedIn).toBe(true); + expect(status.phone).toBe("+1234567890"); + expect(status.authenticatedAt).toBeDefined(); + }); + + it("should handle server connection errors gracefully", async () => { + // Create mock credentials with invalid token + const credentials = { + token: "invalid-token", + phone: "+1234567890", + authenticatedAt: new Date().toISOString(), + }; + fs.writeFileSync(testConfigFile, JSON.stringify(credentials, null, 2)); + + const status = await checkStatus(); + + expect(status.loggedIn).toBe(true); + expect(status.serverReachable).toBe(false); + expect(status.tokenValid).toBe(false); + expect(status.error).toBeDefined(); + }); + }); + + describe("status display information", () => { + it("should calculate days since authentication", () => { + const authDate = new Date(); + authDate.setDate(authDate.getDate() - 5); // 5 days ago + + const now = new Date(); + const daysSince = Math.floor( + (now.getTime() - authDate.getTime()) / (1000 * 60 * 60 * 24) + ); + + expect(daysSince).toBe(5); + }); + + it("should handle recent authentication (same day)", () => { + const authDate = new Date(); + const now = new Date(); + const daysSince = Math.floor( + (now.getTime() - authDate.getTime()) / (1000 * 60 * 60 * 24) + ); + + expect(daysSince).toBe(0); + }); + }); +}); diff --git a/src/auth.ts b/src/auth.ts index 187baf3..a7b183d 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -7,8 +7,6 @@ import { createGrpcClient } from "better-grpc"; import { FluxService } from "./service"; const GRPC_SERVER_ADDRESS = process.env.FLUX_SERVER_ADDRESS || "fluxy.photon.codes:443"; -const CONFIG_DIR = path.join(process.env.HOME || "~", ".flux"); -const CONFIG_FILE = path.join(CONFIG_DIR, "credentials.json"); const VERIFICATION_NUMBER = "+16286298650"; // Flux iMessage number for verification interface FluxCredentials { @@ -17,25 +15,38 @@ interface FluxCredentials { authenticatedAt?: string; } +// Helper functions to get config paths dynamically (for testing) +function getConfigDir(): string { + return path.join(process.env.HOME || "~", ".flux"); +} + +function getConfigFile(): string { + return path.join(getConfigDir(), "credentials.json"); +} + export function loadCredentials(): FluxCredentials { try { - if (fs.existsSync(CONFIG_FILE)) { - return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")); + const configFile = getConfigFile(); + if (fs.existsSync(configFile)) { + return JSON.parse(fs.readFileSync(configFile, "utf-8")); } } catch {} return {}; } function saveCredentials(credentials: FluxCredentials): void { - if (!fs.existsSync(CONFIG_DIR)) { - fs.mkdirSync(CONFIG_DIR, { recursive: true }); + const configDir = getConfigDir(); + const configFile = getConfigFile(); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); } - fs.writeFileSync(CONFIG_FILE, JSON.stringify(credentials, null, 2)); + fs.writeFileSync(configFile, JSON.stringify(credentials, null, 2)); } function clearCredentials(): void { - if (fs.existsSync(CONFIG_FILE)) { - fs.unlinkSync(CONFIG_FILE); + const configFile = getConfigFile(); + if (fs.existsSync(configFile)) { + fs.unlinkSync(configFile); } } @@ -222,3 +233,54 @@ export function loadConfig(): { phoneNumber?: string } { const credentials = loadCredentials(); return { phoneNumber: credentials.phone }; } + +// Export server address for status checks +export function getServerAddress(): string { + return GRPC_SERVER_ADDRESS; +} + +export async function checkStatus(): Promise<{ + loggedIn: boolean; + phone?: string; + tokenValid?: boolean; + serverReachable?: boolean; + authenticatedAt?: string; + serverAddress?: string; + error?: string; +}> { + const credentials = loadCredentials(); + + // Not logged in + if (!credentials.token || !credentials.phone) { + return { + loggedIn: false, + serverReachable: undefined, + serverAddress: GRPC_SERVER_ADDRESS, + }; + } + + // Try to validate token with server + try { + const client = await createGrpcClientWithRetry(); + const result = await client.FluxService.validateToken(credentials.token); + + return { + loggedIn: true, + phone: result.phone || credentials.phone, + tokenValid: result.valid, + serverReachable: true, + authenticatedAt: credentials.authenticatedAt, + serverAddress: GRPC_SERVER_ADDRESS, + }; + } catch (error: any) { + return { + loggedIn: true, + phone: credentials.phone, + tokenValid: false, + serverReachable: false, + authenticatedAt: credentials.authenticatedAt, + serverAddress: GRPC_SERVER_ADDRESS, + error: error.message, + }; + } +} diff --git a/src/index.ts b/src/index.ts index 79dd610..e129cc6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import * as path from "path"; import * as readline from "readline"; import { FluxClient } from "./flux-client"; -import { login, logout, loadConfig, getAuthToken } from "./auth"; +import { login, logout, loadConfig, getAuthToken, checkStatus } from "./auth"; import { findAgentFile, validateAgentFile, loadAgent } from "./agent-loader"; async function validateCommand(): Promise { @@ -137,6 +137,82 @@ async function runProd() { await new Promise(() => {}); } +async function statusCommand() { + console.log("[FLUX] Checking status...\n"); + + const status = await checkStatus(); + + // Display status information + console.log("═══════════════════════════════════════"); + console.log(" FLUX STATUS REPORT "); + console.log("═══════════════════════════════════════\n"); + + // Login status + if (status.loggedIn) { + console.log("✅ Authentication: Logged in"); + console.log(`📱 Phone Number: ${status.phone}`); + + if (status.authenticatedAt) { + const authDate = new Date(status.authenticatedAt); + const now = new Date(); + const daysSince = Math.floor((now.getTime() - authDate.getTime()) / (1000 * 60 * 60 * 24)); + console.log(`📅 Authenticated: ${authDate.toLocaleString()}`); + console.log(`⏱️ Time Since Login: ${daysSince} day(s) ago`); + } + } else { + console.log("❌ Authentication: Not logged in"); + console.log("💡 Hint: Run 'flux login' to authenticate"); + } + + console.log(); + + // Server connectivity + if (status.serverReachable === true) { + console.log("✅ Server: Connected"); + console.log(`🌐 Server Address: ${status.serverAddress}`); + } else if (status.serverReachable === false) { + console.log("❌ Server: Unreachable"); + console.log(`🌐 Server Address: ${status.serverAddress}`); + if (status.error) { + console.log(`⚠️ Error: ${status.error}`); + } + } else { + console.log("⏸️ Server: Not checked (no credentials)"); + console.log(`🌐 Server Address: ${status.serverAddress}`); + } + + console.log(); + + // Token validity + if (status.tokenValid === true) { + console.log("✅ Token: Valid"); + console.log("🔐 Status: Ready to run agents"); + } else if (status.tokenValid === false) { + console.log("❌ Token: Invalid or expired"); + console.log("💡 Hint: Run 'flux login' to refresh"); + } else { + console.log("⏸️ Token: No token found"); + } + + console.log(); + console.log("═══════════════════════════════════════\n"); + + // Summary and next steps + if (status.loggedIn && status.tokenValid && status.serverReachable) { + console.log("🎉 All systems operational! You're ready to deploy agents."); + console.log(" Run 'flux run --prod' to start your agent.\n"); + } else if (!status.loggedIn) { + console.log("⚠️ Please log in to use Flux."); + console.log(" Run 'flux login' to get started.\n"); + } else if (!status.serverReachable) { + console.log("⚠️ Cannot reach Flux server."); + console.log(" Check your internet connection or try again later.\n"); + } else if (!status.tokenValid) { + console.log("⚠️ Your session has expired."); + console.log(" Run 'flux login' to refresh your authentication.\n"); + } +} + async function main() { const command = process.argv[2]; const flag = process.argv[3]; @@ -169,11 +245,15 @@ async function main() { console.log("[FLUX] Not logged in."); } break; + case "status": + await statusCommand(); + break; default: console.log("Flux CLI - Connect LangChain agents to iMessage\n"); console.log("Commands:"); console.log(" flux login - Log in with your phone number"); console.log(" flux logout - Log out"); + console.log(" flux status - Check server connectivity and auth status"); console.log(" flux validate - Check if agent.ts exports correctly"); console.log(" flux run --local - Test agent locally (no server connection)"); console.log(" flux run --prod - Run agent connected to bridge (default)"); diff --git a/tsconfig.json b/tsconfig.json index 5c2387b..2baa4ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "declaration": false }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/__tests__/fixtures"] }