@faable/faable 0.0.0-dev

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.
Files changed (32) hide show
  1. package/README.md +24 -0
  2. package/bin/faable.js +3 -0
  3. package/dist/api/FaableApi.js +88 -0
  4. package/dist/api/base_client.js +11 -0
  5. package/dist/api/context.js +30 -0
  6. package/dist/api/strategies/apikey.strategy.js +15 -0
  7. package/dist/api/strategies/oidc.strategy.js +31 -0
  8. package/dist/commands/apps/index.js +17 -0
  9. package/dist/commands/configure/index.js +34 -0
  10. package/dist/commands/deploy/check_environment.js +13 -0
  11. package/dist/commands/deploy/deploy_command.js +50 -0
  12. package/dist/commands/deploy/index.js +22 -0
  13. package/dist/commands/deploy/node-pipeline/analyze_package.js +34 -0
  14. package/dist/commands/deploy/node-pipeline/build_docker.js +48 -0
  15. package/dist/commands/deploy/node-pipeline/build_project.js +26 -0
  16. package/dist/commands/deploy/node-pipeline/index.js +31 -0
  17. package/dist/commands/deploy/node-pipeline/templates/Dockerfile +18 -0
  18. package/dist/commands/deploy/node-pipeline/templates/entrypoint.sh +8 -0
  19. package/dist/commands/deploy/runtime-detect/helpers/has_any_of_files.js +14 -0
  20. package/dist/commands/deploy/runtime-detect/runtime_detection.js +19 -0
  21. package/dist/commands/deploy/runtime-detect/strategies/docker.js +9 -0
  22. package/dist/commands/deploy/runtime-detect/strategies/nodejs.js +49 -0
  23. package/dist/commands/deploy/upload_tag.js +22 -0
  24. package/dist/commands/init/index.js +22 -0
  25. package/dist/commands/init/writeGithubAction.js +108 -0
  26. package/dist/config.js +10 -0
  27. package/dist/index.js +48 -0
  28. package/dist/lib/Configuration.js +38 -0
  29. package/dist/lib/CredentialsStore.js +36 -0
  30. package/dist/lib/cmd.js +40 -0
  31. package/dist/log.js +13 -0
  32. package/package.json +38 -0
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ <p align="center">
2
+ <a href="https://faable.com">
3
+ <img src="https://www.faable.com/assets/logo/Emblem.png" height="96">
4
+ <h3 align="center">Faable</h3>
5
+ </a>
6
+ </p>
7
+
8
+ <p align="center">
9
+ Your React, Node.js or Python apps, up to the cloud in seconds.
10
+ </p>
11
+
12
+ ## Faable
13
+
14
+ Faable is the best platform to build modern architectures that scale precisely to meet demand. We handle the hard stuff so you can focus on building cloud ready apps. Make your business cloud driven and join those awesome companies.
15
+
16
+ To install the latest version of Faable CLI:
17
+
18
+ ```bash
19
+ npm i -g @faable/faable
20
+ ```
21
+
22
+ ## Documentation
23
+
24
+ For details on how to use Faable CLI, check out our [documentation](https://docs.faable.com/).
package/bin/faable.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "../dist/index.js";
@@ -0,0 +1,88 @@
1
+ import { __decorate } from 'tslib';
2
+ import { create_base_client } from './base_client.js';
3
+
4
+ function handleError() {
5
+ return function (target, propertyKey, descriptor) {
6
+ const method = descriptor.value;
7
+ descriptor.value = async function (...args) {
8
+ try {
9
+ return await method.bind(this).apply(target, args);
10
+ }
11
+ catch (error) {
12
+ const e = error;
13
+ if (e.isAxiosError) {
14
+ const res = e.response;
15
+ if (res) {
16
+ throw new Error(`FaableApi ${e.config.url} ${res.status}: ${res?.data.message}`, { cause: error });
17
+ }
18
+ else {
19
+ throw new Error(`FaableApi ${e.message}`, { cause: error });
20
+ }
21
+ }
22
+ throw error;
23
+ }
24
+ };
25
+ };
26
+ }
27
+ const firstPage = async (res) => {
28
+ const items = (await res).results;
29
+ return items;
30
+ };
31
+ const data = async (res) => {
32
+ const items = (await res).data;
33
+ return items;
34
+ };
35
+ class FaableApi {
36
+ client;
37
+ constructor(config) {
38
+ const { authStrategy, auth } = config;
39
+ this.client = create_base_client();
40
+ const strategy = authStrategy && authStrategy(auth);
41
+ this.client.interceptors.request.use(async function (config) {
42
+ // Do something before request is sent
43
+ const headers = strategy ? await strategy.headers() : {};
44
+ config.headers.set(headers);
45
+ // console.log("all headers");
46
+ // console.log(headers);
47
+ return config;
48
+ }, function (error) {
49
+ // Do something with request error
50
+ return Promise.reject(error);
51
+ });
52
+ }
53
+ static create(config = {}) {
54
+ return new FaableApi(config);
55
+ }
56
+ async list() {
57
+ return firstPage(data(this.client.get(`/app`)));
58
+ }
59
+ async getBySlug(slug) {
60
+ return data(this.client.get(`/app/slug/${slug}`));
61
+ }
62
+ async getRegistry(app_id) {
63
+ return data(this.client.get(`/app/${app_id}/registry`));
64
+ }
65
+ async createDeployment(params) {
66
+ return data(this.client.post(`/deployment`, params));
67
+ }
68
+ async getAppSecrets(app_id) {
69
+ return firstPage(data(this.client.get(`/secret/${app_id}`)));
70
+ }
71
+ }
72
+ __decorate([
73
+ handleError()
74
+ ], FaableApi.prototype, "list", null);
75
+ __decorate([
76
+ handleError()
77
+ ], FaableApi.prototype, "getBySlug", null);
78
+ __decorate([
79
+ handleError()
80
+ ], FaableApi.prototype, "getRegistry", null);
81
+ __decorate([
82
+ handleError()
83
+ ], FaableApi.prototype, "createDeployment", null);
84
+ __decorate([
85
+ handleError()
86
+ ], FaableApi.prototype, "getAppSecrets", null);
87
+
88
+ export { FaableApi };
@@ -0,0 +1,11 @@
1
+ import axios from 'axios';
2
+
3
+ const BASE_URL = process.env.BASE_URL || "https://api.faable.com";
4
+ const create_base_client = () => {
5
+ return axios.create({
6
+ baseURL: BASE_URL,
7
+ timeout: 10000
8
+ });
9
+ };
10
+
11
+ export { create_base_client };
@@ -0,0 +1,30 @@
1
+ import { FaableApi } from './FaableApi.js';
2
+ import { apikey_strategy } from './strategies/apikey.strategy.js';
3
+ import { getIDToken } from '@actions/core';
4
+ import { oidc_strategy } from './strategies/oidc.strategy.js';
5
+
6
+ // import { CredentialsStore } from "../lib/CredentialsStore";
7
+ const context = async () => {
8
+ let api;
9
+ if (process.env.FAABLE_APIKEY) {
10
+ const apikey = process.env.FAABLE_APIKEY;
11
+ api = FaableApi.create({ authStrategy: apikey_strategy, auth: { apikey } });
12
+ }
13
+ //const store = new CredentialsStore();
14
+ //(await store.loadCredentials())?.apikey
15
+ // Github actions environment
16
+ if (process.env.GITHUB_ACTIONS === 'true') {
17
+ try {
18
+ const idToken = process.env.FAABLE_ID_TOKEN || await getIDToken("https://faable.com");
19
+ api = FaableApi.create({ authStrategy: oidc_strategy, auth: { idToken } });
20
+ }
21
+ catch (_) {
22
+ console.error("Error fetching token, configure 'permissions: id-token: write'");
23
+ }
24
+ }
25
+ return {
26
+ api,
27
+ };
28
+ };
29
+
30
+ export { context };
@@ -0,0 +1,15 @@
1
+ const apikey_strategy = (config) => {
2
+ const { apikey } = config;
3
+ if (!apikey) {
4
+ throw new Error("Missing apikey.");
5
+ }
6
+ return {
7
+ headers: async () => {
8
+ return {
9
+ Authorization: `Basic ${Buffer.from(`${apikey}:`).toString("base64")}`,
10
+ };
11
+ },
12
+ };
13
+ };
14
+
15
+ export { apikey_strategy };
@@ -0,0 +1,31 @@
1
+ import { create_base_client } from '../base_client.js';
2
+
3
+ const exchangeGithubOidcToken = async (gh_token) => {
4
+ const client = create_base_client();
5
+ const res = await client.post("/auth/github-oidc", {
6
+ token: gh_token
7
+ });
8
+ const { token } = res.data;
9
+ console.log("Obtained github token exchange");
10
+ console.log(token);
11
+ return token;
12
+ };
13
+ const oidc_strategy = (config) => {
14
+ const { idToken } = config;
15
+ if (!idToken) {
16
+ throw new Error("Missing idToken.");
17
+ }
18
+ let token = "";
19
+ return {
20
+ headers: async () => {
21
+ if (!token) {
22
+ token = await exchangeGithubOidcToken(idToken);
23
+ }
24
+ return {
25
+ Authorization: `Bearer ${token}`,
26
+ };
27
+ },
28
+ };
29
+ };
30
+
31
+ export { oidc_strategy };
@@ -0,0 +1,17 @@
1
+ import { context } from '../../api/context.js';
2
+
3
+ const apps = {
4
+ command: "apps",
5
+ describe: "Manage Faable Cloud Apps",
6
+ builder: (yargs) => {
7
+ return yargs.showHelpOnFail(false);
8
+ },
9
+ handler: async (args) => {
10
+ const { api } = await context();
11
+ const apps = await api.list();
12
+ console.log("Apps...");
13
+ console.log(apps.map((app) => app.name));
14
+ },
15
+ };
16
+
17
+ export { apps };
@@ -0,0 +1,34 @@
1
+ import { CredentialsStore } from '../../lib/CredentialsStore.js';
2
+ import prompts from 'prompts';
3
+
4
+ const configure = {
5
+ command: "configure",
6
+ describe: "Configure Faable CLI",
7
+ builder: (yargs) => {
8
+ return yargs
9
+ .option("remove", {
10
+ alias: "d",
11
+ type: "boolean",
12
+ description: "Delete current configuration",
13
+ default: false,
14
+ })
15
+ .showHelpOnFail(false);
16
+ },
17
+ handler: async (args) => {
18
+ const { app_name, workdir, api, remove } = args;
19
+ const store = new CredentialsStore();
20
+ if (remove) {
21
+ await store.deleteCredentials();
22
+ }
23
+ const { apikey } = await prompts([
24
+ {
25
+ type: "text",
26
+ name: "apikey",
27
+ message: "What is your Faable ApiKey?",
28
+ },
29
+ ]);
30
+ await store.saveApiKey({ apikey });
31
+ },
32
+ };
33
+
34
+ export { configure };
@@ -0,0 +1,13 @@
1
+ import { cmd } from '../../lib/cmd.js';
2
+
3
+ const check_environment = async () => {
4
+ try {
5
+ await cmd("docker ps");
6
+ }
7
+ catch (error) {
8
+ console.log(error);
9
+ throw new Error(`Docker is not running`);
10
+ }
11
+ };
12
+
13
+ export { check_environment };
@@ -0,0 +1,50 @@
1
+ import { log } from '../../log.js';
2
+ import { upload_tag } from './upload_tag.js';
3
+ import { check_environment } from './check_environment.js';
4
+ import { context } from '../../api/context.js';
5
+ import { build_node } from './node-pipeline/index.js';
6
+ import { runtime_detection } from './runtime-detect/runtime_detection.js';
7
+ import { cmd } from '../../lib/cmd.js';
8
+
9
+ const deploy_command = async (args) => {
10
+ const workdir = args.workdir || process.cwd();
11
+ const { api } = await context();
12
+ // Resolve runtime
13
+ const { app_name, runtime } = await runtime_detection(workdir);
14
+ const name = args.app_slug || app_name;
15
+ if (!name) {
16
+ throw new Error("Missing <app_name>");
17
+ }
18
+ // Get app from Faable API
19
+ const app = await api.getBySlug(name);
20
+ // Check if we can build docker images
21
+ await check_environment();
22
+ log.info(`🚀 Deploying ${app.name} (${app.id}) runtime=${runtime.name}-${runtime.version}`);
23
+ // get environment variables
24
+ const env_vars = await api.getAppSecrets(app.id);
25
+ let type;
26
+ if (runtime.name == "node") {
27
+ const node_result = await build_node(app, {
28
+ workdir,
29
+ runtime,
30
+ env_vars,
31
+ });
32
+ type = node_result.type;
33
+ }
34
+ else if (runtime.name == "docker") {
35
+ type = "node";
36
+ await cmd(`docker build -t ${app.id} .`, {
37
+ enableOutput: true,
38
+ });
39
+ }
40
+ else {
41
+ throw new Error(`No build pipeline for runtime=${runtime.name}`);
42
+ }
43
+ // Upload to Faable registry
44
+ const { upload_tagname } = await upload_tag({ app, api });
45
+ // Create a deployment for this image
46
+ await api.createDeployment({ app_id: app.id, image: upload_tagname, type });
47
+ log.info(`🌍 Deployment created -> https://${app.url}`);
48
+ };
49
+
50
+ export { deploy_command };
@@ -0,0 +1,22 @@
1
+ import { deploy_command } from './deploy_command.js';
2
+
3
+ const deploy = {
4
+ command: "deploy [app_slug]",
5
+ describe: "Deploy a faable app",
6
+ builder: (yargs) => {
7
+ return yargs
8
+ .positional("app_slug", {
9
+ type: "string",
10
+ description: "App slug",
11
+ })
12
+ .option("workdir", {
13
+ alias: "w",
14
+ type: "string",
15
+ description: "Working directory",
16
+ })
17
+ .showHelpOnFail(false);
18
+ },
19
+ handler: deploy_command,
20
+ };
21
+
22
+ export { deploy };
@@ -0,0 +1,34 @@
1
+ import fs from 'fs-extra';
2
+ import path__default from 'path';
3
+ import { log } from '../../../log.js';
4
+ import * as R from 'ramda';
5
+
6
+ const analyze_package = async (params) => {
7
+ const workdir = params.workdir;
8
+ const package_file = path__default.join(path__default.resolve(workdir), "package.json");
9
+ log.info(`Loading config from package.json`);
10
+ const pkg = await fs.readJSON(package_file);
11
+ // Check if build is required to run
12
+ const build_script = process.env.FAABLE_NPM_BUILD_SCRIPT
13
+ ? process.env.FAABLE_NPM_BUILD_SCRIPT
14
+ : pkg?.scripts["build"]
15
+ ? "build"
16
+ : null;
17
+ if (!build_script) {
18
+ log.info(`No build script on package.json`);
19
+ }
20
+ let type = "node";
21
+ // Detect nextjs deployment type
22
+ const next_dep = R.lensPath(["dependencies", "next"]);
23
+ const next_devdep = R.lensPath(["devDependencies", "next"]);
24
+ if (R.view(next_dep, pkg) || R.view(next_devdep, pkg)) {
25
+ type = "next";
26
+ }
27
+ log.info(`⚡️ Detected deployment type=${type}`);
28
+ return {
29
+ build_script,
30
+ type,
31
+ };
32
+ };
33
+
34
+ export { analyze_package };
@@ -0,0 +1,48 @@
1
+ import { log } from '../../../log.js';
2
+ import { cmd } from '../../../lib/cmd.js';
3
+ import fs from 'fs-extra';
4
+ import Handlebars from 'handlebars';
5
+ import * as path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import { Configuration } from '../../../lib/Configuration.js';
8
+
9
+ const __filename$1 = fileURLToPath(import.meta.url);
10
+ const __dirname$1 = path.dirname(__filename$1);
11
+ const templates_dir = path.join(__dirname$1, "templates");
12
+ const dockerfile = fs.readFileSync(`${templates_dir}/Dockerfile`).toString();
13
+ const entrypoint = fs
14
+ .readFileSync(`${templates_dir}/entrypoint.sh`)
15
+ .toString("utf-8");
16
+ Handlebars.registerHelper("escape", function (variable) {
17
+ //const escaped_quotes = variable.replace(/(['"])/g, "\\$1");
18
+ const escaped_lines = variable
19
+ .replace(/(['`\\])/g, "\\$1")
20
+ .replace(/([$])/g, "\\$1");
21
+ return escaped_lines.split("\n").join("\\n");
22
+ //return escaped_lines.split("\n").join("\\n");
23
+ });
24
+ // Docker template file
25
+ const docker_template = Handlebars.compile(dockerfile);
26
+ const entrypoint_template = Handlebars.compile(entrypoint);
27
+ const build_docker = async (props) => {
28
+ const { app, workdir, template_context } = props;
29
+ const entrypoint_custom = entrypoint_template(template_context);
30
+ const start_command = Configuration.instance().startCommand;
31
+ log.info(`⚙️ Start command: ${start_command}`);
32
+ // NOTE: use slim to build projects
33
+ const linux_distro = "slim";
34
+ const from = [template_context.from, linux_distro].filter((e) => e).join("-");
35
+ log.info(`Using docker image ${from}`);
36
+ // run template
37
+ const dockerfile = docker_template({
38
+ from,
39
+ entry_script: entrypoint_custom,
40
+ start_command,
41
+ });
42
+ log.info(`📦 Packaging inside a docker image`);
43
+ // Build options
44
+ const timeout = 10 * 60 * 1000; // 10 minute timeout
45
+ await cmd(`docker build --platform linux/amd64 -t ${app.id} ${workdir} -f -<<EOF\n${dockerfile}\nEOF`, { timeout, enableOutput: true });
46
+ };
47
+
48
+ export { build_docker };
@@ -0,0 +1,26 @@
1
+ import { log } from '../../../log.js';
2
+ import { cmd } from '../../../lib/cmd.js';
3
+ import { Configuration } from '../../../lib/Configuration.js';
4
+
5
+ const build_project = async (args) => {
6
+ const build_script = args.build_script;
7
+ const build_command = build_script
8
+ ? `npm run ${build_script}`
9
+ : Configuration.instance().buildCommand;
10
+ if (build_command) {
11
+ const cwd = args.cwd || process.cwd();
12
+ log.info(`⚙️ Building project [${build_command}]...`);
13
+ const timeout = 1000 * 60 * 30; // 30 minute timeout
14
+ await cmd(build_command, {
15
+ timeout,
16
+ cwd,
17
+ enableOutput: true,
18
+ ...(args?.env ? { env: args?.env } : {}),
19
+ });
20
+ }
21
+ else {
22
+ log.info(`⚡️ No build step`);
23
+ }
24
+ };
25
+
26
+ export { build_project };
@@ -0,0 +1,31 @@
1
+ import { build_docker } from './build_docker.js';
2
+ import { analyze_package } from './analyze_package.js';
3
+ import { build_project } from './build_project.js';
4
+ import * as R from 'ramda';
5
+ import { log } from '../../../log.js';
6
+
7
+ const build_node = async (app, options) => {
8
+ // log.info(`🚀 Build Toolchain ${app.name} [${app.id}]`);
9
+ const { workdir, runtime, env_vars = [] } = options;
10
+ if (!runtime.version) {
11
+ throw new Error("Runtime version not specified for node");
12
+ }
13
+ // Analyze package.json to check if build is needed
14
+ const { build_script, type } = await analyze_package({ workdir });
15
+ // Environment variables
16
+ const env = R.fromPairs(env_vars.map((e) => [e.name, e.value]));
17
+ log.info(`Building with env variables ${Object.keys(env).join(",")}`);
18
+ // Do build
19
+ await build_project({ build_script, env });
20
+ // Bundle project inside a docker image
21
+ await build_docker({
22
+ app,
23
+ workdir,
24
+ template_context: {
25
+ from: `node:${runtime.version}`,
26
+ },
27
+ });
28
+ return { type };
29
+ };
30
+
31
+ export { build_node };
@@ -0,0 +1,18 @@
1
+ FROM {{from}}
2
+ LABEL com.faable.cloud="FaableCloud"
3
+ LABEL description="Faablecloud automatic deployment"
4
+
5
+ WORKDIR /faable/app
6
+
7
+ # Environment variables for runtime
8
+ ENV PORT=80
9
+ ENV NODE_ENV=production
10
+ ENV START_COMMAND="{{start_command}}"
11
+
12
+ # Copy Usercode
13
+ COPY . .
14
+
15
+ # Entrypoint stript
16
+ RUN echo '{{{escape entry_script}}}' >> entrypoint.sh
17
+
18
+ CMD ["/bin/sh", "./entrypoint.sh"]
@@ -0,0 +1,8 @@
1
+ #!/bin/sh
2
+
3
+ NODE_VERSION=$(node --version)
4
+ NPM_VERSION=$(npm --version)
5
+ YARN_VERSION=$(yarn --version)
6
+
7
+ echo "Faable Cloud · [node $NODE_VERSION] [npm $NPM_VERSION] [yarn $YARN_VERSION]"
8
+ eval $START_COMMAND
@@ -0,0 +1,14 @@
1
+ import fs from 'fs-extra';
2
+ import path__default from 'path';
3
+
4
+ const has_any_of_files = (files, workdir) => {
5
+ for (let name of files) {
6
+ const filename = path__default.join(workdir, name);
7
+ if (fs.existsSync(filename)) {
8
+ return true;
9
+ }
10
+ }
11
+ return false;
12
+ };
13
+
14
+ export { has_any_of_files };
@@ -0,0 +1,19 @@
1
+ import { strategy_nodejs } from './strategies/nodejs.js';
2
+ import * as R from 'ramda';
3
+ import { has_any_of_files } from './helpers/has_any_of_files.js';
4
+ import { strategy_docker } from './strategies/docker.js';
5
+
6
+ const runtime_detection = async (workdir) => {
7
+ const has = R.curry(has_any_of_files);
8
+ const strategy = R.cond([
9
+ [has(["package.json"]), R.always(strategy_nodejs)],
10
+ // [has(["requirements.txt"]), R.always(strategy_python)],
11
+ [has(["Dockerfile"]), R.always(strategy_docker)],
12
+ ])(workdir);
13
+ if (!strategy) {
14
+ throw new Error("Cannot detect project type");
15
+ }
16
+ return strategy(workdir);
17
+ };
18
+
19
+ export { runtime_detection };
@@ -0,0 +1,9 @@
1
+ const strategy_docker = async (workdir) => {
2
+ return {
3
+ runtime: {
4
+ name: "docker",
5
+ },
6
+ };
7
+ };
8
+
9
+ export { strategy_docker };
@@ -0,0 +1,49 @@
1
+ import fs from 'fs-extra';
2
+ import path__default from 'path';
3
+ import { cmd } from '../../../../lib/cmd.js';
4
+ import { log } from '../../../../log.js';
5
+
6
+ const getCurrentNodeVersion = async () => {
7
+ const out = await cmd(`node --version`);
8
+ const [_, version] = out.stdout.toString().trim().split("v");
9
+ return version;
10
+ };
11
+ /**
12
+ * Strategy to detect app name from package.json
13
+ * @param api
14
+ * @param workdir
15
+ * @returns
16
+ */
17
+ const strategy_nodejs = async (workdir) => {
18
+ const packageJSONFile = path__default.join(workdir, "package.json");
19
+ // Check we have a valid name
20
+ const { name, engines } = fs.readJSONSync(packageJSONFile);
21
+ if (!name) {
22
+ throw new Error("Missing name in package.json");
23
+ }
24
+ // Use engines.node if found
25
+ let runtime_version = await getCurrentNodeVersion();
26
+ if (engines?.node) {
27
+ try {
28
+ const check_cmd = `npm view node@"${engines.node}" version | tail -n 1 | cut -d "'" -f2`;
29
+ const out = await cmd(check_cmd);
30
+ runtime_version = out.stdout.toString().trim();
31
+ log.info(`Using node@${runtime_version} from engines in package.json (${engines.node})`);
32
+ }
33
+ catch (e) {
34
+ log.info(`Node version defined in engines in package.json is not valid (${engines.node}), using current version ${runtime_version}`);
35
+ }
36
+ }
37
+ else {
38
+ log.info(`Node version ${runtime_version}`);
39
+ }
40
+ return {
41
+ app_name: name,
42
+ runtime: {
43
+ name: "node",
44
+ version: runtime_version,
45
+ },
46
+ };
47
+ };
48
+
49
+ export { strategy_nodejs };
@@ -0,0 +1,22 @@
1
+ import { log } from '../../log.js';
2
+ import { cmd } from '../../lib/cmd.js';
3
+
4
+ const upload_tag = async (args) => {
5
+ const { api, app } = args;
6
+ log.info(`🔁 Uploading...`);
7
+ const registry = await api.getRegistry(app.id);
8
+ // Registry login
9
+ const { user, password, hostname, image } = registry;
10
+ await cmd(`echo "${password}" | docker login --username "${user}" --password-stdin ${hostname}`);
11
+ // Tag image for production
12
+ const upload_tagname = `${hostname}/${image}`;
13
+ await cmd(`docker tag ${app.id} ${upload_tagname}`);
14
+ // Upload the image to faable registry
15
+ await cmd(`docker push ${upload_tagname}`);
16
+ log.info(`✅ Upload completed.`);
17
+ return {
18
+ upload_tagname,
19
+ };
20
+ };
21
+
22
+ export { upload_tag };
@@ -0,0 +1,22 @@
1
+ import { writeGithubAction } from './writeGithubAction.js';
2
+
3
+ const init = {
4
+ command: ["initialize", "$0"],
5
+ describe: "Initialize Faable",
6
+ builder: (yargs) => {
7
+ return yargs
8
+ .option("overwrite", {
9
+ alias: "o",
10
+ type: "boolean",
11
+ description: "Overwrite generated file",
12
+ default: false,
13
+ })
14
+ .showHelpOnFail(false);
15
+ },
16
+ handler: async (args) => {
17
+ const { overwrite } = args;
18
+ await writeGithubAction({ overwrite });
19
+ },
20
+ };
21
+
22
+ export { init };
@@ -0,0 +1,108 @@
1
+ import path__default from 'path';
2
+ import fs from 'fs-extra';
3
+ import { log } from '../../log.js';
4
+ import prompts from 'prompts';
5
+ import { CredentialsStore } from '../../lib/CredentialsStore.js';
6
+ import yaml from 'yaml';
7
+
8
+ const store = new CredentialsStore();
9
+ const get_workflows_dir = () => {
10
+ return path__default.join(process.cwd(), ".github", "workflows");
11
+ };
12
+ const get_default_action = () => {
13
+ return path__default.join(get_workflows_dir(), "deploy.yml");
14
+ };
15
+ const demandConfig = async (force = false) => {
16
+ const creds = await store.loadCredentials();
17
+ if (creds?.apikey) {
18
+ return;
19
+ }
20
+ const { apikey } = await prompts([
21
+ {
22
+ type: "text",
23
+ name: "apikey",
24
+ message: "What is your Faable ApiKey?",
25
+ },
26
+ ]);
27
+ await store.saveApiKey({ apikey });
28
+ };
29
+ const tentativeName = async () => {
30
+ try {
31
+ const pkg = path__default.join(process.cwd(), "package.json");
32
+ const { name } = await fs.readJSON(pkg);
33
+ return name;
34
+ }
35
+ catch (error) {
36
+ return;
37
+ }
38
+ };
39
+ const checkPackageManager = async () => {
40
+ if (fs.existsSync(path__default.join(process.cwd(), "package-lock.json"))) {
41
+ return "npm";
42
+ }
43
+ if (fs.existsSync(path__default.join(process.cwd(), "yarn.lock"))) {
44
+ return "yarn";
45
+ }
46
+ throw new Error("No package-lock.json or yarn.lock file found");
47
+ };
48
+ const writeGithubAction = async (params = {}) => {
49
+ const { overwrite } = params;
50
+ await demandConfig();
51
+ const gh_action_file = get_default_action();
52
+ // Already configured
53
+ if (!overwrite && fs.pathExistsSync(gh_action_file)) {
54
+ log.info(`Github action is already configured.`);
55
+ return;
56
+ }
57
+ const onCancel = (prompt) => {
58
+ log.info("Cancel");
59
+ process.exit(0);
60
+ };
61
+ const { app_name } = await prompts([
62
+ {
63
+ type: "text",
64
+ name: "app_name",
65
+ initial: await tentativeName(),
66
+ message: "Which app are you deploying",
67
+ },
68
+ ], { onCancel });
69
+ const manager = await checkPackageManager();
70
+ const action = {
71
+ name: "Deploy to Faable",
72
+ on: {
73
+ push: {
74
+ branches: ["main"],
75
+ },
76
+ },
77
+ permissions: {
78
+ "id-token": "write",
79
+ "contents": "read"
80
+ },
81
+ jobs: {
82
+ deploy: {
83
+ "runs-on": "ubuntu-latest",
84
+ steps: [
85
+ { uses: "actions/checkout@v2" },
86
+ {
87
+ uses: "actions/setup-node@v4",
88
+ with: {
89
+ "node-version": "lts/*",
90
+ },
91
+ },
92
+ ...(manager == "npm" ? [{ run: "npm ci" }] : []),
93
+ ...(manager == "yarn"
94
+ ? [{ run: "yarn install --frozen-lockfile" }]
95
+ : []),
96
+ {
97
+ name: "Deploy to Faable",
98
+ run: `npx @faable/faable deploy ${app_name}`,
99
+ },
100
+ ],
101
+ },
102
+ },
103
+ };
104
+ await fs.writeFile(gh_action_file, yaml.stringify(action));
105
+ log.info(`Written ${gh_action_file}`);
106
+ };
107
+
108
+ export { writeGithubAction };
package/dist/config.js ADDED
@@ -0,0 +1,10 @@
1
+ import fs from 'fs-extra';
2
+ import path__default from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename$1 = fileURLToPath(import.meta.url);
6
+ const __dirname$1 = path__default.dirname(__filename$1);
7
+ const pkg = fs.readJSONSync(path__default.join(__dirname$1, "..", "package.json"));
8
+ const version = pkg.version;
9
+
10
+ export { version };
package/dist/index.js ADDED
@@ -0,0 +1,48 @@
1
+ import yargs from 'yargs';
2
+ import { hideBin } from 'yargs/helpers';
3
+ import { apps } from './commands/apps/index.js';
4
+ import { configure } from './commands/configure/index.js';
5
+ import { deploy } from './commands/deploy/index.js';
6
+ import { log } from './log.js';
7
+ import { init } from './commands/init/index.js';
8
+ import { version } from './config.js';
9
+ import { Configuration } from './lib/Configuration.js';
10
+
11
+ const yg = yargs();
12
+ yg.scriptName("faable")
13
+ .middleware(function (argv) {
14
+ console.log(`Faable CLI ${version}`);
15
+ }, true)
16
+ .option("c", {
17
+ alias: "config",
18
+ description: "Path to the local `faable.json` file",
19
+ string: true,
20
+ })
21
+ .middleware(function (argv) {
22
+ if (argv.config) {
23
+ Configuration.instance().setConfigFile(argv.config, {
24
+ ignoreWarnings: false,
25
+ });
26
+ }
27
+ else {
28
+ Configuration.instance();
29
+ }
30
+ }, true)
31
+ .command(deploy)
32
+ .command(apps)
33
+ .command(configure)
34
+ .command(init)
35
+ .demandCommand(1)
36
+ .help()
37
+ .fail(function (msg, err) {
38
+ if (err) {
39
+ log.error(`❌ ${err.message}`);
40
+ process.exit(1);
41
+ return;
42
+ }
43
+ if (msg) {
44
+ yg.showHelp();
45
+ log.info(msg);
46
+ }
47
+ })
48
+ .parse(hideBin(process.argv), {});
@@ -0,0 +1,38 @@
1
+ import path__default from 'path';
2
+ import fs from 'fs-extra';
3
+ import { log } from '../log.js';
4
+
5
+ class Configuration {
6
+ static _instance;
7
+ config = {};
8
+ constructor() {
9
+ // Try to read default config file
10
+ this.setConfigFile("faable.json", { ignoreWarnings: true });
11
+ }
12
+ setConfigFile(file, options) {
13
+ const config_file = path__default.join(process.cwd(), file);
14
+ if (fs.existsSync(config_file)) {
15
+ this.config = fs.readJSONSync(config_file);
16
+ log.info(`Loaded configuration from: ${file}`);
17
+ }
18
+ else {
19
+ if (!options.ignoreWarnings) {
20
+ log.warn(`Cannot read Faable config file ${file}`);
21
+ }
22
+ }
23
+ }
24
+ static instance() {
25
+ if (!Configuration._instance) {
26
+ Configuration._instance = new Configuration();
27
+ }
28
+ return Configuration._instance;
29
+ }
30
+ get startCommand() {
31
+ return this.config.startCommand || "npm run start";
32
+ }
33
+ get buildCommand() {
34
+ return this.config.buildCommand;
35
+ }
36
+ }
37
+
38
+ export { Configuration };
@@ -0,0 +1,36 @@
1
+ import path__default from 'path';
2
+ import os from 'os';
3
+ import fs from 'fs-extra';
4
+ import { log } from '../log.js';
5
+
6
+ class CredentialsStore {
7
+ log;
8
+ faable_home;
9
+ constructor(log$1 = log) {
10
+ this.log = log$1;
11
+ this.faable_home = path__default.join(os.homedir(), ".faable");
12
+ }
13
+ async deleteCredentials() {
14
+ await fs.remove(this.faable_home);
15
+ this.log.info(`Deleted credentials`);
16
+ }
17
+ get credentials_path() {
18
+ return path__default.join(this.faable_home, "credentials");
19
+ }
20
+ async saveApiKey(config) {
21
+ await fs.ensureDir(this.faable_home);
22
+ await fs.writeJSON(this.credentials_path, config);
23
+ this.log.info(`Stored apikey`);
24
+ }
25
+ async loadCredentials() {
26
+ if (!fs.existsSync(this.credentials_path)) {
27
+ // No credentials found
28
+ return;
29
+ }
30
+ // Return credentials
31
+ const config = await fs.readJSON(this.credentials_path);
32
+ return config || {};
33
+ }
34
+ }
35
+
36
+ export { CredentialsStore };
@@ -0,0 +1,40 @@
1
+ import { spawn } from 'promisify-child-process';
2
+ import { log } from '../log.js';
3
+
4
+ const cmd = async (cmd, config) => {
5
+ // Defaults
6
+ const enableOutput = config?.enableOutput || false;
7
+ const timeout = config?.timeout;
8
+ const cwd = config?.cwd;
9
+ const child = spawn("/bin/bash", ["-c", cmd], {
10
+ encoding: "utf8",
11
+ stdio: enableOutput ? "inherit" : "pipe",
12
+ timeout,
13
+ cwd,
14
+ env: {
15
+ ...process.env,
16
+ ...config?.env,
17
+ },
18
+ });
19
+ const out_data = [];
20
+ child.stderr?.on("data", (data) => {
21
+ out_data.push(data);
22
+ });
23
+ child.stdout?.on("data", (data) => {
24
+ out_data.push(data);
25
+ });
26
+ try {
27
+ const result = await child;
28
+ return result;
29
+ }
30
+ catch (error) {
31
+ const output = out_data.map((b) => b.toString()).join("\n");
32
+ log.error(error?.message);
33
+ if (output) {
34
+ log.error(output);
35
+ }
36
+ throw new Error(`Command error: ${cmd}`);
37
+ }
38
+ };
39
+
40
+ export { cmd };
package/dist/log.js ADDED
@@ -0,0 +1,13 @@
1
+ import pino from 'pino';
2
+ import 'pino-pretty';
3
+
4
+ const log = pino({
5
+ transport: {
6
+ target: "pino-pretty",
7
+ options: {
8
+ colorize: true,
9
+ },
10
+ },
11
+ });
12
+
13
+ export { log };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@faable/faable",
3
+ "main": "dist/index.js",
4
+ "dependencies": {
5
+ "@actions/core": "^3.0.0",
6
+ "axios": "^1.13.2",
7
+ "fs-extra": "^11.3.2",
8
+ "handlebars": "^4.7.8",
9
+ "pino": "^10.1.0",
10
+ "pino-pretty": "^13.1.3",
11
+ "promisify-child-process": "^4.1.2",
12
+ "prompts": "^2.4.2",
13
+ "ramda": "^0.32.0",
14
+ "tslib": "^2.8.1",
15
+ "yaml": "^2.8.2",
16
+ "yargs": "^18.0.0"
17
+ },
18
+ "version": "0.0.0-dev",
19
+ "bin": {
20
+ "faable": "bin/faable.js"
21
+ },
22
+ "type": "module",
23
+ "license": "MIT",
24
+ "author": "Marc Pomar <marc@faable.com>",
25
+ "types": "./dist/index.d.ts",
26
+ "bugs": {
27
+ "url": "https://github.com/faablecloud/faable/issues"
28
+ },
29
+ "publishConfig": {
30
+ "registry": "https://registry.npmjs.org/",
31
+ "access": "public"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/faablecloud/faable.git"
36
+ },
37
+ "homepage": "https://github.com/faablecloud/faable#readme"
38
+ }