Skip to content

Commit 3f7f742

Browse files
committed
fix: link faable app
1 parent 4594ee4 commit 3f7f742

8 files changed

Lines changed: 145 additions & 21 deletions

File tree

src/api/FaableApi.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,14 @@ type FaableApiConfig<T> = {} & FaableClientConfig<T>;
7474

7575
export class FaableApi<T = any> {
7676
client: AxiosInstance;
77+
strategy?: AuthStrategy;
7778

7879
constructor(config: FaableApiConfig<T>) {
7980
const {authStrategy,auth} = config
8081
this.client = create_base_client()
81-
const strategy: AuthStrategy | undefined = authStrategy && authStrategy(auth);
82+
this.strategy = authStrategy && authStrategy(auth);
8283

84+
const strategy = this.strategy;
8385
this.client.interceptors.request.use(
8486
async function (config) {
8587
// Do something before request is sent
@@ -110,6 +112,11 @@ export class FaableApi<T = any> {
110112
return data(this.client.get<FaableApp>(`/app/slug/${slug}`));
111113
}
112114

115+
@handleError()
116+
async getApp(app_id: string) {
117+
return data(this.client.get<FaableApp>(`/app/${app_id}`));
118+
}
119+
113120
@handleError()
114121
async getRegistry(app_id: string) {
115122
return data(this.client.get<FaableAppRegistry>(`/app/${app_id}/registry`));
@@ -127,4 +134,9 @@ export class FaableApi<T = any> {
127134
async getAppSecrets(app_id: string) {
128135
return firstPage(data(this.client.get<Page<Secret>>(`/secret/${app_id}`)));
129136
}
137+
138+
@handleError()
139+
async updateApp(app_id: string, params: Partial<FaableApp> & { github_repo?: string }) {
140+
return data(this.client.patch<FaableApp>(`/app/${app_id}`, params));
141+
}
130142
}

src/api/context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ export const context = async () => {
2727
}
2828

2929

30+
const appId = await api?.strategy?.app_id?.();
31+
3032
return {
3133
api,
34+
appId
3235
};
3336
};

src/api/strategies/oidc.strategy.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import { create_base_client } from "../base_client";
22
import { AuthStrategyBuilder } from "./types";
33

44

5+
type TokenExchange = {token:string, app_id:string}
6+
57
const exchangeGithubOidcToken = async(gh_token:string)=>{
68
const client = create_base_client()
7-
const res = await client.post("/auth/github-oidc",{
9+
const res = await client.post<TokenExchange>("/auth/github-oidc",{
810
token:gh_token
911
})
10-
const {token} =res.data
11-
console.log("Obtained github token exchange")
12-
console.log(token)
13-
return token
12+
const {token, app_id} =res.data
13+
return {token, app_id}
1414
}
1515

1616
export const oidc_strategy: AuthStrategyBuilder<{idToken:string}> = (
@@ -21,16 +21,24 @@ export const oidc_strategy: AuthStrategyBuilder<{idToken:string}> = (
2121
throw new Error("Missing idToken.");
2222
}
2323

24-
let token:string="";
24+
let token_ex:TokenExchange;
2525

2626
return {
2727
headers: async () => {
28-
if(!token){
29-
token = await exchangeGithubOidcToken(idToken)
28+
if(!token_ex){
29+
const ex = await exchangeGithubOidcToken(idToken)
30+
token_ex = ex
3031
}
3132
return {
32-
Authorization: `Bearer ${token}`,
33+
Authorization: `Bearer ${token_ex.token}`,
3334
};
3435
},
36+
app_id: async () => {
37+
if(!token_ex){
38+
const ex = await exchangeGithubOidcToken(idToken)
39+
token_ex = ex
40+
}
41+
return token_ex.app_id;
42+
}
3543
};
3644
};

src/api/strategies/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type AuthStrategy = {
22
headers: () => Promise<Record<string, string>>;
3+
app_id?: () => Promise<string | undefined>;
34
};
45
export type AuthStrategyBuilder<T> = (...params: T[]) => AuthStrategy;

src/commands/deploy/deploy_command.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,26 @@ export interface DeployCommandArgs {
1515
export const deploy_command = async (args: DeployCommandArgs) => {
1616
const workdir = args.workdir || process.cwd();
1717

18-
const { api } = await context();
18+
const { api, appId } = await context();
1919

2020
// Resolve runtime
2121
const { app_name, runtime } = await runtime_detection(workdir);
2222

23-
const name = args.app_slug || app_name;
24-
if (!name) {
25-
throw new Error("Missing <app_name>");
23+
let app;
24+
if (args.app_slug) {
25+
app = await api.getBySlug(args.app_slug);
26+
} else {
27+
const oidc_app_id = appId;
28+
if (oidc_app_id) {
29+
app = await api.getApp(oidc_app_id);
30+
} else if (app_name) {
31+
app = await api.getBySlug(app_name);
32+
}
2633
}
2734

28-
// Get app from Faable API
29-
const app = await api.getBySlug(name);
35+
if (!app) {
36+
throw new Error("Missing <app_name>");
37+
}
3038

3139
// Check if we can build docker images
3240
await check_environment();

src/commands/link/index.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { CommandModule } from "yargs";
2+
import { context } from "../../api/context";
3+
import prompts from "prompts";
4+
import { log } from "../../log";
5+
import { cmd } from "../../lib/cmd";
6+
import { Configuration } from "../../lib/Configuration";
7+
8+
type Options = { workdir: string };
9+
10+
const getGitRemoteUrl = async (workdir: string): Promise<string | undefined> => {
11+
try {
12+
const { stdout } = await cmd("git remote get-url origin", { cwd: workdir });
13+
return stdout?.toString().trim();
14+
} catch (error) {
15+
log.warn("Could not detect git remote origin URL.");
16+
return undefined;
17+
}
18+
};
19+
20+
export const link: CommandModule<object, Options> = {
21+
command: "link",
22+
describe: "Link the local repository with a Faable app",
23+
builder: (yargs) => {
24+
return yargs
25+
.option("workdir", {
26+
alias: "w",
27+
type: "string",
28+
description: "Working directory",
29+
})
30+
.showHelpOnFail(false) as any;
31+
},
32+
handler: async (args) => {
33+
const workdir = args.workdir || process.cwd();
34+
const { api } = await context();
35+
36+
log.info("Checking local git repository...");
37+
const gitUrl = await getGitRemoteUrl(workdir);
38+
39+
log.info("Fetching your Faable apps...");
40+
const apps = await api.list();
41+
42+
if (apps.length === 0) {
43+
log.error("No apps found in your account. Create one first at https://faable.cloud");
44+
return;
45+
}
46+
47+
const { selectedApp } = await prompts({
48+
type: "select",
49+
name: "selectedApp",
50+
message: "Select the Faable app to link with this repository:",
51+
choices: apps.map((app) => ({
52+
title: `${app.name} (${app.url})`,
53+
value: app,
54+
})),
55+
});
56+
57+
if (!selectedApp) {
58+
log.info("Link cancelled.");
59+
return;
60+
}
61+
62+
log.info(`Linking to ${selectedApp.name}...`);
63+
64+
// Update the app in the API
65+
if (gitUrl) {
66+
await api.updateApp(selectedApp.id, { github_repo: gitUrl });
67+
log.info(`Updated app ${selectedApp.name} with github_repo: ${gitUrl}`);
68+
} else {
69+
log.warn("No git remote URL detected. Skipping API update for github_repo.");
70+
}
71+
72+
// Save locally for CLI convenience
73+
Configuration.instance().saveConfig({ app_slug: selectedApp.name });
74+
log.info(`Successfully linked local repository to ${selectedApp.name}.`);
75+
},
76+
};

src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import { apps } from "./commands/apps";
44
import { configure } from "./commands/configure";
55
import { deploy } from "./commands/deploy";
66
import { log } from "./log";
7+
import { link } from "./commands/link";
78
import { init } from "./commands/init";
89
import { version } from "./config";
910
import { Configuration } from "./lib/Configuration";
1011

1112
const yg = yargs();
1213
yg.scriptName("faable")
13-
.middleware(function (argv) {
14-
console.log(`Faable CLI ${version}`);
14+
.middleware(function (_argv) {
15+
log.info(`Faable CLI ${version}`);
1516
}, true)
1617
.option("c", {
1718
alias: "config",
@@ -31,6 +32,7 @@ yg.scriptName("faable")
3132
.command(apps)
3233
.command(configure)
3334
.command(init)
35+
.command(link)
3436
.demandCommand(1)
3537
.help()
3638
.fail(function (msg, err) {

src/lib/Configuration.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,24 @@ import { log } from "../log";
44
interface ProjectConfig {
55
startCommand?: string;
66
buildCommand?: string;
7+
app_slug?: string;
78
}
89

910
export class Configuration {
1011
private static _instance: Configuration;
1112
private config: ProjectConfig = {};
13+
private config_file: string = "faable.json";
1214

1315
private constructor() {
1416
// Try to read default config file
1517
this.setConfigFile("faable.json", { ignoreWarnings: true });
1618
}
1719

1820
setConfigFile(file: string, options: { ignoreWarnings: boolean }) {
19-
const config_file = path.join(process.cwd(), file);
20-
if (fs.existsSync(config_file)) {
21-
this.config = fs.readJSONSync(config_file);
21+
this.config_file = file;
22+
const config_path = path.join(process.cwd(), file);
23+
if (fs.existsSync(config_path)) {
24+
this.config = fs.readJSONSync(config_path);
2225
log.info(`Loaded configuration from: ${file}`);
2326
} else {
2427
if (!options.ignoreWarnings) {
@@ -27,6 +30,13 @@ export class Configuration {
2730
}
2831
}
2932

33+
saveConfig(updates: Partial<ProjectConfig>) {
34+
this.config = { ...this.config, ...updates };
35+
const config_path = path.join(process.cwd(), this.config_file);
36+
fs.writeJSONSync(config_path, this.config, { spaces: 2 });
37+
log.info(`Configuration saved to: ${this.config_file}`);
38+
}
39+
3040
public static instance() {
3141
if (!Configuration._instance) {
3242
Configuration._instance = new Configuration();
@@ -41,4 +51,8 @@ export class Configuration {
4151
get buildCommand() {
4252
return this.config.buildCommand;
4353
}
54+
55+
get app_slug() {
56+
return this.config.app_slug;
57+
}
4458
}

0 commit comments

Comments
 (0)