cloudron 7.0.2 → 7.0.4

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/bin/cloudron CHANGED
@@ -165,6 +165,7 @@ program.command('install')
165
165
  .option('-a, --alias-domains [domain,...]', 'Alias domains')
166
166
  .option('-m, --memory-limit [domain,...]', 'Memory Limit (e.g 1.5G, 512M)')
167
167
  .option('--appstore-id <appid[@version]>', 'Use app from the store')
168
+ .option('--versions-url <url>', 'Install community app from CloudronVersions.json URL')
168
169
  .option('--no-sso', 'Disable Cloudron SSO [false]')
169
170
  .option('--debug [cmd...]', 'Enable debug mode', false)
170
171
  .option('--readonly', 'Mount filesystem readonly. Default is read/write in debug mode.')
@@ -33,9 +33,9 @@ program.command('build', { isDefault: true })
33
33
  .option('--tag <docker image tag>', 'Docker image tag. Note that this does not include the repository name')
34
34
  .action(buildActions.build);
35
35
 
36
- program.command('clear')
37
- .description('Clears build information')
38
- .action(buildActions.clear);
36
+ program.command('reset')
37
+ .description('Reset build configuration for this directory')
38
+ .action(buildActions.reset);
39
39
 
40
40
  program.command('info')
41
41
  .description('Print build information')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudron",
3
- "version": "7.0.2",
3
+ "version": "7.0.4",
4
4
  "license": "MIT",
5
5
  "description": "Cloudron Commandline Tool",
6
6
  "type": "module",
package/src/actions.js CHANGED
@@ -103,35 +103,13 @@ async function stopActiveTask(app, options) {
103
103
  if (response.status !== 204) throw `Failed to stop active task: ${requestError(response)}`;
104
104
  }
105
105
 
106
- async function selectAppWithRepository(repository, options) {
107
- assert.strictEqual(typeof repository, 'string');
108
- assert.strictEqual(typeof options, 'object');
109
-
110
- const response = await createRequest('GET', '/api/v1/apps', options);
111
- if (response.status !== 200) throw new Error(`Failed to install app: ${requestError(response)}`);
112
-
113
- const matchingApps = response.body.apps.filter(function (app) {
114
- return !app.appStoreId && app.manifest.dockerImage.startsWith(repository); // never select apps from the store
115
- });
116
-
117
- if (matchingApps.length === 0) return [ ];
118
- if (matchingApps.length === 1) return matchingApps[0];
119
-
120
- console.log();
121
- console.log('Available apps using same repository %s:', repository);
122
- matchingApps.sort(function (a, b) { return a.fqdn < b.fqdn ? -1 : 1; });
123
- matchingApps.forEach(function (app, index) {
124
- console.log('[%s]\t%s', index, app.fqdn);
125
- });
126
-
127
- let index;
128
- while (true) {
129
- index = parseInt(await readline.question('Choose app [0-' + (matchingApps.length-1) + ']: ', {}), 10);
130
- if (isNaN(index) || index < 0 || index > matchingApps.length-1) console.log('Invalid selection');
131
- else break;
132
- }
106
+ function saveCwdAppId(appId, manifestFilePath) {
107
+ if (!manifestFilePath) return;
133
108
 
134
- return matchingApps[index];
109
+ const sourceDir = path.dirname(manifestFilePath);
110
+ const cwdConfig = config.getCwdConfig(sourceDir);
111
+ cwdConfig.appId = appId;
112
+ config.setCwdConfig(sourceDir, cwdConfig);
135
113
  }
136
114
 
137
115
  // appId may be the appId or the location
@@ -140,20 +118,19 @@ async function getApp(options) {
140
118
 
141
119
  const app = options.app || null;
142
120
 
143
- if (!app) { // determine based on repository name given during 'build'
121
+ if (!app) {
144
122
  const manifestFilePath = locateManifest();
145
-
146
123
  if (!manifestFilePath) throw new Error(NO_APP_FOUND_ERROR_STRING);
147
124
 
148
125
  const sourceDir = path.dirname(manifestFilePath);
149
- const appConfig = config.getAppBuildConfig(sourceDir);
126
+ const cwdConfig = config.getCwdConfig(sourceDir);
150
127
 
151
- if (!appConfig.repository) throw new Error(NO_APP_FOUND_ERROR_STRING);
128
+ if (!cwdConfig.appId) throw new Error(NO_APP_FOUND_ERROR_STRING);
152
129
 
153
- const [error, result] = await safe(selectAppWithRepository(appConfig.repository, options));
154
- if (error || result.length === 0) return null;
130
+ const response = await createRequest('GET', `/api/v1/apps/${cwdConfig.appId}`, options);
131
+ if (response.status !== 200) throw new Error(NO_APP_FOUND_ERROR_STRING);
155
132
 
156
- return result;
133
+ return response.body;
157
134
  } else if (app.match(/.{8}-.{4}-.{4}-.{4}-.{8}/)) { // it is an id
158
135
  const response = await createRequest('GET', `/api/v1/apps/${app}`, options);
159
136
  if (response.status !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
@@ -554,34 +531,46 @@ async function install(localOptions, cmd) {
554
531
  const options = cmd.optsWithGlobals();
555
532
 
556
533
  try {
557
- const result = await getManifest(options.appstoreId || '');
558
- const { manifest, manifestFilePath } = result;
559
-
534
+ let manifest = null, manifestFilePath = null, versionsUrl = null;
560
535
  let sourceArchiveFilePath = null; // will be set if we need to send a tarball
561
536
 
562
- if (!manifest.dockerImage) { // not a manifest from appstore
563
- const sourceDir = path.dirname(manifestFilePath);
564
- const image = options.image || config.getAppBuildConfig(sourceDir).dockerImage;
537
+ if (options.versionsUrl) {
538
+ const [url, version] = options.versionsUrl.split('@');
539
+ const request = createRequest('GET', `/api/v1/community/app?url=${encodeURIComponent(url)}&version=${encodeURIComponent(version || 'latest')}`, options);
540
+ const response = await request;
541
+ if (response.status !== 200) return exit(`Failed to get community app: ${requestError(response)}`);
565
542
 
566
- if (!image) {
567
- console.log('No build detected. This package will be built on the server.');
543
+ manifest = response.body.manifest;
544
+ versionsUrl = response.body.versionsUrl;
545
+ } else {
546
+ const result = await getManifest(options.appstoreId || '');
547
+ manifest = result.manifest;
548
+ manifestFilePath = result.manifestFilePath;
568
549
 
569
- const dockerignoreFilePath = path.join(sourceDir, '.dockerignore');
570
- const ignoreMatcher = dockerignoreMatcher(dockerignoreFilePath);
550
+ if (!manifest.dockerImage) { // not a manifest from appstore
551
+ const sourceDir = path.dirname(manifestFilePath);
552
+ const image = options.image || config.getCwdConfig(sourceDir).dockerImage;
571
553
 
572
- const tarStream = tar.pack(sourceDir, {
573
- ignore: function (name) {
574
- return ignoreMatcher(name.slice(sourceDir.length + 1)); // make name as relative path
575
- }
576
- });
554
+ if (!image) {
555
+ console.log('No build detected. This package will be built on the server.');
577
556
 
578
- sourceArchiveFilePath = path.join(os.tmpdir(), `cloudron-source-${Date.now()}.tar`);
579
- const sourceArchiveStream = fs.createWriteStream(sourceArchiveFilePath);
557
+ const dockerignoreFilePath = path.join(sourceDir, '.dockerignore');
558
+ const ignoreMatcher = dockerignoreMatcher(dockerignoreFilePath);
580
559
 
581
- const [tarError] = await safe(stream.pipeline(tarStream, sourceArchiveStream));
582
- if (tarError) return exit(`Could not create source archive: ${tarError.message}`);
583
- } else {
584
- manifest.dockerImage = image;
560
+ const tarStream = tar.pack(sourceDir, {
561
+ ignore: function (name) {
562
+ return ignoreMatcher(name.slice(sourceDir.length + 1)); // make name as relative path
563
+ }
564
+ });
565
+
566
+ sourceArchiveFilePath = path.join(os.tmpdir(), `cloudron-source-${Date.now()}.tar`);
567
+ const sourceArchiveStream = fs.createWriteStream(sourceArchiveFilePath);
568
+
569
+ const [tarError] = await safe(stream.pipeline(tarStream, sourceArchiveStream));
570
+ if (tarError) return exit(`Could not create source archive: ${tarError.message}`);
571
+ } else {
572
+ manifest.dockerImage = image;
573
+ }
585
574
  }
586
575
  }
587
576
 
@@ -600,10 +589,10 @@ async function install(localOptions, cmd) {
600
589
  const tmp = kv.split('=');
601
590
  secondaryDomains[tmp[0]] = await selectDomain(tmp[1], options);
602
591
  }
603
- } else {
592
+ } else if (manifest) {
604
593
  secondaryDomains = await querySecondaryDomains(null /* existing app */, manifest, options);
605
594
  }
606
- } else if (manifest.httpPorts) { // just put in defaults
595
+ } else if (manifest && manifest.httpPorts) { // just put in defaults
607
596
  for (const env in manifest.httpPorts) {
608
597
  secondaryDomains[env] = await selectDomain(manifest.httpPorts[env].defaultValue, options);
609
598
  }
@@ -629,10 +618,10 @@ async function install(localOptions, cmd) {
629
618
  if (isNaN(parseInt(tmp[1], 10))) return; // disable the port
630
619
  ports[tmp[0]] = parseInt(tmp[1], 10);
631
620
  });
632
- } else {
621
+ } else if (manifest) {
633
622
  ports = await queryPortBindings(null /* existing app */, manifest);
634
623
  }
635
- } else { // just put in defaults
624
+ } else if (manifest) { // just put in defaults
636
625
  const allPorts = Object.assign({}, manifest.tcpPorts, manifest.udpPorts);
637
626
  for (const portName in allPorts) {
638
627
  ports[portName] = allPorts[portName].defaultValue;
@@ -655,7 +644,6 @@ async function install(localOptions, cmd) {
655
644
 
656
645
  const data = {
657
646
  appStoreId: options.appstoreId || '', // note case change
658
- manifest: options.appstoreId ? null : manifest, // cloudron ignores manifest anyway if appStoreId is set
659
647
  location: domainObject.subdomain, // LEGACY
660
648
  subdomain: domainObject.subdomain,
661
649
  domain: domainObject.domain,
@@ -667,8 +655,14 @@ async function install(localOptions, cmd) {
667
655
  env
668
656
  };
669
657
 
658
+ if (versionsUrl) {
659
+ data.versionsUrl = versionsUrl;
660
+ } else {
661
+ data.manifest = options.appstoreId ? null : manifest; // cloudron ignores manifest anyway if appStoreId is set
662
+ }
663
+
670
664
  // the sso only applies for apps which allow optional sso
671
- if (manifest.optionalSso) data.sso = options.sso;
665
+ if (manifest && manifest.optionalSso) data.sso = options.sso;
672
666
 
673
667
  if (options.debug) { // 'true' when no args. otherwise, array
674
668
  const debugCmd = options.debug === true ? [ '/bin/bash', '-c', 'echo "Repair mode. Use the webterminal or cloudron exec to repair. Sleeping" && sleep infinity' ] : options.debug;
@@ -680,7 +674,7 @@ async function install(localOptions, cmd) {
680
674
  options.wait = false; // in debug mode, health check never succeeds
681
675
  }
682
676
 
683
- if (!options.appstoreId && manifest.icon) {
677
+ if (!options.appstoreId && !options.versionsUrl && manifest && manifest.icon) {
684
678
  let iconFilename = manifest.icon.slice(0, 7) === 'file://' ? manifest.icon.slice(7) : manifest.icon;
685
679
  iconFilename = path.resolve(path.dirname(manifestFilePath), iconFilename); // resolve filename wrt manifest
686
680
  data.icon = safe.fs.readFileSync(iconFilename, { encoding: 'base64' });
@@ -720,6 +714,8 @@ async function install(localOptions, cmd) {
720
714
 
721
715
  const appId = response.body.id;
722
716
 
717
+ saveCwdAppId(appId, manifestFilePath);
718
+
723
719
  console.log('App is being installed.');
724
720
 
725
721
  if (!options.wait) return;
@@ -841,7 +837,7 @@ async function update(localOptions, cmd) {
841
837
 
842
838
  if (!manifest.dockerImage) { // not a manifest from appstore
843
839
  const sourceDir = path.dirname(manifestFilePath);
844
- const image = options.image || config.getAppBuildConfig(sourceDir).dockerImage;
840
+ const image = options.image || config.getCwdConfig(sourceDir).dockerImage;
845
841
 
846
842
  if (!image) {
847
843
  console.log('No docker image detected. Creating source archive from this folder.');
@@ -900,6 +896,8 @@ async function update(localOptions, cmd) {
900
896
  }
901
897
  if (response.status !== 202) return exit(`Failed to update app: ${requestError(response)}`);
902
898
 
899
+ saveCwdAppId(app.id, manifestFilePath);
900
+
903
901
  process.stdout.write('\n => ' + 'Waiting for app to be updated ');
904
902
 
905
903
  await waitForFinishInstallation(app.id, response.body.taskId, options);
@@ -1007,6 +1005,14 @@ async function uninstall(localOptions, cmd) {
1007
1005
  await waitForTask(response.body.taskId, options);
1008
1006
  const response2 = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
1009
1007
  if (response2.status === 404) {
1008
+ const manifestFilePath = locateManifest();
1009
+ if (manifestFilePath) {
1010
+ const sourceDir = path.dirname(manifestFilePath);
1011
+ const cwdConfig = config.getCwdConfig(sourceDir);
1012
+ delete cwdConfig.appId;
1013
+ config.setCwdConfig(sourceDir, cwdConfig);
1014
+ }
1015
+
1010
1016
  console.log('\n\nApp %s successfully uninstalled.', app.fqdn);
1011
1017
  } else if (response2.body.installationState === 'error') {
1012
1018
  console.log('\n\nApp uninstallation failed.\n');
@@ -235,7 +235,7 @@ async function verifyManifest(localOptions, cmd) {
235
235
  const manifest = result.manifest;
236
236
 
237
237
  const sourceDir = path.dirname(manifestFilePath);
238
- const appConfig = config.getAppBuildConfig(sourceDir);
238
+ const appConfig = config.getCwdConfig(sourceDir);
239
239
 
240
240
  // image can be passed in options for buildbot
241
241
  if (options.image) {
@@ -282,7 +282,7 @@ async function upload(localOptions, cmd) {
282
282
  const manifest = result.manifest;
283
283
 
284
284
  const sourceDir = path.dirname(manifestFilePath);
285
- const appConfig = config.getAppBuildConfig(sourceDir);
285
+ const appConfig = config.getCwdConfig(sourceDir);
286
286
 
287
287
  // image can be passed in options for buildbot
288
288
  if (options.image) {
@@ -202,7 +202,7 @@ async function buildLocal(manifest, sourceDir, appConfig, options) {
202
202
 
203
203
  appConfig.dockerImage = dockerImage;
204
204
  appConfig.dockerImageSha256 = match[1]; // stash this separately for now
205
- config.setAppBuildConfig(sourceDir, appConfig);
205
+ config.setCwdConfig(sourceDir, appConfig);
206
206
  }
207
207
 
208
208
  async function buildRemote(manifest, sourceDir, appConfig, options, buildServiceConfig) {
@@ -272,7 +272,7 @@ async function buildRemote(manifest, sourceDir, appConfig, options, buildService
272
272
 
273
273
  appConfig.dockerImage = dockerImage;
274
274
  // appConfig.dockerImageSha256 = match[1]; // stash this separately for now
275
- config.setAppBuildConfig(sourceDir, appConfig);
275
+ config.setCwdConfig(sourceDir, appConfig);
276
276
 
277
277
  console.log(`Docker image: ${dockerImage}`);
278
278
  console.log('\nBuild successful');
@@ -293,8 +293,13 @@ async function build(localOptions, cmd) {
293
293
  const manifest = result.manifest;
294
294
  const sourceDir = path.dirname(manifestFilePath);
295
295
 
296
- const appConfig = config.getAppBuildConfig(sourceDir);
296
+ const appConfig = config.getCwdConfig(sourceDir);
297
297
  const buildServiceConfig = getEffectiveBuildServiceConfig(options);
298
+ if (buildServiceConfig.type === 'remote' && buildServiceConfig.url) {
299
+ console.log('Building using remote build service at %s', buildServiceConfig.url);
300
+ } else {
301
+ console.log('Building locally with Docker.');
302
+ }
298
303
 
299
304
  let repository = appConfig.repository;
300
305
  if (!repository || options.repository) {
@@ -310,13 +315,15 @@ async function build(localOptions, cmd) {
310
315
  if (parts.length > 1) exit(`repository should not be a URL. Try again without ${parts[0]}://`);
311
316
 
312
317
  appConfig.repository = repository;
313
- config.setAppBuildConfig(sourceDir, appConfig);
318
+ config.setCwdConfig(sourceDir, appConfig);
314
319
  }
315
320
 
316
321
  appConfig.gitCommit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim(); // when the build gets saved, save the gitCommit also
317
322
  if (buildServiceConfig.type === 'remote' && buildServiceConfig.url) {
323
+ console.log('Building using remote build service at %s', buildServiceConfig.url);
318
324
  await buildRemote(manifest, sourceDir, appConfig, options, buildServiceConfig);
319
325
  } else {
326
+ console.log('Building locally with Docker.');
320
327
  await buildLocal(manifest, sourceDir, appConfig, options);
321
328
  }
322
329
  }
@@ -377,16 +384,25 @@ async function push(localOptions, cmd) {
377
384
  if (buildStatus !== 'success') return exit('Failed to push app. See log output above.');
378
385
  }
379
386
 
380
- async function clear(/* localOptions, cmd */) {
381
- // const options = cmd.optsWithGlobals();
382
-
383
- // try to find the manifest of this project
387
+ async function reset(/* localOptions, cmd */) {
384
388
  const manifestFilePath = helper.locateManifest();
385
389
  if (!manifestFilePath) return exit('No CloudronManifest.json found');
386
390
 
387
391
  const sourceDir = path.dirname(manifestFilePath);
392
+ const cwdConfig = config.getCwdConfig(sourceDir);
393
+
394
+ if (!cwdConfig.repository && !cwdConfig.dockerImage) return console.log('Nothing to reset.');
395
+
396
+ const answer = await readline.question('Clear saved repository, image, and build info for this directory? [y/N] ', {});
397
+ if (answer.toLowerCase() !== 'y') return;
398
+
399
+ delete cwdConfig.repository;
400
+ delete cwdConfig.dockerImage;
401
+ delete cwdConfig.dockerImageSha256;
402
+ delete cwdConfig.gitCommit;
388
403
 
389
- config.unsetAppBuildConfig(sourceDir);
404
+ config.setCwdConfig(sourceDir, cwdConfig);
405
+ console.log('Build configuration reset.');
390
406
  }
391
407
 
392
408
  async function info(localOptions, cmd) {
@@ -401,7 +417,7 @@ async function info(localOptions, cmd) {
401
417
  if (!manifestFilePath) return exit();
402
418
 
403
419
  const sourceDir = path.dirname(manifestFilePath);
404
- const appConfig = config.getAppBuildConfig(sourceDir);
420
+ const appConfig = config.getCwdConfig(sourceDir);
405
421
 
406
422
  console.log('Build info');
407
423
  if (appConfig?.dockerImage) {
@@ -420,7 +436,7 @@ export default {
420
436
  logs,
421
437
  status,
422
438
  push,
423
- clear,
439
+ reset,
424
440
  info,
425
441
  dockerignoreMatcher
426
442
  };
package/src/config.js CHANGED
@@ -60,9 +60,9 @@ function setAppStoreToken(value) {
60
60
  set(['appStore', appStoreOrigin().replace('https://', ''), 'token'], value);
61
61
  }
62
62
 
63
- const getAppBuildConfig = (p) => get(['apps', p]) || {};
64
- const setAppBuildConfig = (p, c) => set(['apps', p], c);
65
- const unsetAppBuildConfig = (p) => unset(['apps', p]);
63
+ const getCwdConfig = (p) => get(['apps', p]) || {};
64
+ const setCwdConfig = (p, c) => set(['apps', p], c);
65
+ const unsetCwdConfig = (p) => unset(['apps', p]);
66
66
 
67
67
  const getBuildServiceConfig = () => get('buildService') || {};
68
68
  const setBuildServiceConfig = (c) => set('buildService', c);
@@ -94,9 +94,9 @@ export {
94
94
  appStoreOrigin,
95
95
 
96
96
  // per app
97
- getAppBuildConfig,
98
- setAppBuildConfig,
99
- unsetAppBuildConfig,
97
+ getCwdConfig,
98
+ setCwdConfig,
99
+ unsetCwdConfig,
100
100
 
101
101
  // build service
102
102
  getBuildServiceConfig,
@@ -41,7 +41,14 @@ async function init(/*localOptions, cmd*/) {
41
41
  if (fs.existsSync(versionsFilePath)) return exit(`${path.relative(process.cwd(), versionsFilePath)} already exists.`);
42
42
 
43
43
  await writeVersions(versionsFilePath, { stable: true, versions: {} });
44
- console.log(`Created ${path.relative(process.cwd(), versionsFilePath)}. Use "cloudron versions add" to add a version.`);
44
+ console.log(`Created ${path.relative(process.cwd(), versionsFilePath)}.`);
45
+
46
+ const result = manifestFormat.parseFile(manifestFilePath);
47
+ if (!result.error) {
48
+ ensurePublishFields(result.manifest, manifestFilePath);
49
+ }
50
+
51
+ console.log('\nUse "cloudron versions add" to add a version.');
45
52
  }
46
53
 
47
54
  async function resolveManifest(manifest, baseDir) {
@@ -69,6 +76,92 @@ async function resolveManifest(manifest, baseDir) {
69
76
  }
70
77
  }
71
78
 
79
+ function createStubFile(filePath, content) {
80
+ if (fs.existsSync(filePath)) return false;
81
+ fs.writeFileSync(filePath, content, 'utf8');
82
+ return true;
83
+ }
84
+
85
+ function ensurePublishFields(manifest, manifestFilePath) {
86
+ const baseDir = path.dirname(manifestFilePath);
87
+ const rawManifest = JSON.parse(fs.readFileSync(manifestFilePath, 'utf8'));
88
+ const added = [];
89
+ const created = [];
90
+
91
+ const defaultFields = {
92
+ id: 'com.example.app',
93
+ title: '',
94
+ author: '',
95
+ tagline: '',
96
+ website: '',
97
+ contactEmail: '',
98
+ iconUrl: '',
99
+ packagerName: '',
100
+ packagerUrl: '',
101
+ minBoxVersion: '9.1.0',
102
+ };
103
+
104
+ for (const [key, defaultValue] of Object.entries(defaultFields)) {
105
+ if (rawManifest[key]) continue;
106
+
107
+ rawManifest[key] = defaultValue;
108
+ manifest[key] = defaultValue;
109
+ added.push(key);
110
+ }
111
+
112
+ if (!rawManifest.description) {
113
+ rawManifest.description = 'file://DESCRIPTION.md';
114
+ manifest.description = 'file://DESCRIPTION.md';
115
+ added.push('description');
116
+ if (createStubFile(path.join(baseDir, 'DESCRIPTION.md'), 'Describe your app here.\n')) {
117
+ created.push('DESCRIPTION.md');
118
+ }
119
+ }
120
+
121
+ if (!rawManifest.changelog) {
122
+ rawManifest.changelog = 'file://CHANGELOG';
123
+ manifest.changelog = 'file://CHANGELOG';
124
+ added.push('changelog');
125
+ const version = rawManifest.version || '0.1.0';
126
+ if (createStubFile(path.join(baseDir, 'CHANGELOG'), `[${version}]\n* Initial release\n`)) {
127
+ created.push('CHANGELOG');
128
+ }
129
+ }
130
+
131
+ if (!rawManifest.postInstallMessage) {
132
+ rawManifest.postInstallMessage = 'file://POSTINSTALL.md';
133
+ manifest.postInstallMessage = 'file://POSTINSTALL.md';
134
+ added.push('postInstallMessage');
135
+ if (createStubFile(path.join(baseDir, 'POSTINSTALL.md'), 'Post-installation instructions.\n')) {
136
+ created.push('POSTINSTALL.md');
137
+ }
138
+ }
139
+
140
+ if (!rawManifest.tags || rawManifest.tags.length === 0) {
141
+ rawManifest.tags = ['uncategorized'];
142
+ manifest.tags = ['uncategorized'];
143
+ added.push('tags');
144
+ }
145
+
146
+ if (!rawManifest.mediaLinks || rawManifest.mediaLinks.length === 0) {
147
+ rawManifest.mediaLinks = ['https://example.com/screenshot.png'];
148
+ manifest.mediaLinks = ['https://example.com/screenshot.png'];
149
+ added.push('mediaLinks');
150
+ }
151
+
152
+ if (added.length === 0) return;
153
+
154
+ fs.writeFileSync(manifestFilePath, JSON.stringify(rawManifest, null, 2) + '\n', 'utf8');
155
+
156
+ console.log(`\nAdded missing fields to ${path.relative(process.cwd(), manifestFilePath)}: ${added.join(', ')}`);
157
+ if (created.length > 0) {
158
+ console.log(`Created stub files: ${created.join(', ')}`);
159
+ }
160
+ console.log('\nEdit the following fields in CloudronManifest.json before publishing:');
161
+ console.log(' id, title, author, tagline, website, contactEmail, iconUrl, packagerName, packagerUrl');
162
+ console.log(' tags, mediaLinks, DESCRIPTION.md, CHANGELOG, POSTINSTALL.md');
163
+ }
164
+
72
165
  async function addOrUpdate(localOptions, cmd) {
73
166
  const isUpdate = cmd.parent.args[0] === 'update';
74
167
  const versionsFilePath = await locateVersions();
@@ -85,7 +178,7 @@ async function addOrUpdate(localOptions, cmd) {
85
178
  const manifest = result.manifest;
86
179
 
87
180
  const sourceDir = path.dirname(manifestFilePath);
88
- const appConfig = config.getAppBuildConfig(sourceDir);
181
+ const appConfig = config.getCwdConfig(sourceDir);
89
182
 
90
183
  if (options.image) {
91
184
  manifest.dockerImage = options.image;