cloudron 6.0.0 → 7.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/CHANGELOG.md +4 -0
- package/bin/cloudron +15 -29
- package/bin/cloudron-appstore +4 -7
- package/bin/cloudron-build +18 -8
- package/bin/cloudron-versions +33 -0
- package/eslint.config.js +7 -6
- package/package.json +16 -15
- package/src/actions.js +230 -128
- package/src/appstore-actions.js +31 -59
- package/src/backup-tools.js +22 -24
- package/src/build-actions.js +113 -99
- package/src/completion.js +4 -8
- package/src/config.js +78 -53
- package/src/helper.js +155 -13
- package/src/readline.js +8 -10
- package/src/templates/CloudronManifest.appstore.json.ejs +2 -2
- package/src/versions-actions.js +184 -0
- package/test/test.js +131 -160
- package/src/superagent.js +0 -225
package/src/config.js
CHANGED
|
@@ -1,74 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import safe from '@cloudron/safetydance';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
const HOME = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
path = require('path'),
|
|
7
|
-
safe = require('safetydance');
|
|
7
|
+
const _configFilePath = path.join(HOME, '.cloudron.json');
|
|
8
8
|
|
|
9
|
-
var
|
|
10
|
-
|
|
11
|
-
exports = module.exports = {
|
|
12
|
-
setActive: setActive,
|
|
13
|
-
|
|
14
|
-
set: set,
|
|
15
|
-
get: get,
|
|
16
|
-
|
|
17
|
-
cloudronGet: cloudronGet,
|
|
18
|
-
cloudronSet: cloudronSet,
|
|
19
|
-
|
|
20
|
-
// appstore
|
|
21
|
-
appStoreToken,
|
|
22
|
-
setAppStoreToken,
|
|
23
|
-
appStoreOrigin,
|
|
24
|
-
|
|
25
|
-
// per app
|
|
26
|
-
getAppConfig: (path) => get(['apps', path]) || {},
|
|
27
|
-
setAppConfig: (path, c) => set(['apps', path], c),
|
|
28
|
-
|
|
29
|
-
// build service
|
|
30
|
-
getBuildServiceConfig: () => get('buildService') || {},
|
|
31
|
-
setBuildServiceConfig: (c) => set('buildService', c),
|
|
32
|
-
|
|
33
|
-
// current cloudron
|
|
34
|
-
token: cloudronGet.bind(null, 'token'),
|
|
35
|
-
setToken: cloudronSet.bind(null, 'token'),
|
|
36
|
-
|
|
37
|
-
cloudron: cloudronGet.bind(null, 'cloudron'),
|
|
38
|
-
setCloudron: cloudronSet.bind(null, 'cloudron'),
|
|
39
|
-
|
|
40
|
-
apiEndpoint: cloudronGet.bind(null, 'apiEndpoint'),
|
|
41
|
-
setApiEndpoint: cloudronSet.bind(null, 'apiEndpoint'),
|
|
42
|
-
|
|
43
|
-
allowSelfsigned: cloudronGet.bind(null, 'allowSelfsigned'),
|
|
44
|
-
setAllowSelfsigned: cloudronSet.bind(null, 'allowSelfsigned'),
|
|
45
|
-
|
|
46
|
-
// for testing
|
|
47
|
-
_configFilePath: path.join(HOME, '.cloudron.json')
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
var gConfig = safe.JSON.parse(safe.fs.readFileSync(exports._configFilePath)) || {};
|
|
9
|
+
var gConfig = safe.JSON.parse(safe.fs.readFileSync(_configFilePath)) || {};
|
|
51
10
|
|
|
52
11
|
// per default we fetch the last cloudron which a user performed an explicit login
|
|
53
12
|
var gCurrent = safe.query(gConfig, 'cloudrons.default') || null;
|
|
54
13
|
|
|
55
14
|
// This is mostly called from helper.detectCloudronApiEndpoint() which will select the correct config section then
|
|
56
|
-
function setActive(
|
|
57
|
-
gCurrent =
|
|
15
|
+
function setActive(endpoint) {
|
|
16
|
+
gCurrent = endpoint;
|
|
58
17
|
}
|
|
59
18
|
|
|
60
19
|
function save() {
|
|
61
|
-
fs.writeFileSync(
|
|
20
|
+
fs.writeFileSync(_configFilePath, JSON.stringify(gConfig, null, 4));
|
|
62
21
|
}
|
|
63
22
|
|
|
64
23
|
function set(key, value) {
|
|
65
24
|
// reload config from disk and set. A parallel cli process could have adjusted values
|
|
66
|
-
gConfig = safe.JSON.parse(safe.fs.readFileSync(
|
|
25
|
+
gConfig = safe.JSON.parse(safe.fs.readFileSync(_configFilePath)) || {};
|
|
67
26
|
|
|
68
27
|
safe.set(gConfig, key, value);
|
|
69
28
|
save();
|
|
70
29
|
}
|
|
71
30
|
|
|
31
|
+
function unset(key) {
|
|
32
|
+
// reload config from disk and set. A parallel cli process could have adjusted values
|
|
33
|
+
gConfig = safe.JSON.parse(safe.fs.readFileSync(_configFilePath)) || {};
|
|
34
|
+
|
|
35
|
+
safe.unset(gConfig, key);
|
|
36
|
+
save();
|
|
37
|
+
}
|
|
38
|
+
|
|
72
39
|
function get(key) {
|
|
73
40
|
return safe.query(gConfig, key);
|
|
74
41
|
}
|
|
@@ -89,7 +56,65 @@ function appStoreToken() {
|
|
|
89
56
|
return get(['appStore', appStoreOrigin().replace('https://', ''), 'token']);
|
|
90
57
|
}
|
|
91
58
|
|
|
92
|
-
function setAppStoreToken(
|
|
93
|
-
set(['appStore', appStoreOrigin().replace('https://', ''), 'token'],
|
|
59
|
+
function setAppStoreToken(value) {
|
|
60
|
+
set(['appStore', appStoreOrigin().replace('https://', ''), 'token'], value);
|
|
94
61
|
}
|
|
95
62
|
|
|
63
|
+
const getAppBuildConfig = (p) => get(['apps', p]) || {};
|
|
64
|
+
const setAppBuildConfig = (p, c) => set(['apps', p], c);
|
|
65
|
+
const unsetAppBuildConfig = (p) => unset(['apps', p]);
|
|
66
|
+
|
|
67
|
+
const getBuildServiceConfig = () => get('buildService') || {};
|
|
68
|
+
const setBuildServiceConfig = (c) => set('buildService', c);
|
|
69
|
+
|
|
70
|
+
const token = cloudronGet.bind(null, 'token');
|
|
71
|
+
const setToken = cloudronSet.bind(null, 'token');
|
|
72
|
+
|
|
73
|
+
const cloudron = cloudronGet.bind(null, 'cloudron');
|
|
74
|
+
const setCloudron = cloudronSet.bind(null, 'cloudron');
|
|
75
|
+
|
|
76
|
+
const apiEndpoint = cloudronGet.bind(null, 'apiEndpoint');
|
|
77
|
+
const setApiEndpoint = cloudronSet.bind(null, 'apiEndpoint');
|
|
78
|
+
|
|
79
|
+
const allowSelfsigned = cloudronGet.bind(null, 'allowSelfsigned');
|
|
80
|
+
const setAllowSelfsigned = cloudronSet.bind(null, 'allowSelfsigned');
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
setActive,
|
|
84
|
+
|
|
85
|
+
set,
|
|
86
|
+
get,
|
|
87
|
+
|
|
88
|
+
cloudronGet,
|
|
89
|
+
cloudronSet,
|
|
90
|
+
|
|
91
|
+
// appstore
|
|
92
|
+
appStoreToken,
|
|
93
|
+
setAppStoreToken,
|
|
94
|
+
appStoreOrigin,
|
|
95
|
+
|
|
96
|
+
// per app
|
|
97
|
+
getAppBuildConfig,
|
|
98
|
+
setAppBuildConfig,
|
|
99
|
+
unsetAppBuildConfig,
|
|
100
|
+
|
|
101
|
+
// build service
|
|
102
|
+
getBuildServiceConfig,
|
|
103
|
+
setBuildServiceConfig,
|
|
104
|
+
|
|
105
|
+
// current cloudron
|
|
106
|
+
token,
|
|
107
|
+
setToken,
|
|
108
|
+
|
|
109
|
+
cloudron,
|
|
110
|
+
setCloudron,
|
|
111
|
+
|
|
112
|
+
apiEndpoint,
|
|
113
|
+
setApiEndpoint,
|
|
114
|
+
|
|
115
|
+
allowSelfsigned,
|
|
116
|
+
setAllowSelfsigned,
|
|
117
|
+
|
|
118
|
+
// for testing
|
|
119
|
+
_configFilePath
|
|
120
|
+
};
|
package/src/helper.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
exports = module.exports = {
|
|
10
|
-
exit,
|
|
11
|
-
|
|
12
|
-
locateManifest,
|
|
13
|
-
};
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import safe from '@cloudron/safetydance';
|
|
7
|
+
import superagent from '@cloudron/superagent';
|
|
8
|
+
import util from 'util';
|
|
14
9
|
|
|
15
10
|
function exit(error) {
|
|
16
11
|
if (error instanceof Error) console.log(error.message);
|
|
@@ -34,3 +29,150 @@ function locateManifest() {
|
|
|
34
29
|
|
|
35
30
|
return null;
|
|
36
31
|
}
|
|
32
|
+
|
|
33
|
+
async function locateVersions() {
|
|
34
|
+
let curdir = process.cwd();
|
|
35
|
+
do {
|
|
36
|
+
const candidate = path.join(curdir, 'CloudronVersions.json');
|
|
37
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
38
|
+
|
|
39
|
+
// check if we can't go further up (the previous check for '/' breaks on windows)
|
|
40
|
+
if (curdir === path.resolve(curdir, '..')) break;
|
|
41
|
+
|
|
42
|
+
curdir = path.resolve(curdir, '..');
|
|
43
|
+
// eslint-disable-next-line no-constant-condition
|
|
44
|
+
} while (true);
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseChangelog(file, version) {
|
|
50
|
+
let changelog = '';
|
|
51
|
+
const data = safe.fs.readFileSync(file, 'utf8');
|
|
52
|
+
if (!data) return null;
|
|
53
|
+
const lines = data.split('\n');
|
|
54
|
+
|
|
55
|
+
version = version.replace(/-.*/, ''); // remove any prerelease
|
|
56
|
+
|
|
57
|
+
let i;
|
|
58
|
+
for (i = 0; i < lines.length; i++) {
|
|
59
|
+
if (lines[i] === '[' + version + ']') break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (i = i + 1; i < lines.length; i++) {
|
|
63
|
+
if (lines[i] === '') continue;
|
|
64
|
+
if (lines[i][0] === '[') break;
|
|
65
|
+
|
|
66
|
+
changelog += lines[i] + '\n';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return changelog;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function performOidcLogin(adminFqdn, { rejectUnauthorized = true } = {}) {
|
|
73
|
+
const OIDC_CLIENT_ID = 'cid-cli';
|
|
74
|
+
const OIDC_CLIENT_SECRET = 'notused';
|
|
75
|
+
const OIDC_PROVIDER = `https://${adminFqdn}`;
|
|
76
|
+
const OIDC_CALLBACK_URL = 'http://localhost:1312/callback';
|
|
77
|
+
|
|
78
|
+
// Discover OIDC endpoints
|
|
79
|
+
const discoveryRequest = superagent.get(`${OIDC_PROVIDER}/.well-known/openid-configuration`).timeout(60000);
|
|
80
|
+
if (!rejectUnauthorized) discoveryRequest.disableTLSCerts();
|
|
81
|
+
const discoveryResponse = await discoveryRequest;
|
|
82
|
+
const { authorization_endpoint, token_endpoint } = discoveryResponse.body;
|
|
83
|
+
|
|
84
|
+
// Generate PKCE code_verifier and code_challenge (S256)
|
|
85
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
86
|
+
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
87
|
+
|
|
88
|
+
// Generate state for CSRF protection
|
|
89
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
90
|
+
|
|
91
|
+
// Build authorization URL
|
|
92
|
+
const authUrl = new URL(authorization_endpoint);
|
|
93
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
94
|
+
authUrl.searchParams.set('client_id', OIDC_CLIENT_ID);
|
|
95
|
+
authUrl.searchParams.set('redirect_uri', OIDC_CALLBACK_URL);
|
|
96
|
+
authUrl.searchParams.set('state', state);
|
|
97
|
+
authUrl.searchParams.set('scope', 'openid');
|
|
98
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
99
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
100
|
+
|
|
101
|
+
// Start local HTTP server and wait for the OIDC callback
|
|
102
|
+
const code = await new Promise((resolve, reject) => {
|
|
103
|
+
const server = http.createServer((req, res) => {
|
|
104
|
+
const url = new URL(req.url, 'http://localhost:1312');
|
|
105
|
+
if (url.pathname !== '/callback') {
|
|
106
|
+
res.writeHead(404);
|
|
107
|
+
res.end('Not found');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const error = url.searchParams.get('error');
|
|
112
|
+
if (error) {
|
|
113
|
+
const description = url.searchParams.get('error_description') || error;
|
|
114
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
115
|
+
res.end('<html><body><h1>Login failed</h1><p>' + description + '</p><p>You can close this window.</p></body></html>');
|
|
116
|
+
server.close();
|
|
117
|
+
reject(new Error(`OIDC login failed: ${description}`));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const receivedState = url.searchParams.get('state');
|
|
122
|
+
if (receivedState !== state) {
|
|
123
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
124
|
+
res.end('<html><body><h1>Login failed</h1><p>State mismatch.</p></body></html>');
|
|
125
|
+
server.close();
|
|
126
|
+
reject(new Error('OIDC state mismatch'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const receivedCode = url.searchParams.get('code');
|
|
131
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
132
|
+
res.end('<html><body><h1>Login successful!</h1><p>You can close this window and return to the terminal.</p></body></html>');
|
|
133
|
+
server.close();
|
|
134
|
+
resolve(receivedCode);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
server.listen(1312, () => {
|
|
138
|
+
console.log('Opening browser for authentication...');
|
|
139
|
+
open(authUrl.toString());
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
server.on('error', (err) => {
|
|
143
|
+
reject(new Error(`Failed to start local server: ${err.message}`));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Timeout after 2 minutes so the CLI doesn't hang indefinitely
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
server.close();
|
|
149
|
+
reject(new Error('Login timed out after 2 minutes'));
|
|
150
|
+
}, 120000);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Exchange authorization code for access token
|
|
154
|
+
const tokenBody = new URLSearchParams({
|
|
155
|
+
grant_type: 'authorization_code',
|
|
156
|
+
code,
|
|
157
|
+
redirect_uri: OIDC_CALLBACK_URL,
|
|
158
|
+
client_id: OIDC_CLIENT_ID,
|
|
159
|
+
client_secret: OIDC_CLIENT_SECRET,
|
|
160
|
+
code_verifier: codeVerifier,
|
|
161
|
+
}).toString();
|
|
162
|
+
const tokenRequest = superagent.post(token_endpoint)
|
|
163
|
+
.timeout(60000)
|
|
164
|
+
.set('Content-Type', 'application/x-www-form-urlencoded')
|
|
165
|
+
.send(tokenBody);
|
|
166
|
+
if (!rejectUnauthorized) tokenRequest.disableTLSCerts();
|
|
167
|
+
const tokenResponse = await tokenRequest;
|
|
168
|
+
|
|
169
|
+
return tokenResponse.body.access_token;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
exit,
|
|
174
|
+
locateManifest,
|
|
175
|
+
parseChangelog,
|
|
176
|
+
locateVersions,
|
|
177
|
+
performOidcLogin,
|
|
178
|
+
};
|
package/src/readline.js
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
question
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
const readline = require('node:readline/promises'),
|
|
8
|
-
{ Writable } = require('node:stream');
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import safe from '@cloudron/safetydance';
|
|
3
|
+
import { Writable } from 'node:stream';
|
|
9
4
|
|
|
10
5
|
async function question(query, options) {
|
|
11
6
|
const output = options.noEchoBack
|
|
12
7
|
? new Writable({ write: function (chunk, encoding, callback) { callback(); } })
|
|
13
8
|
: process.stdout;
|
|
14
|
-
process.stdin.setRawMode(options.noEchoBack); // raw mode gives each keypress as
|
|
9
|
+
process.stdin.setRawMode(options.noEchoBack); // raw mode gives each keypress as opposed to line based cooked mode
|
|
15
10
|
const rl = readline.createInterface({ input: process.stdin, output });
|
|
16
11
|
if (options.noEchoBack) process.stdout.write(query);
|
|
17
|
-
const answer = await rl.question(query, options);
|
|
12
|
+
const [error, answer] = await safe(rl.question(query, options));
|
|
13
|
+
if (error) return process.exit(1); // like AbortError for ^C
|
|
18
14
|
rl.close();
|
|
19
15
|
if (options.noEchoBack) process.stdout.write('\n');
|
|
20
16
|
return answer;
|
|
21
17
|
}
|
|
18
|
+
|
|
19
|
+
export { question };
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
"id": "",
|
|
3
3
|
"version": "<%- version %>",
|
|
4
4
|
"upstreamVersion": "",
|
|
5
|
-
"minBoxVersion": "
|
|
5
|
+
"minBoxVersion": "9.0.0",
|
|
6
6
|
"title": "",
|
|
7
7
|
"author": "",
|
|
8
8
|
"description": "file://DESCRIPTION.md",
|
|
9
9
|
"tagline": "",
|
|
10
10
|
"website": "",
|
|
11
11
|
"contactEmail": "",
|
|
12
|
-
"
|
|
12
|
+
"iconUrl": "https-logo.png",
|
|
13
13
|
"healthCheckPath": "/",
|
|
14
14
|
"mediaLinks": [],
|
|
15
15
|
"httpPort": <%- httpPort %>,
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { exit, locateManifest, locateVersions, parseChangelog } from './helper.js';
|
|
2
|
+
import * as config from './config.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import fsPromises from 'fs/promises';
|
|
5
|
+
import manifestFormat from '@cloudron/manifest-format';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import safe from '@cloudron/safetydance';
|
|
8
|
+
import Table from 'easy-table';
|
|
9
|
+
|
|
10
|
+
const NO_MANIFEST_FOUND_ERROR_STRING = 'No CloudronManifest.json found';
|
|
11
|
+
const NO_VERSIONS_FOUND_ERROR_STRING = 'No CloudronVersions.json found. Use "cloudron versions init" to create one.';
|
|
12
|
+
|
|
13
|
+
const PUBLISH_STATE_TESTING = 'testing';
|
|
14
|
+
const PUBLISH_STATE_PUBLISHED = 'published';
|
|
15
|
+
const PUBLISH_STATE_REVOKED = 'revoked';
|
|
16
|
+
|
|
17
|
+
async function readVersions(versionsFilePath) {
|
|
18
|
+
if (!fs.existsSync(versionsFilePath)) exit('No CloudronVersions.json found, create it first with "cloudron versions init"');
|
|
19
|
+
|
|
20
|
+
const data = fs.readFileSync(versionsFilePath, 'utf8');
|
|
21
|
+
const versionsRoot = safe.JSON.parse(data);
|
|
22
|
+
|
|
23
|
+
const error = manifestFormat.parseVersions(versionsRoot);
|
|
24
|
+
if (error) throw new Error(`${path.relative(process.cwd(), versionsFilePath)} is corrupt: ${error.message}`);
|
|
25
|
+
|
|
26
|
+
return versionsRoot;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function writeVersions(versionsFilePath, versionsRoot) {
|
|
30
|
+
const [error] = await safe(fsPromises.writeFile(versionsFilePath, JSON.stringify(versionsRoot, null, 4), 'utf8'));
|
|
31
|
+
if (error) return exit(`Unable to write to ${path.relative(process.cwd(), versionsFilePath)}: ${error.message}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function init(/*localOptions, cmd*/) {
|
|
35
|
+
const manifestFilePath = locateManifest();
|
|
36
|
+
if (!manifestFilePath) return exit(`${NO_MANIFEST_FOUND_ERROR_STRING}. You must run this command in the package dir.`);
|
|
37
|
+
|
|
38
|
+
const baseDir = path.dirname(manifestFilePath);
|
|
39
|
+
const versionsFilePath = `${baseDir}/CloudronVersions.json`;
|
|
40
|
+
|
|
41
|
+
if (fs.existsSync(versionsFilePath)) return exit(`${path.relative(process.cwd(), versionsFilePath)} already exists.`);
|
|
42
|
+
|
|
43
|
+
await writeVersions(versionsFilePath, { stable: true, versions: {} });
|
|
44
|
+
console.log(`Created ${path.relative(process.cwd(), versionsFilePath)}. Use "cloudron versions add" to add a version.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function resolveManifest(manifest, baseDir) {
|
|
48
|
+
if (manifest.description.slice(0, 7) === 'file://') {
|
|
49
|
+
let descriptionFilePath = manifest.description.slice(7);
|
|
50
|
+
descriptionFilePath = path.isAbsolute(descriptionFilePath) ? descriptionFilePath : path.join(baseDir, descriptionFilePath);
|
|
51
|
+
manifest.description = safe.fs.readFileSync(descriptionFilePath, 'utf8');
|
|
52
|
+
if (!manifest.description && safe.error) throw(new Error('Could not read/parse description ' + safe.error.message));
|
|
53
|
+
if (!manifest.description) throw new Error('Description cannot be empty');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (manifest.postInstallMessage && manifest.postInstallMessage.slice(0, 7) === 'file://') {
|
|
57
|
+
let postInstallFilePath = manifest.postInstallMessage.slice(7);
|
|
58
|
+
postInstallFilePath = path.isAbsolute(postInstallFilePath) ? postInstallFilePath : path.join(baseDir, postInstallFilePath);
|
|
59
|
+
manifest.postInstallMessage = safe.fs.readFileSync(postInstallFilePath, 'utf8');
|
|
60
|
+
if (!manifest.postInstallMessage && safe.error) throw(new Error('Could not read/parse postInstall ' + safe.error.message));
|
|
61
|
+
if (!manifest.postInstallMessage) throw new Error('PostInstall file specified but it is empty');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (manifest.changelog.slice(0, 7) === 'file://') {
|
|
65
|
+
let changelogPath = manifest.changelog.slice(7);
|
|
66
|
+
changelogPath = path.isAbsolute(changelogPath) ? changelogPath : path.join(baseDir, changelogPath);
|
|
67
|
+
manifest.changelog = parseChangelog(changelogPath, manifest.version);
|
|
68
|
+
if (!manifest.changelog) throw new Error('Bad changelog format or missing changelog for this version');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function addOrUpdate(localOptions, cmd) {
|
|
73
|
+
const isUpdate = cmd.parent.args[0] === 'update';
|
|
74
|
+
const versionsFilePath = await locateVersions();
|
|
75
|
+
if (!versionsFilePath) return exit(NO_VERSIONS_FOUND_ERROR_STRING);
|
|
76
|
+
|
|
77
|
+
const options = cmd.optsWithGlobals();
|
|
78
|
+
// try to find the manifest of this project
|
|
79
|
+
const manifestFilePath = locateManifest();
|
|
80
|
+
if (!manifestFilePath) return exit(NO_MANIFEST_FOUND_ERROR_STRING);
|
|
81
|
+
|
|
82
|
+
const result = manifestFormat.parseFile(manifestFilePath);
|
|
83
|
+
if (result.error) return exit(result.error.message);
|
|
84
|
+
|
|
85
|
+
const manifest = result.manifest;
|
|
86
|
+
|
|
87
|
+
const sourceDir = path.dirname(manifestFilePath);
|
|
88
|
+
const appConfig = config.getAppBuildConfig(sourceDir);
|
|
89
|
+
|
|
90
|
+
if (options.image) {
|
|
91
|
+
manifest.dockerImage = options.image;
|
|
92
|
+
} else {
|
|
93
|
+
manifest.dockerImage = appConfig.dockerImage;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!manifest.dockerImage) exit('No docker image found, run `cloudron build` first');
|
|
97
|
+
|
|
98
|
+
const error = manifestFormat.checkVersionsRequirements(manifest);
|
|
99
|
+
if (error) return exit(error);
|
|
100
|
+
|
|
101
|
+
const versionsRoot = await readVersions(versionsFilePath);
|
|
102
|
+
const versions = versionsRoot.versions;
|
|
103
|
+
|
|
104
|
+
await resolveManifest(manifest, path.dirname(manifestFilePath));
|
|
105
|
+
|
|
106
|
+
if (options.state && options.state !== PUBLISH_STATE_PUBLISHED && options.state !== PUBLISH_STATE_TESTING) {
|
|
107
|
+
return exit(`Invalid state "${options.state}". Must be "published" or "testing".`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (isUpdate) {
|
|
111
|
+
const targetVersion = options.version || manifest.version;
|
|
112
|
+
if (!(targetVersion in versions)) exit(`${targetVersion} does not exist in ${path.relative(process.cwd(), versionsFilePath)}.`);
|
|
113
|
+
|
|
114
|
+
versions[targetVersion].manifest = manifest;
|
|
115
|
+
versions[targetVersion].ts = (new Date()).toUTCString();
|
|
116
|
+
if (options.state) versions[targetVersion].publishState = options.state;
|
|
117
|
+
} else {
|
|
118
|
+
if (manifest.version in versions) exit(`${manifest.version} already exists in ${path.relative(process.cwd(), versionsFilePath)}.`);
|
|
119
|
+
versions[manifest.version] = {
|
|
120
|
+
manifest,
|
|
121
|
+
creationDate: (new Date()).toUTCString(),
|
|
122
|
+
ts: (new Date()).toUTCString(),
|
|
123
|
+
publishState: options.state || PUBLISH_STATE_PUBLISHED
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await writeVersions(versionsFilePath, versionsRoot);
|
|
128
|
+
|
|
129
|
+
if (isUpdate) {
|
|
130
|
+
const targetVersion = options.version || manifest.version;
|
|
131
|
+
console.log(`Updated ${targetVersion} in ${path.relative(process.cwd(), versionsFilePath)}`);
|
|
132
|
+
} else {
|
|
133
|
+
console.log(`Added ${manifest.version} in ${path.relative(process.cwd(), versionsFilePath)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function list(/*localOptions, cmd*/) {
|
|
138
|
+
const versionsFilePath = await locateVersions();
|
|
139
|
+
if (!versionsFilePath) return exit(NO_VERSIONS_FOUND_ERROR_STRING);
|
|
140
|
+
|
|
141
|
+
const versionsRoot = await readVersions(versionsFilePath);
|
|
142
|
+
const versions = versionsRoot.versions;
|
|
143
|
+
const sortedVersions = Object.keys(versions).sort(manifestFormat.packageVersionCompare);
|
|
144
|
+
if (sortedVersions.length === 0) {
|
|
145
|
+
console.log('No versions');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const t = new Table();
|
|
150
|
+
|
|
151
|
+
for (const version of sortedVersions) {
|
|
152
|
+
t.cell('Version', versions[version].manifest.version);
|
|
153
|
+
t.cell('Creation Date', versions[version].creationDate);
|
|
154
|
+
t.cell('Image', versions[version].manifest.dockerImage);
|
|
155
|
+
t.cell('Publish state', versions[version].publishState);
|
|
156
|
+
t.newRow();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(t.toString());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function revoke() {
|
|
163
|
+
const versionsFilePath = await locateVersions();
|
|
164
|
+
if (!versionsFilePath) return exit(NO_VERSIONS_FOUND_ERROR_STRING);
|
|
165
|
+
|
|
166
|
+
const versionsRoot = await readVersions(versionsFilePath);
|
|
167
|
+
const versions = versionsRoot.versions;
|
|
168
|
+
const sortedVersions = Object.keys(versions).sort(manifestFormat.packageVersionCompare);
|
|
169
|
+
const latestVersion = sortedVersions.at(-1);
|
|
170
|
+
if (versions[latestVersion].publishState !== PUBLISH_STATE_PUBLISHED) {
|
|
171
|
+
return exit(`Only versions in "${PUBLISH_STATE_PUBLISHED}" can be revoked. ${latestVersion} is currently marked as "${versions[latestVersion].publishState}"`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
versions[latestVersion].publishState = PUBLISH_STATE_REVOKED;
|
|
175
|
+
await writeVersions(versionsFilePath, versionsRoot);
|
|
176
|
+
console.log(`Marked ${latestVersion} as revoked in ${path.relative(process.cwd(), versionsFilePath)}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default {
|
|
180
|
+
init,
|
|
181
|
+
addOrUpdate,
|
|
182
|
+
list,
|
|
183
|
+
revoke,
|
|
184
|
+
};
|