@budibase/cli 0.0.1
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/.eslintrc +12 -0
- package/LICENSE.md +674 -0
- package/dist/analytics/Client.js +32 -0
- package/dist/analytics/index.js +55 -0
- package/dist/backups/index.js +119 -0
- package/dist/backups/objectStore.js +82 -0
- package/dist/backups/utils.js +110 -0
- package/dist/constants.js +9 -0
- package/dist/core/db.js +55 -0
- package/dist/environment.js +4 -0
- package/dist/events.js +13 -0
- package/dist/exec.js +49 -0
- package/dist/hosting/genUser.js +32 -0
- package/dist/hosting/index.js +21 -0
- package/dist/hosting/init.js +117 -0
- package/dist/hosting/makeFiles.js +144 -0
- package/dist/hosting/start.js +63 -0
- package/dist/hosting/status.js +30 -0
- package/dist/hosting/stop.js +30 -0
- package/dist/hosting/types.js +2 -0
- package/dist/hosting/update.js +62 -0
- package/dist/hosting/utils.js +159 -0
- package/dist/hosting/watch.js +47 -0
- package/dist/index.js +38 -0
- package/dist/options.js +14 -0
- package/dist/plugins/index.js +199 -0
- package/dist/plugins/skeleton.js +75 -0
- package/dist/prebuilds.js +46 -0
- package/dist/questions.js +58 -0
- package/dist/structures/Command.js +75 -0
- package/dist/structures/ConfigManager.js +39 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/utils.js +126 -0
- package/package.json +62 -0
- package/src/analytics/Client.ts +33 -0
- package/src/analytics/index.ts +60 -0
- package/src/backups/index.ts +130 -0
- package/src/backups/objectStore.ts +69 -0
- package/src/backups/utils.ts +99 -0
- package/src/constants.ts +4 -0
- package/src/core/db.ts +39 -0
- package/src/environment.ts +3 -0
- package/src/events.ts +11 -0
- package/src/exec.ts +27 -0
- package/src/hosting/genUser.ts +23 -0
- package/src/hosting/index.ts +48 -0
- package/src/hosting/init.ts +76 -0
- package/src/hosting/makeFiles.ts +140 -0
- package/src/hosting/start.ts +30 -0
- package/src/hosting/status.ts +13 -0
- package/src/hosting/stop.ts +13 -0
- package/src/hosting/types.ts +4 -0
- package/src/hosting/update.ts +55 -0
- package/src/hosting/utils.ts +127 -0
- package/src/hosting/watch.ts +37 -0
- package/src/index.ts +27 -0
- package/src/options.ts +8 -0
- package/src/plugins/index.ts +198 -0
- package/src/plugins/skeleton.ts +65 -0
- package/src/prebuilds.ts +50 -0
- package/src/questions.ts +40 -0
- package/src/structures/Command.ts +94 -0
- package/src/structures/ConfigManager.ts +49 -0
- package/src/utils.ts +112 -0
- package/start.sh +3 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Command } from "../structures/Command"
|
|
2
|
+
import { CommandWord } from "../constants"
|
|
3
|
+
import { success, error } from "../utils"
|
|
4
|
+
import { AnalyticsClient } from "./Client"
|
|
5
|
+
|
|
6
|
+
const client = new AnalyticsClient()
|
|
7
|
+
|
|
8
|
+
async function optOut() {
|
|
9
|
+
try {
|
|
10
|
+
// opt them out
|
|
11
|
+
client.disable()
|
|
12
|
+
console.log(
|
|
13
|
+
success(
|
|
14
|
+
"Successfully opted out of Budibase analytics. You can opt in at any time by running 'budi analytics opt-in'"
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
} catch (err: any) {
|
|
18
|
+
console.log(
|
|
19
|
+
error(
|
|
20
|
+
`Error opting out of Budibase analytics. Please try again later - ${err}`
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function optIn() {
|
|
27
|
+
try {
|
|
28
|
+
// opt them in
|
|
29
|
+
client.enable()
|
|
30
|
+
console.log(
|
|
31
|
+
success(
|
|
32
|
+
"Successfully opted in to Budibase analytics. Thank you for helping us make Budibase better!"
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.log(
|
|
37
|
+
error("Error opting in to Budibase analytics. Please try again later.")
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function status() {
|
|
43
|
+
try {
|
|
44
|
+
console.log(success(`Budibase analytics ${client.status()}`))
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.log(
|
|
47
|
+
error("Error fetching analytics status. Please try again later.")
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default new Command(`${CommandWord.ANALYTICS}`)
|
|
53
|
+
.addHelp("Control the analytics you send to Budibase.")
|
|
54
|
+
.addSubOption("--optin", "Opt in to sending analytics to Budibase", optIn)
|
|
55
|
+
.addSubOption("--optout", "Opt out of sending analytics to Budibase.", optOut)
|
|
56
|
+
.addSubOption(
|
|
57
|
+
"--status",
|
|
58
|
+
"Check whether you are currently opted in to Budibase analytics.",
|
|
59
|
+
status
|
|
60
|
+
)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Command } from "../structures/Command"
|
|
2
|
+
import { CommandWord } from "../constants"
|
|
3
|
+
import fs from "fs"
|
|
4
|
+
import { join } from "path"
|
|
5
|
+
import { getAllDbs } from "../core/db"
|
|
6
|
+
import { progressBar, httpCall } from "../utils"
|
|
7
|
+
import {
|
|
8
|
+
TEMP_DIR,
|
|
9
|
+
COUCH_DIR,
|
|
10
|
+
MINIO_DIR,
|
|
11
|
+
getConfig,
|
|
12
|
+
replication,
|
|
13
|
+
getPouches,
|
|
14
|
+
} from "./utils"
|
|
15
|
+
import { exportObjects, importObjects } from "./objectStore"
|
|
16
|
+
const tar = require("tar")
|
|
17
|
+
|
|
18
|
+
type BackupOpts = { env?: string; import?: string; export?: string }
|
|
19
|
+
|
|
20
|
+
async function exportBackup(opts: BackupOpts) {
|
|
21
|
+
const envFile = opts.env || undefined
|
|
22
|
+
let filename = opts["export"] || (opts as string)
|
|
23
|
+
if (typeof filename !== "string") {
|
|
24
|
+
filename = `backup-${new Date().toISOString()}.tar.gz`
|
|
25
|
+
}
|
|
26
|
+
const config = await getConfig(envFile)
|
|
27
|
+
const dbList = (await getAllDbs(config["COUCH_DB_URL"])) as string[]
|
|
28
|
+
const { Remote, Local } = getPouches(config)
|
|
29
|
+
if (fs.existsSync(TEMP_DIR)) {
|
|
30
|
+
fs.rmSync(TEMP_DIR, { recursive: true })
|
|
31
|
+
}
|
|
32
|
+
const couchDir = join(TEMP_DIR, COUCH_DIR)
|
|
33
|
+
fs.mkdirSync(TEMP_DIR)
|
|
34
|
+
fs.mkdirSync(couchDir)
|
|
35
|
+
console.log("CouchDB Export")
|
|
36
|
+
const bar = progressBar(dbList.length)
|
|
37
|
+
let count = 0
|
|
38
|
+
for (let db of dbList) {
|
|
39
|
+
bar.update(++count)
|
|
40
|
+
const remote = new Remote(db)
|
|
41
|
+
const local = new Local(join(TEMP_DIR, COUCH_DIR, db))
|
|
42
|
+
await replication(remote, local)
|
|
43
|
+
}
|
|
44
|
+
bar.stop()
|
|
45
|
+
console.log("S3 Export")
|
|
46
|
+
await exportObjects()
|
|
47
|
+
tar.create(
|
|
48
|
+
{
|
|
49
|
+
sync: true,
|
|
50
|
+
gzip: true,
|
|
51
|
+
file: filename,
|
|
52
|
+
cwd: join(TEMP_DIR),
|
|
53
|
+
},
|
|
54
|
+
[COUCH_DIR, MINIO_DIR]
|
|
55
|
+
)
|
|
56
|
+
fs.rmSync(TEMP_DIR, { recursive: true })
|
|
57
|
+
console.log(`Generated export file - ${filename}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function importBackup(opts: BackupOpts) {
|
|
61
|
+
const envFile = opts.env || undefined
|
|
62
|
+
const filename = opts["import"] || (opts as string)
|
|
63
|
+
const config = await getConfig(envFile)
|
|
64
|
+
if (!filename || !fs.existsSync(filename)) {
|
|
65
|
+
console.error("Cannot import without specifying a valid file to import")
|
|
66
|
+
process.exit(-1)
|
|
67
|
+
}
|
|
68
|
+
if (fs.existsSync(TEMP_DIR)) {
|
|
69
|
+
fs.rmSync(TEMP_DIR, { recursive: true })
|
|
70
|
+
}
|
|
71
|
+
fs.mkdirSync(TEMP_DIR)
|
|
72
|
+
tar.extract({
|
|
73
|
+
sync: true,
|
|
74
|
+
cwd: join(TEMP_DIR),
|
|
75
|
+
file: filename,
|
|
76
|
+
})
|
|
77
|
+
const { Remote, Local } = getPouches(config)
|
|
78
|
+
const dbList = fs.readdirSync(join(TEMP_DIR, COUCH_DIR))
|
|
79
|
+
console.log("CouchDB Import")
|
|
80
|
+
const bar = progressBar(dbList.length)
|
|
81
|
+
let count = 0
|
|
82
|
+
for (let db of dbList) {
|
|
83
|
+
bar.update(++count)
|
|
84
|
+
const remote = new Remote(db)
|
|
85
|
+
const local = new Local(join(TEMP_DIR, COUCH_DIR, db))
|
|
86
|
+
await replication(local, remote)
|
|
87
|
+
}
|
|
88
|
+
bar.stop()
|
|
89
|
+
console.log("MinIO Import")
|
|
90
|
+
await importObjects()
|
|
91
|
+
// finish by letting the system know that a restore has occurred
|
|
92
|
+
try {
|
|
93
|
+
await httpCall(
|
|
94
|
+
`http://localhost:${config.MAIN_PORT}/api/system/restored`,
|
|
95
|
+
"POST"
|
|
96
|
+
)
|
|
97
|
+
} catch (err) {
|
|
98
|
+
// ignore error - it will be an older system
|
|
99
|
+
}
|
|
100
|
+
console.log("Import complete")
|
|
101
|
+
fs.rmSync(TEMP_DIR, { recursive: true })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function pickOne(opts: BackupOpts) {
|
|
105
|
+
if (opts["import"]) {
|
|
106
|
+
return importBackup(opts)
|
|
107
|
+
} else if (opts["export"]) {
|
|
108
|
+
return exportBackup(opts)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default new Command(`${CommandWord.BACKUPS}`)
|
|
113
|
+
.addHelp(
|
|
114
|
+
"Allows building backups of Budibase, as well as importing a backup to a new instance."
|
|
115
|
+
)
|
|
116
|
+
.addSubOption(
|
|
117
|
+
"--export [filename]",
|
|
118
|
+
"Export a backup from an existing Budibase installation.",
|
|
119
|
+
exportBackup
|
|
120
|
+
)
|
|
121
|
+
.addSubOption(
|
|
122
|
+
"--import [filename]",
|
|
123
|
+
"Import a backup to a new Budibase installation.",
|
|
124
|
+
importBackup
|
|
125
|
+
)
|
|
126
|
+
.addSubOption(
|
|
127
|
+
"--env [envFile]",
|
|
128
|
+
"Provide an environment variable file to configure the CLI.",
|
|
129
|
+
pickOne
|
|
130
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { objectStore } from "@budibase/backend-core"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import { join } from "path"
|
|
4
|
+
import { TEMP_DIR, MINIO_DIR } from "./utils"
|
|
5
|
+
import { progressBar } from "../utils"
|
|
6
|
+
const {
|
|
7
|
+
ObjectStoreBuckets,
|
|
8
|
+
ObjectStore,
|
|
9
|
+
retrieve,
|
|
10
|
+
uploadDirectory,
|
|
11
|
+
makeSureBucketExists,
|
|
12
|
+
} = objectStore
|
|
13
|
+
|
|
14
|
+
const bucketList = Object.values(ObjectStoreBuckets)
|
|
15
|
+
|
|
16
|
+
export async function exportObjects() {
|
|
17
|
+
const path = join(TEMP_DIR, MINIO_DIR)
|
|
18
|
+
fs.mkdirSync(path)
|
|
19
|
+
let fullList: any[] = []
|
|
20
|
+
let errorCount = 0
|
|
21
|
+
for (let bucket of bucketList) {
|
|
22
|
+
const client = ObjectStore(bucket)
|
|
23
|
+
try {
|
|
24
|
+
await client.headBucket().promise()
|
|
25
|
+
} catch (err) {
|
|
26
|
+
errorCount++
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
const list = (await client.listObjectsV2().promise()) as { Contents: any[] }
|
|
30
|
+
fullList = fullList.concat(list.Contents.map(el => ({ ...el, bucket })))
|
|
31
|
+
}
|
|
32
|
+
if (errorCount === bucketList.length) {
|
|
33
|
+
throw new Error("Unable to access MinIO/S3 - check environment config.")
|
|
34
|
+
}
|
|
35
|
+
const bar = progressBar(fullList.length)
|
|
36
|
+
let count = 0
|
|
37
|
+
for (let object of fullList) {
|
|
38
|
+
const filename = object.Key
|
|
39
|
+
const data = await retrieve(object.bucket, filename)
|
|
40
|
+
const possiblePath = filename.split("/")
|
|
41
|
+
if (possiblePath.length > 1) {
|
|
42
|
+
const dirs = possiblePath.slice(0, possiblePath.length - 1)
|
|
43
|
+
fs.mkdirSync(join(path, object.bucket, ...dirs), { recursive: true })
|
|
44
|
+
}
|
|
45
|
+
fs.writeFileSync(join(path, object.bucket, ...possiblePath), data)
|
|
46
|
+
bar.update(++count)
|
|
47
|
+
}
|
|
48
|
+
bar.stop()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function importObjects() {
|
|
52
|
+
const path = join(TEMP_DIR, MINIO_DIR)
|
|
53
|
+
const buckets = fs.readdirSync(path)
|
|
54
|
+
let total = 0
|
|
55
|
+
buckets.forEach(bucket => {
|
|
56
|
+
const files = fs.readdirSync(join(path, bucket))
|
|
57
|
+
total += files.length
|
|
58
|
+
})
|
|
59
|
+
const bar = progressBar(total)
|
|
60
|
+
let count = 0
|
|
61
|
+
for (let bucket of buckets) {
|
|
62
|
+
const client = ObjectStore(bucket)
|
|
63
|
+
await makeSureBucketExists(client, bucket)
|
|
64
|
+
const files = await uploadDirectory(bucket, join(path, bucket), "/")
|
|
65
|
+
count += files.length
|
|
66
|
+
bar.update(count)
|
|
67
|
+
}
|
|
68
|
+
bar.stop()
|
|
69
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import dotenv from "dotenv"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import { string } from "../questions"
|
|
4
|
+
import { getPouch } from "../core/db"
|
|
5
|
+
import { env as environment } from "@budibase/backend-core"
|
|
6
|
+
import PouchDB from "pouchdb"
|
|
7
|
+
|
|
8
|
+
export const TEMP_DIR = ".temp"
|
|
9
|
+
export const COUCH_DIR = "couchdb"
|
|
10
|
+
export const MINIO_DIR = "minio"
|
|
11
|
+
|
|
12
|
+
const REQUIRED = [
|
|
13
|
+
{ value: "MAIN_PORT", default: "10000" },
|
|
14
|
+
{
|
|
15
|
+
value: "COUCH_DB_URL",
|
|
16
|
+
default: "http://budibase:budibase@localhost:10000/db/",
|
|
17
|
+
},
|
|
18
|
+
{ value: "MINIO_URL", default: "http://localhost:10000" },
|
|
19
|
+
{ value: "MINIO_ACCESS_KEY" },
|
|
20
|
+
{ value: "MINIO_SECRET_KEY" },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
export function checkURLs(config: Record<string, string>) {
|
|
24
|
+
const mainPort = config["MAIN_PORT"],
|
|
25
|
+
username = config["COUCH_DB_USER"],
|
|
26
|
+
password = config["COUCH_DB_PASSWORD"]
|
|
27
|
+
if (!config["COUCH_DB_URL"] && mainPort && username && password) {
|
|
28
|
+
config[
|
|
29
|
+
"COUCH_DB_URL"
|
|
30
|
+
] = `http://${username}:${password}@localhost:${mainPort}/db/`
|
|
31
|
+
}
|
|
32
|
+
if (!config["MINIO_URL"]) {
|
|
33
|
+
config["MINIO_URL"] = `http://localhost:${mainPort}/`
|
|
34
|
+
}
|
|
35
|
+
return config
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function askQuestions() {
|
|
39
|
+
console.log(
|
|
40
|
+
"*** NOTE: use a .env file to load these parameters repeatedly ***"
|
|
41
|
+
)
|
|
42
|
+
let config: Record<string, string> = {}
|
|
43
|
+
for (let property of REQUIRED) {
|
|
44
|
+
config[property.value] = await string(property.value, property.default)
|
|
45
|
+
}
|
|
46
|
+
return config
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function loadEnvironment(path: string) {
|
|
50
|
+
if (!fs.existsSync(path)) {
|
|
51
|
+
throw "Unable to file specified .env file"
|
|
52
|
+
}
|
|
53
|
+
const env = fs.readFileSync(path, "utf8")
|
|
54
|
+
const config = checkURLs(dotenv.parse(env))
|
|
55
|
+
for (let required of REQUIRED) {
|
|
56
|
+
if (!config[required.value]) {
|
|
57
|
+
throw `Cannot find "${required.value}" property in .env file`
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return config
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// true is the default value passed by commander
|
|
64
|
+
export async function getConfig(envFile: boolean | string = true) {
|
|
65
|
+
let config
|
|
66
|
+
if (envFile !== true) {
|
|
67
|
+
config = loadEnvironment(envFile as string)
|
|
68
|
+
} else {
|
|
69
|
+
config = await askQuestions()
|
|
70
|
+
}
|
|
71
|
+
// fill out environment
|
|
72
|
+
for (let key of Object.keys(config)) {
|
|
73
|
+
environment._set(key, config[key])
|
|
74
|
+
}
|
|
75
|
+
return config
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function replication(
|
|
79
|
+
from: PouchDB.Database,
|
|
80
|
+
to: PouchDB.Database
|
|
81
|
+
) {
|
|
82
|
+
const pouch = getPouch()
|
|
83
|
+
try {
|
|
84
|
+
await pouch.replicate(from, to, {
|
|
85
|
+
batch_size: 1000,
|
|
86
|
+
batches_limit: 5,
|
|
87
|
+
// @ts-ignore
|
|
88
|
+
style: "main_only",
|
|
89
|
+
})
|
|
90
|
+
} catch (err) {
|
|
91
|
+
throw new Error(`Replication failed - ${JSON.stringify(err)}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getPouches(config: Record<string, string>) {
|
|
96
|
+
const Remote = getPouch(config["COUCH_DB_URL"])
|
|
97
|
+
const Local = getPouch()
|
|
98
|
+
return { Remote, Local }
|
|
99
|
+
}
|
package/src/constants.ts
ADDED
package/src/core/db.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import PouchDB from "pouchdb"
|
|
2
|
+
import { checkSlashesInUrl } from "../utils"
|
|
3
|
+
import fetch from "node-fetch"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fully qualified URL including username and password, or nothing for local
|
|
7
|
+
*/
|
|
8
|
+
export function getPouch(url?: string) {
|
|
9
|
+
let POUCH_DB_DEFAULTS
|
|
10
|
+
if (!url) {
|
|
11
|
+
POUCH_DB_DEFAULTS = {
|
|
12
|
+
prefix: undefined,
|
|
13
|
+
adapter: "leveldb",
|
|
14
|
+
}
|
|
15
|
+
} else {
|
|
16
|
+
POUCH_DB_DEFAULTS = {
|
|
17
|
+
prefix: url,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const replicationStream = require("pouchdb-replication-stream")
|
|
21
|
+
PouchDB.plugin(replicationStream.plugin)
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
PouchDB.adapter("writableStream", replicationStream.adapters.writableStream)
|
|
24
|
+
return PouchDB.defaults(POUCH_DB_DEFAULTS) as PouchDB.Static
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getAllDbs(url: string) {
|
|
28
|
+
const response = await fetch(
|
|
29
|
+
checkSlashesInUrl(encodeURI(`${url}/_all_dbs`)),
|
|
30
|
+
{
|
|
31
|
+
method: "GET",
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
if (response.status === 200) {
|
|
35
|
+
return await response.json()
|
|
36
|
+
} else {
|
|
37
|
+
throw "Cannot connect to CouchDB instance"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/events.ts
ADDED
package/src/exec.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import util from "util"
|
|
2
|
+
const runCommand = util.promisify(require("child_process").exec)
|
|
3
|
+
|
|
4
|
+
export async function exec(command: string, dir = "./") {
|
|
5
|
+
const { stdout } = await runCommand(command, { cwd: dir })
|
|
6
|
+
return stdout
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function utilityInstalled(utilName: string) {
|
|
10
|
+
try {
|
|
11
|
+
await exec(`${utilName} --version`)
|
|
12
|
+
return true
|
|
13
|
+
} catch (err) {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runPkgCommand(command: string, dir = "./") {
|
|
19
|
+
const yarn = await exports.utilityInstalled("yarn")
|
|
20
|
+
const npm = await exports.utilityInstalled("npm")
|
|
21
|
+
if (!yarn && !npm) {
|
|
22
|
+
throw new Error("Must have yarn or npm installed to run build.")
|
|
23
|
+
}
|
|
24
|
+
const npmCmd = command === "install" ? `npm ${command}` : `npm run ${command}`
|
|
25
|
+
const cmd = yarn ? `yarn ${command} --ignore-engines` : npmCmd
|
|
26
|
+
await exports.exec(cmd, dir)
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { success } = require("../utils")
|
|
2
|
+
const { updateDockerComposeService } = require("./utils")
|
|
3
|
+
const randomString = require("randomstring")
|
|
4
|
+
const { GENERATED_USER_EMAIL } = require("../constants")
|
|
5
|
+
import { DockerCompose } from "./types"
|
|
6
|
+
|
|
7
|
+
export async function generateUser(password: string | null, silent: boolean) {
|
|
8
|
+
const email = GENERATED_USER_EMAIL
|
|
9
|
+
if (!password) {
|
|
10
|
+
password = randomString.generate({ length: 6 })
|
|
11
|
+
}
|
|
12
|
+
updateDockerComposeService((service: DockerCompose) => {
|
|
13
|
+
service.environment["BB_ADMIN_USER_EMAIL"] = email
|
|
14
|
+
service.environment["BB_ADMIN_USER_PASSWORD"] = password as string
|
|
15
|
+
})
|
|
16
|
+
if (!silent) {
|
|
17
|
+
console.log(
|
|
18
|
+
success(
|
|
19
|
+
`User admin credentials configured, access with email: ${email} - password: ${password}`
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Command } from "../structures/Command"
|
|
2
|
+
import { CommandWord } from "../constants"
|
|
3
|
+
import { init } from "./init"
|
|
4
|
+
import { start } from "./start"
|
|
5
|
+
import { stop } from "./stop"
|
|
6
|
+
import { status } from "./status"
|
|
7
|
+
import { update } from "./update"
|
|
8
|
+
import { generateUser } from "./genUser"
|
|
9
|
+
import { watchPlugins } from "./watch"
|
|
10
|
+
|
|
11
|
+
export default new Command(`${CommandWord.HOSTING}`)
|
|
12
|
+
.addHelp("Controls self hosting on the Budibase platform.")
|
|
13
|
+
.addSubOption(
|
|
14
|
+
"--init [type]",
|
|
15
|
+
"Configure a self hosted platform in current directory, type can be unspecified, 'quick' or 'single'.",
|
|
16
|
+
init
|
|
17
|
+
)
|
|
18
|
+
.addSubOption(
|
|
19
|
+
"--start",
|
|
20
|
+
"Start the configured platform in current directory.",
|
|
21
|
+
start
|
|
22
|
+
)
|
|
23
|
+
.addSubOption(
|
|
24
|
+
"--status",
|
|
25
|
+
"Check the status of currently running services.",
|
|
26
|
+
status
|
|
27
|
+
)
|
|
28
|
+
.addSubOption(
|
|
29
|
+
"--stop",
|
|
30
|
+
"Stop the configured platform in the current directory.",
|
|
31
|
+
stop
|
|
32
|
+
)
|
|
33
|
+
.addSubOption(
|
|
34
|
+
"--update",
|
|
35
|
+
"Update the Budibase images to the latest version.",
|
|
36
|
+
update
|
|
37
|
+
)
|
|
38
|
+
.addSubOption(
|
|
39
|
+
"--watch-plugin-dir [directory]",
|
|
40
|
+
"Add plugin directory watching to a Budibase install.",
|
|
41
|
+
watchPlugins
|
|
42
|
+
)
|
|
43
|
+
.addSubOption(
|
|
44
|
+
"--gen-user",
|
|
45
|
+
"Create an admin user automatically as part of first start.",
|
|
46
|
+
generateUser
|
|
47
|
+
)
|
|
48
|
+
.addSubOption("--single", "Specify this with init to use the single image.")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { InitType, AnalyticsEvent } from "../constants"
|
|
2
|
+
import { confirmation } from "../questions"
|
|
3
|
+
import { captureEvent } from "../events"
|
|
4
|
+
import * as makeFiles from "./makeFiles"
|
|
5
|
+
import { parseEnv } from "../utils"
|
|
6
|
+
import { checkDockerConfigured, downloadDockerCompose } from "./utils"
|
|
7
|
+
import { watchPlugins } from "./watch"
|
|
8
|
+
import { generateUser } from "./genUser"
|
|
9
|
+
import fetch from "node-fetch"
|
|
10
|
+
|
|
11
|
+
const DO_USER_DATA_URL = "http://169.254.169.254/metadata/v1/user-data"
|
|
12
|
+
|
|
13
|
+
async function getInitConfig(type: string, isQuick: boolean, port: number) {
|
|
14
|
+
const config: any = isQuick ? makeFiles.QUICK_CONFIG : {}
|
|
15
|
+
if (type === InitType.DIGITAL_OCEAN) {
|
|
16
|
+
try {
|
|
17
|
+
const output = await fetch(DO_USER_DATA_URL)
|
|
18
|
+
const data = await output.text()
|
|
19
|
+
const response = parseEnv(data)
|
|
20
|
+
for (let [key, value] of Object.entries(makeFiles.ConfigMap)) {
|
|
21
|
+
if (response[key]) {
|
|
22
|
+
config[value as string] = response[key]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
// don't need to handle error, just don't do anything
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// override port
|
|
30
|
+
if (port) {
|
|
31
|
+
config[makeFiles.ConfigMap.MAIN_PORT] = port
|
|
32
|
+
}
|
|
33
|
+
return config
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function init(opts: any) {
|
|
37
|
+
let type, isSingle, watchDir, genUser, port, silent
|
|
38
|
+
if (typeof opts === "string") {
|
|
39
|
+
type = opts
|
|
40
|
+
} else {
|
|
41
|
+
type = opts["init"]
|
|
42
|
+
isSingle = opts["single"]
|
|
43
|
+
watchDir = opts["watchPluginDir"]
|
|
44
|
+
genUser = opts["genUser"]
|
|
45
|
+
port = opts["port"]
|
|
46
|
+
silent = opts["silent"]
|
|
47
|
+
}
|
|
48
|
+
const isQuick = type === InitType.QUICK || type === InitType.DIGITAL_OCEAN
|
|
49
|
+
await checkDockerConfigured()
|
|
50
|
+
if (!isQuick) {
|
|
51
|
+
const shouldContinue = await confirmation(
|
|
52
|
+
"This will create multiple files in current directory, should continue?"
|
|
53
|
+
)
|
|
54
|
+
if (!shouldContinue) {
|
|
55
|
+
console.log("Stopping.")
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
captureEvent(AnalyticsEvent.SelfHostInit, {
|
|
60
|
+
type,
|
|
61
|
+
})
|
|
62
|
+
const config = await getInitConfig(type, isQuick, port)
|
|
63
|
+
if (!isSingle) {
|
|
64
|
+
await downloadDockerCompose()
|
|
65
|
+
await makeFiles.makeEnv(config, silent)
|
|
66
|
+
} else {
|
|
67
|
+
await makeFiles.makeSingleCompose(config, silent)
|
|
68
|
+
}
|
|
69
|
+
if (watchDir) {
|
|
70
|
+
await watchPlugins(watchDir, silent)
|
|
71
|
+
}
|
|
72
|
+
if (genUser) {
|
|
73
|
+
const inputPassword = typeof genUser === "string" ? genUser : null
|
|
74
|
+
await generateUser(inputPassword, silent)
|
|
75
|
+
}
|
|
76
|
+
}
|