cloudmason2 1.3.0 → 1.4.0

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.
@@ -1,6 +1,7 @@
1
1
  const { S3Client, CreateBucketCommand, HeadBucketCommand, PutBucketPolicyCommand, PutPublicAccessBlockCommand } = require("@aws-sdk/client-s3");
2
2
  const { IAMClient, GetRoleCommand, CreateRoleCommand, AttachRolePolicyCommand, PutRolePolicyCommand } = require("@aws-sdk/client-iam");
3
3
  const { SSMClient, PutParameterCommand } = require("@aws-sdk/client-ssm");
4
+ const Apps = require('./helpers/apps');
4
5
 
5
6
  // All CDN resources are created in us-east-1
6
7
  const REGION = 'us-east-1';
@@ -45,6 +46,15 @@ exports.main = async function(args){
45
46
  await s3.send(new PutBucketPolicyCommand({ Bucket: bucket, Policy: JSON.stringify(policy) }));
46
47
  console.log('Set public access policy (keys starting with "_" are private)');
47
48
 
49
+ // Root index.html listing the cdn's public apps
50
+ await s3.send(new PutObjectCommand({
51
+ Bucket: bucket,
52
+ Key: 'index.html',
53
+ Body: Apps.indexTemplate(),
54
+ ContentType: 'text/html'
55
+ }));
56
+ console.log('Created index.html');
57
+
48
58
  // --- III MARKETPLACE ROLE ---
49
59
  const roleArn = await createMarketplaceRole(bucket);
50
60
  const ssm = new SSMClient({ region: REGION });
@@ -92,6 +92,9 @@ exports.delete_app = async function(args){
92
92
  // --- IV DELETE IAM ROLE ---
93
93
  await deleteGithubRole(appName);
94
94
 
95
+ // --- V REMOVE FROM INDEX ---
96
+ await Apps.removeFromIndex(s3, bucket, prefix, appPrefix);
97
+
95
98
  console.log(`Deleted app ${args.app}`);
96
99
  return true;
97
100
  }
@@ -1,4 +1,10 @@
1
- const { GetObjectCommand } = require("@aws-sdk/client-s3");
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { GetObjectCommand, PutObjectCommand } = require("@aws-sdk/client-s3");
4
+
5
+ const INDEX_TEMPLATE = path.resolve(__dirname, '..', 'index.html');
6
+ // Marker in the template <head> after which app tags are inserted
7
+ const INDEX_MARKER = '<!-- mason-apps -->';
2
8
 
3
9
  // Resolve an app name to its s3 folder and manifest. Private apps live under
4
10
  // a _ prefixed folder, so both locations are probed - callers always use the
@@ -18,3 +24,75 @@ exports.resolveApp = async function(s3, bucket, orgPrefix, appName){
18
24
  }
19
25
  return null;
20
26
  }
27
+
28
+ // The cdn root index.html carries one head tag per public app pointing at its manifest:
29
+ // <meta name="mason-app" content="<app>/manifest.json">
30
+
31
+ exports.indexTemplate = function(){
32
+ return fs.readFileSync(INDEX_TEMPLATE, 'utf-8');
33
+ }
34
+
35
+ exports.addToIndex = async function(s3, bucket, orgPrefix, appPrefix){
36
+ const key = indexKey(orgPrefix);
37
+ let html = await readIndex(s3, bucket, key);
38
+ if (html === null){ html = exports.indexTemplate() }
39
+
40
+ const tag = appTag(appPrefix);
41
+ if (html.includes(tag)){ return }
42
+ if (!html.includes(INDEX_MARKER)){
43
+ console.log(`WARNING: index.html has no ${INDEX_MARKER} marker. App not added to index`);
44
+ return;
45
+ }
46
+ html = html.replace(INDEX_MARKER, `${INDEX_MARKER}\n ${tag}`);
47
+ await writeIndex(s3, bucket, key, html);
48
+ console.log(`Added ${appPrefix} to index.html`);
49
+ }
50
+
51
+ exports.removeFromIndex = async function(s3, bucket, orgPrefix, appPrefix){
52
+ const key = indexKey(orgPrefix);
53
+ const html = await readIndex(s3, bucket, key);
54
+ if (html === null){ return }
55
+
56
+ const rgx = new RegExp(`[ \\t]*${escapeRegex(appTag(appPrefix))}\\r?\\n?`);
57
+ const updated = html.replace(rgx, '');
58
+ if (updated === html){ return }
59
+ await writeIndex(s3, bucket, key, updated);
60
+ console.log(`Removed ${appPrefix} from index.html`);
61
+ }
62
+
63
+ exports.appTag = function(appPrefix){
64
+ return `<meta name="mason-app" content="${appPrefix}/manifest.json">`;
65
+ }
66
+
67
+ exports.indexMarker = INDEX_MARKER;
68
+
69
+ function indexKey(orgPrefix){
70
+ return orgPrefix ? `${orgPrefix}/index.html` : 'index.html';
71
+ }
72
+
73
+ function appTag(appPrefix){
74
+ return exports.appTag(appPrefix);
75
+ }
76
+
77
+ function escapeRegex(s){
78
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
79
+ }
80
+
81
+ async function readIndex(s3, bucket, key){
82
+ try {
83
+ const r = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
84
+ return await r.Body.transformToString();
85
+ } catch (e){
86
+ if (/NoSuchKey|NotFound/.test(e)){ return null }
87
+ throw e;
88
+ }
89
+ }
90
+
91
+ async function writeIndex(s3, bucket, key, html){
92
+ await s3.send(new PutObjectCommand({
93
+ Bucket: bucket,
94
+ Key: key,
95
+ Body: html,
96
+ ContentType: 'text/html'
97
+ }));
98
+ }
@@ -0,0 +1,328 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Apps</title>
7
+ <!-- mason-apps -->
8
+ <style>
9
+ @font-face {
10
+ font-family: 'Jost';
11
+ font-style: normal;
12
+ font-weight: 400 700;
13
+ font-display: swap;
14
+ src: url(data:font/woff2;base64,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) format('woff2');
15
+ }
16
+ * { box-sizing: border-box; }
17
+ body {
18
+ margin: 0;
19
+ min-height: 100vh;
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ background: #f4f4f4;
24
+ font-family: 'Jost', 'Helvetica Neue', Arial, sans-serif;
25
+ color: #161616;
26
+ }
27
+ main#apps {
28
+ width: 600px;
29
+ height: 600px;
30
+ background: #ffffff;
31
+ border: 1px solid #e0e0e0;
32
+ display: flex;
33
+ flex-direction: column;
34
+ overflow: hidden;
35
+ }
36
+ .hint {
37
+ margin: 0;
38
+ padding: 16px;
39
+ font-size: 14px;
40
+ letter-spacing: 0.16px;
41
+ color: #525252;
42
+ border-bottom: 1px solid #e0e0e0;
43
+ background: #ffffff;
44
+ }
45
+ #list {
46
+ flex: 1;
47
+ overflow-y: auto;
48
+ overflow-x: hidden;
49
+ }
50
+ .none {
51
+ padding: 16px;
52
+ font-size: 14px;
53
+ color: #525252;
54
+ }
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <main id="apps">
59
+ <p class="hint">Click the copy icon to copy the Cloudformation Stack URL</p>
60
+ <div id="list"></div>
61
+ </main>
62
+
63
+ <script>
64
+ class ELMNT extends HTMLElement
65
+ {
66
+ // Constructor
67
+ constructor(){
68
+ super();
69
+ this.shadow = this.attachShadow({ mode: 'open' });
70
+ this.shadowRoot.adoptedStyleSheets = [this.constructor.Stylesheet];
71
+ this.shadow.innerHTML = this.constructor.Html || '';
72
+ // Map Event
73
+ if (this.constructor['@Events']){
74
+ for (const ev of this.constructor['@Events']){
75
+ const $el = this.shadow.querySelector(ev[0]);
76
+ if (!$el) throw 'Missing element in ' + this.constructor.Tag + ' ' + ev;
77
+ $el.addEventListener(ev[1], this[ev[2]].bind(this))
78
+ }
79
+ }
80
+ // Map Outputs
81
+ this.dispatch = {};
82
+ if (this.constructor['@Outputs']){
83
+ for (const key of this.constructor['@Outputs']){
84
+ const wantsResponse = key.charAt(0) === '@';
85
+ const name = wantsResponse ? key.slice(1) : key;
86
+ this.dispatch[name] = (p)=>{
87
+ const detail = { target: this, params: p };
88
+ let rProm = null;
89
+ if (wantsResponse){
90
+ const { promise, resolve } = Promise.withResolvers();
91
+ rProm = promise;
92
+ detail.respond = resolve;
93
+ }
94
+ this.dispatchEvent(new CustomEvent(name, {
95
+ detail,
96
+ bubbles: true,
97
+ composed: true
98
+ }));
99
+ return rProm;
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ // --- Public Properties --- //
106
+ dispatch = {};
107
+
108
+ attributeChangedCallback(name, oldValue, newValue){
109
+ if (oldValue !== newValue){ this[name] = newValue }
110
+ }
111
+
112
+ map(obj){
113
+ for (const key in obj){ this[key] = obj[key]}
114
+ }
115
+
116
+ static Define(cls){
117
+ cls.Stylesheet = new CSSStyleSheet();
118
+ cls.Stylesheet.replace(cls.Css).then(() => {
119
+ customElements.define(cls.Tag, cls);
120
+ })
121
+ }
122
+ }
123
+ </script>
124
+
125
+ <script>
126
+ class MasonApp extends ELMNT
127
+ {
128
+
129
+ // Constructor
130
+ constructor(){
131
+ super();
132
+ }
133
+
134
+ // --- Static Definitions --- //
135
+ static observedAttributes = [];
136
+ static '@Events' = [
137
+ ['.head', 'click', '_onToggle'],
138
+ ['.versions', 'click', '_onCopy']
139
+ ];
140
+ static '@Outputs' = [];
141
+
142
+ // --- Private Properties --- //
143
+ #app;
144
+
145
+ // --- Setters & Getters --- //
146
+ set app(v){
147
+ this.#app = v;
148
+ this.#_render();
149
+ }
150
+
151
+ // --- Public Methods --- //
152
+ expand(){
153
+ this.shadow.querySelector('.item').classList.add('open');
154
+ }
155
+
156
+ // --- User Triggered Events --- //
157
+ _onToggle(){
158
+ this.shadow.querySelector('.item').classList.toggle('open');
159
+ }
160
+
161
+ _onCopy(e){
162
+ const btn = e.target.closest('.copy');
163
+ if (!btn){ return }
164
+ navigator.clipboard.writeText(btn.dataset.url);
165
+ btn.classList.add('done');
166
+ setTimeout(()=>{ btn.classList.remove('done') }, 1500);
167
+ }
168
+
169
+ // --- Private SubHelpers --- //
170
+ #_render(){
171
+ this.shadow.querySelector('.name').textContent = this.#app.name;
172
+ const versions = MasonApp.PublishedVersions(this.#app.manifest);
173
+ this.shadow.querySelector('.count').textContent = `${versions.length} version${versions.length === 1 ? '' : 's'}`;
174
+ this.shadow.querySelector('.versions').innerHTML = versions.length
175
+ ? versions.map(v=>{ return MasonApp.VersionRow(v) }).join('')
176
+ : '<div class="empty">No published versions</div>';
177
+ }
178
+
179
+ // --- Static Methods --- //
180
+ static PublishedVersions(manifest){
181
+ return (manifest.versions || [])
182
+ .filter(v=>{ return v.published === true })
183
+ .sort((a,b)=>{ return String(b.publish_date || b.version).localeCompare(String(a.publish_date || a.version)) });
184
+ }
185
+
186
+ static VersionRow(v){
187
+ const mm = v.version.split('.').slice(0, 2).join('.');
188
+ return `<div class="row">
189
+ <span class="ver">v${mm}<span class="full">${v.version}</span></span>
190
+ <span class="date">${MasonApp.#FormatDate(v.publish_date)}</span>
191
+ <button class="copy" data-url="${v.stack_url || ''}" aria-label="Copy Cloudformation Stack URL">
192
+ <svg class="ic-copy" viewBox="0 0 32 32" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z"/><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z"/></svg>
193
+ <svg class="ic-check" viewBox="0 0 32 32" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M13 24l-9-9 1.41-1.41L13 21.17 26.59 7.59 28 9 13 24z"/></svg>
194
+ </button>
195
+ </div>`;
196
+ }
197
+
198
+ // --- Static SubHelpers --- //
199
+ static #FormatDate(iso){
200
+ if (!iso){ return '' }
201
+ return new Date(iso).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
202
+ }
203
+
204
+ // --- Static Defs --- //
205
+ static Tag = 'mason-app';
206
+ static Html = (`
207
+ <div class="item">
208
+ <div class="head">
209
+ <span class="name"></span>
210
+ <span class="count"></span>
211
+ <svg class="chev" viewBox="0 0 32 32" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M16 22L6 12l1.4-1.4 8.6 8.6 8.6-8.6L26 12z"/></svg>
212
+ </div>
213
+ <div class="versions"></div>
214
+ </div>
215
+ `);
216
+ static Css = (`
217
+ :host { display: block; font-family: 'Jost', 'Helvetica Neue', Arial, sans-serif; color: #161616; }
218
+ .head {
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 12px;
222
+ padding: 14px 16px;
223
+ font-size: 15px;
224
+ font-weight: 500;
225
+ cursor: pointer;
226
+ border-bottom: 1px solid #e0e0e0;
227
+ background: #ffffff;
228
+ }
229
+ .head:hover { background: #e8e8e8; }
230
+ .count { margin-left: auto; font-size: 12px; font-weight: 400; color: #525252; }
231
+ .chev { transition: transform 0.15s; color: #525252; }
232
+ .open .chev { transform: rotate(180deg); }
233
+ .versions { display: none; }
234
+ .open .versions { display: block; }
235
+ .row {
236
+ display: flex;
237
+ align-items: center;
238
+ gap: 12px;
239
+ padding: 10px 16px 10px 32px;
240
+ font-size: 14px;
241
+ border-bottom: 1px solid #e0e0e0;
242
+ background: #ffffff;
243
+ }
244
+ .row:hover { background: #f4f4f4; }
245
+ .ver { font-weight: 500; position: relative; }
246
+ .full {
247
+ position: absolute;
248
+ bottom: calc(100% + 6px);
249
+ left: 0;
250
+ padding: 4px 8px;
251
+ background: #161616;
252
+ color: #ffffff;
253
+ font-size: 12px;
254
+ font-weight: 400;
255
+ white-space: nowrap;
256
+ opacity: 0;
257
+ visibility: hidden;
258
+ transition: opacity 0.1s 0s, visibility 0s 0.1s;
259
+ pointer-events: none;
260
+ z-index: 1;
261
+ }
262
+ .ver:hover .full {
263
+ opacity: 1;
264
+ visibility: visible;
265
+ transition-delay: 5s, 5s;
266
+ }
267
+ .date { margin-left: auto; color: #525252; }
268
+ .copy {
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ padding: 5px;
273
+ background: none;
274
+ border: none;
275
+ cursor: pointer;
276
+ color: #161616;
277
+ }
278
+ .copy:hover { background: #e8e8e8; }
279
+ .copy:focus { outline: 2px solid #0f62fe; outline-offset: -2px; }
280
+ .ic-check { display: none; color: #24a148; }
281
+ .done .ic-copy { display: none; }
282
+ .done .ic-check { display: block; }
283
+ .empty { padding: 12px 16px 12px 32px; font-size: 14px; color: #525252; border-bottom: 1px solid #e0e0e0; }
284
+ `);
285
+
286
+ }
287
+
288
+ ELMNT.Define(MasonApp);
289
+ </script>
290
+
291
+ <script>
292
+ (async function(){
293
+ const list = document.getElementById('list');
294
+ const filter = new URLSearchParams(location.search).get('app');
295
+
296
+ const apps = [];
297
+ document.querySelectorAll('meta[name="mason-app"]').forEach(m=>{
298
+ const path = m.getAttribute('content');
299
+ const name = path.split('/').slice(-2, -1)[0].replace(/^_/, '');
300
+ if (filter && name.toLowerCase() !== filter.toLowerCase()){ return }
301
+ apps.push({ path: path, name: name });
302
+ });
303
+
304
+ const loaded = await Promise.all(apps.map(a=>{
305
+ return fetch(a.path)
306
+ .then(r=>{ if (!r.ok){ throw new Error(r.status) } return r.json() })
307
+ .then(manifest=>{ return { name: a.name, manifest: manifest } })
308
+ .catch(()=>{ return null });
309
+ }));
310
+
311
+ await customElements.whenDefined('mason-app');
312
+ let count = 0;
313
+ for (const a of loaded){
314
+ if (!a){ continue }
315
+ const el = document.createElement('mason-app');
316
+ el.app = { name: a.manifest.name || a.name, manifest: a.manifest };
317
+ list.appendChild(el);
318
+ count += 1;
319
+ }
320
+ if (count === 0){
321
+ list.innerHTML = '<p class="none">No apps found</p>';
322
+ } else if (filter && count === 1){
323
+ list.querySelector('mason-app').expand();
324
+ }
325
+ })();
326
+ </script>
327
+ </body>
328
+ </html>
@@ -5,7 +5,11 @@ const OrgConfig = require('./helpers/org_config');
5
5
  const Apps = require('./helpers/apps');
6
6
 
7
7
  // CLI args consumed by the command itself; everything else is passed through as a CF stack parameter
8
- const CLI_ARGS = ['app', 'v', 'title', 'region'];
8
+ const CLI_ARGS = ['app', 'v', 'title', 'region', 'await'];
9
+
10
+ // Terminal/settling stack statuses that mean the launch succeeded.
11
+ // UPDATE_COMPLETE_CLEANUP_IN_PROGRESS means the update itself is done
12
+ const SUCCESS_STATUSES = ['CREATE_COMPLETE', 'UPDATE_COMPLETE', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS'];
9
13
 
10
14
  exports.main = async function(args){
11
15
  const cfg = OrgConfig.read();
@@ -110,6 +114,11 @@ exports.main = async function(args){
110
114
  }
111
115
  }
112
116
  console.log('Stack:', stackId);
117
+
118
+ // --- III AWAIT ---
119
+ if (args.await !== undefined){
120
+ await awaitStack(client, stackName);
121
+ }
113
122
  return true;
114
123
  }
115
124
 
@@ -163,13 +172,41 @@ async function waitForAMI(amiId, region){
163
172
  async function getStack(client, stackName){
164
173
  try {
165
174
  const r = await client.send(new DescribeStacksCommand({ StackName: stackName }));
166
- return { status: r.Stacks[0].StackStatus, parameters: r.Stacks[0].Parameters || [] };
175
+ return {
176
+ status: r.Stacks[0].StackStatus,
177
+ reason: r.Stacks[0].StackStatusReason || '',
178
+ parameters: r.Stacks[0].Parameters || []
179
+ };
167
180
  } catch (e){
168
181
  if (e.name === 'ValidationError'){ return null }
169
182
  throw e;
170
183
  }
171
184
  }
172
185
 
186
+ // Poll every 30s for up to 30 minutes until the stack reaches a success status.
187
+ // Still in progress: CREATE_IN_PROGRESS, UPDATE_IN_PROGRESS. Anything containing
188
+ // FAILED, ROLLBACK, or DELETE means the launch did not succeed
189
+ async function awaitStack(client, stackName){
190
+ console.log('Waiting for stack to complete');
191
+ for (let i=0; i<60; i++){
192
+ await new Promise(res=>{ setTimeout(res, 30000) });
193
+ const stack = await getStack(client, stackName);
194
+ if (!stack){
195
+ // OnFailure: DELETE removes a failed create entirely
196
+ throw new Error('Stack no longer exists. Create failed and was deleted');
197
+ }
198
+ console.log(`\tStack status @${(i+1)*30}s: ${stack.status}`);
199
+ if (SUCCESS_STATUSES.includes(stack.status)){
200
+ console.log(`Stack launch succeeded: ${stack.status}`);
201
+ return;
202
+ }
203
+ if (/FAILED|ROLLBACK|DELETE/.test(stack.status)){
204
+ throw new Error(`Stack launch failed: ${stack.status}${stack.reason ? ' - ' + stack.reason : ''}`);
205
+ }
206
+ }
207
+ throw new Error('Stack not complete after 30 minutes');
208
+ }
209
+
173
210
  async function getTemplateKeys(client, templateUrl){
174
211
  const r = await client.send(new GetTemplateSummaryCommand({ TemplateURL: templateUrl }));
175
212
  return new Set((r.Parameters || []).map(p=>{ return p.ParameterKey }));
@@ -4,6 +4,7 @@ const { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command } = r
4
4
  const { IAMClient, GetRoleCommand, CreateRoleCommand, PutRolePolicyCommand, GetOpenIDConnectProviderCommand, CreateOpenIDConnectProviderCommand } = require("@aws-sdk/client-iam");
5
5
  const { STSClient, GetCallerIdentityCommand } = require("@aws-sdk/client-sts");
6
6
  const OrgConfig = require('./helpers/org_config');
7
+ const Apps = require('./helpers/apps');
7
8
 
8
9
  const TEMPLATE_DIR = path.resolve(__dirname, '..', '.marketplace');
9
10
 
@@ -38,7 +39,11 @@ exports.main = async function(args){
38
39
  await createManifest(s3, bucket, appPrefix, args.name, args.noami !== undefined, args.private !== undefined);
39
40
  }
40
41
 
41
- // --- II LOCAL TEMPLATES ---
42
+ // --- II INDEX + LOCAL TEMPLATES ---
43
+ // Public apps are listed in the cdn root index.html
44
+ if (args.private === undefined){
45
+ await Apps.addToIndex(s3, bucket, prefix, appPrefix);
46
+ }
42
47
  createLocalMarketplace();
43
48
 
44
49
  // --- III REPO ROLE ---
@@ -0,0 +1,43 @@
1
+ const { S3Client, ListObjectsV2Command, PutObjectCommand } = require("@aws-sdk/client-s3");
2
+ const Apps = require('./helpers/apps');
3
+
4
+ // All core resources live in us-east-1
5
+ const REGION = 'us-east-1';
6
+
7
+ // refresh-cdn: rebuild the cdn root index.html from all public app manifests
8
+ exports.main = async function(args){
9
+ const bucket = args.cdn;
10
+ const s3 = new S3Client({ region: REGION });
11
+
12
+ // Scan for app manifests: one folder deep, private "_" folders excluded
13
+ const apps = [];
14
+ let token;
15
+ do {
16
+ const r = await s3.send(new ListObjectsV2Command({ Bucket: bucket, ContinuationToken: token }));
17
+ (r.Contents || []).forEach(o=>{
18
+ const parts = o.Key.split('/');
19
+ if (parts.length === 2 && parts[1] === 'manifest.json' && !parts[0].startsWith('_')){
20
+ apps.push(parts[0]);
21
+ }
22
+ });
23
+ token = r.NextContinuationToken;
24
+ } while (token);
25
+ apps.sort();
26
+
27
+ // Rebuild from the boilerplate template. Tags insert at the marker,
28
+ // so add in reverse to end up in alphabetical order
29
+ let html = Apps.indexTemplate();
30
+ [...apps].reverse().forEach(a=>{
31
+ html = html.replace(Apps.indexMarker, `${Apps.indexMarker}\n ${Apps.appTag(a)}`);
32
+ });
33
+
34
+ await s3.send(new PutObjectCommand({
35
+ Bucket: bucket,
36
+ Key: 'index.html',
37
+ Body: html,
38
+ ContentType: 'text/html'
39
+ }));
40
+ apps.forEach(a=>{ console.log(' ' + a) });
41
+ console.log(`Rebuilt index.html with ${apps.length} app(s)`);
42
+ return true;
43
+ }
package/main.js CHANGED
@@ -8,6 +8,13 @@ const Commands = {
8
8
  {n: 'name', desc: 'CDN bucket name (lowercase letters, numbers, hyphens)', pattern: `^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$`, r: true}
9
9
  ]
10
10
  },
11
+ 'refresh-cdn': {
12
+ desc: "Rebuild the cdn index.html from all public app manifests",
13
+ exec: require('./commands/refresh_cdn').main,
14
+ args: [
15
+ {n: 'cdn', desc: 'CDN bucket name', pattern: `^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$`, r: true}
16
+ ]
17
+ },
11
18
  'set-org': {
12
19
  desc: "Set the organization CDN",
13
20
  exec: require('./commands/init_org').main,
@@ -62,6 +69,7 @@ const Commands = {
62
69
  {n: 'v', desc: 'Version to launch', pattern: `[0-9]{1,20}`, r: true},
63
70
  {n: 'title', desc: 'Title of the version', pattern: `^[A-Za-z][A-Za-z0-9-]{1,30}$`, r: true},
64
71
  {n: 'region', desc: 'AWS Region to launch the instance', pattern: `^[a-z]{2}-[a-z]+-[0-9]$`, r: true},
72
+ {n: 'await', desc: 'Wait for the stack launch to complete (up to 30m)', r: false},
65
73
  ]
66
74
  },
67
75
  'publish': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmason2",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "",
5
5
  "main": "main.js",
6
6
  "files": [