@bongos/core 1.18.0 → 1.18.3

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.
Files changed (113) hide show
  1. package/.bongos-core.json +132 -322
  2. package/README.md +1 -1
  3. package/config/branding.neutral.json +5 -1
  4. package/docs/adr/0142-project-context-is-prose-only.md +41 -0
  5. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  6. package/docs/adr/README.md +5 -2
  7. package/docs/api/openapi.json +1789 -450
  8. package/docs/api-reference.md +52 -5
  9. package/docs/file-map.md +7 -7
  10. package/docs/recipes/packaging-the-core.md +13 -0
  11. package/docs/recipes/upgrading-the-core.md +5 -2
  12. package/migrations/039_newcomer_friendly_column.sql +5 -0
  13. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  14. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  15. package/modules/hall-ui/public/my-projects.html +78 -0
  16. package/modules/hall-ui/public/my-projects.js +142 -0
  17. package/modules/hall-ui/public/scouting.html +45 -0
  18. package/modules/hall-ui/public/scouting.js +73 -0
  19. package/modules/status-ui/public/explore.html +54 -0
  20. package/modules/status-ui/public/explore.js +67 -0
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/public-landing/create.html +211 -14
  24. package/scripts/gds/cli-lib.js +11 -5
  25. package/scripts/gds/lint-migrations.js +102 -0
  26. package/scripts/gds/login.js +42 -10
  27. package/scripts/gds/mirror-redact.js +5 -0
  28. package/scripts/gds/package-core.js +263 -4
  29. package/scripts/gds/provision.js +145 -7
  30. package/scripts/gds/publish-manifest.js +15 -0
  31. package/scripts/gds/run-unit-tests.js +38 -14
  32. package/scripts/gds/setup.js +41 -0
  33. package/scripts/gds/ship.js +115 -31
  34. package/scripts/gds/upgrade.js +147 -5
  35. package/scripts/migrate.sh +10 -0
  36. package/src/bongos/auth.js +362 -5
  37. package/src/bongos/db.js +66 -0
  38. package/src/bongos/github-repos.js +93 -0
  39. package/src/bongos/routes/auth.js +93 -11
  40. package/src/bongos/routes/github.js +90 -0
  41. package/src/bongos/routes/instance.js +15 -3
  42. package/src/bongos/routes.js +2 -0
  43. package/src/branding.js +17 -2
  44. package/src/instance-config.js +16 -0
  45. package/src/module-api.js +29 -1
  46. package/tests/branding.mjs +29 -23
  47. package/tests/github_repos_helpers.mjs +91 -0
  48. package/tests/github_token_store.mjs +121 -0
  49. package/tests/idp_device_exchange.mjs +105 -0
  50. package/tests/idp_federation.mjs +100 -0
  51. package/tests/migrations_forward_safe.mjs +86 -0
  52. package/tests/module_api.mjs +1 -1
  53. package/tests/package_core.mjs +246 -1
  54. package/tests/provision.mjs +157 -0
  55. package/tests/pruned_lockfile_integrity.mjs +79 -0
  56. package/tests/ship_ci_deploy.mjs +60 -0
  57. package/tests/upgrade.mjs +254 -5
  58. package/.gitignore +0 -91
  59. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
  60. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
  61. package/scripts/hall-preview/fixtures/access-requests.json +0 -20
  62. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  63. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  64. package/scripts/hall-preview/fixtures/audit-log.json +0 -68
  65. package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
  66. package/scripts/hall-preview/fixtures/box__me.json +0 -5
  67. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
  68. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  69. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  70. package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
  71. package/scripts/hall-preview/fixtures/goals.json +0 -502
  72. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  73. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  74. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  75. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  76. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  77. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  78. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  79. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  80. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
  81. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  82. package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
  83. package/scripts/hall-preview/fixtures/instance.json +0 -124
  84. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  85. package/scripts/hall-preview/fixtures/me.json +0 -476
  86. package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
  87. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
  88. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  89. package/scripts/hall-preview/fixtures/me__render.json +0 -5
  90. package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
  91. package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
  92. package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
  93. package/scripts/hall-preview/fixtures/override-requests.json +0 -15
  94. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  95. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  96. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  97. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  98. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  99. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  100. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  101. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  102. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  103. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  104. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  105. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  106. package/scripts/hall-preview/fixtures/security__docs.json +0 -10
  107. package/scripts/hall-preview/fixtures/security__reports.json +0 -31
  108. package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
  109. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  110. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  111. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  112. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  113. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -0,0 +1,54 @@
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">
6
+ <title>Explore — featured projects</title>
7
+ <style>
8
+ /* Self-contained so the showcase renders correctly whether or not the host
9
+ injects theme tokens. Warm Cloud Bongos defaults; overridable by the pack. */
10
+ :root {
11
+ --bg: #fff7ed; --card: #ffffff; --ink: #2a1f17; --ink-soft: #6b5d52;
12
+ --rule: #fed7aa; --accent: #ea580c;
13
+ }
14
+ * { box-sizing: border-box; }
15
+ body {
16
+ margin: 0; padding: 2.5rem 1.25rem 4rem;
17
+ background: var(--bg); color: var(--ink);
18
+ font-family: "Quicksand", system-ui, -apple-system, "Segoe UI", sans-serif;
19
+ line-height: 1.5;
20
+ }
21
+ .wrap { max-width: 960px; margin: 0 auto; }
22
+ header { text-align: center; margin-bottom: 2.5rem; }
23
+ h1 { font-size: 2rem; margin: 0 0 .35rem; letter-spacing: .01em; }
24
+ .sub { color: var(--ink-soft); margin: 0; }
25
+ .grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
26
+ .card {
27
+ background: var(--card); border: 1px solid var(--rule); border-radius: 14px;
28
+ padding: 1.1rem 1.2rem; text-decoration: none; color: inherit;
29
+ display: flex; flex-direction: column; gap: .4rem; transition: transform .12s ease, box-shadow .12s ease;
30
+ }
31
+ .card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(234,88,12,.12); }
32
+ .card h2 { font-size: 1.15rem; margin: 0; }
33
+ .card p { margin: 0; color: var(--ink-soft); font-size: .95rem; }
34
+ .card .origin { color: var(--accent); font-size: .82rem; word-break: break-all; margin-top: auto; }
35
+ .card .art { width: 100%; height: 120px; object-fit: cover; border-radius: 10px; margin-bottom: .3rem; }
36
+ .empty, .err { text-align: center; color: var(--ink-soft); padding: 3rem 1rem; }
37
+ @media (prefers-color-scheme: dark) {
38
+ :root { --bg: #1a1410; --card: #241c16; --ink: #f5ece3; --ink-soft: #b9a998; --rule: #4a3728; }
39
+ }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ <div class="wrap">
44
+ <header>
45
+ <h1>Explore</h1>
46
+ <p class="sub">Featured projects built on the platform.</p>
47
+ </header>
48
+ <div id="grid" class="grid" hidden></div>
49
+ <div id="empty" class="empty" hidden>No featured projects yet — check back soon.</div>
50
+ <div id="err" class="err" hidden>Couldn't load projects right now.</div>
51
+ </div>
52
+ <script src="/explore.js"></script>
53
+ </body>
54
+ </html>
@@ -0,0 +1,67 @@
1
+ // explore.js — the public Explore showcase (ADR 0141 §5, task #1002110). Fetches the
2
+ // hub's featured projects and renders cards. On a NON-hub instance (platform-identity
3
+ // off) the endpoint 404s / returns nothing, so the page shows a graceful empty state.
4
+ //
5
+ // Project fields come from the network, so every value is set via textContent /
6
+ // attribute assignment — never innerHTML — so a project name/tagline can't inject markup.
7
+ (function () {
8
+ 'use strict';
9
+ var grid = document.getElementById('grid');
10
+ var empty = document.getElementById('empty');
11
+ var err = document.getElementById('err');
12
+
13
+ function show(el) { if (el) el.hidden = false; }
14
+
15
+ // Only http(s) origins are linkified — never javascript:/data: etc.
16
+ function safeHref(origin) {
17
+ try {
18
+ var u = new URL(String(origin));
19
+ return (u.protocol === 'https:' || u.protocol === 'http:') ? u.toString() : null;
20
+ } catch (_) { return null; }
21
+ }
22
+
23
+ function card(p) {
24
+ var href = safeHref(p.origin);
25
+ var el = document.createElement(href ? 'a' : 'div');
26
+ el.className = 'card';
27
+ if (href) { el.href = href; el.target = '_blank'; el.rel = 'noopener noreferrer'; }
28
+
29
+ if (p.art_url && safeHref(p.art_url)) {
30
+ var img = document.createElement('img');
31
+ img.className = 'art';
32
+ img.src = p.art_url;
33
+ img.alt = '';
34
+ img.loading = 'lazy';
35
+ el.appendChild(img);
36
+ }
37
+ var h = document.createElement('h2');
38
+ h.textContent = p.name || p.origin || 'Untitled project';
39
+ el.appendChild(h);
40
+ if (p.tagline) {
41
+ var tag = document.createElement('p');
42
+ tag.textContent = p.tagline;
43
+ el.appendChild(tag);
44
+ }
45
+ if (p.origin) {
46
+ var o = document.createElement('span');
47
+ o.className = 'origin';
48
+ o.textContent = String(p.origin).replace(/^https?:\/\//, '');
49
+ el.appendChild(o);
50
+ }
51
+ return el;
52
+ }
53
+
54
+ fetch('/api/gds/projects/featured', { headers: { Accept: 'application/json' } })
55
+ .then(function (r) { if (!r.ok) throw new Error('http ' + r.status); return r.json(); })
56
+ .then(function (data) {
57
+ var projects = (data && Array.isArray(data.projects)) ? data.projects : [];
58
+ if (projects.length === 0) { show(empty); return; }
59
+ projects.forEach(function (p) { grid.appendChild(card(p)); });
60
+ show(grid);
61
+ })
62
+ .catch(function () {
63
+ // A missing endpoint (non-hub instance) is not an error to the visitor — show
64
+ // the empty state rather than a scary failure.
65
+ show(empty);
66
+ });
67
+ }());
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.18.0",
3
+ "version": "1.18.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.18.0",
9
+ "version": "1.18.3",
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.18.0",
3
+ "version": "1.18.3",
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",
@@ -29,6 +29,13 @@
29
29
  --font-mono:"Fira Code",ui-monospace,SFMono-Regular,Menlo,monospace;
30
30
  }
31
31
  *{box-sizing:border-box;margin:0;padding:0;}
32
+ /* The `hidden` attribute must always win. Several blocks below set an explicit
33
+ `display` via a class (.field→block, .row2→grid); an author rule like that
34
+ silently defeats the user-agent [hidden] rule, so `el.hidden = true` fails to
35
+ hide them. The Home step (task 1002228) toggles el.hidden to switch between the
36
+ adopt-picker / fresh-name / manual sub-panels — without this they ALL render at
37
+ once. Make hidden authoritative for the whole page. */
38
+ [hidden]{display:none !important;}
32
39
  html{scroll-behavior:smooth;}
33
40
  body{
34
41
  font-family:var(--font-body); color:var(--ink);
@@ -327,6 +334,16 @@ input[type=text].bad{border-color:var(--err);}
327
334
  @media (prefers-reduced-motion:reduce){
328
335
  *,*::before,*::after{animation:none !important;transition:none !important;}
329
336
  }
337
+ /* GitHub repo picker (task 1002099) */
338
+ .linklike{background:none;border:0;padding:0;margin-left:12px;color:var(--accent-deep);font:inherit;font-size:13px;cursor:pointer;text-decoration:underline;}
339
+ #ghConnect .btn{margin-top:4px;}
340
+ .ghlist{margin-top:8px;max-height:220px;overflow-y:auto;border:1px solid var(--line,#e7d8c4);border-radius:10px;background:var(--bg-deep,#fff);}
341
+ .ghlist:empty{display:none;}
342
+ .ghrow{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:none;border:0;border-bottom:1px solid rgba(42,31,23,0.06);padding:9px 12px;font:inherit;font-size:13.5px;color:var(--ink,#2a1f17);cursor:pointer;}
343
+ .ghrow:last-child{border-bottom:0;}
344
+ .ghrow:hover{background:#fff9f2;}
345
+ .ghrow[aria-selected=true]{background:#fff3e6;box-shadow:inset 3px 0 0 var(--accent,#f97316);font-weight:600;}
346
+ .ghrow .priv{margin-left:auto;font-size:11px;font-weight:600;color:var(--ink-faint,#9b8a78);background:rgba(42,31,23,0.06);border-radius:5px;padding:1px 6px;}
330
347
  </style>
331
348
  </head>
332
349
  <body>
@@ -405,7 +422,31 @@ input[type=text].bad{border-color:var(--err);}
405
422
  </button>
406
423
  </div>
407
424
  <div class="reassure" id="adoptNote">Adopting never overwrites your files: you get a conflict pre-flight first, nothing is written until you accept, and your existing backlog (issues, TODOs) is imported as real tasks.</div>
408
- <div class="row2">
425
+
426
+ <!-- GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099.
427
+ Progressive enhancement: when the signed-in user grants repo access we let
428
+ them PICK a repo (adopt) or NAME one we auto-create (fresh). Without that
429
+ access — not granted yet, or the server can't store the token — we fall back
430
+ to the manual owner/repo inputs below (#manualHome), which stay authoritative. -->
431
+ <div id="ghConnect" class="reassure">
432
+ <p id="ghConnectMsg">&nbsp;</p>
433
+ <button type="button" class="btn" id="ghConnectBtn">Connect GitHub</button>
434
+ <button type="button" class="linklike" id="ghManualToggle">or enter it manually</button>
435
+ </div>
436
+ <div id="ghBusy" class="hint" hidden>Reading your GitHub repositories…</div>
437
+ <div id="ghAdopt" class="field" hidden>
438
+ <span>Pick one of your repositories</span>
439
+ <input type="text" id="ghSearch" placeholder="Search your repos…" autocomplete="off">
440
+ <div id="ghList" class="ghlist" role="listbox" aria-label="Your repositories"></div>
441
+ <div class="hint" id="ghAdoptHint">&nbsp;</div>
442
+ </div>
443
+ <div id="ghFresh" class="field" hidden>
444
+ <span>New repository name</span>
445
+ <input type="text" id="ghNewName" placeholder="mercury" autocomplete="off" maxlength="100">
446
+ <div class="hint" id="ghFreshHint">We’ll create an empty repo at <code id="ghFreshPreview" style="font-family:var(--font-mono)">github.com/you/mercury</code>.</div>
447
+ </div>
448
+
449
+ <div class="row2" id="manualHome">
409
450
  <label class="field"><span>GitHub owner <small>(user or org)</small></span>
410
451
  <input type="text" id="f-owner" placeholder="acme" autocomplete="off" maxlength="60">
411
452
  </label>
@@ -595,6 +636,7 @@ input[type=text].bad{border-color:var(--err);}
595
636
  saveDraft();
596
637
  var h = $(panels[step]).querySelector('h1');
597
638
  if (h) { h.setAttribute('tabindex', '-1'); h.focus({ preventScroll: false }); }
639
+ if (step === 2) { syncHome(); ensureRepoAccess(); }
598
640
  if (step === 4) renderReview();
599
641
  }
600
642
 
@@ -641,11 +683,108 @@ input[type=text].bad{border-color:var(--err);}
641
683
  $('ghCreate').classList.toggle('on', mode === 'greenfield');
642
684
  updateGhCreateHref();
643
685
  saveDraft();
686
+ syncHome();
644
687
  }
645
688
  $('c-green').addEventListener('click', function () { setMode('greenfield'); });
646
689
  $('c-adopt').addEventListener('click', function () { setMode('adopt'); });
647
690
  setMode(state.mode);
648
691
 
692
+ /* ── GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099 / ADR 0143.
693
+ Progressive enhancement: probe GET /github/repos once we reach step 2.
694
+ 200 → we hold repo access → show the picker (adopt) / name field (fresh);
695
+ 403 → offer "Connect GitHub" (a ?repos=1 re-sign-in);
696
+ 503 (server can't store the token) or any error → the manual inputs.
697
+ #manualHome (the classic owner/repo fields) is the always-safe fallback. ── */
698
+ var repoAccess = null; // null=unknown/loading · true · 'need-connect' · 'unavailable' · 'manual'
699
+ var repoList = []; // the user's repos (from the probe) — feeds the picker + the fresh-name clash hint
700
+ var ghProbed = false;
701
+
702
+ function isManualHome() { return repoAccess === 'manual' || repoAccess === 'unavailable'; }
703
+ function validRepoName(s) { return /^[A-Za-z0-9._-]{1,100}$/.test(String(s || '')); }
704
+ function myLogin() { return (me && me.github_login) || 'you'; }
705
+
706
+ function connectRepos(e) {
707
+ if (e) e.preventDefault();
708
+ // Same-tab navigation so the sessionStorage draft round-trips back to step 2.
709
+ window.location.assign(API + '/auth/web/start?return=/create&repos=1');
710
+ }
711
+ $('ghConnectBtn').addEventListener('click', connectRepos);
712
+ $('ghManualToggle').addEventListener('click', function () { repoAccess = 'manual'; syncHome(); $('f-owner').focus(); });
713
+
714
+ function syncHome() {
715
+ var need = repoAccess === 'need-connect';
716
+ var ok = repoAccess === true;
717
+ var adopt = state.mode === 'adopt';
718
+ $('ghBusy').hidden = !!repoAccess; // busy only while still probing (null)
719
+ $('ghConnect').classList.toggle('on', need); // .reassure box: shown via .on (not hidden — .reassure sets display:none)
720
+ $('ghAdopt').hidden = !(ok && adopt);
721
+ $('ghFresh').hidden = !(ok && !adopt);
722
+ $('manualHome').hidden = !isManualHome();
723
+ if (need) {
724
+ $('ghConnectMsg').textContent = adopt
725
+ ? 'Connect your GitHub account and pick the repository to adopt.'
726
+ : 'Connect your GitHub account and we’ll create the empty repo for you.';
727
+ }
728
+ if (ok && adopt) renderRepoList();
729
+ if (ok && !adopt) syncFresh();
730
+ }
731
+
732
+ function syncFresh() {
733
+ // Fresh + access: the repo is created under the signed-in account. Seed owner for
734
+ // the review/target now; the create response confirms the real full_name.
735
+ state.owner = myLogin();
736
+ $('ghNewName').value = state.repo || '';
737
+ updateFreshPreview();
738
+ }
739
+ function updateFreshPreview() {
740
+ var hint = $('ghFreshHint');
741
+ var clash = state.repo && repoList.some(function (r) { return String(r.name).toLowerCase() === state.repo.toLowerCase(); });
742
+ if (state.repo && !validRepoName(state.repo)) {
743
+ hint.className = 'hint err'; hint.textContent = 'Use letters, numbers, and . _ - (1–100 chars).';
744
+ } else if (clash) {
745
+ hint.className = 'hint err'; hint.innerHTML = 'You already have a repo named <strong>' + esc(state.repo) + '</strong> — pick another name, or adopt it instead.';
746
+ } else {
747
+ hint.className = 'hint'; hint.innerHTML = 'We’ll create an empty repo at <code style="font-family:var(--font-mono)">github.com/' + esc(myLogin()) + '/' + esc(state.repo || 'name') + '</code>.';
748
+ }
749
+ }
750
+ $('ghNewName').addEventListener('input', function () { state.repo = this.value.trim(); updateFreshPreview(); saveDraft(); });
751
+
752
+ function renderRepoList() {
753
+ var q = ($('ghSearch').value || '').trim().toLowerCase();
754
+ var rows = repoList.filter(function (r) { return !q || String(r.full_name).toLowerCase().indexOf(q) >= 0; }).slice(0, 100);
755
+ var sel = state.owner && state.repo ? (state.owner + '/' + state.repo) : '';
756
+ $('ghList').innerHTML = rows.length
757
+ ? rows.map(function (r) {
758
+ return '<button type="button" class="ghrow" role="option" data-full="' + esc(r.full_name) + '" aria-selected="' + (r.full_name === sel) + '">' +
759
+ '<span>' + esc(r.full_name) + '</span>' + (r.private ? '<span class="priv">Private</span>' : '') + '</button>';
760
+ }).join('')
761
+ : '<div class="hint" style="padding:10px 12px">No repositories match “' + esc(q) + '”.</div>';
762
+ }
763
+ $('ghSearch').addEventListener('input', renderRepoList);
764
+ $('ghList').addEventListener('click', function (ev) {
765
+ var row = ev.target.closest('.ghrow'); if (!row) return;
766
+ var full = row.getAttribute('data-full'); var slash = full.indexOf('/');
767
+ state.owner = full.slice(0, slash); state.repo = full.slice(slash + 1);
768
+ saveDraft();
769
+ Array.prototype.forEach.call($('ghList').querySelectorAll('.ghrow'), function (b) { b.setAttribute('aria-selected', String(b === row)); });
770
+ var h = $('ghAdoptHint'); h.className = 'hint ok'; h.innerHTML = 'Adopting <strong>' + esc(full) + '</strong>.';
771
+ });
772
+
773
+ function ensureRepoAccess() {
774
+ if (ghProbed && repoAccess !== null) return; // already known this session
775
+ if (isManualHome()) return; // user chose manual — don't nag
776
+ repoAccess = null; syncHome(); // show "reading your repos…"
777
+ fetch(API + '/github/repos', { credentials: 'same-origin' })
778
+ .then(function (res) {
779
+ ghProbed = true;
780
+ if (res.status === 200) return res.json().then(function (d) { repoList = (d && d.repos) || []; repoAccess = true; });
781
+ if (res.status === 403 || res.status === 401) { repoAccess = 'need-connect'; return; }
782
+ repoAccess = 'unavailable'; return; // 503 or anything else → manual fallback
783
+ })
784
+ .catch(function () { ghProbed = true; repoAccess = 'unavailable'; })
785
+ .then(syncHome);
786
+ }
787
+
649
788
  $('f-owner').addEventListener('input', function () { state.owner = this.value.trim(); state.ownerTouched = true; updateGhCreateHref(); saveDraft(); });
650
789
  $('f-repo').addEventListener('input', function () { state.repo = this.value.trim(); state.repoTouched = true; updateGhCreateHref(); saveDraft(); });
651
790
 
@@ -675,7 +814,17 @@ input[type=text].bad{border-color:var(--err);}
675
814
  return true;
676
815
  }
677
816
  if (step === 2) {
678
- if (!state.owner || !state.repo) { $('h-home').textContent = 'Both owner and repository name are needed.'; $('h-home').className = 'hint err'; return false; }
817
+ var homeErr = function (msg) { $('h-home').textContent = msg; $('h-home').className = 'hint err'; return false; };
818
+ if (repoAccess === 'need-connect') return homeErr('Connect GitHub to pick or create your repository — or choose “enter it manually”.');
819
+ if (repoAccess === true && state.mode !== 'adopt') {
820
+ // Fresh with access: just the new repo NAME (owner is the signed-in account).
821
+ if (!validRepoName(state.repo)) return homeErr('Name the new repository (letters, numbers, and . _ -).');
822
+ } else {
823
+ // Adopt (picker or manual) and the manual fallback: need a concrete owner + repo.
824
+ if (!state.owner || !state.repo) {
825
+ return homeErr(state.mode === 'adopt' && repoAccess === true ? 'Pick one of your repositories.' : 'Both owner and repository name are needed.');
826
+ }
827
+ }
679
828
  $('h-home').innerHTML = '&nbsp;'; $('h-home').className = 'hint';
680
829
  return true;
681
830
  }
@@ -725,20 +874,58 @@ input[type=text].bad{border-color:var(--err);}
725
874
  return 'Couldn’t file the request: ' + (err && err.message ? err.message : 'something went sideways. Try again.');
726
875
  }
727
876
 
877
+ /* Fresh + repo access: auto-create the empty GitHub repo NOW (the commit moment),
878
+ then hand its real full_name to provisioning. No-op for adopt / manual / no-access.
879
+ Idempotent across a retry — once created we reuse the stashed full_name so a second
880
+ click doesn't trip GitHub's 422 "name already exists". Errors set err.gotoHome so
881
+ the caller bounces the user back to the Home step to fix the name / reconnect. */
882
+ function ensureFreshRepo() {
883
+ if (state.mode === 'adopt' || repoAccess !== true) return Promise.resolve();
884
+ if (state.repoAutoCreated && state.owner && state.repo) return Promise.resolve();
885
+ return fetch(API + '/github/repos', {
886
+ method: 'POST', credentials: 'same-origin',
887
+ headers: { 'Content-Type': 'application/json' },
888
+ body: JSON.stringify({ name: state.repo })
889
+ }).then(function (res) {
890
+ return res.json().catch(function () { return {}; }).then(function (data) {
891
+ if (res.status === 401 || res.status === 403) {
892
+ repoAccess = 'need-connect';
893
+ var e0 = new Error('reconnect'); e0.gotoHome = true;
894
+ e0.homeMsg = 'Your GitHub authorization expired — reconnect on the Home step to create the repo.';
895
+ throw e0;
896
+ }
897
+ if (!res.ok) {
898
+ var e = new Error((data.error && (data.error.message || data.error.code)) || ('HTTP ' + res.status));
899
+ e.data = data;
900
+ if (data.error && data.error.code === 'name_taken') { e.gotoHome = true; e.homeMsg = data.error.message; }
901
+ throw e;
902
+ }
903
+ var full = data.repo && data.repo.full_name;
904
+ if (full && full.indexOf('/') > 0) {
905
+ state.owner = full.slice(0, full.indexOf('/'));
906
+ state.repo = full.slice(full.indexOf('/') + 1);
907
+ }
908
+ state.repoAutoCreated = true; saveDraft();
909
+ });
910
+ });
911
+ }
912
+
728
913
  $('doCreate').addEventListener('click', function () {
729
914
  var btn = this;
730
915
  var alertBox = $('createAlert');
731
916
  alertBox.classList.remove('on');
732
917
  btn.disabled = true; btn.textContent = 'Raising it into the sky…';
733
- fetch(API + '/provisioning/instances', {
734
- method: 'POST', credentials: 'same-origin',
735
- headers: { 'Content-Type': 'application/json' },
736
- body: JSON.stringify({
737
- slug: state.slug,
738
- hosting_shape: 'standalone',
739
- domain: state.domain || undefined,
740
- target_ref: state.owner + '/' + state.repo
741
- })
918
+ ensureFreshRepo().then(function () {
919
+ return fetch(API + '/provisioning/instances', {
920
+ method: 'POST', credentials: 'same-origin',
921
+ headers: { 'Content-Type': 'application/json' },
922
+ body: JSON.stringify({
923
+ slug: state.slug,
924
+ hosting_shape: 'standalone',
925
+ domain: state.domain || undefined,
926
+ target_ref: state.owner + '/' + state.repo
927
+ })
928
+ });
742
929
  }).then(function (res) {
743
930
  if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/create'); throw new Error('signing in'); }
744
931
  return res.json().catch(function () { return {}; }).then(function (data) {
@@ -761,9 +948,17 @@ input[type=text].bad{border-color:var(--err);}
761
948
  startProgress(data.instance && data.instance.id);
762
949
  }).catch(function (err) {
763
950
  if (String(err && err.message) === 'signing in') return;
951
+ btn.disabled = false; btn.textContent = 'Create my project';
952
+ if (err && err.gotoHome) {
953
+ // A repo-creation problem (name taken / reconnect) — bounce back to the Home
954
+ // step with the reason, so the user fixes it where the repo is chosen.
955
+ show(2);
956
+ $('h-home').textContent = err.homeMsg || 'Please revisit the repository step.';
957
+ $('h-home').className = 'hint err';
958
+ return;
959
+ }
764
960
  alertBox.textContent = friendlyCreateError(err);
765
961
  alertBox.classList.add('on');
766
- btn.disabled = false; btn.textContent = 'Create my project';
767
962
  });
768
963
  });
769
964
 
@@ -786,8 +981,10 @@ input[type=text].bad{border-color:var(--err);}
786
981
  var ghIcon = '<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>';
787
982
  steps.push(adopt
788
983
  ? '<li data-onboard-step="github-repo"><b>Layer onto your existing repo</b><p>In your repo run <code style="font-family:var(--font-mono)">bongos init --adopt</code>. It shows a conflict pre-flight and writes <em>nothing</em> until you accept — your files and history stay yours, and your existing backlog is imported. Commit and push.</p></li>'
789
- : '<li data-onboard-step="github-repo"><b>Create the empty GitHub repo</b><p>Do this first — the build needs the repo to exist. Click below (GitHub opens with the name already filled in), press <strong>Create repository</strong>, and leave it completely empty. Seeing “repository not found” above? That just means the repo isn’t created yet — make it here.</p>' +
790
- '<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + ghIcon + 'Create ' + repoLabel + ' on GitHub</a></li>');
984
+ : (state.repoAutoCreated
985
+ ? '<li data-onboard-step="github-repo"><b>Your empty repo is ready</b><p>We created ' + repoLabel + ' on GitHub for you — empty and waiting. The scaffold lands there when the build runs; nothing for you to do here.</p></li>'
986
+ : '<li data-onboard-step="github-repo"><b>Create the empty GitHub repo</b><p>Do this first — the build needs the repo to exist. Click below (GitHub opens with the name already filled in), press <strong>Create repository</strong>, and leave it completely empty. Seeing “repository not found” above? That just means the repo isn’t created yet — make it here.</p>' +
987
+ '<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + ghIcon + 'Create ' + repoLabel + ' on GitHub</a></li>'));
791
988
  if (!state.domain) {
792
989
  steps.push('<li><b>Add a domain you own</b><p>Sign-in on your new instance needs a real domain. When you have one, come back here or ask your hall — the address gets wired in minutes.</p></li>');
793
990
  }
@@ -27,10 +27,16 @@ const SESSION_READ_PATHS = [
27
27
  ...ic.configReadPaths('gds-session.json'),
28
28
  ...ic.configReadPaths('pms-session.json'),
29
29
  ];
30
- // Default to live production. Override with <PREFIX>_API_BASE for local dev:
30
+ // The last-resort default API base — THIS instance's own publicOrigin from the
31
+ // branding pack (task 1002201). One source of truth in instance-config so no CLI
32
+ // script hard-codes another instance's URL; pre-ADR-0139 the literal
33
+ // 'https://amazonprimea.com' was baked in at four sites here, so an unconfigured
34
+ // consumer targeted the founder's (game-only) box. Override with <PREFIX>_API_BASE
35
+ // for local dev / staging:
31
36
  // CLOUDBONGOS_API_BASE=http://localhost:3000 node scripts/gds/start.js
32
37
  // Resolves the configured prefix first, then legacy GDS_/OTB_/PMS_API_BASE.
33
- const API_BASE = ic.resolveEnv('API_BASE') || 'https://amazonprimea.com';
38
+ const DEFAULT_API_BASE = ic.defaultApiBase();
39
+ const API_BASE = ic.resolveEnv('API_BASE') || DEFAULT_API_BASE;
34
40
 
35
41
  async function readSessionFile(p) {
36
42
  try {
@@ -102,7 +108,7 @@ function readSessionToken() {
102
108
  if (!j || !j.token) return null;
103
109
  return {
104
110
  token: j.token,
105
- apiBase: ic.resolveEnv('API_BASE') || j.api_base || 'https://amazonprimea.com',
111
+ apiBase: ic.resolveEnv('API_BASE') || j.api_base || DEFAULT_API_BASE,
106
112
  };
107
113
  }
108
114
 
@@ -278,7 +284,7 @@ async function apiCall(method, urlPath, body = null, opts = {}) {
278
284
  // (local dev / staging override), then the logged-in instance, then prod. OTB
279
285
  // sessions already store api_base='https://amazonprimea.com', so this is a no-op
280
286
  // for the OTB instance and only bites once someone logs in to a different one.
281
- const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || 'https://amazonprimea.com';
287
+ const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || DEFAULT_API_BASE;
282
288
  const headers = {
283
289
  'Content-Type': 'application/json',
284
290
  'User-Agent': 'amazonprimea-gds-cli/1.0',
@@ -349,7 +355,7 @@ async function cliClient(opts = {}) {
349
355
  printInstanceMismatchAndExit(mm);
350
356
  }
351
357
  }
352
- const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || 'https://amazonprimea.com';
358
+ const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || DEFAULT_API_BASE;
353
359
  const token = (session && session.token) || null;
354
360
  const key = `${base}|${token || ''}|${allowUnauthenticated}|${allowBoxScope}`;
355
361
  if (_clientCache.has(key)) return _clientCache.get(key);
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // scripts/gds/lint-migrations.js — core-migration FORWARD-SAFETY guard (task 1002223 / audit H10,
4
+ // finding F6).
5
+ //
6
+ // `bongos upgrade` auto-rollback reverts the CODE pin but DELIBERATELY NOT the database
7
+ // (upgrade.js: "the DB is deliberately NOT rolled back … core migrations are additive + forward-
8
+ // safe"). That safety rested on a convention nothing checked. A core migration is FORWARD-SAFE when
9
+ // the PREVIOUS code version still runs against the post-migration schema — so a code-only rollback
10
+ // never strands old code on a schema it can't read. This lints core migrations for operations that
11
+ // BREAK that property and fails unless the migration explicitly acknowledges the exception with a
12
+ // -- forward-unsafe: <reason>
13
+ // pragma (a deliberate, documented decision — e.g. a historical migration, or one paired with a
14
+ // two-step deploy).
15
+ //
16
+ // SAFE (never flagged): CREATE TABLE/INDEX/…, ADD COLUMN, INSERT/UPDATE, and DROP-then-CREATE of an
17
+ // INDEX / TRIGGER / VIEW / FUNCTION / POLICY (the recreate-in-place idempotency pattern) — none of
18
+ // these remove state the old code depends on.
19
+ // UNSAFE (flagged): DROP TABLE, DROP COLUMN, RENAME COLUMN, ALTER TABLE … RENAME TO, ALTER COLUMN …
20
+ // TYPE, SET NOT NULL — each removes / renames / retypes existing state old code reads.
21
+ //
22
+ // Usage:
23
+ // node scripts/gds/lint-migrations.js # STRICT: exit 1 on any unacknowledged unsafe migration
24
+ // node scripts/gds/lint-migrations.js --warn # report only, always exit 0 (migrate.sh uses this)
25
+ // node scripts/gds/lint-migrations.js --dir <path> # scan a specific migrations dir (default: <repo>/migrations)
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+
30
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
31
+
32
+ // The acknowledgement pragma. Lives in a SQL comment; matched on the raw file text.
33
+ const ACK_RE = /^\s*--\s*forward-unsafe\s*:\s*(.+)$/im;
34
+
35
+ // Genuinely forward-unsafe statement shapes. Ordered; a file matching ANY is unsafe.
36
+ const UNSAFE = [
37
+ ['drop-table', /\bdrop\s+table\b/i],
38
+ ['drop-column', /\bdrop\s+column\b/i],
39
+ ['rename-column', /\brename\s+column\b/i],
40
+ ['rename-table', /\balter\s+table\s+[^\s;]+\s+rename\s+to\b/i],
41
+ ['alter-column-type', /\balter\s+column\b[^;]*\btype\b/i],
42
+ ['set-not-null', /\bset\s+not\s+null\b/i],
43
+ ];
44
+
45
+ // Strip block + line comments so a pattern that appears only in prose/comments never counts as an
46
+ // operation. (The ACK pragma is detected separately, on the raw text, since it IS a comment.)
47
+ function stripSqlComments(sql) {
48
+ return String(sql).replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/--[^\n]*/g, ' ');
49
+ }
50
+
51
+ // Classify one migration's SQL: { unsafe: [names], acknowledged: bool, ackReason }.
52
+ function scanMigrationSql(sql) {
53
+ const code = stripSqlComments(sql);
54
+ const unsafe = [];
55
+ for (const [name, re] of UNSAFE) if (re.test(code)) unsafe.push(name);
56
+ const ack = String(sql).match(ACK_RE);
57
+ return { unsafe, acknowledged: !!ack, ackReason: ack ? ack[1].trim() : null };
58
+ }
59
+
60
+ // Scan a migrations directory. Returns { violations, acknowledged } — each entry { file, unsafe, ackReason }.
61
+ // `violations` = unsafe WITHOUT the pragma (fail-closed); `acknowledged` = unsafe WITH it (documented).
62
+ function scanMigrationsDir(dir, fsImpl = fs) {
63
+ let files;
64
+ try { files = fsImpl.readdirSync(dir).filter((f) => f.endsWith('.sql')); }
65
+ catch { return { violations: [], acknowledged: [], scanned: 0 }; }
66
+ const violations = [];
67
+ const acknowledged = [];
68
+ for (const f of files.sort()) {
69
+ const r = scanMigrationSql(fsImpl.readFileSync(path.join(dir, f), 'utf8'));
70
+ if (r.unsafe.length === 0) continue;
71
+ (r.acknowledged ? acknowledged : violations).push({ file: f, unsafe: r.unsafe, ackReason: r.ackReason });
72
+ }
73
+ return { violations, acknowledged, scanned: files.length };
74
+ }
75
+
76
+ function arg(name, fallback = null) {
77
+ const i = process.argv.indexOf(name);
78
+ return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : fallback;
79
+ }
80
+
81
+ function main() {
82
+ const warnOnly = process.argv.includes('--warn');
83
+ const dir = path.resolve(arg('--dir', path.join(REPO_ROOT, 'migrations')));
84
+ const { violations, acknowledged, scanned } = scanMigrationsDir(dir);
85
+
86
+ for (const a of acknowledged) {
87
+ console.error(`[migrations] forward-unsafe (acknowledged): ${a.file} [${a.unsafe.join(', ')}] — ${a.ackReason}`);
88
+ }
89
+ if (violations.length === 0) {
90
+ console.error(`[migrations] forward-safety: OK — ${scanned} core migration(s) scanned, ${acknowledged.length} acknowledged exception(s), 0 unacknowledged.`);
91
+ return 0;
92
+ }
93
+ console.error(`[migrations] forward-safety: ${warnOnly ? 'WARNING' : 'FAILED'} — ${violations.length} core migration(s) contain a forward-UNSAFE operation with no acknowledgement:`);
94
+ for (const v of violations) console.error(` ${v.file} [${v.unsafe.join(', ')}]`);
95
+ console.error(' A code-only `bongos upgrade` rollback does NOT revert the DB, so old code would run on this schema.');
96
+ console.error(' If intentional, add a top-of-file SQL comment: -- forward-unsafe: <why it is safe here / the two-step plan>');
97
+ return warnOnly ? 0 : 1;
98
+ }
99
+
100
+ module.exports = { scanMigrationSql, scanMigrationsDir, stripSqlComments, UNSAFE, ACK_RE };
101
+
102
+ if (require.main === module) process.exit(main());