@bongos/core 1.25.20 → 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 +213 -78
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +18 -0
- package/clients/bongos-client/index.cjs +18 -0
- package/clients/bongos-client/index.d.ts +34 -8
- package/clients/bongos-client/index.mjs +18 -0
- package/docs/adr/0406-every-project-gets-a-sandbox-that-follows-main-and-live-moves-only-when-a-person-promotes.md +18 -0
- package/docs/adr/0407-a-project-grant-is-made-by-the-owner-not-witnessed-at-sign-in.md +3 -1
- package/docs/api/openapi.json +693 -125
- package/docs/api-reference.md +13 -4
- package/docs/copy-inventory.md +432 -427
- package/docs/copy-registry.json +571 -526
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +350 -344
- package/modules/government/board.js +8 -7
- package/modules/government/charter-advice.js +10 -3
- package/modules/government/charter.js +88 -4
- package/modules/government/migrations/government_039_vote_cast_via_filed.sql +22 -0
- package/modules/government/routes/government.js +12 -12
- package/modules/government/sole-member.js +75 -0
- package/modules/hall-ui/public/genesis-home.js +12 -5
- package/modules/lifecycle/kickoff-checklist.js +1 -1
- package/modules/lifecycle/routes/pay-type-gates.js +7 -2
- package/modules/onboarding/genesis-home.js +14 -2
- package/modules/onboarding/project-facts.js +318 -0
- package/modules/onboarding/routes/genesis-work.js +16 -0
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/project-standing.js +49 -0
- package/modules/platform-identity/routes/my-projects.js +8 -0
- package/modules/provisioning/grants.js +126 -0
- package/modules/provisioning/migrations/provisioning_038_render_promote.sql +43 -0
- package/modules/provisioning/migrations/provisioning_039_project_grants.sql +40 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/project-facts.js +55 -0
- package/modules/provisioning/project-refusal.js +107 -0
- package/modules/provisioning/provisioning.js +6 -3
- package/modules/provisioning/render-lookup.js +36 -1
- package/modules/provisioning/render-promote.js +80 -0
- package/modules/provisioning/render-sandbox.js +6 -0
- package/modules/provisioning/route-helpers.js +70 -16
- package/modules/provisioning/routes/env-manifest.js +21 -5
- package/modules/provisioning/routes/grants.js +141 -0
- package/modules/provisioning/routes/provisioning.js +8 -7
- package/modules/provisioning/routes/render-standup.js +82 -3
- package/modules/public-landing/public/projects.html +290 -32
- package/package-lock.json +2 -2
- package/package.json +2 -1
- package/release-notes.json +32 -0
- package/scripts/bongos/genesis.js +49 -3
- package/scripts/bongos/government-walk-sole.js +41 -0
- package/scripts/bongos/government-walk.js +15 -16
- package/scripts/bongos/init.js +13 -0
- package/scripts/bongos/package-core.js +2 -0
- package/scripts/bongos/provision-render-common.js +48 -0
- package/scripts/bongos/provision-render-promote.js +93 -0
- package/scripts/bongos/provision-render-sandbox.js +19 -3
- package/scripts/bongos/provision-render.js +4 -36
- package/scripts/bongos/provision.js +1 -0
- package/scripts/bongos/render-api.js +17 -2
- package/scripts/bongos/render-payload.js +5 -2
- package/scripts/gds/government-walk-sole.js +6 -0
- package/scripts/gds/provision-render-common.js +6 -0
- package/scripts/gds/provision-render-promote.js +6 -0
- 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/fleet_cost_view.mjs +2 -0
- package/tests/folder_upload_project.mjs +396 -0
- package/tests/genesis_government.mjs +5 -1
- package/tests/genesis_home.mjs +17 -0
- package/tests/genesis_work_routes.mjs +25 -1
- package/tests/government_charter_advice.mjs +49 -0
- package/tests/government_charter_forms.mjs +14 -3
- package/tests/government_seed.mjs +1 -0
- package/tests/government_sole_member.mjs +153 -0
- package/tests/project_catalog_routes.mjs +2 -0
- 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_env_manifest.mjs +4 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +2 -0
- package/tests/provisioning_project_grants.mjs +458 -0
- package/tests/provisioning_project_owner_gate.mjs +3 -1
- package/tests/provisioning_project_refusal.mjs +316 -0
- package/tests/provisioning_render_route.mjs +1 -1
- package/tests/provisioning_restart.mjs +2 -0
- package/tests/provisioning_settings.mjs +2 -0
- package/tests/provisioning_settings_apply.mjs +1 -1
- package/tests/provisioning_settings_env.mjs +2 -2
- package/tests/provisioning_teardown_intent.mjs +2 -2
- package/tests/public_landing_projects.mjs +27 -5
- package/tests/render_promote.mjs +309 -0
- package/tests/render_sandbox.mjs +16 -8
- 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>
|
|
@@ -2313,6 +2337,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2313
2337
|
}
|
|
2314
2338
|
if (app.links) v.root.appendChild(appPara([appLink(app.links.service, 'Open it on Render')]));
|
|
2315
2339
|
sandboxView(data.sandbox, v);
|
|
2340
|
+
promoteView(data.promote, data.sandbox, v);
|
|
2316
2341
|
var vars = (data.env_vars || []).filter(function (x) { return !x.set_for_you; });
|
|
2317
2342
|
if (vars.length) {
|
|
2318
2343
|
v.root.appendChild(appEl('h3', null, 'Still to set on Render'));
|
|
@@ -2356,6 +2381,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2356
2381
|
line.push('. It sleeps when nobody visits and takes about a minute to wake.');
|
|
2357
2382
|
box.appendChild(appPara(line));
|
|
2358
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'));
|
|
2359
2387
|
}
|
|
2360
2388
|
if (sb.database && sb.database.note) box.appendChild(appPara([String(sb.database.note)], 'finePrint'));
|
|
2361
2389
|
if (sb.can_add) {
|
|
@@ -2380,8 +2408,60 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2380
2408
|
}
|
|
2381
2409
|
v.root.appendChild(box);
|
|
2382
2410
|
}
|
|
2411
|
+
/* ── promote: a person moves what ran on the sandbox to the live app (task 1005290, ADR 0406) ──
|
|
2412
|
+
The look at what is waiting and the press each take the owner's Render key, held in this
|
|
2413
|
+
closure and the field for the two requests and then dropped. The second press is the
|
|
2414
|
+
confirm: it names the change that goes live. Nothing here moves the live app by itself. */
|
|
2415
|
+
function promoteView(p, sb, v) {
|
|
2416
|
+
if (!p || !sb || ['none', 'requested'].indexOf(sb.state) >= 0) return;
|
|
2417
|
+
var box = appEl('div', 'appPromote');
|
|
2418
|
+
box.appendChild(appEl('h3', null, 'Going live'));
|
|
2419
|
+
box.appendChild(appPara([p.live_follows_main
|
|
2420
|
+
? 'Your live app still takes every finished change by itself. It stops once your sandbox is running or you promote for the first time.'
|
|
2421
|
+
: 'Your live app changes only when you promote. Look at your sandbox first, then promote what you are happy with.'], 'finePrint'));
|
|
2422
|
+
var last = p.last;
|
|
2423
|
+
if (last && last.state === 'requested') box.appendChild(appPara(['Promoting ' + (last.commit || 'your change') + ' to your live app…']));
|
|
2424
|
+
else if (last && last.state === 'sent') box.appendChild(appPara(['Sent ' + (last.commit || 'your change') + ' to your live app. Render is building it; that usually takes a few minutes.']));
|
|
2425
|
+
else if (last && last.state === 'failed') box.appendChild(appPara([String(last.error || 'The last promotion did not go through.')], 'err'));
|
|
2426
|
+
if (p.can) {
|
|
2427
|
+
v.prBtn = appEl('button', 'btn ghost small appPrBtn', 'See what is waiting to go live');
|
|
2428
|
+
v.prBtn.type = 'button';
|
|
2429
|
+
box.appendChild(v.prBtn);
|
|
2430
|
+
v.prForm = appEl('div', 'appRetry appPrForm');
|
|
2431
|
+
v.prForm.hidden = true;
|
|
2432
|
+
var lab = appEl('label', 'wizField');
|
|
2433
|
+
lab.appendChild(appEl('span', null, 'Render API key'));
|
|
2434
|
+
v.prKey = appEl('input', 'appRetryKey');
|
|
2435
|
+
v.prKey.type = 'password'; v.prKey.autocomplete = 'off'; v.prKey.spellcheck = false; v.prKey.maxLength = 200; v.prKey.placeholder = 'rnd_…';
|
|
2436
|
+
lab.appendChild(v.prKey);
|
|
2437
|
+
v.prForm.appendChild(lab);
|
|
2438
|
+
v.prLook = appEl('button', 'btn small', 'Look');
|
|
2439
|
+
v.prLook.type = 'button';
|
|
2440
|
+
v.prForm.appendChild(v.prLook);
|
|
2441
|
+
v.prList = appEl('div', 'appPrList');
|
|
2442
|
+
v.prForm.appendChild(v.prList);
|
|
2443
|
+
v.prHint = appEl('div', 'hint appRetryHint', 'Your key is used for this look and the promotion, then thrown away.');
|
|
2444
|
+
v.prHint.setAttribute('role', 'status');
|
|
2445
|
+
v.prForm.appendChild(v.prHint);
|
|
2446
|
+
box.appendChild(v.prForm);
|
|
2447
|
+
} else if (p.reason && sb.state !== 'failed') {
|
|
2448
|
+
box.appendChild(appPara([String(p.reason)], 'finePrint'));
|
|
2449
|
+
}
|
|
2450
|
+
if (p.recent && p.recent.length) {
|
|
2451
|
+
box.appendChild(appEl('h4', null, 'Recent promotions'));
|
|
2452
|
+
var ul = appEl('ul', 'appPrRecent');
|
|
2453
|
+
p.recent.forEach(function (r) {
|
|
2454
|
+
var when = r.at ? new Date(r.at).toLocaleString() : '';
|
|
2455
|
+
var what = r.state === 'failed' ? ' — did not go through' : (r.state === 'started' ? ' — being sent' : '');
|
|
2456
|
+
ul.appendChild(appEl('li', null, when + ': ' + r.commit + (r.message ? ' — ' + r.message : '') + what));
|
|
2457
|
+
});
|
|
2458
|
+
box.appendChild(ul);
|
|
2459
|
+
}
|
|
2460
|
+
v.root.appendChild(box);
|
|
2461
|
+
}
|
|
2462
|
+
function promotePending(data) { var l = data && data.promote && data.promote.last; return !!(l && l.state === 'requested'); }
|
|
2383
2463
|
function sandboxSettled(sb) { return !sb || ['none', 'left', 'removed'].indexOf(sb.state) >= 0 || !!sb.error || sb.stage === 'live' || sb.stage === 'quiet'; }
|
|
2384
|
-
function appTerminal(data) { var a = data && data.app; return (!a || a.state === 'disconnected' || !!a.error || a.stage === 'live') && sandboxSettled(data && data.sandbox); }
|
|
2464
|
+
function appTerminal(data) { var a = data && data.app; return (!a || a.state === 'disconnected' || !!a.error || a.stage === 'live') && sandboxSettled(data && data.sandbox) && !promotePending(data); }
|
|
2385
2465
|
function fetchAppStatus(id) {
|
|
2386
2466
|
return fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/render', { credentials: 'same-origin' })
|
|
2387
2467
|
.then(function (res) { return res.ok ? res.json() : null; })
|
|
@@ -2416,6 +2496,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2416
2496
|
function tick() { fetchAppStatus(id).then(paint); }
|
|
2417
2497
|
function wire(v) {
|
|
2418
2498
|
if (v.sbBtn) wireSandbox(v);
|
|
2499
|
+
if (v.prBtn) wirePromote(v);
|
|
2419
2500
|
if (!v.retryBtn) return;
|
|
2420
2501
|
v.retryBtn.addEventListener('click', function () {
|
|
2421
2502
|
v.retryForm.hidden = false; v.retryBtn.hidden = true; v.keyEl.focus();
|
|
@@ -2445,6 +2526,50 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2445
2526
|
});
|
|
2446
2527
|
});
|
|
2447
2528
|
}
|
|
2529
|
+
function wirePromote(v) {
|
|
2530
|
+
var key = '';
|
|
2531
|
+
function hint(t, bad) { v.prHint.textContent = t; v.prHint.className = 'hint appRetryHint' + (bad ? ' err' : ''); }
|
|
2532
|
+
function send(path, body) {
|
|
2533
|
+
return fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + path, {
|
|
2534
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
2535
|
+
}).then(function (res) {
|
|
2536
|
+
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
2537
|
+
if (!res.ok) throw new Error((d.error && d.error.message) || 'That did not go through.');
|
|
2538
|
+
return d;
|
|
2539
|
+
});
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
function drop() { key = ''; v.prKey.value = ''; }
|
|
2543
|
+
v.prBtn.addEventListener('click', function () { v.prForm.hidden = false; v.prBtn.hidden = true; v.prKey.focus(); });
|
|
2544
|
+
v.prLook.addEventListener('click', function () {
|
|
2545
|
+
key = String(v.prKey.value || '').trim();
|
|
2546
|
+
if (!/^rnd_[A-Za-z0-9]{16,128}$/.test(key)) { key = ''; hint('That doesn’t look like a Render API key. It starts with “rnd_”.', true); return; }
|
|
2547
|
+
v.prLook.disabled = true; hint('Asking Render…');
|
|
2548
|
+
send('/render/promote/look', { key: key }).then(function (d) {
|
|
2549
|
+
v.prList.textContent = ''; v.prLook.disabled = false;
|
|
2550
|
+
v.prList.appendChild(appPara([d.live ? 'Live now: ' + d.live.short + (d.live.message ? ' — ' + d.live.message : '') : 'Nothing is live on your app yet.']));
|
|
2551
|
+
if (!d.waiting || !d.waiting.length) { drop(); hint('Nothing new is waiting: your live app already runs what your sandbox runs.'); return; }
|
|
2552
|
+
var ul = appEl('ul', 'appPrWaiting');
|
|
2553
|
+
d.waiting.forEach(function (w) { ul.appendChild(appEl('li', null, w.short + (w.message ? ' — ' + w.message : ''))); });
|
|
2554
|
+
v.prList.appendChild(appPara(['Waiting on your sandbox, newest first:']));
|
|
2555
|
+
v.prList.appendChild(ul);
|
|
2556
|
+
var go = appEl('button', 'btn small appPrGo', 'Promote ' + d.waiting[0].short + ' to my live app');
|
|
2557
|
+
go.type = 'button';
|
|
2558
|
+
v.prList.appendChild(go);
|
|
2559
|
+
hint('Promoting the newest brings everything listed above.');
|
|
2560
|
+
go.addEventListener('click', function () {
|
|
2561
|
+
if (!go.dataset.sure) { go.dataset.sure = '1'; go.textContent = 'Yes, put ' + d.waiting[0].short + ' live'; hint('Press again to confirm. Your live app will change.'); return; }
|
|
2562
|
+
go.disabled = true; hint('Sending…');
|
|
2563
|
+
var body = { key: key, commit_id: d.waiting[0].commit_id };
|
|
2564
|
+
if (d.live && d.live.commit_id) body.expect_live_commit = d.live.commit_id;
|
|
2565
|
+
send('/render/promote', body).then(function (r) {
|
|
2566
|
+
body.key = ''; drop(); hint(r.message || 'Queued.');
|
|
2567
|
+
v.prForm.hidden = true; view = null; clearTimeout(t); ticks = 0; tick();
|
|
2568
|
+
}).catch(function (err) { body.key = ''; drop(); go.disabled = false; hint(err.message + ' Enter your key again to retry.', true); });
|
|
2569
|
+
});
|
|
2570
|
+
}).catch(function (err) { drop(); v.prLook.disabled = false; hint(err.message, true); });
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2448
2573
|
function wireSandbox(v) {
|
|
2449
2574
|
v.sbBtn.addEventListener('click', function () { v.sbForm.hidden = false; v.sbBtn.hidden = true; v.sbKey.focus(); });
|
|
2450
2575
|
v.sbSend.addEventListener('click', function () {
|
|
@@ -5605,6 +5730,84 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5605
5730
|
}
|
|
5606
5731
|
$('ghNewName').addEventListener('input', function () { state.repo = this.value.trim(); updateFreshPreview(); saveDraft(); });
|
|
5607
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
|
+
|
|
5608
5811
|
/* ── whose GitHub account: mine or an organisation's (task 1004651) ──
|
|
5609
5812
|
Two pickers share this: the new repo's owner (part 02's Code row) and the app's
|
|
5610
5813
|
(part 03). The founder's organisations come from GET /github/orgs; GitHub may not
|
|
@@ -6492,6 +6695,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6492
6695
|
the organisation the founder picked, task 1004651). */
|
|
6493
6696
|
if (!validRepoName(state.repo)) return homeErr('Name the new repository (letters, numbers, and . _ -).');
|
|
6494
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());
|
|
6495
6699
|
} else {
|
|
6496
6700
|
/* Adopt (picker or manual) and the manual fallback: need a concrete owner + repo. */
|
|
6497
6701
|
if (!state.owner || !state.repo) {
|
|
@@ -6764,7 +6968,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6764
6968
|
if (key === 'code') {
|
|
6765
6969
|
var adopt = state.mode === 'adopt';
|
|
6766
6970
|
var where = state.owner && state.repo ? 'github.com/' + state.owner + '/' + state.repo : adopt ? 'Pick the repo to adopt.' : '';
|
|
6767
|
-
|
|
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' : '') };
|
|
6768
6973
|
}
|
|
6769
6974
|
if (key === 'address') {
|
|
6770
6975
|
var custom = state.domain && looksLikeDomain(state.domain);
|
|
@@ -6904,40 +7109,91 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6904
7109
|
/* the founder's own account sends exactly what it always did; an organisation
|
|
6905
7110
|
rides as `org`, and the server checks the name again (task 1004651) */
|
|
6906
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
|
+
}
|
|
6907
7123
|
return fetch(API + '/github/repos', {
|
|
6908
7124
|
method: 'POST', credentials: 'same-origin',
|
|
6909
7125
|
headers: { 'Content-Type': 'application/json' },
|
|
6910
7126
|
body: JSON.stringify(repoBody)
|
|
6911
|
-
}).then(
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
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;
|
|
6934
7183
|
}
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
state.
|
|
6940
|
-
|
|
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();
|
|
6941
7197
|
});
|
|
6942
7198
|
}
|
|
6943
7199
|
|
|
@@ -7005,7 +7261,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7005
7261
|
existing history instead of pushing a fresh tree, which a repo that already
|
|
7006
7262
|
has commits rejects as unrelated history. state.mode already tracked
|
|
7007
7263
|
greenfield|adopt for the UI — it has to reach the server too. */
|
|
7008
|
-
|
|
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',
|
|
7009
7267
|
/* the page filled itself in from the guide's plan (task 1005274): the hub
|
|
7010
7268
|
carries the scope answers and the hand-off file into the new project */
|
|
7011
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
|
@@ -11868,5 +11868,37 @@
|
|
|
11868
11868
|
"id": "1005274",
|
|
11869
11869
|
"text": "A founder who planned their project with the guide now finds the project form and the first Genesis question already filled in, plus a plan file for their own AI."
|
|
11870
11870
|
}
|
|
11871
|
+
],
|
|
11872
|
+
"1.25.21": [
|
|
11873
|
+
{
|
|
11874
|
+
"id": "1004162",
|
|
11875
|
+
"text": "Someone already connected to a hosted project who can't manage it is now told what to do next (apply to join, accept their invite, or ask the owner) instead of \"not found\"; strangers still learn nothing, and private projects s"
|
|
11876
|
+
},
|
|
11877
|
+
{
|
|
11878
|
+
"id": "1005130",
|
|
11879
|
+
"text": "When founding a project, the charter step now tells the founder the project is already a monarchy, what every form of government will ask of them day to day, and plainly when a choice changes nothing."
|
|
11880
|
+
},
|
|
11881
|
+
{
|
|
11882
|
+
"id": "1004161",
|
|
11883
|
+
"text": "A project owner can now give a named teammate access to help run their hosted project (view it, restart it, keep its settings list) and take it back; signing in alone still gives nobody access."
|
|
11884
|
+
},
|
|
11885
|
+
{
|
|
11886
|
+
"id": "1005290",
|
|
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
|
+
}
|
|
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
|
+
}
|
|
11871
11903
|
]
|
|
11872
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)' };
|
|
@@ -58,7 +61,17 @@ function formatStage(s) {
|
|
|
58
61
|
// Pure: the charter stage's recommendation from the founder's answers (task 1005119).
|
|
59
62
|
// A recommendation only: choosing is still the founder's.
|
|
60
63
|
function formatAdvice(a) {
|
|
61
|
-
const out = [
|
|
64
|
+
const out = [];
|
|
65
|
+
// task 1005130: what is in force now, and what each form asks of you day to day.
|
|
66
|
+
if (a.in_force) out.push(` In force now: ${a.in_force.line}`);
|
|
67
|
+
if (a.forms) {
|
|
68
|
+
out.push(' The forms you can choose:');
|
|
69
|
+
for (const f of a.forms.filter((x) => x.available)) {
|
|
70
|
+
out.push(` ${f.name}${f.current ? ' (in force now)' : ''}: ${f.who_decides} ${f.to_close}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
out.push(` Recommended government: ${a.charter.name}`, ` Why: ${a.charter.why}`);
|
|
74
|
+
if (a.charter.on_choose) out.push(` ${a.charter.on_choose.line}`);
|
|
62
75
|
if (a.charter.council_rank) out.push(` It is governed by a council rank you create: ${a.charter.council_rank}.`);
|
|
63
76
|
out.push(' It is a recommendation. Any form, or your own settings, is still yours to choose.');
|
|
64
77
|
if (a.preset) {
|
|
@@ -72,6 +85,15 @@ function formatAdvice(a) {
|
|
|
72
85
|
return out;
|
|
73
86
|
}
|
|
74
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
|
+
|
|
75
97
|
// Pure: parse `answer` arguments into the PUT body, or { error }.
|
|
76
98
|
function parseAnswer(rest) {
|
|
77
99
|
const args = [...rest];
|
|
@@ -130,6 +152,30 @@ async function main(argv) {
|
|
|
130
152
|
console.log(d.markdown || 'The founder has not written anything in genesis yet.');
|
|
131
153
|
return 0;
|
|
132
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
|
+
}
|
|
133
179
|
if (!stage) { console.error(USAGE); return 2; }
|
|
134
180
|
if (cmd === 'document') {
|
|
135
181
|
const d = await call('GET', `/founding/work/${enc(stage)}/document`, null, 'read the document');
|
|
@@ -204,4 +250,4 @@ if (require.main === module) {
|
|
|
204
250
|
.catch((err) => { console.error('fatal:', err && err.message ? err.message : err); cliExit(1); });
|
|
205
251
|
}
|
|
206
252
|
|
|
207
|
-
module.exports = { main, formatStage, parseAnswer };
|
|
253
|
+
module.exports = { main, formatStage, parseAnswer, parseFactValue };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// scripts/bongos/government-walk-sole.js — the government walk's board-of-one rows
|
|
3
|
+
// (task 1005130, part C). Its own file so government-walk.js stays under the size
|
|
4
|
+
// ratchet; the walk calls it with its own helpers, so the rows run through the same
|
|
5
|
+
// real routers and in-memory store as every other row.
|
|
6
|
+
//
|
|
7
|
+
// WHY. A board of one decides by filing: when the sole member of a first_ratifier
|
|
8
|
+
// board files an amendment, their yes is cast in the same call (sole-member.js).
|
|
9
|
+
// These rows prove the other side of that rule through the real routes: a lower rank
|
|
10
|
+
// is refused by the route's own permission gate before anything is filed, and a
|
|
11
|
+
// filing by a builder the board does not seat — a charter change and a rules change —
|
|
12
|
+
// waits for the board with no vote cast.
|
|
13
|
+
|
|
14
|
+
// What a filing decided in the same call, when a board of one decided it.
|
|
15
|
+
const decidedOf = (r) => (r.json && r.json.decided) || {};
|
|
16
|
+
|
|
17
|
+
// The genesis pick was adopted by filing: the item passed, and the vote that passed it
|
|
18
|
+
// is the founder's own, recorded as cast by filing.
|
|
19
|
+
function adoptedByFiling({ store, stateOf, itemId, voterId }) {
|
|
20
|
+
const via = (store.votes.find((v) => v.item_id === String(itemId) && v.voter_id === String(voterId)) || {}).cast_via || 'none';
|
|
21
|
+
return { got: `${stateOf(itemId)}; the founder's yes, cast by ${via === 'filed' ? 'filing' : via}`, pass: stateOf(itemId) === 'passed' && via === 'filed' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function soleMemberRows({ check, call, vote, code, store, stateOf, itemIdOf, ids: { EVE, BEN, FOUNDER_ID }, SHORT_LAW }) {
|
|
25
|
+
await check('A builder without the right to propose is refused before anything is filed', '403; nothing filed', async () => {
|
|
26
|
+
const before = store.items.length;
|
|
27
|
+
const r = await call(EVE, 'POST', '/government/board/laws', { enact: [SHORT_LAW] });
|
|
28
|
+
return { got: `${r.status}; ${store.items.length === before ? 'nothing filed' : 'a sitting was opened'}`, pass: r.status === 403 && store.items.length === before };
|
|
29
|
+
});
|
|
30
|
+
await check('A charter and a rules change filed by a builder the board does not seat wait for the board', 'both sitting open; no vote cast', async () => {
|
|
31
|
+
const c = await call(BEN, 'POST', '/government/board/charter', { form: 'direct-democracy' });
|
|
32
|
+
const d = await call(BEN, 'POST', '/government/board/decision-rules', { rules: [{ area: 'review', decided_by: 'rank:council', rule: 'majority' }] });
|
|
33
|
+
const items = [itemIdOf(c), itemIdOf(d)];
|
|
34
|
+
const open = items.every((id) => id && stateOf(id) === 'open');
|
|
35
|
+
const cast = store.votes.filter((v) => items.includes(v.item_id)).length;
|
|
36
|
+
for (const id of items) if (id) await vote(FOUNDER_ID, id, 'no'); // the board returns both, so nothing is left standing
|
|
37
|
+
return { got: `${open ? 'both sitting open' : `${c.status} ${code(c)} ${stateOf(items[0])}, ${d.status} ${code(d)} ${stateOf(items[1])}`}; ${cast ? `${cast} vote cast` : 'no vote cast'}`, pass: open && cast === 0 };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { decidedOf, adoptedByFiling, soleMemberRows };
|