Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

This is the San Francisco Compute command line tool.

### End-User Install
### Install (MacOS/Linux)

End users can install the command line tool with:
Install the command line tool by running:

```bash
curl -fsSL https://sfcompute.com/cli/install | bash
```

Then can run the cli:
Then, you can run the cli:

```bash
sfc --version # 0.1.0
sf --version # 0.1.0
```
4 changes: 4 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"organizeImports": {
"enabled": true
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2
},
"linter": {
"enabled": true,
"rules": {
Expand Down
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"commander": "^12.1.0",
"dayjs": "^1.11.11",
"dotenv": "^16.4.5",
"inquirer": "^10.1.2",
"node-fetch": "^3.3.2",
"ora": "^8.0.1",
"parse-duration": "^1.1.0"
Expand Down
114 changes: 80 additions & 34 deletions src/helpers/config.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,106 @@
import { unlinkSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { EmptyObject } from "../types/empty";

export interface Config {
token?: string;
isDevelopment?: boolean;
api_url: string;
webapp_url: string;
api_url: string;
webapp_url: string;
auth_token?: string;
}

const ProductionConfigDefaults = {
api_url: "https://api.sfcompute.com",
webapp_url: "https://sfcompute.com",
api_url: "https://api.sfcompute.com",
webapp_url: "https://sfcompute.com",
};

const DevelopmentConfigDefaults = {
api_url: "http://localhost:8080",
webapp_url: "http://localhost:3000",
api_url: "http://localhost:8080",
webapp_url: "http://localhost:3000",
};

const ConfigDefaults = process.env.IS_DEVELOPMENT_CLI_ENV
? DevelopmentConfigDefaults
: ProductionConfigDefaults;
? DevelopmentConfigDefaults
: ProductionConfigDefaults;

export async function saveConfig(config: Partial<Config>): Promise<void> {
const configDir = join(homedir(), ".sfcompute");
const configPath = join(configDir, "config");
const configData = JSON.stringify(config, null, 2);
const configPath = getConfigPath();
const configData = JSON.stringify(config, null, 2);

try {
await Bun.write(configPath, configData);
console.log("Config saved successfully.");
} catch (error) {
console.error("Failed to save config:", error);
}
try {
await Bun.write(configPath, configData);
console.log("Config saved successfully.");
} catch (error) {
console.error("Failed to save config:", error);
}
}

export async function loadConfig(): Promise<Config> {
const configDir = join(homedir(), ".sfcompute");
const configPath = join(configDir, "config");
const configFileData = await readConfigFile();

try {
const file = Bun.file(configPath);
const configData = await file.text();
const config = JSON.parse(configData) as Config;
return { ...ConfigDefaults, ...config };
} catch (error) {
return ConfigDefaults;
}
return { ...ConfigDefaults, ...configFileData };
}

export async function getToken() {
const config = await loadConfig();
return config?.token;
// only for development
export async function deleteConfig() {
const exists = await configFileExists();
if (!exists) {
return;
}

const configPath = getConfigPath();

try {
unlinkSync(configPath);
console.log("Config deleted successfully.");
} catch (error) {
console.error("Failed to delete config:", error);
}
}

// --

export function getConfigPath(): string {
const configDir = join(homedir(), ".sfcompute");
const configPath = join(configDir, "config");

return configPath;
}

function configFileExists(): Promise<boolean> {
const configPath = getConfigPath();
return Bun.file(configPath).exists();
}

async function readConfigFile(): Promise<Config | EmptyObject> {
const exists = await configFileExists();
if (!exists) {
return {};
}

const configPath = getConfigPath();
try {
const configData = await Bun.file(configPath).text();
const config = JSON.parse(configData);
if (typeof config === "object" && config !== null) {
return config;
}

return {};
} catch (error) {
console.error("Error reading config file:", error);
return {};
}
}

// --

export async function getAuthToken() {
const config = await loadConfig();
return config?.auth_token;
}

export async function getAuthorizationHeader() {
const token = await getToken();
return { Authorization: `Bearer ${token}` };
const token = await getAuthToken();
return { Authorization: `Bearer ${token}` };
}
10 changes: 7 additions & 3 deletions src/helpers/errors.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
export function logAndQuit(message: string) {
console.error(message);
process.exit(1);
console.error(message);
process.exit(1);
}

export function logLoginMessageAndQuit() {
logAndQuit("You need to login first.\n\n\t$ sf login\n");
const loginCommand = process.env.IS_DEVELOPMENT_CLI_ENV
? "bun dev login"
: "sf login";

logAndQuit(`You need to login first.\n\n\t$ ${loginCommand}\n`);
}
2 changes: 1 addition & 1 deletion src/helpers/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export function clearScreen() {
process.stdout.write("\x1Bc");
process.stdout.write("\x1Bc");
}
2 changes: 2 additions & 0 deletions src/helpers/units.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type Cents = number;
export type Centicents = number;
69 changes: 37 additions & 32 deletions src/helpers/urls.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,58 @@
import { loadConfig } from "./config";

const webPaths = {
cli_session_create: "/cli/session",
cli_session_get: ({ token }: { token: string }) =>
`/cli/session?token=${token}`,
cli_session_create: "/cli/session",
cli_session_get: ({ token }: { token: string }) =>
`/cli/session?token=${token}`,
};

const apiPaths = {
credentials_create: "/v0/credentials",
credentials_list: "/v0/credentials",
instances_list: "/v0/instances",
instances_get: ({ id }: { id: string }) => `/v0/instances/${id}`,
contracts_list: "/v0/contracts",
contracts_get: ({ id }: { id: string }) => `/v0/contracts/${id}`,
orders_create: "/v0/orders",
orders_list: "/v0/orders",
orders_get: ({ id }: { id: string }) => `/v0/orders/${id}`,
orders_create: "/v0/orders",
orders_list: "/v0/orders",
orders_get: ({ id }: { id: string }) => `/v0/orders/${id}`,

instances_list: "/v0/instances",
instances_get: ({ id }: { id: string }) => `/v0/instances/${id}`,

credentials_create: "/v0/credentials",
credentials_list: "/v0/credentials",

contracts_list: "/v0/contracts",
contracts_get: ({ id }: { id: string }) => `/v0/contracts/${id}`,

balance_get: "/v0/balance",
};

export async function getWebAppUrl<
K extends keyof typeof webPaths,
V extends Extract<(typeof webPaths)[K], (...args: any) => any>,
K extends keyof typeof webPaths,
V extends Extract<(typeof webPaths)[K], (...args: any) => any>,
>(key: K, params: Parameters<V>[0]): Promise<string>;
export async function getWebAppUrl(key: keyof typeof webPaths): Promise<string>;
export async function getWebAppUrl(
key: keyof typeof webPaths,
params?: any,
key: keyof typeof webPaths,
params?: any,
): Promise<string> {
const config = await loadConfig();
const path = webPaths[key];
if (typeof path === "function") {
return config.webapp_url + path(params);
}
return config.webapp_url + path;
const config = await loadConfig();
const path = webPaths[key];
if (typeof path === "function") {
return config.webapp_url + path(params);
}
return config.webapp_url + path;
}

export async function getApiUrl<
K extends keyof typeof apiPaths,
V extends Extract<(typeof apiPaths)[K], (...args: any) => any>,
K extends keyof typeof apiPaths,
V extends Extract<(typeof apiPaths)[K], (...args: any) => any>,
>(key: K, params: Parameters<V>[0]): Promise<string>;
export async function getApiUrl(key: keyof typeof apiPaths): Promise<string>;
export async function getApiUrl(
key: keyof typeof apiPaths,
params?: any,
key: keyof typeof apiPaths,
params?: any,
): Promise<string> {
const config = await loadConfig();
const path = apiPaths[key];
if (typeof path === "function") {
return config.api_url + path(params);
}
return config.api_url + path;
const config = await loadConfig();
const path = apiPaths[key];
if (typeof path === "function") {
return config.api_url + path(params);
}
return config.api_url + path;
}
18 changes: 11 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
#!/usr/bin/env bun

import { Command } from "commander";
import { version } from "../package.json";
import { registerBalance } from "./lib/balance";
import { registerBuy } from "./lib/buy";
import { registerContracts } from "./lib/contracts";
import { registerDev } from "./lib/dev";
import { registerLogin } from "./lib/login";
import { registerSSH } from "./lib/ssh";
import { registerUpgrade } from "./lib/upgrade";
import { version } from "../package.json";
import { registerBuy } from "./lib/buy";
import { registerContracts } from "./lib/contracts";
import { registerSell } from "./lib/sell";

const program = new Command();

program
.name("sfc")
.description("San Francisco Compute command line tool")
.name("sf")
.description("San Francisco Compute command line tool.")
.version(version);

// commands
registerLogin(program);
registerSSH(program);
registerUpgrade(program);
registerBuy(program);
registerSSH(program);
registerContracts(program);
registerSell(program);
registerBalance(program);
registerUpgrade(program);

// (only development commands)
registerDev(program);
Expand Down
Loading