@bongos/core 1.25.21 → 1.25.22
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/.bongos-core.json +84 -44
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +11 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0406-every-project-gets-a-sandbox-that-follows-main-and-live-moves-only-when-a-person-promotes.md +8 -0
- package/docs/api/openapi.json +198 -3
- package/docs/api-reference.md +7 -3
- package/docs/copy-inventory.md +412 -407
- package/docs/copy-registry.json +548 -503
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +349 -343
- package/modules/lifecycle/kickoff-checklist.js +1 -1
- package/modules/lifecycle/routes/pay-type-gates.js +7 -2
- package/modules/onboarding/project-facts.js +318 -0
- package/modules/onboarding/routes/genesis-work.js +16 -0
- package/modules/provisioning/project-facts.js +55 -0
- package/modules/provisioning/provisioning.js +4 -1
- package/modules/provisioning/render-sandbox.js +6 -0
- package/modules/public-landing/public/projects.html +191 -31
- package/package-lock.json +2 -2
- package/package.json +2 -1
- package/release-notes.json +14 -0
- package/scripts/bongos/genesis.js +38 -2
- package/scripts/bongos/init.js +13 -0
- package/scripts/bongos/package-core.js +2 -0
- package/scripts/bongos/provision-render-sandbox.js +5 -1
- package/scripts/bongos/render-payload.js +5 -2
- package/src/bongos/folder-upload.js +243 -0
- package/src/bongos/routes/github.js +109 -16
- package/src/bongos/routes.js +3 -0
- package/src/branding.js +4 -0
- package/src/module-api.js +1 -1
- package/src/sandbox/gate.js +164 -0
- package/src/sandbox/index.js +144 -0
- package/tests/folder_upload_project.mjs +396 -0
- package/tests/genesis_work_routes.mjs +25 -1
- package/tests/project_facts.mjs +158 -0
- package/tests/projects_hub.mjs +1 -1
- package/tests/provision_settings_apply.mjs +2 -1
- package/tests/provisioning_settings_env.mjs +2 -2
- package/tests/public_landing_projects.mjs +27 -5
- package/tests/render_sandbox.mjs +7 -4
- package/tests/sandbox_gate.mjs +182 -0
|
@@ -478,6 +478,13 @@ body:has(#view-new:not([hidden])) footer.foot{position:relative;z-index:1;backgr
|
|
|
478
478
|
.acctPick .acctTyped{display:flex;align-items:flex-end;gap:14px;}
|
|
479
479
|
.acctPick .acctTyped .wizField{flex:1;min-width:0;}
|
|
480
480
|
.acctPick .acctTyped .btn{flex:none;margin:0 0 2px;}
|
|
481
|
+
/* start it from a folder (task 1005282): two buttons that wrap at phone width, then
|
|
482
|
+
the count or each refused file with its reason, one per line */
|
|
483
|
+
.folderPick{max-width:680px;margin-top:18px;padding-top:14px;border-top:1px solid var(--rule);}
|
|
484
|
+
.folderPick .folderBtns{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:8px;}
|
|
485
|
+
.folderPick .folderProblems{margin:6px 0 0;padding:0;list-style:none;max-height:180px;overflow-y:auto;}
|
|
486
|
+
.folderPick .folderProblems li{margin:4px 0;overflow-wrap:anywhere;}
|
|
487
|
+
.folderPick .folderProblems b{font-weight:600;}
|
|
481
488
|
|
|
482
489
|
/* the choice (start fresh / adopt): two cells of one band on hairlines, the
|
|
483
490
|
chosen one carrying the dot — the dot is the mark, so the icon leaves */
|
|
@@ -1798,6 +1805,23 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1798
1805
|
</div>
|
|
1799
1806
|
<p class="hint" id="raNote" aria-live="polite"></p>
|
|
1800
1807
|
</div>
|
|
1808
|
+
<!-- start it from a folder on this computer (task 1005282): papers,
|
|
1809
|
+
artwork or scaffolding go into the new private repo as its first
|
|
1810
|
+
commit. The files stay in this tab until Create; the server checks
|
|
1811
|
+
them again and refuses before anything is made. -->
|
|
1812
|
+
<div class="wizField folderPick" id="folderPick">
|
|
1813
|
+
<span>Start it with your files <small>(optional)</small></span>
|
|
1814
|
+
<p class="hint">Papers, artwork or a half-built project: choose the folder and everything in it goes into the new repository. Only you and the people you invite can see it.</p>
|
|
1815
|
+
<div class="folderBtns">
|
|
1816
|
+
<button type="button" class="btn small ghost" id="folderDirBtn">Choose a folder</button>
|
|
1817
|
+
<button type="button" class="btn small ghost" id="folderFilesBtn">Choose files</button>
|
|
1818
|
+
<button type="button" class="btn small ghost" id="folderClear" hidden>Remove</button>
|
|
1819
|
+
<input type="file" id="folderDir" webkitdirectory multiple hidden aria-hidden="true" tabindex="-1">
|
|
1820
|
+
<input type="file" id="folderFilesIn" multiple hidden aria-hidden="true" tabindex="-1">
|
|
1821
|
+
</div>
|
|
1822
|
+
<p class="hint" id="folderNote" aria-live="polite">Up to 50 MB in all, 25 MB a file. No programs, zipped files or password files.</p>
|
|
1823
|
+
<ul class="folderProblems hint err" id="folderProblems" hidden></ul>
|
|
1824
|
+
</div>
|
|
1801
1825
|
</div>
|
|
1802
1826
|
<div class="row2" id="manualHome">
|
|
1803
1827
|
<label class="wizField"><span>GitHub owner <small>(user or org)</small></span>
|
|
@@ -2357,6 +2381,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2357
2381
|
line.push('. It sleeps when nobody visits and takes about a minute to wake.');
|
|
2358
2382
|
box.appendChild(appPara(line));
|
|
2359
2383
|
if (sb.links) box.appendChild(appPara([appLink(sb.links.service, 'Open the sandbox on Render')]));
|
|
2384
|
+
/* the sandbox's safety (task 1005291): what it refused, and the key to lend */
|
|
2385
|
+
if (sb.safety_page) box.appendChild(appPara([appLink(sb.safety_page, 'See what your sandbox refused')]));
|
|
2386
|
+
box.appendChild(appPara(['It has no real keys unless you add them. If you lend one, lend a read-only key: the sandbox refuses sends, payments and changes it can see, but a read-only key is what makes it safe.'], 'finePrint'));
|
|
2360
2387
|
}
|
|
2361
2388
|
if (sb.database && sb.database.note) box.appendChild(appPara([String(sb.database.note)], 'finePrint'));
|
|
2362
2389
|
if (sb.can_add) {
|
|
@@ -5703,6 +5730,84 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5703
5730
|
}
|
|
5704
5731
|
$('ghNewName').addEventListener('input', function () { state.repo = this.value.trim(); updateFreshPreview(); saveDraft(); });
|
|
5705
5732
|
|
|
5733
|
+
/* ── start it from a folder (task 1005282) ──
|
|
5734
|
+
A person with papers, artwork or scaffolding and no repository chooses a folder (or
|
|
5735
|
+
files, where the browser has no folder picker — phones) and it becomes the new
|
|
5736
|
+
repo's first commit. The File objects live in this tab only: a draft cannot carry
|
|
5737
|
+
them, so after a reload or a GitHub round-trip the row asks for the folder again.
|
|
5738
|
+
These sizes are a courtesy echo of the server's (src/bongos/folder-upload.js), which
|
|
5739
|
+
checks every file again — type and path included — and refuses before anything is
|
|
5740
|
+
made; its reasons are shown here, one per file. */
|
|
5741
|
+
var FOLDER_MB = 1024 * 1024;
|
|
5742
|
+
var FOLDER_LIMITS = { fileBytes: 25 * FOLDER_MB, totalBytes: 50 * FOLDER_MB, files: 1000 };
|
|
5743
|
+
var FOLDER_SKIP = /(^|\/)(\.ds_store|thumbs\.db|desktop\.ini|ehthumbs\.db)$|(^|\/)(node_modules|__macosx)\//i;
|
|
5744
|
+
var folderFiles = []; /* [{ path, file }] */
|
|
5745
|
+
var folderProblems = []; /* [{ path, reason }], ours or the server's */
|
|
5746
|
+
function folderSize(n) { return n >= FOLDER_MB ? (n / FOLDER_MB).toFixed(1) + ' MB' : Math.max(1, Math.round(n / 1024)) + ' KB'; }
|
|
5747
|
+
/* the problems a size alone can show, before anything is sent */
|
|
5748
|
+
function folderLocalProblems(list) {
|
|
5749
|
+
var out = [], total = 0;
|
|
5750
|
+
list.forEach(function (f) {
|
|
5751
|
+
total += f.file.size;
|
|
5752
|
+
if (f.file.size > FOLDER_LIMITS.fileBytes) out.push({ path: f.path, reason: 'Each file can be at most 25 MB.' });
|
|
5753
|
+
});
|
|
5754
|
+
if (list.length > FOLDER_LIMITS.files) out.unshift({ path: '', reason: 'The folder has ' + list.length + ' files; the most a folder can have is ' + FOLDER_LIMITS.files + '.' });
|
|
5755
|
+
if (total > FOLDER_LIMITS.totalBytes) out.unshift({ path: '', reason: 'The folder is ' + folderSize(total) + '; the most a folder can be is 50 MB.' });
|
|
5756
|
+
return out;
|
|
5757
|
+
}
|
|
5758
|
+
function takeFolder(fileList, fromDir) {
|
|
5759
|
+
var list = [], skipped = 0;
|
|
5760
|
+
Array.prototype.forEach.call(fileList || [], function (f) {
|
|
5761
|
+
var p = (fromDir && f.webkitRelativePath) || f.name;
|
|
5762
|
+
/* the chosen folder's own name is not part of the project */
|
|
5763
|
+
if (fromDir && p.indexOf('/') > 0) p = p.slice(p.indexOf('/') + 1);
|
|
5764
|
+
if (FOLDER_SKIP.test(p)) { skipped++; return; }
|
|
5765
|
+
list.push({ path: p, file: f });
|
|
5766
|
+
});
|
|
5767
|
+
folderFiles = list;
|
|
5768
|
+
folderProblems = folderLocalProblems(list);
|
|
5769
|
+
state.fromFolder = list.length > 0;
|
|
5770
|
+
state.folderCount = list.length;
|
|
5771
|
+
state.folderBytes = list.reduce(function (n, f) { return n + f.file.size; }, 0);
|
|
5772
|
+
state.folderSkipped = skipped;
|
|
5773
|
+
state.folderUploaded = false;
|
|
5774
|
+
state.uploadInto = undefined;
|
|
5775
|
+
saveDraft(); paintFolder(); paintCreate();
|
|
5776
|
+
}
|
|
5777
|
+
function paintFolder() {
|
|
5778
|
+
var note = $('folderNote'), ul = $('folderProblems');
|
|
5779
|
+
while (ul.firstChild) ul.removeChild(ul.firstChild);
|
|
5780
|
+
$('folderClear').hidden = !state.fromFolder;
|
|
5781
|
+
ul.hidden = !folderProblems.length;
|
|
5782
|
+
folderProblems.slice(0, 50).forEach(function (pr) {
|
|
5783
|
+
var li = document.createElement('li');
|
|
5784
|
+
if (pr.path) { var b = document.createElement('b'); b.textContent = pr.path; li.appendChild(b); li.appendChild(document.createTextNode(': ')); }
|
|
5785
|
+
li.appendChild(document.createTextNode(pr.reason));
|
|
5786
|
+
ul.appendChild(li);
|
|
5787
|
+
});
|
|
5788
|
+
if (!state.fromFolder) { note.className = 'hint'; note.textContent = 'Up to 50 MB in all, 25 MB a file. No programs, zipped files or password files.'; return; }
|
|
5789
|
+
if (state.folderUploaded) { note.className = 'hint'; note.textContent = 'Your ' + state.folderCount + ' files are in the repository.'; return; }
|
|
5790
|
+
if (!folderFiles.length) { note.className = 'hint err'; note.textContent = 'Choose your folder again: files aren’t kept while you’re away from this page.'; return; }
|
|
5791
|
+
note.className = folderProblems.length ? 'hint err' : 'hint';
|
|
5792
|
+
note.textContent = folderProblems.length
|
|
5793
|
+
? (folderProblems.length === 1 ? 'One thing to fix before Create:' : folderProblems.length + ' things to fix before Create:')
|
|
5794
|
+
: state.folderCount + (state.folderCount === 1 ? ' file, ' : ' files, ') + folderSize(state.folderBytes) + ', ready to go into the new repository' + (state.folderSkipped ? ' (' + state.folderSkipped + ' system files left out)' : '') + '.';
|
|
5795
|
+
}
|
|
5796
|
+
/* why the folder cannot go in as it stands, or '' */
|
|
5797
|
+
function folderWhy() {
|
|
5798
|
+
if (!state.fromFolder || state.folderUploaded) return '';
|
|
5799
|
+
if (!folderFiles.length) return 'Choose your folder again: files aren’t kept while you’re away from this page.';
|
|
5800
|
+
return folderProblems.length ? 'Some files can’t go in. Fix them below, then create.' : '';
|
|
5801
|
+
}
|
|
5802
|
+
$('folderDirBtn').addEventListener('click', function () { $('folderDir').click(); });
|
|
5803
|
+
$('folderFilesBtn').addEventListener('click', function () { $('folderFilesIn').click(); });
|
|
5804
|
+
$('folderDir').addEventListener('change', function () { takeFolder(this.files, true); this.value = ''; });
|
|
5805
|
+
$('folderFilesIn').addEventListener('change', function () { takeFolder(this.files, false); this.value = ''; });
|
|
5806
|
+
$('folderClear').addEventListener('click', function () { takeFolder([], false); });
|
|
5807
|
+
/* a browser with no folder picker (phones) offers the files one only */
|
|
5808
|
+
if (!('webkitdirectory' in $('folderDir'))) $('folderDirBtn').hidden = true;
|
|
5809
|
+
paintFolder();
|
|
5810
|
+
|
|
5706
5811
|
/* ── whose GitHub account: mine or an organisation's (task 1004651) ──
|
|
5707
5812
|
Two pickers share this: the new repo's owner (part 02's Code row) and the app's
|
|
5708
5813
|
(part 03). The founder's organisations come from GET /github/orgs; GitHub may not
|
|
@@ -6590,6 +6695,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6590
6695
|
the organisation the founder picked, task 1004651). */
|
|
6591
6696
|
if (!validRepoName(state.repo)) return homeErr('Name the new repository (letters, numbers, and . _ -).');
|
|
6592
6697
|
if (state.repoAcct === 'org' && !state.repoOrg) return homeErr('Pick the organisation for the repo, or put it on your own account.');
|
|
6698
|
+
if (folderWhy()) return homeErr(folderWhy());
|
|
6593
6699
|
} else {
|
|
6594
6700
|
/* Adopt (picker or manual) and the manual fallback: need a concrete owner + repo. */
|
|
6595
6701
|
if (!state.owner || !state.repo) {
|
|
@@ -6862,7 +6968,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6862
6968
|
if (key === 'code') {
|
|
6863
6969
|
var adopt = state.mode === 'adopt';
|
|
6864
6970
|
var where = state.owner && state.repo ? 'github.com/' + state.owner + '/' + state.repo : adopt ? 'Pick the repo to adopt.' : '';
|
|
6865
|
-
|
|
6971
|
+
var folder = !adopt && state.fromFolder ? ' · with your ' + (state.folderCount || '') + ' files' : '';
|
|
6972
|
+
return { v: adopt ? 'A repo I already have' : 'A new GitHub repo', s: where + folder + (a.project_state === 'existing' ? (where || folder ? ' · ' : '') + 'already started' : '') };
|
|
6866
6973
|
}
|
|
6867
6974
|
if (key === 'address') {
|
|
6868
6975
|
var custom = state.domain && looksLikeDomain(state.domain);
|
|
@@ -7002,40 +7109,91 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7002
7109
|
/* the founder's own account sends exactly what it always did; an organisation
|
|
7003
7110
|
rides as `org`, and the server checks the name again (task 1004651) */
|
|
7004
7111
|
var repoBody = state.repoAcct === 'org' && state.repoOrg ? { name: state.repo, org: state.repoOrg } : { name: state.repo };
|
|
7112
|
+
/* from a folder (task 1005282): the files ride the same commit moment, and the
|
|
7113
|
+
server refuses the lot, with each file's reason, before the repo exists */
|
|
7114
|
+
if (state.fromFolder) {
|
|
7115
|
+
return folderBody(repoBody).then(function (body) {
|
|
7116
|
+
return fetch(API + '/github/repos/upload', {
|
|
7117
|
+
method: 'POST', credentials: 'same-origin',
|
|
7118
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7119
|
+
body: JSON.stringify(body)
|
|
7120
|
+
});
|
|
7121
|
+
}).then(repoMade);
|
|
7122
|
+
}
|
|
7005
7123
|
return fetch(API + '/github/repos', {
|
|
7006
7124
|
method: 'POST', credentials: 'same-origin',
|
|
7007
7125
|
headers: { 'Content-Type': 'application/json' },
|
|
7008
7126
|
body: JSON.stringify(repoBody)
|
|
7009
|
-
}).then(
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7127
|
+
}).then(repoMade);
|
|
7128
|
+
}
|
|
7129
|
+
/* the upload's body: every file as base64, or a retry into the repo a failed push left */
|
|
7130
|
+
function readBase64(file) {
|
|
7131
|
+
return new Promise(function (resolve, reject) {
|
|
7132
|
+
var r = new FileReader();
|
|
7133
|
+
r.onload = function () { var u = String(r.result || ''); resolve(u.slice(u.indexOf(',') + 1)); };
|
|
7134
|
+
r.onerror = function () { reject(r.error || new Error('read failed')); };
|
|
7135
|
+
r.readAsDataURL(file);
|
|
7136
|
+
});
|
|
7137
|
+
}
|
|
7138
|
+
function folderBody(repoBody) {
|
|
7139
|
+
var why = folderWhy();
|
|
7140
|
+
if (why) { var e0 = new Error(why); e0.gotoHome = true; e0.homeMsg = why; return Promise.reject(e0); }
|
|
7141
|
+
return Promise.all(folderFiles.map(function (f) {
|
|
7142
|
+
return readBase64(f.file).then(function (data) { return { path: f.path, data: data }; });
|
|
7143
|
+
})).then(function (files) {
|
|
7144
|
+
return state.uploadInto ? { into: state.uploadInto, files: files } : { name: repoBody.name, org: repoBody.org, files: files };
|
|
7145
|
+
}, function () {
|
|
7146
|
+
var e1 = new Error('read'); e1.gotoHome = true;
|
|
7147
|
+
e1.homeMsg = 'A file couldn’t be read from your computer. Choose the folder again.';
|
|
7148
|
+
throw e1;
|
|
7149
|
+
});
|
|
7150
|
+
}
|
|
7151
|
+
function repoMade(res) {
|
|
7152
|
+
return res.json().catch(function () { return {}; }).then(function (data) {
|
|
7153
|
+
if (res.status === 401 || res.status === 403) {
|
|
7154
|
+
/* the token went between the probe and Create: the hop fetches it again and
|
|
7155
|
+
brings the founder back to this screen; if it cannot, the manual fields */
|
|
7156
|
+
var hopped = hopForRepoAccess();
|
|
7157
|
+
if (!hopped) { repoAccess = 'unavailable'; syncHome(); }
|
|
7158
|
+
var e0 = new Error('reconnect'); e0.gotoHome = !hopped;
|
|
7159
|
+
e0.homeMsg = 'We couldn’t create the repo on GitHub just now — enter one you made yourself below.';
|
|
7160
|
+
throw e0;
|
|
7161
|
+
}
|
|
7162
|
+
if (res.status === 413) {
|
|
7163
|
+
var big = new Error('too big'); big.gotoHome = true;
|
|
7164
|
+
big.homeMsg = 'The folder is bigger than 50 MB, so nothing was created. Choose a smaller folder.';
|
|
7165
|
+
throw big;
|
|
7166
|
+
}
|
|
7167
|
+
if (!res.ok) {
|
|
7168
|
+
var e = new Error((data.error && (data.error.message || data.error.code)) || ('HTTP ' + res.status));
|
|
7169
|
+
e.data = data;
|
|
7170
|
+
var code = (data.error && data.error.code) || '';
|
|
7171
|
+
if (code === 'name_taken') { e.gotoHome = true; e.homeMsg = data.error.message; }
|
|
7172
|
+
/* the organisation said no: its reason, where the owner is chosen */
|
|
7173
|
+
if (/^(org_app_restricted|org_forbidden|org_not_found|bad_org)$/.test(code)) { e.gotoHome = true; e.homeMsg = data.error.message; }
|
|
7174
|
+
/* the folder was refused before anything was made: each file and its reason */
|
|
7175
|
+
if (/^upload_/.test(code)) {
|
|
7176
|
+
var det = data.error.details || {};
|
|
7177
|
+
if (Array.isArray(det.problems)) folderProblems = det.problems.map(function (pr) { return { path: String(pr.path || ''), reason: String(pr.reason || '') }; });
|
|
7178
|
+
/* the repo was made but the files did not reach it: the retry fills that one */
|
|
7179
|
+
if (code === 'upload_failed' && det.repo && det.repo.full_name) state.uploadInto = det.repo.full_name;
|
|
7180
|
+
if (code === 'upload_retry_refused') state.uploadInto = undefined;
|
|
7181
|
+
saveDraft(); paintFolder();
|
|
7182
|
+
e.gotoHome = true; e.homeMsg = data.error.message;
|
|
7032
7183
|
}
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
state.
|
|
7038
|
-
|
|
7184
|
+
throw e;
|
|
7185
|
+
}
|
|
7186
|
+
var full = data.repo && data.repo.full_name;
|
|
7187
|
+
if (full && full.indexOf('/') > 0) {
|
|
7188
|
+
state.owner = full.slice(0, full.indexOf('/'));
|
|
7189
|
+
state.repo = full.slice(full.indexOf('/') + 1);
|
|
7190
|
+
}
|
|
7191
|
+
/* Keep GitHub's own html_url so the done panel can link the repo it made
|
|
7192
|
+
(task 1002855). It rides saveDraft(), so a resumed draft keeps the link. */
|
|
7193
|
+
var made = data.repo && data.repo.html_url;
|
|
7194
|
+
if (typeof made === 'string' && made.indexOf('https://github.com/') === 0) state.repoUrl = made;
|
|
7195
|
+
if (data.uploaded) { state.folderUploaded = true; state.uploadInto = undefined; folderFiles = []; paintFolder(); }
|
|
7196
|
+
state.repoAutoCreated = true; saveDraft();
|
|
7039
7197
|
});
|
|
7040
7198
|
}
|
|
7041
7199
|
|
|
@@ -7103,7 +7261,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7103
7261
|
existing history instead of pushing a fresh tree, which a repo that already
|
|
7104
7262
|
has commits rejects as unrelated history. state.mode already tracked
|
|
7105
7263
|
greenfield|adopt for the UI — it has to reach the server too. */
|
|
7106
|
-
|
|
7264
|
+
/* a repo made from a folder already has commits, so the scaffold LAYERS onto
|
|
7265
|
+
it (and never overwrites a file) instead of pushing a fresh tree (task 1005282) */
|
|
7266
|
+
onboard_mode: state.mode === 'adopt' || state.folderUploaded ? 'adopt' : 'greenfield',
|
|
7107
7267
|
/* the page filled itself in from the guide's plan (task 1005274): the hub
|
|
7108
7268
|
carries the scope answers and the hand-off file into the new project */
|
|
7109
7269
|
from_guide: state.fromGuide ? true : undefined,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.22",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.22",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.22",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"./bongos/api-path-404": "./src/bongos/api-path-404.js",
|
|
15
15
|
"./module-loader/loader": "./src/module-loader/loader.js",
|
|
16
16
|
"./modules": "./src/modules.js",
|
|
17
|
+
"./sandbox": "./src/sandbox/index.js",
|
|
17
18
|
"./module-seams": "./src/module-seams.js"
|
|
18
19
|
},
|
|
19
20
|
"engines": {
|
package/release-notes.json
CHANGED
|
@@ -11886,5 +11886,19 @@
|
|
|
11886
11886
|
"id": "1005290",
|
|
11887
11887
|
"text": "Finished work now reaches a project's test copy first; the owner sees what is waiting and presses promote to put it on the live site, and every promotion is recorded."
|
|
11888
11888
|
}
|
|
11889
|
+
],
|
|
11890
|
+
"1.25.22": [
|
|
11891
|
+
{
|
|
11892
|
+
"id": "1005282",
|
|
11893
|
+
"text": "People can now start a project by uploading a folder from their computer: papers, artwork or a half-built project. Bongos makes the private repository and puts the files in it, so nobody has to learn git. Uploads over 50 MB, f"
|
|
11894
|
+
},
|
|
11895
|
+
{
|
|
11896
|
+
"id": "1005291",
|
|
11897
|
+
"text": "A project's sandbox is now clearly marked on every page, starts with no real keys, and if real keys are lent it can only read: anything that would send, charge or change a record for real is refused and listed for the owner."
|
|
11898
|
+
},
|
|
11899
|
+
{
|
|
11900
|
+
"id": "1005129",
|
|
11901
|
+
"text": "Choosing how contributors are rewarded now shows, for every option, what the platform really requires and what is closed — and the project now remembers where it is and where its builders are, so those notes fit it."
|
|
11902
|
+
}
|
|
11889
11903
|
]
|
|
11890
11904
|
}
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
// Usage:
|
|
13
13
|
// npx bongos genesis show [stage] every stage, or one, with its answers
|
|
14
14
|
// npx bongos genesis document <stage> the stage's document (task 1005115): what it left behind, editable any time
|
|
15
|
+
// npx bongos genesis facts what the project has said about itself (task 1005129)
|
|
16
|
+
// npx bongos genesis facts set <key> <value> | --clear change one (reward_intents: a,b; minors_on_board: true|false)
|
|
17
|
+
// npx bongos genesis options each reward option: what the server enforces, and the facts beside it
|
|
15
18
|
// npx bongos genesis context what the founder said, as markdown (for planning sessions)
|
|
16
19
|
// npx bongos genesis assist <stage> the brief for the founder's AI session (the assisted door)
|
|
17
20
|
// npx bongos genesis answer <stage> <question> "<words>" [--origin founder|assisted]
|
|
@@ -29,7 +32,7 @@
|
|
|
29
32
|
const fs = require('node:fs');
|
|
30
33
|
const { apiCall, requireSession, errCode, cliExit } = require('./cli-lib');
|
|
31
34
|
|
|
32
|
-
const USAGE = 'usage: npx bongos genesis show [stage] | context | document <stage> | assist <stage> | answer <stage> <question> "<words>" [--origin founder|assisted] | answer <stage> <question> --not-sure|--skip ["why"] | clear <stage> <question> | history <stage> <question> | door <stage> guided|open|assisted | page <stage> "<words>"|--file <path>|--remove | link <stage> <url>|--remove';
|
|
35
|
+
const USAGE = 'usage: npx bongos genesis show [stage] | context | facts [set <key> <value>|--clear] | options | document <stage> | assist <stage> | answer <stage> <question> "<words>" [--origin founder|assisted] | answer <stage> <question> --not-sure|--skip ["why"] | clear <stage> <question> | history <stage> <question> | door <stage> guided|open|assisted | page <stage> "<words>"|--file <path>|--remove | link <stage> <url>|--remove';
|
|
33
36
|
|
|
34
37
|
const STATE_LABEL = { answered: '', not_sure: '[not sure yet] ', skipped: '[skipped] ' };
|
|
35
38
|
const ORIGIN_LABEL = { assisted: ' (with AI help)', carried_over: ' (carried over — confirm or edit)' };
|
|
@@ -82,6 +85,15 @@ function formatAdvice(a) {
|
|
|
82
85
|
return out;
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
// Pure: a fact's value from the command line. --clear un-answers it.
|
|
89
|
+
function parseFactValue(key, words) {
|
|
90
|
+
const w = words.join(' ').trim();
|
|
91
|
+
if (w === '--clear') return null;
|
|
92
|
+
if (key === 'minors_on_board') return w === 'true' ? true : w === 'false' ? false : w;
|
|
93
|
+
if (key === 'reward_intents') return w.split(',').map((s) => s.trim()).filter(Boolean);
|
|
94
|
+
return w;
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
// Pure: parse `answer` arguments into the PUT body, or { error }.
|
|
86
98
|
function parseAnswer(rest) {
|
|
87
99
|
const args = [...rest];
|
|
@@ -140,6 +152,30 @@ async function main(argv) {
|
|
|
140
152
|
console.log(d.markdown || 'The founder has not written anything in genesis yet.');
|
|
141
153
|
return 0;
|
|
142
154
|
}
|
|
155
|
+
if (cmd === 'facts' && !stage) {
|
|
156
|
+
const d = await call('GET', '/founding/facts', null, 'read the project facts');
|
|
157
|
+
if (!d) return 1;
|
|
158
|
+
for (const f of d.facts) console.log(`${f.label}: ${f.answered ? (Array.isArray(f.value) ? f.value.join(', ') : String(f.value)) + (f.origin === 'carried_over' ? ' (from project creation)' : '') : 'not answered'}`);
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
if (cmd === 'facts' && stage === 'set') {
|
|
162
|
+
const [key, ...val] = rest;
|
|
163
|
+
if (!key || !val.length) { console.error(USAGE); return 2; }
|
|
164
|
+
const d = await call('PUT', '/founding/facts', { facts: { [key]: parseFactValue(key, val) } }, 'save the fact');
|
|
165
|
+
if (!d) return 1;
|
|
166
|
+
console.log('Saved.');
|
|
167
|
+
for (const c of d.changed) {
|
|
168
|
+
for (const n of c.new_notes) console.log(` ${c.label}: now — ${n}`);
|
|
169
|
+
for (const n of c.gone_notes) console.log(` ${c.label}: no longer — ${n}`);
|
|
170
|
+
}
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
if (cmd === 'options') {
|
|
174
|
+
const d = await call('GET', '/founding/legalize/options', null, 'read the reward options');
|
|
175
|
+
if (!d) return 1;
|
|
176
|
+
console.log(d.markdown);
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
143
179
|
if (!stage) { console.error(USAGE); return 2; }
|
|
144
180
|
if (cmd === 'document') {
|
|
145
181
|
const d = await call('GET', `/founding/work/${enc(stage)}/document`, null, 'read the document');
|
|
@@ -214,4 +250,4 @@ if (require.main === module) {
|
|
|
214
250
|
.catch((err) => { console.error('fatal:', err && err.message ? err.message : err); cliExit(1); });
|
|
215
251
|
}
|
|
216
252
|
|
|
217
|
-
module.exports = { main, formatStage, parseAnswer };
|
|
253
|
+
module.exports = { main, formatStage, parseAnswer, parseFactValue };
|
package/scripts/bongos/init.js
CHANGED
|
@@ -545,6 +545,19 @@ function buildReadme(branding = {}) {
|
|
|
545
545
|
'',
|
|
546
546
|
'Requires Node >= 22 and Postgres installed locally (macOS/Linux; on Windows use WSL). If anything looks off, `bongos doctor` checks your setup and prints the fix.',
|
|
547
547
|
'',
|
|
548
|
+
// The project's sandbox (task 1005291, ADR 0406 §5): the one line that makes the app
|
|
549
|
+
// behave safely when it runs as its sandbox copy, and nothing at all on the live site.
|
|
550
|
+
"## Your app's sandbox",
|
|
551
|
+
'',
|
|
552
|
+
"If your app runs on Render, Bongos also runs a free sandbox copy of it that takes every finished change first. Put this at the top of a Node app so the sandbox only reads from payment, email and messaging providers, and shows a \"Sandbox\" band on every page:",
|
|
553
|
+
'',
|
|
554
|
+
'```js',
|
|
555
|
+
"const sandbox = require('@bongos/core/sandbox').install(); // does nothing on the live site",
|
|
556
|
+
'app.use(sandbox.middleware); // Express-style apps: the band, and /__bongos/sandbox',
|
|
557
|
+
'```',
|
|
558
|
+
'',
|
|
559
|
+
'Seed test data when `BONGOS_SANDBOX=1`, and give the sandbox test keys. If you lend a real key, lend a read-only one.',
|
|
560
|
+
'',
|
|
548
561
|
'## Where to look next',
|
|
549
562
|
'',
|
|
550
563
|
'- `CLAUDE.md` — how this project is built (the operating manual agents read first).',
|
|
@@ -330,6 +330,8 @@ function buildPackageJson({ version, sourceDeps, sourceOverrides }) {
|
|
|
330
330
|
'./bongos/api-path-404': './src/bongos/api-path-404.js',
|
|
331
331
|
'./module-loader/loader': './src/module-loader/loader.js',
|
|
332
332
|
'./modules': './src/modules.js',
|
|
333
|
+
// A project's app loads this to behave as its sandbox (task 1005291, ADR 0406 §5).
|
|
334
|
+
'./sandbox': './src/sandbox/index.js',
|
|
333
335
|
'./module-seams': './src/module-seams.js',
|
|
334
336
|
},
|
|
335
337
|
engines: { node: '>=22' },
|
|
@@ -112,10 +112,14 @@ async function standupSandbox(api, inst, host, deps) {
|
|
|
112
112
|
let svc = sb.serviceId ? await api.getService(sb.serviceId).catch(() => null) : null;
|
|
113
113
|
if (!svc) svc = await api.findServiceByName(name);
|
|
114
114
|
if (!svc) {
|
|
115
|
+
// The sandbox page's key (task 1005291): kept on the row, which only the owner's
|
|
116
|
+
// project page reads, and set on the sandbox, which checks it.
|
|
117
|
+
const pageToken = require('node:crypto').randomBytes(24).toString('hex');
|
|
115
118
|
const created = await api.createService(payload.sandboxServicePayload(inst, {
|
|
116
119
|
ownerId: api.ownerId, repoUrl: `https://github.com/${repo.owner}/${repo.name}`, region, build,
|
|
117
|
-
dbUrl: dbStage.state === 'ready' ? dbStage.url : null,
|
|
120
|
+
dbUrl: dbStage.state === 'ready' ? dbStage.url : null, pageToken,
|
|
118
121
|
}));
|
|
122
|
+
await recordSandbox(db, inst.id, { pageKey: pageToken });
|
|
119
123
|
svc = created.service;
|
|
120
124
|
log(` [render] created sandbox service ${svc.id} (free, follows main)`);
|
|
121
125
|
}
|
|
@@ -87,11 +87,14 @@ function renderServicePayload(inst, { ownerId, repoUrl, branch, webPlan, region,
|
|
|
87
87
|
// with BONGOS_SANDBOX=1 so the app (and the starter's outbound gate, task 1005291) knows
|
|
88
88
|
// it is not the live site. `dbUrl` is the sandbox's OWN database, never the live one.
|
|
89
89
|
const SANDBOX_SUFFIX = 'sandbox';
|
|
90
|
-
|
|
90
|
+
// `pageToken` is the sandbox's own page key: its /__bongos/sandbox shows what was refused
|
|
91
|
+
// and which real keys are lent only to a visitor who carries it (the owner, via their
|
|
92
|
+
// owner-only project page). Not a credential to anything else (task 1005291).
|
|
93
|
+
function sandboxServicePayload(inst, { ownerId, repoUrl, region, build, dbUrl, pageToken }) {
|
|
91
94
|
const p = renderServicePayload(inst, { ownerId, repoUrl, branch: 'main', webPlan: 'free', region, build, dbUrl });
|
|
92
95
|
p.name = resourceName(inst, SANDBOX_SUFFIX);
|
|
93
96
|
p.autoDeploy = 'yes';
|
|
94
|
-
p.envVars = [{ key: 'BONGOS_SANDBOX', value: '1' }, ...p.envVars];
|
|
97
|
+
p.envVars = [{ key: 'BONGOS_SANDBOX', value: '1' }, ...(pageToken ? [{ key: 'BONGOS_SANDBOX_PAGE_KEY', value: pageToken }] : []), ...p.envVars];
|
|
95
98
|
return p;
|
|
96
99
|
}
|
|
97
100
|
|