@bongos/core 1.21.93 → 1.21.95

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 (37) hide show
  1. package/.bongos-core.json +63 -38
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +6 -0
  4. package/clients/bongos-client/index.cjs +6 -0
  5. package/clients/bongos-client/index.d.ts +9 -2
  6. package/clients/bongos-client/index.mjs +6 -0
  7. package/docs/api/openapi.json +112 -9
  8. package/docs/api-reference.md +6 -3
  9. package/docs/copy-inventory.md +354 -354
  10. package/docs/copy-registry.json +420 -420
  11. package/docs/design/project-startup-direction.md +15 -0
  12. package/docs/module-api-changelog.md +4 -0
  13. package/docs/page-readings.json +963 -992
  14. package/modules/provisioning/github-setup.js +129 -0
  15. package/modules/provisioning/module.json +2 -1
  16. package/modules/provisioning/provisioning.js +3 -0
  17. package/modules/provisioning/routes/callback-page.js +83 -8
  18. package/modules/provisioning/routes/github-setup.js +221 -0
  19. package/modules/provisioning/routes/provisioning.js +11 -45
  20. package/modules/public-landing/public/projects.html +207 -66
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/release-notes.json +12 -0
  24. package/scripts/gds/oauth-secret.js +4 -2
  25. package/scripts/gds/provision-github-setup.js +148 -0
  26. package/scripts/gds/provision.js +19 -5
  27. package/src/module-api.js +1 -1
  28. package/tests/github_handback_any_device.mjs +340 -0
  29. package/tests/github_one_trip.mjs +297 -0
  30. package/tests/projects_hub.mjs +6 -2
  31. package/tests/projects_hub_dns_ready.mjs +2 -1
  32. package/tests/projects_hub_pre_uat.mjs +17 -9
  33. package/tests/provision.mjs +51 -9
  34. package/tests/provisioning_callback_page.mjs +3 -2
  35. package/tests/provisioning_github_app_routes.mjs +103 -12
  36. package/tests/wizard_draft_resume.mjs +4 -0
  37. package/tests/wizard_github_feedback.mjs +9 -5
@@ -601,6 +601,8 @@ body:has(#view-new:not([hidden])) footer.foot{position:relative;z-index:1;backgr
601
601
  display:inline-flex;align-items:center;justify-content:center;font-family:var(--font-body);font-size:13px;letter-spacing:0;}
602
602
  #panel-done .humanSteps>li.sdone::before{content:"✓";text-transform:none;color:var(--ok);background:color-mix(in srgb,var(--ok) 12%,transparent);border:1px solid var(--ok);}
603
603
  #panel-done .humanSteps>li.slocked::before{border:1px solid var(--rule-soft);}
604
+ /* the optional step (command-line sign-in, task 1004649): a dashed ring until it ticks */
605
+ #panel-done .humanSteps>li.soptional:not(.sdone)::before{border-style:dashed;}
604
606
  #panel-done .humanSteps b{font-size:15px;line-height:24px;}
605
607
  .alive{margin-top:18px;padding-top:18px;border-top:1px solid var(--rule);}
606
608
  #view-new .aliveH{margin:0;font-family:var(--font-display);font-size:28px;font-weight:200;letter-spacing:-.02em;line-height:1.2;text-transform:none;color:var(--ink);}
@@ -4581,6 +4583,9 @@ summary{min-height:24px;padding:3px 0;}
4581
4583
  survives the GitHub round-trip; the server, not this page, names the terms version. */
4582
4584
  var DRAFT_KEY = 'cb-create-draft-v10';
4583
4585
  var CREATED_KEY = 'cb-created-instance';
4586
+ /* the project "Start another project" set down (task 1004650): the server's resume
4587
+ read does not pull this tab back to it */
4588
+ var DECLINED_KEY = 'cb-resume-declined';
4584
4589
  /* `description` + `teamShape` are the optional About step (task 1002334) —
4585
4590
  both legitimately stay empty, and empty is what the create request omits. */
4586
4591
  /* `modules` is the picker's answer (task 1002339) and is THREE-VALUED like
@@ -6603,7 +6608,7 @@ summary{min-height:24px;padding:3px 0;}
6603
6608
  return '<a href="' + (instanceId ? '?view=manage&id=' + encodeURIComponent(instanceId) : '?view=mine') +
6604
6609
  '">' + label + '</a>';
6605
6610
  }
6606
- var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null };
6611
+ var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, setup: null, installUrl: null };
6607
6612
  var planPromises = {}; /* mode|domain → Promise<steps[]> */
6608
6613
  var lastStepsHtml = '';
6609
6614
  var lastProgHtml = ''; /* same change-only contract, for the progress strip */
@@ -6632,6 +6637,9 @@ summary{min-height:24px;padding:3px 0;}
6632
6637
  (task 1002562) — the owner no longer runs `bongos init --adopt` here. */
6633
6638
  function renderStepGithubRepo() {
6634
6639
  var landed = live.instStatus === 'active';
6640
+ /* the one-page checklist (task 1004649) shows only the GitHub steps the founder
6641
+ takes; a repo the platform made or adopted needs nothing, so it is not a row */
6642
+ if (state.mode === 'adopt' || state.repoAutoCreated || landed) return '';
6635
6643
  var repoLabel = state.owner && state.repo ? '<strong>' + esc(state.owner + '/' + state.repo) + '</strong>' : 'your new repository';
6636
6644
  /* Naming a repo without linking it stranded the owner on exactly the path where the
6637
6645
  platform did the work (task 1002855). Adopt links the repo the owner picked;
@@ -6697,27 +6705,34 @@ summary{min-height:24px;padding:3px 0;}
6697
6705
  else window.location.assign(url);
6698
6706
  }
6699
6707
 
6708
+ /* The four GitHub steps of part 03, in the mockup's words and order (task 1004649,
6709
+ docs/design/mocks/day-night-rooms/): Approve the <name> app on GitHub · Install it
6710
+ on your repo · Builders can sign in · Command-line sign-in is on. One click starts
6711
+ ONE trip — approve, straight on to installing, back here — and every tick is what
6712
+ the server verified (live.setup, the status poll's `checklist`). Every value that
6713
+ reaches the markup goes through esc(). */
6714
+ function ghStep(key) { return live.setup && live.setup[key]; }
6715
+ function appTitle() { return 'Approve the ' + esc(state.name || state.slug || 'project') + ' app on GitHub'; }
6716
+
6700
6717
  /* oauth-app — the ADR 0133 one-click GitHub-App manifest flow is the primary path
6701
6718
  (gap 3 / idea 1000676); locked until a domain exists (the App needs the real
6702
6719
  callback host — the server refuses domainless with domain_required). */
6703
6720
  function renderStepOauthApp() {
6704
6721
  var domain = resolvedDomain();
6705
6722
  if (!domain) {
6706
- return '<li class="slocked" data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Unlocks once your project has an address — sign-in needs a real domain. Attach one any time from ' + projectPageLink('this project’s own page') + '.</p></li>';
6723
+ return '<li class="slocked" data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>Unlocks once your project has an address — sign-in needs a real domain. Attach one any time from ' + projectPageLink('this project’s own page') + '.</p></li>';
6724
+ }
6725
+ /* GitHub made the app and the platform placed its keys: the slug is recorded only
6726
+ after the placement (the runner), so a known slug outside a failure is the tick —
6727
+ a fact, whatever the venue says about starting a NEW trip */
6728
+ if (live.manifest !== 'error' && live.manifest !== 'expired' && (ghStep('approve') === 'done' || live.appSlug)) {
6729
+ return '<li class="sdone" data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>GitHub made the app, and its keys are on your project.</p></li>';
6707
6730
  }
6708
6731
  /* honesty about the venue (owner walk, 2026-08-12): GitHub rejects app
6709
6732
  manifests whose hook URL isn't publicly reachable, so a locally-hosted
6710
6733
  platform can't complete the one-click flow — say it BEFORE the bounce. */
6711
6734
  if (!apexBase()) {
6712
- return '<li class="slocked" data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>This platform is running on a local address, and GitHub refuses app setups it can’t reach over the public internet — so the one-click flow can’t complete here. On a publicly-addressed platform this is one click.</p>' + manualOauthDetails() + '</li>';
6713
- }
6714
- if (live.manifest === 'done') {
6715
- /* the wait ends HERE now, not on the callback page (task 1004548): once the
6716
- project answers too, the step carries the door itself */
6717
- var open = live.instStatus === 'active' && live.addrReady;
6718
- return '<li class="sdone" data-onboard-step="oauth-app"><b>GitHub sign-in is wired</b><p>Credentials placed and verified — sign-in on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code> is live.' +
6719
- (open ? '' : ' The door below opens once your project is answering.') + '</p>' +
6720
- (open ? '<a class="ghbtn" href="https://' + esc(domain) + '" target="_blank" rel="noopener">Open ' + esc(domain) + ' →</a>' : '') + '</li>';
6735
+ return '<li class="slocked" data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>This platform is running on a local address, and GitHub refuses app setups it can’t reach over the public internet — so the one-click flow can’t complete here. On a publicly-addressed platform this is one click.</p>' + manualOauthDetails() + '</li>';
6721
6736
  }
6722
6737
  if (live.manifest === 'error' || live.manifest === 'expired') {
6723
6738
  /* a plain sentence first; the raw route text folds under Technical
@@ -6731,86 +6746,98 @@ summary{min-height:24px;padding:3px 0;}
6731
6746
  else if (live.manifestCode === 'domain_required') snag = 'This project needs an address first — attach one from ' + projectPageLink('its own page') + ', then come back to this step.';
6732
6747
  else if (live.manifestFailure === 'sign_in_unconfirmed') {
6733
6748
  var appLink = live.appSlug ? '<a href="https://github.com/settings/apps/' + esc(live.appSlug) + '" target="_blank" rel="noopener">your app on GitHub</a>' : 'the app on your GitHub account';
6734
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>GitHub approved the app and the platform placed its credentials on your project, but your project didn’t confirm that sign-in works. Trying again would make a second app on your GitHub account and is unlikely to fix this — it needs a look from the platform team. <a href="/contact">Contact us</a> and mention ' + appLink + '.</p>' +
6749
+ return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>GitHub approved the app and the platform placed its credentials on your project, but your project didn’t confirm that sign-in works. Trying again would make a second app on your GitHub account and is unlikely to fix this — it needs a look from the platform team. <a href="/contact">Contact us</a> and mention ' + appLink + '.</p>' +
6735
6750
  progressMarkup(1, 1, 'Stopped: your project didn’t confirm sign-in.', 'isErr', 'signin') +
6736
6751
  '<button type="button" class="linklike" data-action="manifest-start">Try again anyway</button></li>';
6737
6752
  }
6738
6753
  else if (live.manifestFailure === 'finish_failed') snag = 'GitHub approved the app, but the platform couldn’t finish setting up sign-in on your project. The app GitHub made is still on your account — delete it first (GitHub → Settings → Developer settings → GitHub Apps) so you don’t end up with two, then try again.';
6739
6754
  else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
6740
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' + snag + '</p>' +
6755
+ return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>' + snag + '</p>' +
6741
6756
  (raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') +
6742
- '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
6757
+ '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Connect GitHub ↗</button>' + manualOauthDetails() + '</li>';
6743
6758
  }
6744
6759
  if (live.manifest === 'code_received' || live.manifest === 'converting') {
6745
- /* GitHub's half is done — say so, rather than keep telling the founder
6746
- to approve an app they just approved */
6760
+ /* GitHub's half of the approval is done — say so, rather than keep telling the
6761
+ founder to approve an app they just approved */
6747
6762
  autoOpenArmed = true;
6748
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>GitHub’s part is done. The platform is placing the credentials on your project now — this usually takes a few minutes, and this step ticks itself.</p>' +
6749
- progressMarkup(1, 1, 'Placing credentials and checking sign-in works…', '', 'signin') + autoOpenTick() + '</li>';
6763
+ return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>Approved on GitHub. The platform is placing the app’s keys on your project, and the GitHub tab goes on to installing it on your repo. This step ticks itself.</p>' +
6764
+ progressMarkup(1, 1, 'Placing the app’s keys on your project…', '', 'signin') + autoOpenTick() + '</li>';
6750
6765
  }
6751
6766
  if (live.manifest) { /* pending — GitHub holds the manifest, nobody has approved it yet */
6752
6767
  autoOpenArmed = true;
6753
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Waiting on GitHub — approve the app in the tab that opened. No GitHub tab? It may have opened behind this window or in another one. Once approved, the platform places the credentials and verifies sign-in on its own; this step ticks itself.</p>' +
6768
+ return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>Waiting on GitHub — approve the app in the tab that opened. No GitHub tab? It may have opened behind this window or in another one. Once approved, GitHub takes you straight on to installing it on your repo, then back here; every step ticks itself.</p>' +
6754
6769
  progressMarkup(0, 0, 'Waiting for your approval on GitHub…', '', 'signin') + autoOpenTick() +
6755
6770
  (manifestStale ? '<button type="button" class="linklike" data-action="manifest-start">Didn’t get to GitHub? Start the setup again</button>' : '') + '</li>';
6756
6771
  }
6757
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in — one click</b><p>Approve a pre-filled GitHub App and the platform wires sign-in by itself: no keys to copy, and the secret never leaves the server side.</p>' +
6758
- '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
6772
+ return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>One trip to GitHub: approve the app, install it on your repo, and you land back here. No keys to copy, and the secret never leaves the server side.</p>' +
6773
+ '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Connect GitHub ↗</button>' + manualOauthDetails() + '</li>';
6759
6774
  }
6760
6775
 
6761
- /* device-flow — the one-checkbox repair for terminal sign-in (task 1003051):
6762
- GitHub creates a manifest-born App with Device Flow OFF and offers no API to
6763
- enable it, so `bongos setup` in a terminal is refused until the owner ticks
6764
- the box. Web sign-in works either way; the CLI also has a web-token fallback. */
6776
+ /* device-flow — "Command-line sign-in is on", the OPTIONAL fourth step (task 1004649).
6777
+ GitHub creates a manifest-born App with Device Flow OFF and offers no manifest
6778
+ field and no API to switch it on (the spike in task 1004649), so only the founder's
6779
+ own tick in the app's settings can — and the runner ticks this step when GitHub
6780
+ starts a device sign-in for the app. It never holds the project back: web sign-in
6781
+ works without it, and the CLI falls back to a web token meanwhile. */
6765
6782
  function renderStepDeviceFlow() {
6783
+ var title = '<b>Command-line sign-in is on</b>';
6784
+ if (ghStep('device_flow') === 'done') {
6785
+ return '<li class="sdone soptional" data-onboard-step="device-flow">' + title + '<p>Builders can sign in from a terminal too — the <code style="font-family:var(--font-mono)">bongos setup</code> command builders run on their own machine.</p></li>';
6786
+ }
6766
6787
  if (live.manifest !== 'done') {
6767
- return '<li class="slocked" data-onboard-step="device-flow"><b>Let builders sign in from a command line — one checkbox</b><p>Unlocks once GitHub sign-in above is wired. It lets builders sign in from a terminal — the <code style="font-family:var(--font-mono)">bongos setup</code> command builders run on their own machine; web sign-in works without it.</p></li>';
6788
+ return '<li class="slocked soptional" data-onboard-step="device-flow">' + title + '<p>Optional — turn it on later. It lets builders sign in from a terminal, with the <code style="font-family:var(--font-mono)">bongos setup</code> command they run on their own machine; web sign-in works without it.</p></li>';
6768
6789
  }
6769
6790
  var appUrl = live.appSlug ? 'https://github.com/settings/apps/' + esc(live.appSlug) : null;
6770
- return '<li data-onboard-step="device-flow"><b>Let builders sign in from a command line — one checkbox</b><p>' +
6791
+ return '<li class="soptional" data-onboard-step="device-flow">' + title + '<p>Optional — turn it on later. GitHub leaves it off on new apps and lets only you switch it on: ' +
6771
6792
  (appUrl
6772
- ? 'Open <a href="' + appUrl + '" target="_blank" rel="noopener">your app’s settings</a>, check <strong>Enable Device Flow</strong>, and save. '
6793
+ ? 'open <a href="' + appUrl + '" target="_blank" rel="noopener">your app’s settings</a>, check <strong>Enable Device Flow</strong>, and save. '
6773
6794
  : 'GitHub → Settings → Developer settings → GitHub Apps → your app → check <strong>Enable Device Flow</strong> → save. ') +
6774
- 'GitHub leaves it off on new apps and only offers it as this checkbox — until it’s ticked, terminal sign-in falls back to a web token from the hall’s Settings page.</p></li>';
6795
+ 'We tick this when GitHub has it on. Until then builders sign in on the web, and terminal sign-in falls back to a web token from the hall’s Settings page.</p></li>';
6775
6796
  }
6776
6797
 
6777
- /* app-install — the ADR 0055 publish credential (task 1003049): the sign-in App
6778
- doubles as it, but only an INSTALLED App can mint push tokens; without this
6779
- one click every finished task waits at confirmed for a manual merge. */
6798
+ /* app-install — "Install it on your repo": the ADR 0055 publish credential (task
6799
+ 1003049). The same trip carries the founder here straight from the approval (task
6800
+ 1004649); this step ticks only once the runner has asked GitHub, with the app's own
6801
+ key, and GitHub said the app is on this repo. The button is the way back for a
6802
+ founder who returned without installing. */
6780
6803
  function renderStepAppInstall() {
6781
- if (live.manifest !== 'done') {
6782
- return '<li class="slocked" data-onboard-step="app-install"><b>Install the GitHub App on your repo</b><p>Unlocks once GitHub sign-in above is wired — the same app, installed on your repo, is what lets finished work merge and ship itself.</p></li>';
6804
+ var title = '<b>Install it on your repo</b>';
6805
+ if (ghStep('install') === 'done') {
6806
+ return '<li class="sdone" data-onboard-step="app-install">' + title + '<p>The app is on ' + (state.owner && state.repo ? esc(state.owner + '/' + state.repo) : 'your repo') + ', so finished work can merge and ship itself.</p></li>';
6783
6807
  }
6784
- var installUrl = live.appSlug ? 'https://github.com/apps/' + esc(live.appSlug) + '/installations/new' : null;
6785
- return '<li data-onboard-step="app-install"><b>Install the GitHub App on your repo</b><p>One click: install the app you just approved on this project’s repo. Without it, finished work waits for a manual merge instead of shipping itself.</p>' +
6786
- (installUrl
6787
- ? '<a class="ghbtn" href="' + installUrl + '" target="_blank" rel="noopener">' + GH_ICON + 'Install the app on your repo</a>'
6788
- : '<p>GitHub → Settings → Developer settings → GitHub Apps → your app → <strong>Install App</strong> → pick this project’s repo.</p>') +
6789
- '</li>';
6808
+ if (!live.appSlug) {
6809
+ return '<li class="slocked" data-onboard-step="app-install">' + title + '<p>Comes straight after the approval, on the same trip: pick your repo and you land back here.</p></li>';
6810
+ }
6811
+ var installUrl = live.installUrl || ('https://github.com/apps/' + encodeURIComponent(live.appSlug) + '/installations/new');
6812
+ return '<li data-onboard-step="app-install">' + title + '<p>Your GitHub tab goes here next. We check with GitHub that the app is on your repo, and this step ticks itself. Came back without installing it?</p>' +
6813
+ '<a class="ghbtn" href="' + esc(installUrl) + '" target="_blank" rel="noopener">' + GH_ICON + 'Install it on your repo ↗</a></li>';
6790
6814
  }
6791
6815
 
6816
+ /* first-sign-in — "Builders can sign in": ticks when the platform has seen sign-in
6817
+ work on the project (the runner's auth_configured check, the manifest 'done'). The
6818
+ door opens only when the project ANSWERS too: a link to an address that does not
6819
+ resolve yet is the worst first impression a founder can get (task 1003232). */
6792
6820
  function renderStepFirstSignIn() {
6793
6821
  var domain = resolvedDomain();
6822
+ var title = '<b>Builders can sign in</b>';
6794
6823
  if (!domain) {
6795
- return '<li class="slocked" data-onboard-step="first-sign-in"><b>First sign-in</b><p>Unlocks with sign-in above — you’ll open your project’s address, sign in with GitHub once, and be seated as the owner (the founding Archon).</p></li>';
6824
+ return '<li class="slocked" data-onboard-step="first-sign-in">' + title + '<p>Unlocks with an address — then you open it, sign in with GitHub once, and take the owner’s seat (the founding Archon).</p></li>';
6796
6825
  }
6797
- /* the door opens only when the project ANSWERS and sign-in is wired: a
6798
- link to an address that does not resolve yet is the worst first
6799
- impression a founder can get of their own project (task 1003232) */
6800
- if (live.instStatus !== 'active' || !live.addrReady || live.manifest !== 'done') {
6801
- return '<li class="slocked" data-onboard-step="first-sign-in"><b>First sign-in</b><p>Unlocks once your project is answering and sign-in above is wired. Then you’ll open <code style="font-family:var(--font-mono)">' + esc(domain) + '</code>, sign in with GitHub once, and be seated as the owner — the founding Archon.</p></li>';
6826
+ if (live.manifest !== 'done') {
6827
+ return '<li class="slocked" data-onboard-step="first-sign-in">' + title + '<p>Ticks once the platform has seen sign-in work on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code>.</p></li>';
6802
6828
  }
6803
- return '<li data-onboard-step="first-sign-in"><b>First sign-in</b><p>Open <a href="https://' + esc(domain) + '" target="_blank" rel="noopener"><code style="font-family:var(--font-mono)">' + esc(domain) + '</code></a> and sign in with GitHub once. That seats you as the owner — the founding Archon.</p></li>';
6829
+ var open = live.instStatus === 'active' && live.addrReady;
6830
+ return '<li class="sdone" data-onboard-step="first-sign-in">' + title + '<p>Sign-in works on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code>.' +
6831
+ (open ? ' Open it and sign in with GitHub once to take the owner’s seat — the founding Archon.' : ' The door opens here once your project is answering.') + '</p>' +
6832
+ (open ? '<a class="ghbtn" href="https://' + esc(domain) + '" target="_blank" rel="noopener">Open ' + esc(domain) + ' →</a>' : '') + '</li>';
6804
6833
  }
6805
6834
 
6835
+ /* enter-hall — not a row of the GitHub checklist (the "is alive" block below carries
6836
+ the way in); kept only for a project with no address, whose hall is local */
6806
6837
  function renderStepEnterHall() {
6807
6838
  var domain = resolvedDomain();
6808
- if (domain && (live.instStatus !== 'active' || !live.addrReady || live.manifest !== 'done')) {
6809
- return '<li class="slocked" data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>Unlocks with the first sign-in — your project’s home will be <code style="font-family:var(--font-mono)">' + esc(domain) + '/builders</code>.</p></li>';
6810
- }
6811
- return domain
6812
- ? '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>Your project’s home — open <a href="https://' + esc(domain) + '/builders" target="_blank" rel="noopener"><code style="font-family:var(--font-mono)">' + esc(domain) + '/builders</code></a> and start building.</p></li>'
6813
- : '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>No address yet — run <code style="font-family:var(--font-mono)">bongos dev</code> in your repo checkout to start the local hall on your machine.</p></li>';
6839
+ if (domain) return '';
6840
+ return '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>No address yet — run <code style="font-family:var(--font-mono)">bongos dev</code> in your repo checkout to start the local hall on your machine (on an address it lives at /builders).</p></li>';
6814
6841
  }
6815
6842
 
6816
6843
  var STEP_RENDERERS = {
@@ -6828,9 +6855,13 @@ summary{min-height:24px;padding:3px 0;}
6828
6855
  control-plane wizard cannot reach. Spun out per the direction's §9
6829
6856
  delivery-split as task 1002739. */
6830
6857
 
6858
+ /* the mockup's order: the repo (only when the founder must make it), approve,
6859
+ install, sign-in, command line; a plan step this page has no row for goes last */
6860
+ var STEP_ORDER = ['github-repo', 'oauth-app', 'app-install', 'first-sign-in', 'device-flow', 'enter-hall'];
6861
+ function stepRank(s) { var i = STEP_ORDER.indexOf(s.key); return i < 0 ? STEP_ORDER.length : i; }
6831
6862
  function renderSteps() {
6832
6863
  fetchPlan().then(function (steps) {
6833
- var html = steps.filter(function (s) { return s && s.kind === 'human'; }).map(function (s) {
6864
+ var html = steps.filter(function (s) { return s && s.kind === 'human'; }).slice().sort(function (a, b) { return stepRank(a) - stepRank(b); }).map(function (s) {
6834
6865
  var r = STEP_RENDERERS[s.key];
6835
6866
  return r ? r(s) : '<li data-onboard-step="' + esc(s.key) + '"><b>' + esc(s.label || s.key) + '</b></li>';
6836
6867
  }).join('');
@@ -6898,18 +6929,36 @@ summary{min-height:24px;padding:3px 0;}
6898
6929
  try { localStorage.setItem(AUTO_OPEN_KEY, box.checked ? '1' : '0'); } catch (e) {}
6899
6930
  });
6900
6931
 
6901
- /* ── C8: the wizard's polls run only on stage, exactly like the sky.
6902
- Off-view or hidden-tab, a due tick parks itself instead of fetching;
6903
- enter() and the visibility flip resume whatever was parked. ── */
6932
+ /* ── C8: the wizard's polls run only while the create view is on stage, like the
6933
+ sky. Off-view a due tick parks itself instead of fetching; enter() resumes it.
6934
+ A HIDDEN tab is different (task 1004650): that is exactly when the founder is on
6935
+ GitHub, in another tab or on their phone, so the polls keep checking there at a
6936
+ slower pace (HIDDEN_POLL_MS), and the moment the tab is seen again a waiting tick
6937
+ runs at once — the laptop's checklist ticks on its own. ── */
6938
+ var HIDDEN_POLL_MS = 20000;
6904
6939
  function wizPollActive() {
6905
- return !document.hidden && currentView === 'new';
6940
+ return currentView === 'new';
6941
+ }
6942
+ /* the next tick's wait: the poll's own pace while the tab is seen, the slow one while hidden */
6943
+ function wizPollDelay(ms) {
6944
+ return document.hidden ? Math.max(ms, HIDDEN_POLL_MS) : ms;
6906
6945
  }
6907
6946
  function resumePolls() {
6908
6947
  if (watchPending && instanceId) { watchPending = false; startProgress(instanceId); }
6909
6948
  if (manifestPending && manifestState) { manifestPending = false; pollManifest(manifestState); }
6910
6949
  }
6950
+ /* each poll's waiting tick, so a return to the tab can run it now instead of later.
6951
+ Only a WAITING tick is run (its timer is set): one mid-fetch re-arms itself, and
6952
+ running it again would start a second chain. */
6953
+ var progressKick = null, manifestKick = null;
6954
+ function kickPolls() {
6955
+ if (pollTimer && progressKick) { clearTimeout(pollTimer); pollTimer = null; progressKick(); }
6956
+ if (manifestPollTimer && manifestKick) { clearTimeout(manifestPollTimer); manifestPollTimer = null; manifestKick(); }
6957
+ }
6911
6958
  document.addEventListener('visibilitychange', function () {
6912
- if (wizPollActive()) resumePolls();
6959
+ if (document.hidden || !wizPollActive()) return;
6960
+ resumePolls();
6961
+ kickPolls();
6913
6962
  });
6914
6963
 
6915
6964
  /* ── GitHub-App manifest flow (ADR 0133): mint a one-time state for OUR instance,
@@ -6929,6 +6978,17 @@ summary{min-height:24px;padding:3px 0;}
6929
6978
  var handbackState = (function () {
6930
6979
  try { var m = /[#&]ghstate=([0-9a-f]{16,128})/i.exec(location.hash || ''); return m ? m[1] : null; } catch (e) { return null; }
6931
6980
  })();
6981
+ /* …and the PROJECT it is for (task 1004650): the return route resolves the state on
6982
+ the server and lands its owner on ?view=new&id=<project>, so a phone, another
6983
+ window or a fresh tab reaches the right project's part 03 with nothing in this
6984
+ tab's storage. Read once at load, for the same reason as the state above. */
6985
+ var handbackProject = (function () {
6986
+ try {
6987
+ var q = new URLSearchParams(location.search || '');
6988
+ var id = q.get('view') === 'new' ? q.get('id') : null;
6989
+ return id && /^\d{1,12}$/.test(id) ? id : null;
6990
+ } catch (e) { return null; }
6991
+ })();
6932
6992
  /* the manifest leg survives a reload: token + last status ride the CREATED
6933
6993
  record, so enter() re-seeds the step instead of re-offering "Set up
6934
6994
  GitHub sign-in" for an app the founder already approved (task 1003232) */
@@ -6991,6 +7051,22 @@ summary{min-height:24px;padding:3px 0;}
6991
7051
  function manifestInFlight() {
6992
7052
  return live.manifest === 'pending' || live.manifest === 'code_received' || live.manifest === 'converting';
6993
7053
  }
7054
+ /* the checklist the server derived (task 1004649) — change-only; true when it moved.
7055
+ The install link rides only on a state-keyed read; the owner's read keeps the last. */
7056
+ function seedSetup(checklist, installUrl) {
7057
+ if (!checklist || typeof checklist !== 'object') return false;
7058
+ var next = JSON.stringify(checklist), was = live.setup ? JSON.stringify(live.setup) : '';
7059
+ var url = typeof installUrl === 'string' && /^https:\/\/github\.com\/apps\//.test(installUrl) ? installUrl : null;
7060
+ var urlMoved = !!url && url !== live.installUrl && (!live.installUrl || /[?&]state=/.test(url) || !/[?&]state=/.test(live.installUrl));
7061
+ if (next === was && !urlMoved) return false;
7062
+ live.setup = checklist;
7063
+ if (urlMoved) live.installUrl = url;
7064
+ return true;
7065
+ }
7066
+ /* still worth watching after sign-in: the install not verified yet, while the runner checks */
7067
+ function setupOpen() {
7068
+ return !!(live.setup && !live.setup.connected && live.manifest === 'done');
7069
+ }
6994
7070
  function noteManifestWait() {
6995
7071
  if (live.manifest === 'pending' && !manifestStale && manifestStartedAt && (Date.now() - manifestStartedAt) > 90000) { manifestStale = true; renderSteps(); }
6996
7072
  }
@@ -7001,19 +7077,21 @@ summary{min-height:24px;padding:3px 0;}
7001
7077
  function tick() {
7002
7078
  if (myGen !== manifestGen) return;
7003
7079
  if (!wizPollActive()) { manifestPending = true; stopManifestPoll(); return; }
7080
+ manifestKick = tick;
7004
7081
  fetch(API + '/provisioning/github-app/status?state=' + encodeURIComponent(stateToken))
7005
7082
  .then(function (res) { if (!res.ok) throw new Error('soft'); return res.json(); })
7006
7083
  .then(function (d) {
7007
7084
  if (myGen !== manifestGen) return;
7008
7085
  if (d.app_slug && d.app_slug !== live.appSlug) { live.appSlug = d.app_slug; rememberManifest(undefined, undefined, d.app_slug); renderSteps(); }
7086
+ if (seedSetup(d.checklist, d.install_url)) renderSteps(); /* the server-verified ticks (task 1004649) */
7009
7087
  var next = d.ready ? 'done' : (d.status || 'pending');
7010
7088
  var failure = d.failure || null;
7011
7089
  if (next !== live.manifest || failure !== live.manifestFailure) { live.manifest = next; live.manifestFailure = failure; rememberManifest(undefined, next); renderSteps(); }
7012
7090
  else noteManifestWait();
7013
- if (!d.terminal) manifestPollTimer = setTimeout(tick, 4000);
7091
+ if (!d.terminal) manifestPollTimer = setTimeout(tick, wizPollDelay(4000));
7014
7092
  else manifestPollTimer = null; /* a stale fired id would read as "still polling" and resume a finished poll */
7015
7093
  })
7016
- .catch(function () { if (myGen === manifestGen) manifestPollTimer = setTimeout(tick, 8000); });
7094
+ .catch(function () { if (myGen === manifestGen) manifestPollTimer = setTimeout(tick, wizPollDelay(8000)); });
7017
7095
  }
7018
7096
  tick();
7019
7097
  }
@@ -7204,6 +7282,7 @@ summary{min-height:24px;padding:3px 0;}
7204
7282
  function tick() {
7205
7283
  if (myGen !== watchGen) return;
7206
7284
  if (!wizPollActive()) { watchPending = true; stopPolling(); return; }
7285
+ progressKick = tick;
7207
7286
  ticks += 1;
7208
7287
  if (ticks > 400) {
7209
7288
  /* ~33 min: this tab stops watching, but the standup keeps going — hand
@@ -7249,6 +7328,8 @@ summary{min-height:24px;padding:3px 0;}
7249
7328
  if (seedChanged) { rememberManifest(undefined, live.manifest, live.appSlug || undefined); renderSteps(); }
7250
7329
  else noteManifestWait();
7251
7330
  }
7331
+ /* the server-verified checklist rides the same read (task 1004649) */
7332
+ if (data.github_setup && !manifestPollTimer && seedSetup(data.github_setup.checklist, data.github_setup.install_url)) renderSteps();
7252
7333
  /* the checklist reflects live state (task 1002694): a status transition —
7253
7334
  e.g. the scaffold landing — re-renders the steps (change-only, so the
7254
7335
  5s tick never clobbers an open dropdown or mid-flight button) */
@@ -7258,7 +7339,7 @@ summary{min-height:24px;padding:3px 0;}
7258
7339
  var terminal = renderProgress(inst);
7259
7340
  /* a GitHub leg still in flight keeps this read alive even once the
7260
7341
  project is up — after a reload it is the ONLY thing watching it */
7261
- if (!terminal || manifestInFlight()) pollTimer = setTimeout(tick, 5000);
7342
+ if (!terminal || manifestInFlight() || setupOpen()) pollTimer = setTimeout(tick, wizPollDelay(5000));
7262
7343
  else stopPolling();
7263
7344
  })
7264
7345
  .catch(function (err) {
@@ -7271,7 +7352,7 @@ summary{min-height:24px;padding:3px 0;}
7271
7352
  return;
7272
7353
  }
7273
7354
  /* transient (network / 5xx): keep the last view, try again a bit later */
7274
- pollTimer = setTimeout(tick, 10000);
7355
+ pollTimer = setTimeout(tick, wizPollDelay(10000));
7275
7356
  });
7276
7357
  }
7277
7358
  tick();
@@ -7288,7 +7369,7 @@ summary{min-height:24px;padding:3px 0;}
7288
7369
  instanceId = null;
7289
7370
  watchPending = false; manifestPending = false; manifestState = null;
7290
7371
  manifestStartedAt = 0; manifestStale = false;
7291
- live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null };
7372
+ live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, setup: null, installUrl: null };
7292
7373
  lastStepsHtml = '';
7293
7374
  lastProgHtml = '';
7294
7375
  hopReset();
@@ -7320,6 +7401,11 @@ summary{min-height:24px;padding:3px 0;}
7320
7401
  resetForNewProject();
7321
7402
  /* the deliberate goodbye: the record goes too, so nothing offers to resume
7322
7403
  the project the founder has just said they are finished watching */
7404
+ try {
7405
+ var gone = createdRecord();
7406
+ if (gone && gone.id) sessionStorage.setItem(DECLINED_KEY, String(gone.id));
7407
+ } catch (e) {}
7408
+ serverRecord = null;
7323
7409
  try { sessionStorage.removeItem(CREATED_KEY); } catch (e) {}
7324
7410
  paintResumeOffer(null);
7325
7411
  show(1, true);
@@ -7722,7 +7808,8 @@ summary{min-height:24px;padding:3px 0;}
7722
7808
  var list = $('humanSteps'), st = $('ghState');
7723
7809
  if (!list || !st) return;
7724
7810
  if (state.step !== 2) { st.textContent = 'opens once your project exists'; return; }
7725
- var items = Array.prototype.slice.call(list.children);
7811
+ /* the optional step (command-line sign-in) never holds "connected" back */
7812
+ var items = Array.prototype.slice.call(list.children).filter(function (li) { return !li.classList.contains('soptional'); });
7726
7813
  var done = items.filter(function (li) { return li.classList.contains('sdone'); }).length;
7727
7814
  st.textContent = items.length && done === items.length ? 'connected ✓'
7728
7815
  : manifestInFlight() ? 'waiting on GitHub…'
@@ -7762,8 +7849,11 @@ summary{min-height:24px;padding:3px 0;}
7762
7849
  })();
7763
7850
 
7764
7851
  /* ── the hub's three doors ── */
7852
+ /* the server's resume answer (task 1004650), kept in memory too: a browser whose
7853
+ storage throws still has the project it was handed */
7854
+ var serverRecord = null;
7765
7855
  function createdRecord() {
7766
- try { return JSON.parse(sessionStorage.getItem(CREATED_KEY) || 'null'); } catch (e) { return null; }
7856
+ try { return JSON.parse(sessionStorage.getItem(CREATED_KEY) || 'null') || serverRecord; } catch (e) { return serverRecord; }
7767
7857
  }
7768
7858
  /* The "still watching …" offer above step 1 (task 1003503). Its markup is baked
7769
7859
  into the panel, so this only names the project and shows or hides the line —
@@ -7828,6 +7918,12 @@ summary{min-height:24px;padding:3px 0;}
7828
7918
  /* restore the opt-out too — without it resolvedDomain() re-derives the
7829
7919
  assigned address and the done panel promises one that doesn't exist */
7830
7920
  state.noAddress = !!created.noaddr;
7921
+ /* a record the server handed over (another device, a fresh tab) carries the repo
7922
+ the project is on, so the install step names it (task 1004650) */
7923
+ if (created.repo_ref && !state.repo) {
7924
+ var rr = String(created.repo_ref).split('/');
7925
+ if (rr.length === 2) { state.owner = rr[0]; state.repo = rr[1]; }
7926
+ }
7831
7927
  /* a hand-back from the callback page names the attempt GitHub just approved */
7832
7928
  if (handbackState) {
7833
7929
  created.manifest_state = handbackState;
@@ -7850,9 +7946,54 @@ summary{min-height:24px;padding:3px 0;}
7850
7946
  identity settles it shows the draft step optimistically (the page stays
7851
7947
  usable while /me is in flight — the boot's own rule); onAuth() re-enters
7852
7948
  once the answer lands. Focus only moves on a deliberate nav (the flag). */
7949
+ /* askServerResume(focus) — which project is this founder connecting, by the SERVER's
7950
+ view, when this tab cannot say (task 1004650)? Asked once per page load. A hand-back
7951
+ names its project (handbackProject, from the return route): it always lands on that
7952
+ project's part 03, and only its owner gets an answer. With no name, the server picks
7953
+ the founder's newest project whose GitHub is not connected yet: an arrival lands on
7954
+ it unless something is being typed, and a deliberate "New project" only offers it on
7955
+ the resume line. Nothing found → the page as it was. */
7956
+ var resumeAsked = false;
7957
+ function recordFromServer(p) {
7958
+ return { id: p.id, slug: p.slug || '', name: p.slug || '', domain: p.domain || '', noaddr: !p.domain,
7959
+ mode: p.onboard_mode === 'adopt' ? 'adopt' : 'greenfield', repo_ref: p.repo || null, at: Date.now(), from_server: true };
7960
+ }
7961
+ function askServerResume(focus) {
7962
+ resumeAsked = true;
7963
+ var named = handbackProject;
7964
+ handbackProject = null;
7965
+ return fetch(API + '/provisioning/github-app/resume' + (named ? '?id=' + encodeURIComponent(named) : ''), { credentials: 'same-origin' })
7966
+ .then(function (res) { return res.ok ? res.json() : null; })
7967
+ .catch(function () { return null; })
7968
+ .then(function (d) {
7969
+ var p = d && d.project;
7970
+ var had = createdRecord();
7971
+ if (!p || !p.id || (!named && had && had.id)) { if (named) enter(focus); return; }
7972
+ var declined = null;
7973
+ try { declined = sessionStorage.getItem(DECLINED_KEY); } catch (e) {}
7974
+ if (!named && declined === String(p.id)) return;
7975
+ var rec = recordFromServer(p);
7976
+ /* the same project this tab already knew keeps what it knew (a demo's teammates) */
7977
+ if (had && String(had.id) === String(p.id)) rec = Object.assign({}, had, rec, { name: had.name || rec.name });
7978
+ else if (had && had.id) resetForNewProject(); /* a hand-back for a different project */
7979
+ serverRecord = rec;
7980
+ try { sessionStorage.setItem(CREATED_KEY, JSON.stringify(rec)); } catch (e) {}
7981
+ if (currentView !== 'new') return;
7982
+ if (named || (!focus && !String(state.name || '').trim())) { resumeIntent = true; enterCreated(rec, false); }
7983
+ else paintResumeOffer(rec);
7984
+ });
7985
+ }
7986
+
7853
7987
  function enter(focus) {
7854
7988
  if (authKnown && !me) { show(0, !!focus); return; }
7855
7989
  var created = authKnown ? createdRecord() : null;
7990
+ /* this tab cannot say which project — ask the server (task 1004650). A named
7991
+ hand-back waits for that answer instead of painting another project first. */
7992
+ if (authKnown && !resumeAsked && (handbackProject || !(created && created.id))) {
7993
+ var waitForIt = !!handbackProject;
7994
+ askServerResume(!!focus);
7995
+ if (waitForIt) return;
7996
+ }
7856
7997
  /* A draft on the page (step 1) beside a stored create is a SECOND project being
7857
7998
  typed. A reload or a GitHub round-trip (repo access) is that same visit
7858
7999
  continuing, so the draft keeps its place and the earlier project stays one
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.93",
3
+ "version": "1.21.95",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.93",
9
+ "version": "1.21.95",
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.21.93",
3
+ "version": "1.21.95",
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",
@@ -9388,5 +9388,17 @@
9388
9388
  "id": "1004657",
9389
9389
  "text": "When a project votes that a part of its government is decided by a group, that vote now holds on every page, not only the People page. Before, someone with the right permission could still switch unattended building on, sign o"
9390
9390
  }
9391
+ ],
9392
+ "1.21.94": [
9393
+ {
9394
+ "id": "1004649",
9395
+ "text": "Connecting GitHub to a new project is now one trip. The founder clicks \"Connect GitHub\", approves the project's app on GitHub, and GitHub carries straight on to installing it on their repo (already picked when the repo is publ"
9396
+ }
9397
+ ],
9398
+ "1.21.95": [
9399
+ {
9400
+ "id": "1004650",
9401
+ "text": "Connecting GitHub no longer depends on staying in one browser tab. After you approve on GitHub, even on your phone or in another window, you land back on your own project's GitHub checklist. Our server remembers which project"
9402
+ }
9391
9403
  ]
9392
9404
  }
@@ -172,12 +172,14 @@ function main() {
172
172
  cp.execSync(`systemctl restart ${slug}`, { stdio: 'inherit' });
173
173
  console.log(`✓ restarted ${slug}`);
174
174
 
175
- // (3) VERIFY — probe /healthz for auth_configured (retry briefly for restart).
175
+ // (3) VERIFY — probe the JSON health answer for auth_configured (retry briefly for
176
+ // restart). /api/bongos/healthz, not the root /healthz, which answers a plain "ok"
177
+ // that never parses (task 1004577).
176
178
  if (!port) { console.error('✖ no PORT in web.env and no --port given — cannot probe /healthz'); process.exit(1); }
177
179
  let probe = { authConfigured: false };
178
180
  for (let i = 0; i < 10; i++) {
179
181
  try {
180
- const out = cp.execSync(`curl -sS --max-time 5 http://127.0.0.1:${port}/healthz`, { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
182
+ const out = cp.execSync(`curl -sS --max-time 5 http://127.0.0.1:${port}/api/bongos/healthz`, { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
181
183
  probe = parseHealthzAuth(out);
182
184
  if (probe.ok || probe.authConfigured) break;
183
185
  } catch { /* not up yet */ }