@bongos/core 1.19.724 → 1.19.725
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 +32 -27
- package/docs/module-api-changelog.md +2 -0
- package/modules/onboarding/approval-broadcast.js +27 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/adopt-preflight.js +68 -2
- package/scripts/gds/build-cli-package.js +1 -0
- package/scripts/gds/claude-md-template.js +18 -0
- package/scripts/gds/device-flow-help.js +72 -0
- package/scripts/gds/init.js +64 -8
- package/scripts/gds/login.js +18 -0
- package/scripts/gds/setup.js +5 -39
- package/src/bongos/repo-info.js +37 -6
- package/src/module-api.js +8 -1
- package/tests/adopt_preflight.mjs +72 -0
- package/tests/bongos_login.mjs +69 -2
- package/tests/claude_materialize.mjs +9 -0
- package/tests/discord_approvals.mjs +45 -0
- package/tests/init.mjs +52 -2
- package/tests/module_api.mjs +1 -0
- package/tests/repo_info_binding.mjs +82 -0
- package/tests/setup_device_flow_fallback.mjs +16 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.725",
|
|
6
|
+
"core_contract": "1.19.725",
|
|
7
|
+
"source_commit": "5a98310e9967d2430aba88122213a7a722ae07ad",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-13T13:
|
|
9
|
+
"built_at": "2026-09-13T13:36:04.522Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 478,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2163,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2665,
|
|
20
|
+
"tree_sha256": "41c25536b149d9641dc2e413a0da01a4e49c306b619ec412c2f2ea7b552944c6",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2807,7 +2807,7 @@
|
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "docs/module-api-changelog.md",
|
|
2809
2809
|
"mode": "0000644",
|
|
2810
|
-
"sha256": "
|
|
2810
|
+
"sha256": "793f097cac5faadbbf7d9aaec4c2b5b413a535b913b2e3258ef17361982f7ef9"
|
|
2811
2811
|
},
|
|
2812
2812
|
{
|
|
2813
2813
|
"path": "docs/modules-contract.md",
|
|
@@ -6137,7 +6137,7 @@
|
|
|
6137
6137
|
{
|
|
6138
6138
|
"path": "modules/onboarding/approval-broadcast.js",
|
|
6139
6139
|
"mode": "0000644",
|
|
6140
|
-
"sha256": "
|
|
6140
|
+
"sha256": "2225b1a2196f6195bdf1b24f38da5295e0f44cbd95395d09c6109d8680fc5b01"
|
|
6141
6141
|
},
|
|
6142
6142
|
{
|
|
6143
6143
|
"path": "modules/onboarding/db.js",
|
|
@@ -7837,12 +7837,12 @@
|
|
|
7837
7837
|
{
|
|
7838
7838
|
"path": "package-lock.json",
|
|
7839
7839
|
"mode": "0000644",
|
|
7840
|
-
"sha256": "
|
|
7840
|
+
"sha256": "82be9f1fd6d0b97fdc7b5ac3d36e9e7537c2b42818af2bb643be5ecf6610d22a"
|
|
7841
7841
|
},
|
|
7842
7842
|
{
|
|
7843
7843
|
"path": "package.json",
|
|
7844
7844
|
"mode": "0000644",
|
|
7845
|
-
"sha256": "
|
|
7845
|
+
"sha256": "f1796793605e11cb891209283c562e08772ee65f956bbe5039ee799e1969a013"
|
|
7846
7846
|
},
|
|
7847
7847
|
{
|
|
7848
7848
|
"path": "public-docs/index.html",
|
|
@@ -7907,7 +7907,7 @@
|
|
|
7907
7907
|
{
|
|
7908
7908
|
"path": "scripts/gds/adopt-preflight.js",
|
|
7909
7909
|
"mode": "0000644",
|
|
7910
|
-
"sha256": "
|
|
7910
|
+
"sha256": "1e2c8a70aec49eaa0d95a9cbf37145fa1c851b46e2ec27f72f773d4198356ab9"
|
|
7911
7911
|
},
|
|
7912
7912
|
{
|
|
7913
7913
|
"path": "scripts/gds/adr-namespace.js",
|
|
@@ -8067,7 +8067,7 @@
|
|
|
8067
8067
|
{
|
|
8068
8068
|
"path": "scripts/gds/build-cli-package.js",
|
|
8069
8069
|
"mode": "0000644",
|
|
8070
|
-
"sha256": "
|
|
8070
|
+
"sha256": "314004ef2af628fca5f572daca5e2404d1c2129b3ad650d715294cc26cc2fe13"
|
|
8071
8071
|
},
|
|
8072
8072
|
{
|
|
8073
8073
|
"path": "scripts/gds/bump-version.js",
|
|
@@ -8117,7 +8117,7 @@
|
|
|
8117
8117
|
{
|
|
8118
8118
|
"path": "scripts/gds/claude-md-template.js",
|
|
8119
8119
|
"mode": "0000644",
|
|
8120
|
-
"sha256": "
|
|
8120
|
+
"sha256": "afd825409590fa0c81cdc072bb925555e70bd543f819b8c68110760919ca67e1"
|
|
8121
8121
|
},
|
|
8122
8122
|
{
|
|
8123
8123
|
"path": "scripts/gds/cleanup-orphans.js",
|
|
@@ -8219,6 +8219,11 @@
|
|
|
8219
8219
|
"mode": "0000644",
|
|
8220
8220
|
"sha256": "79effdf07519d26153f6fe846fd052cd4a37fa1e25b6b1a278146c9d8bcf6766"
|
|
8221
8221
|
},
|
|
8222
|
+
{
|
|
8223
|
+
"path": "scripts/gds/device-flow-help.js",
|
|
8224
|
+
"mode": "0000644",
|
|
8225
|
+
"sha256": "802b8875e2aa7e51b1b95800aae461c5d289d7684ef57e6da95bebc458aa0f5e"
|
|
8226
|
+
},
|
|
8222
8227
|
{
|
|
8223
8228
|
"path": "scripts/gds/diagnose-achievements.js",
|
|
8224
8229
|
"mode": "0000644",
|
|
@@ -8452,7 +8457,7 @@
|
|
|
8452
8457
|
{
|
|
8453
8458
|
"path": "scripts/gds/init.js",
|
|
8454
8459
|
"mode": "0000644",
|
|
8455
|
-
"sha256": "
|
|
8460
|
+
"sha256": "e81ab71bcd7d2f06b5aaf462dd099776c966f9840a109846a9753f12482b5281"
|
|
8456
8461
|
},
|
|
8457
8462
|
{
|
|
8458
8463
|
"path": "scripts/gds/install-git-hooks.js",
|
|
@@ -8522,7 +8527,7 @@
|
|
|
8522
8527
|
{
|
|
8523
8528
|
"path": "scripts/gds/login.js",
|
|
8524
8529
|
"mode": "0000644",
|
|
8525
|
-
"sha256": "
|
|
8530
|
+
"sha256": "b5e00007cf68a5fa719d2603fdd7405dbc2e474a41721acde274e43bcec59ab0"
|
|
8526
8531
|
},
|
|
8527
8532
|
{
|
|
8528
8533
|
"path": "scripts/gds/main-audit.js",
|
|
@@ -8972,7 +8977,7 @@
|
|
|
8972
8977
|
{
|
|
8973
8978
|
"path": "scripts/gds/setup.js",
|
|
8974
8979
|
"mode": "0000644",
|
|
8975
|
-
"sha256": "
|
|
8980
|
+
"sha256": "bd78df6f5f296588721afd9ebf2e477b5d463ca3d12cbee8894e115a0a6779f7"
|
|
8976
8981
|
},
|
|
8977
8982
|
{
|
|
8978
8983
|
"path": "scripts/gds/shell.js",
|
|
@@ -9492,7 +9497,7 @@
|
|
|
9492
9497
|
{
|
|
9493
9498
|
"path": "src/bongos/repo-info.js",
|
|
9494
9499
|
"mode": "0000644",
|
|
9495
|
-
"sha256": "
|
|
9500
|
+
"sha256": "38c98f7cdb9dbc9d73dc53d0ad29466bc2fc8f762cd8db3961ab40df44dae11c"
|
|
9496
9501
|
},
|
|
9497
9502
|
{
|
|
9498
9503
|
"path": "src/bongos/route-rank-check.js",
|
|
@@ -9632,7 +9637,7 @@
|
|
|
9632
9637
|
{
|
|
9633
9638
|
"path": "src/module-api.js",
|
|
9634
9639
|
"mode": "0000644",
|
|
9635
|
-
"sha256": "
|
|
9640
|
+
"sha256": "5aa7b8b121af462b9feba77bbd2a87b997e6dad22d3c8e6bf467ca122016d3d0"
|
|
9636
9641
|
},
|
|
9637
9642
|
{
|
|
9638
9643
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9727,7 +9732,7 @@
|
|
|
9727
9732
|
{
|
|
9728
9733
|
"path": "tests/adopt_preflight.mjs",
|
|
9729
9734
|
"mode": "0000644",
|
|
9730
|
-
"sha256": "
|
|
9735
|
+
"sha256": "2489a19e7c36f53c4046a4a6fab54a55dbdfe8da6daa3452316f9e0abea01742"
|
|
9731
9736
|
},
|
|
9732
9737
|
{
|
|
9733
9738
|
"path": "tests/adr_renumber_integrity.mjs",
|
|
@@ -10052,7 +10057,7 @@
|
|
|
10052
10057
|
{
|
|
10053
10058
|
"path": "tests/bongos_login.mjs",
|
|
10054
10059
|
"mode": "0000644",
|
|
10055
|
-
"sha256": "
|
|
10060
|
+
"sha256": "abad290fe23b159a6c42a70fbc6cec68bfc0406c90776a96b57513e9b4b8ef04"
|
|
10056
10061
|
},
|
|
10057
10062
|
{
|
|
10058
10063
|
"path": "tests/box_access.mjs",
|
|
@@ -10272,7 +10277,7 @@
|
|
|
10272
10277
|
{
|
|
10273
10278
|
"path": "tests/claude_materialize.mjs",
|
|
10274
10279
|
"mode": "0000644",
|
|
10275
|
-
"sha256": "
|
|
10280
|
+
"sha256": "8292d8c7a788e7839599d38f6c78a5dbb68f6ae50a58771a66e6481a562f0ace"
|
|
10276
10281
|
},
|
|
10277
10282
|
{
|
|
10278
10283
|
"path": "tests/clawback_confirmed_backlog.mjs",
|
|
@@ -10572,7 +10577,7 @@
|
|
|
10572
10577
|
{
|
|
10573
10578
|
"path": "tests/discord_approvals.mjs",
|
|
10574
10579
|
"mode": "0000644",
|
|
10575
|
-
"sha256": "
|
|
10580
|
+
"sha256": "1b4df0a8cd7dc48f393cd2c3ca7ec91759270c7df87a50397fb3d18d3b6a29fe"
|
|
10576
10581
|
},
|
|
10577
10582
|
{
|
|
10578
10583
|
"path": "tests/discord_autojoin.mjs",
|
|
@@ -11557,7 +11562,7 @@
|
|
|
11557
11562
|
{
|
|
11558
11563
|
"path": "tests/init.mjs",
|
|
11559
11564
|
"mode": "0000644",
|
|
11560
|
-
"sha256": "
|
|
11565
|
+
"sha256": "5b0eaf9cee1a51a4ff41197c4af844a0ba89e5a44f23d0f00de09e5ca8900f08"
|
|
11561
11566
|
},
|
|
11562
11567
|
{
|
|
11563
11568
|
"path": "tests/instance_config.mjs",
|
|
@@ -11842,7 +11847,7 @@
|
|
|
11842
11847
|
{
|
|
11843
11848
|
"path": "tests/module_api.mjs",
|
|
11844
11849
|
"mode": "0000644",
|
|
11845
|
-
"sha256": "
|
|
11850
|
+
"sha256": "53aa3732865dfce17a3216390b057dae825a6a7a8056309f3f31654c06636bee"
|
|
11846
11851
|
},
|
|
11847
11852
|
{
|
|
11848
11853
|
"path": "tests/module_api_lazy.mjs",
|
|
@@ -12477,7 +12482,7 @@
|
|
|
12477
12482
|
{
|
|
12478
12483
|
"path": "tests/repo_info_binding.mjs",
|
|
12479
12484
|
"mode": "0000644",
|
|
12480
|
-
"sha256": "
|
|
12485
|
+
"sha256": "384a494fab8da8cab77e1241ce8ebd954b658bcc56c578065c6f80bc0fd74f7d"
|
|
12481
12486
|
},
|
|
12482
12487
|
{
|
|
12483
12488
|
"path": "tests/repo_map.mjs",
|
|
@@ -12742,7 +12747,7 @@
|
|
|
12742
12747
|
{
|
|
12743
12748
|
"path": "tests/setup_device_flow_fallback.mjs",
|
|
12744
12749
|
"mode": "0000644",
|
|
12745
|
-
"sha256": "
|
|
12750
|
+
"sha256": "f59a1381d0f71f5eb25c8e691cda2347fc34ae2569db191ae074fe2f07c87f1a"
|
|
12746
12751
|
},
|
|
12747
12752
|
{
|
|
12748
12753
|
"path": "tests/setup_smoke.mjs",
|
|
@@ -1922,5 +1922,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1922
1922
|
landed since 1.19.722 with no explicit bump. run 34758720786. (task 1002620)
|
|
1923
1923
|
1.19.724 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1924
1924
|
landed since 1.19.723 with no explicit bump. run 34759302425. (task 1002620)
|
|
1925
|
+
1.19.725 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1926
|
+
landed since 1.19.724 with no explicit bump. run 34760272743. (task 1002620)
|
|
1925
1927
|
---------------------------------------------------------------------------
|
|
1926
1928
|
```
|
|
@@ -48,12 +48,39 @@ async function postLine(channel, line, deps = {}) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Is the half that CLOSES this loop actually loaded? The line we post invites a 👍,
|
|
52
|
+
// and that reaction is only ever applied by modules/discord/discord-approvals.js —
|
|
53
|
+
// which lives in the `discord` module. onboarding is default-ON and discord is
|
|
54
|
+
// default-OFF, and nothing gated one on the other, so a default instance posted an
|
|
55
|
+
// approve/decline control with nothing listening: an Archon reacts, watches the
|
|
56
|
+
// reaction land, and believes they admitted someone. Nobody is told otherwise.
|
|
57
|
+
//
|
|
58
|
+
// A control that cannot work is worse than no message, so the outbound half now
|
|
59
|
+
// refuses to post without its inbound half. The hall queue is the source of truth
|
|
60
|
+
// either way, so nothing is lost — only a false affordance is withheld.
|
|
61
|
+
// Reached through the published doorway (the one core file a module may require,
|
|
62
|
+
// ADR 0083), never a direct require of src/modules.js. Fails CLOSED: if enablement
|
|
63
|
+
// can't be resolved we do not post, because that is the state we cannot vouch for.
|
|
64
|
+
function discordHandlerLoaded(deps = {}) {
|
|
65
|
+
try {
|
|
66
|
+
const isEnabled = deps.isModuleEnabled || require('../../src/module-api').isModuleEnabled;
|
|
67
|
+
return isEnabled('discord') === true;
|
|
68
|
+
} catch (_) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
51
73
|
async function postMemberApprovalRequest(request, deps = {}) {
|
|
52
74
|
if (!request || request.id == null) return { posted: false, skipped: 'no_request' };
|
|
75
|
+
if (!discordHandlerLoaded(deps)) {
|
|
76
|
+
(deps.log || console.log)('[gds] approval broadcast: skipped — the `discord` module is off, so no reaction handler would apply a 👍. Approve from the hall queue.');
|
|
77
|
+
return { posted: false, skipped: 'discord_module_off' };
|
|
78
|
+
}
|
|
53
79
|
return postLine('member_approvals', memberApprovalLine(request), deps);
|
|
54
80
|
}
|
|
55
81
|
|
|
56
82
|
module.exports = {
|
|
57
83
|
memberApprovalLine,
|
|
58
84
|
postMemberApprovalRequest,
|
|
85
|
+
discordHandlerLoaded, // exported for unit tests — the gate is the whole fix
|
|
59
86
|
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.725",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.725",
|
|
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.19.
|
|
3
|
+
"version": "1.19.725",
|
|
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",
|
|
@@ -11,11 +11,20 @@
|
|
|
11
11
|
// floor (ADR 0043, via gate-review.js's canonical matchesHardFloor).
|
|
12
12
|
// • CI collisions — the repo's workflows vs core's expected unit/secret-scan/
|
|
13
13
|
// gate-review checks.
|
|
14
|
+
// • IGNORED incoming files — a pre-existing .gitignore rule that would swallow what
|
|
15
|
+
// adopt is about to commit (task 1003400). This is the check that was missing when
|
|
16
|
+
// Cloud Bongos was adopted onto a repo whose .gitignore already carried `.claude/`:
|
|
17
|
+
// the skills, hooks and settings.json were materialized onto disk and then dropped
|
|
18
|
+
// by `git add`, so the adopt commit landed 8 files with no methodology surface at
|
|
19
|
+
// all. Nothing failed, nothing was logged, and every clone of that repo has been
|
|
20
|
+
// missing its /builder-* commands since. BLOCKING, because the result is a
|
|
21
|
+
// non-functional instance that looks fine.
|
|
14
22
|
// READ-ONLY: it computes findings; the accept-gate that decides whether to proceed
|
|
15
23
|
// lives in init.js runAdopt. Pure check functions + a thin fs-reading orchestrator.
|
|
16
24
|
|
|
17
25
|
const nodeFs = require('node:fs');
|
|
18
26
|
const nodePath = require('node:path');
|
|
27
|
+
const { execFileSync: nodeExecFileSync } = require('node:child_process');
|
|
19
28
|
const { filenameViolation, INSTANCE_NS } = require('./migration-namespace');
|
|
20
29
|
const { matchesHardFloor } = require('./gate-review');
|
|
21
30
|
|
|
@@ -79,10 +88,66 @@ function checkCiCollisions(workflows = []) {
|
|
|
79
88
|
return [{ category: 'ci', severity: SEV.INFO, message: `${workflows.length} existing CI workflow(s); core will ADD its own unit/secret-scan/gate-review checks alongside them — confirm no required-check conflict.` }];
|
|
80
89
|
}
|
|
81
90
|
|
|
91
|
+
// PURE — [{ path, rule }] of incoming files a .gitignore rule would swallow → findings.
|
|
92
|
+
// BLOCKING: an ignored incoming file is not a style problem, it is silent data loss at
|
|
93
|
+
// `git add` time, and the instance that results is broken in a way nothing reports.
|
|
94
|
+
function checkIgnoredIncoming(ignored = [], { checked = true } = {}) {
|
|
95
|
+
if (!checked) {
|
|
96
|
+
return [{ category: 'ignored-incoming', severity: SEV.INFO, message: 'could not run `git check-ignore` here (no git, or not a repo yet) — cannot confirm the incoming files are committable. Re-check after `git init` if adopt lands nothing under .claude/.' }];
|
|
97
|
+
}
|
|
98
|
+
if (!ignored.length) {
|
|
99
|
+
return [{ category: 'ignored-incoming', severity: SEV.INFO, message: 'no existing ignore rule swallows the files adopt writes — they will all reach the commit.' }];
|
|
100
|
+
}
|
|
101
|
+
const lines = ignored.map((i) => (i.rule ? `${i.path} (ignored by ${i.rule})` : i.path));
|
|
102
|
+
return [{
|
|
103
|
+
category: 'ignored-incoming',
|
|
104
|
+
severity: SEV.BLOCK,
|
|
105
|
+
message: `${ignored.length} file(s) adopt writes are IGNORED by this repo and would be silently dropped from the commit: ${lines.join('; ')}. `
|
|
106
|
+
+ 'Adopt would appear to succeed and produce an instance missing that surface — a repo ignoring `.claude/` gets no /builder-* commands on any clone. '
|
|
107
|
+
+ 'Fix the rule (delete it, or add a negation such as `!.claude/` after it), then re-run.',
|
|
108
|
+
}];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Which of `incoming` does this repo's ignore rules match? Uses `git check-ignore -v`,
|
|
112
|
+
// which reports the SOURCE RULE for each match, so the finding can name the exact line
|
|
113
|
+
// the owner has to change rather than leaving them to hunt for it.
|
|
114
|
+
//
|
|
115
|
+
// Returns { ignored:[{path, rule}], checked:boolean }. `checked:false` means the probe
|
|
116
|
+
// could not run at all (no git, not a repo) — deliberately distinct from "nothing is
|
|
117
|
+
// ignored", because the two warrant different advice. execFileSync is injectable for
|
|
118
|
+
// tests; git exits 1 when nothing matches, which is a normal answer and not an error.
|
|
119
|
+
function probeIgnoredIncoming({ dir, incoming = INCOMING_PATHS, execFileSync = nodeExecFileSync } = {}) {
|
|
120
|
+
// Trailing slashes are ours (they mark dirs in the list); git wants plain pathnames.
|
|
121
|
+
const paths = incoming.map((p) => String(p).replace(/\/+$/, ''));
|
|
122
|
+
let out = '';
|
|
123
|
+
try {
|
|
124
|
+
out = execFileSync('git', ['check-ignore', '-v', '--stdin'], {
|
|
125
|
+
cwd: dir, input: paths.join('\n'), encoding: 'utf8', timeout: 5000,
|
|
126
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
127
|
+
}) || '';
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// Exit 1 = "no path is ignored" (a real answer, stdout empty). Anything else means
|
|
130
|
+
// the probe itself could not run.
|
|
131
|
+
if (err && err.status === 1) return { ignored: [], checked: true };
|
|
132
|
+
return { ignored: [], checked: false };
|
|
133
|
+
}
|
|
134
|
+
const ignored = [];
|
|
135
|
+
for (const line of out.split('\n')) {
|
|
136
|
+
if (!line.trim()) continue;
|
|
137
|
+
// `<source>:<line>:<pattern>\t<pathname>`
|
|
138
|
+
const tab = line.lastIndexOf('\t');
|
|
139
|
+
if (tab === -1) continue;
|
|
140
|
+
const rule = line.slice(0, tab).trim();
|
|
141
|
+
const pathname = line.slice(tab + 1).trim();
|
|
142
|
+
ignored.push({ path: pathname, rule });
|
|
143
|
+
}
|
|
144
|
+
return { ignored, checked: true };
|
|
145
|
+
}
|
|
146
|
+
|
|
82
147
|
// Read the repo (migrations, src/bongos, workflows) and run every check. `stack` is the
|
|
83
148
|
// adopt-detect summary (task 2043); we re-read only what the checks need. Returns
|
|
84
149
|
// { findings, counts:{blocking,warning,info}, hasBlocking }.
|
|
85
|
-
function preflightAdopt({ dir, stack = {}, fs = nodeFs, path = nodePath } = {}) {
|
|
150
|
+
function preflightAdopt({ dir, stack = {}, fs = nodeFs, path = nodePath, execFileSync = nodeExecFileSync } = {}) {
|
|
86
151
|
const listFiles = (rel) => { try { return fs.readdirSync(path.join(dir, rel)).filter((f) => { try { return fs.statSync(path.join(dir, rel, f)).isFile(); } catch { return false; } }); } catch { return []; } };
|
|
87
152
|
const exists = (rel) => { try { return fs.existsSync(path.join(dir, rel)); } catch { return false; } };
|
|
88
153
|
|
|
@@ -105,6 +170,7 @@ function preflightAdopt({ dir, stack = {}, fs = nodeFs, path = nodePath } = {})
|
|
|
105
170
|
...checkFitnessBoundary({ topLevelDirs: stack.topLevelDirs || [], hasSrcBongos: exists('src/bongos') }),
|
|
106
171
|
...checkProtectedPaths(INCOMING_PATHS),
|
|
107
172
|
...checkCiCollisions(workflows),
|
|
173
|
+
...(() => { const { ignored, checked } = probeIgnoredIncoming({ dir, execFileSync }); return checkIgnoredIncoming(ignored, { checked }); })(),
|
|
108
174
|
];
|
|
109
175
|
const counts = { blocking: 0, warning: 0, info: 0 };
|
|
110
176
|
for (const f of findings) counts[f.severity === SEV.BLOCK ? 'blocking' : f.severity === SEV.WARN ? 'warning' : 'info']++;
|
|
@@ -118,4 +184,4 @@ function formatPreflight(report) {
|
|
|
118
184
|
return report.findings.map((f) => ` ${mark[f.severity] || '·'} [${f.category}] ${f.message}`);
|
|
119
185
|
}
|
|
120
186
|
|
|
121
|
-
module.exports = { preflightAdopt, formatPreflight, checkMigrationNamespace, checkFitnessBoundary, checkProtectedPaths, checkCiCollisions, INCOMING_PATHS, CORE_EXPECTED_CHECKS, SEV };
|
|
187
|
+
module.exports = { preflightAdopt, formatPreflight, checkMigrationNamespace, checkFitnessBoundary, checkProtectedPaths, checkCiCollisions, checkIgnoredIncoming, probeIgnoredIncoming, INCOMING_PATHS, CORE_EXPECTED_CHECKS, SEV };
|
|
@@ -51,6 +51,7 @@ const FILES = [
|
|
|
51
51
|
'scripts/gds/login.js',
|
|
52
52
|
'scripts/gds/reauth.js',
|
|
53
53
|
'scripts/gds/setup.js',
|
|
54
|
+
'scripts/gds/device-flow-help.js', // shared leaf: login.js + setup.js both require it (task 1003881)
|
|
54
55
|
'scripts/gds/onboarding-config.js', // lazy dep of setup.js
|
|
55
56
|
'scripts/gds/install-git-hooks.js', // lazy dep of setup.js
|
|
56
57
|
'scripts/gds/preflight.js',
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
// of a mature instance's CLAUDE.md — the instance grows its own. Coordinates with
|
|
13
13
|
// task 1963 (neutralizing the core's own CLAUDE.md into the portable template).
|
|
14
14
|
//
|
|
15
|
+
// FIRST-RUN SECTION. The template used to open by instructing the agent to run
|
|
16
|
+
// `bongos start` without ever saying the CLI ships inside an uninstalled dependency,
|
|
17
|
+
// so an agent opening a fresh clone followed its first instruction straight into
|
|
18
|
+
// `command not found`. The root README carries the same signpost for humans — but only
|
|
19
|
+
// when the repo had no README to begin with, which an ADOPTED repo always does. This
|
|
20
|
+
// file lands on BOTH paths and loads in full every session, so it is the one place the
|
|
21
|
+
// bootstrap is guaranteed to be read.
|
|
22
|
+
//
|
|
15
23
|
// PURE (branding in → markdown out), so it unit-tests standalone and carries no
|
|
16
24
|
// hardcoded host identity (everything comes from the branding object — R76-safe).
|
|
17
25
|
|
|
@@ -48,6 +56,16 @@ function renderInstanceClaudeMd(branding = {}) {
|
|
|
48
56
|
|
|
49
57
|
Read this at the start of every session. Keep it lean — it loads in full every time.
|
|
50
58
|
|
|
59
|
+
## First run — before any \`bongos\` command
|
|
60
|
+
|
|
61
|
+
**If \`node_modules/@bongos/core\` is missing, run \`npm install\` first.** The \`bongos\` CLI ships *inside* that dependency, so until the install completes there is no \`bongos\` command and every instruction below fails with \`command not found\`:
|
|
62
|
+
|
|
63
|
+
\`\`\`bash
|
|
64
|
+
npm install
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
Once per machine, per clone — it is not committed (\`node_modules/\` is ignored on purpose), so a fresh \`git clone\` always needs it. If a \`bongos\` command then still misbehaves, \`bongos doctor\` checks the toolchain and session and prints the fix.
|
|
68
|
+
|
|
51
69
|
## What this is
|
|
52
70
|
|
|
53
71
|
${ident}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// scripts/gds/device-flow-help.js — the one explanation of a Device-Flow refusal, shared
|
|
4
|
+
// by `bongos setup` and `bongos login` (task 1003400).
|
|
5
|
+
//
|
|
6
|
+
// WHY IT EXISTS AT ALL. GitHub creates an App from a manifest with Device Flow OFF and
|
|
7
|
+
// exposes no API to switch it on — only a checkbox in the App's settings. So on every
|
|
8
|
+
// wizard-born instance the documented terminal sign-in is hard-broken from birth, for
|
|
9
|
+
// every builder, until the owner ticks that box. Nothing in the product can do it for
|
|
10
|
+
// them; the least we can do is say so precisely, and name a route that works meanwhile.
|
|
11
|
+
//
|
|
12
|
+
// WHY IT IS ITS OWN FILE. setup.js grew the detector + the guidance first; login.js threw
|
|
13
|
+
// a bare "could not start sign-in (HTTP 424)" — a dead end sitting directly downstream of
|
|
14
|
+
// the CLI's OWN wrong-instance advice to run `bongos login`. setup.js already requires
|
|
15
|
+
// login.js for runDeviceFlow, so having login.js require setup.js back would close a
|
|
16
|
+
// require cycle. Both now depend on this leaf instead: one source of truth, no cycle.
|
|
17
|
+
//
|
|
18
|
+
// PURE — no fs, no network, no process state; both functions unit-test standalone.
|
|
19
|
+
|
|
20
|
+
// Does this /auth/device/start response carry GitHub's device_flow_disabled refusal?
|
|
21
|
+
//
|
|
22
|
+
// Matches on TEXT, not status, deliberately: the CLI talks to many core versions, and the
|
|
23
|
+
// server envelope moved from 502 to 424 (a 502 body was being eaten by the CDN). Both
|
|
24
|
+
// register. Must NOT match auth_not_configured or any other upstream failure — those have
|
|
25
|
+
// different cures, and a wrong cure is worse than a generic error.
|
|
26
|
+
function isDeviceFlowDisabled(start) {
|
|
27
|
+
if (!start || start.ok) return false;
|
|
28
|
+
const err = start.data && start.data.error;
|
|
29
|
+
const text = [
|
|
30
|
+
typeof err === 'string' ? err : '',
|
|
31
|
+
err && typeof err === 'object' ? `${err.code || ''} ${err.message || ''}` : '',
|
|
32
|
+
start.data && start.data.message ? String(start.data.message) : '',
|
|
33
|
+
].join(' ');
|
|
34
|
+
return /device_flow_disabled/i.test(text);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The plain-language guidance: what happened, the web route that works RIGHT NOW, and the
|
|
38
|
+
// owner's one-checkbox repair. Returns an array of lines (the callers join them).
|
|
39
|
+
//
|
|
40
|
+
// `finish` names the caller, because only the closing move differs: setup prompts for the
|
|
41
|
+
// token inline ("paste it below"), login has no such prompt and must tell you to run the
|
|
42
|
+
// one-liner instead. Everything above that step is genuinely shared and stays shared.
|
|
43
|
+
function deviceFlowFallbackLines(base, { finish = 'setup' } = {}) {
|
|
44
|
+
const settings = `${base}/builders/settings`;
|
|
45
|
+
const close = finish === 'login'
|
|
46
|
+
? [
|
|
47
|
+
' 3. Copy the one-liner it shows and run it in this shell — that writes',
|
|
48
|
+
' the same session `bongos login` would have.',
|
|
49
|
+
]
|
|
50
|
+
: [
|
|
51
|
+
' 3. Copy the token it shows and paste it below (or run the printed',
|
|
52
|
+
' one-liner in another shell, then re-run setup).',
|
|
53
|
+
];
|
|
54
|
+
return [
|
|
55
|
+
'',
|
|
56
|
+
"This project's GitHub App has Device Flow switched off, so terminal sign-in",
|
|
57
|
+
'cannot start. Apps created by the one-click setup begin this way — GitHub',
|
|
58
|
+
'only offers the switch as a checkbox in the App settings; no setup flow can',
|
|
59
|
+
'tick it automatically.',
|
|
60
|
+
'',
|
|
61
|
+
'Sign in through the web instead (works right now):',
|
|
62
|
+
` 1. Open ${settings} and sign in with GitHub.`,
|
|
63
|
+
' 2. Under "CLI Access", click "Re-issue CLI token".',
|
|
64
|
+
...close,
|
|
65
|
+
'',
|
|
66
|
+
'To repair terminal sign-in for everyone (the project owner, once):',
|
|
67
|
+
' GitHub → Settings → Developer settings → GitHub Apps → your app →',
|
|
68
|
+
' check "Enable Device Flow" → Save. Web sign-in is unaffected either way.',
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { isDeviceFlowDisabled, deviceFlowFallbackLines };
|
package/scripts/gds/init.js
CHANGED
|
@@ -525,6 +525,62 @@ function buildReadme(branding = {}) {
|
|
|
525
525
|
].join('\n');
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
+
// The START-HERE content as an APPENDABLE section, for a repo that already has a
|
|
529
|
+
// README of its own. Fenced by a stable marker so re-running init is idempotent and so
|
|
530
|
+
// an owner can find (and delete) the block we added.
|
|
531
|
+
//
|
|
532
|
+
// WHY this is not just buildReadme: an ADOPTED repo always has a README, and the
|
|
533
|
+
// write-if-absent rule below therefore skipped the first-run guide on every single adopt
|
|
534
|
+
// — silently, logging "already present — left untouched" as though nothing were owed.
|
|
535
|
+
// That removed the one human-facing instruction to run `npm install`, without which the
|
|
536
|
+
// repo has no `bongos` command at all. Appending mirrors how the adopt path already
|
|
537
|
+
// treats .gitignore: keep what the owner wrote, add only what the instance needs.
|
|
538
|
+
const README_MARKER = '<!-- cloud-bongos:start-here -->';
|
|
539
|
+
|
|
540
|
+
function buildReadmeSection(branding = {}) {
|
|
541
|
+
const guide = buildReadme(branding);
|
|
542
|
+
// Reuse the canonical guide, demoted one heading level so it nests under the owner's
|
|
543
|
+
// own README, and drop its H1 title (their README already has one).
|
|
544
|
+
const body = guide
|
|
545
|
+
.split('\n')
|
|
546
|
+
.slice(1) // drop "# <product>"
|
|
547
|
+
.join('\n')
|
|
548
|
+
.replace(/^## /gm, '### ')
|
|
549
|
+
.trim();
|
|
550
|
+
return `${README_MARKER}\n\n## Working on this project (Cloud Bongos)\n\n${body}\n`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Append the START-HERE section to an existing README unless it is already there.
|
|
554
|
+
// PURE (text in → text out) so the idempotence is unit-testable without fs.
|
|
555
|
+
// Returns { text, action } where action is 'appended' | 'present'.
|
|
556
|
+
function appendReadmeSection(existing, section) {
|
|
557
|
+
const prev = String(existing == null ? '' : existing);
|
|
558
|
+
if (prev.includes(README_MARKER)) return { text: prev, action: 'present' };
|
|
559
|
+
return { text: `${prev.replace(/\n*$/, '\n')}\n---\n\n${section}`, action: 'appended' };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Put the first-run guide in the repo's README whichever state it is in: mint the whole
|
|
563
|
+
// guide when there is no README, else append the marked section. Shared by the greenfield
|
|
564
|
+
// and adopt paths so neither can silently owe it. Returns the action taken.
|
|
565
|
+
function layerReadme(branding, { dir, dryRun, log = console.log } = {}) {
|
|
566
|
+
const readmePath = path.join(dir, 'README.md');
|
|
567
|
+
if (!fs.existsSync(readmePath)) {
|
|
568
|
+
if (dryRun) { log(` [dry-run] would write README.md (first-run START-HERE)`); return 'created'; }
|
|
569
|
+
fs.writeFileSync(readmePath, buildReadme(branding));
|
|
570
|
+
log(` ✓ wrote README.md (first-run START-HERE)`);
|
|
571
|
+
return 'created';
|
|
572
|
+
}
|
|
573
|
+
const { text, action } = appendReadmeSection(fs.readFileSync(readmePath, 'utf8'), buildReadmeSection(branding));
|
|
574
|
+
if (action === 'present') {
|
|
575
|
+
log(' • README.md already carries the Cloud Bongos start-here section — left untouched.');
|
|
576
|
+
return 'present';
|
|
577
|
+
}
|
|
578
|
+
if (dryRun) { log(' [dry-run] would append the Cloud Bongos start-here section to the existing README.md'); return 'appended'; }
|
|
579
|
+
fs.writeFileSync(readmePath, text);
|
|
580
|
+
log(' ✓ appended the Cloud Bongos start-here section to README.md (your own content is untouched)');
|
|
581
|
+
return 'appended';
|
|
582
|
+
}
|
|
583
|
+
|
|
528
584
|
function writeConfigs(spec, { dir, force, dryRun, coreDep, log = console.log }) {
|
|
529
585
|
const configDir = path.join(dir, 'config');
|
|
530
586
|
const brandingPath = path.join(configDir, 'branding.json');
|
|
@@ -572,14 +628,9 @@ function writeConfigs(spec, { dir, force, dryRun, coreDep, log = console.log })
|
|
|
572
628
|
log(` ✓ wrote ${path.relative(dir, modulesPath)}`);
|
|
573
629
|
log(` ✓ wrote ${path.relative(dir, hierarchyPath)}`);
|
|
574
630
|
log(` ✓ wrote ${path.relative(dir, pkgPath)} (pins ${CORE_PKG} → ${pinNote})`);
|
|
575
|
-
// README START-HERE (task 1002404):
|
|
576
|
-
// owner's own README
|
|
577
|
-
|
|
578
|
-
fs.writeFileSync(readmePath, buildReadme(branding));
|
|
579
|
-
log(` ✓ wrote ${path.relative(dir, readmePath)} (first-run START-HERE)`);
|
|
580
|
-
} else {
|
|
581
|
-
log(` • ${path.relative(dir, readmePath)} already present — left untouched`);
|
|
582
|
-
}
|
|
631
|
+
// README START-HERE (task 1002404): mint it when absent, else APPEND the marked
|
|
632
|
+
// section. Never a blind overwrite — an owner's own README content always survives.
|
|
633
|
+
layerReadme(branding, { dir, dryRun: false, log });
|
|
583
634
|
return { brandingPath, modulesPath, hierarchyPath, pkgPath, readmePath, wrote: true };
|
|
584
635
|
}
|
|
585
636
|
|
|
@@ -816,6 +867,10 @@ async function runAdopt(spec, { dir, dryRun, noSeed = false, accept = false, cor
|
|
|
816
867
|
|
|
817
868
|
const cfg = layerConfigs(spec, { dir, dryRun, log });
|
|
818
869
|
const pkg = layerPackageJson(spec, { dir, dryRun, log, ...(coreDep ? { coreRange: coreDep } : {}) });
|
|
870
|
+
// The first-run guide, which adopt used to skip entirely: an adopted repo always has a
|
|
871
|
+
// README, so the greenfield write-if-absent rule never fired and the one instruction to
|
|
872
|
+
// run `npm install` was silently owed on every adopt.
|
|
873
|
+
layerReadme(buildBrandingConfig(spec), { dir, dryRun, log });
|
|
819
874
|
|
|
820
875
|
// Vendor the pinned core tarball + generate the lockfile, mirroring the greenfield
|
|
821
876
|
// --vendor-core steps, so the adopted repo is `npm ci --omit=dev`-installable (ADR 0108).
|
|
@@ -1061,6 +1116,7 @@ module.exports = {
|
|
|
1061
1116
|
provisioningSlug, provisioningRequest, productionTopologyKickoff,
|
|
1062
1117
|
// --adopt (brownfield layering, ADR 0121 / task 2042)
|
|
1063
1118
|
mergeCoreDependency, coreDepRange, layerConfigs, layerPackageJson, runAdopt,
|
|
1119
|
+
buildReadmeSection, appendReadmeSection, layerReadme, README_MARKER,
|
|
1064
1120
|
KNOWN_MODULES, HOSTING_SHAPES, CORE_PKG,
|
|
1065
1121
|
// greenfield installability (ADR 0108 gap 1/4, task 2053)
|
|
1066
1122
|
buildInstancePackageJson, coreVersionSafe, INSTANCE_START_SCRIPT, INSTANCE_MIGRATE_SCRIPT,
|
package/scripts/gds/login.js
CHANGED
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
const readline = require('node:readline');
|
|
24
24
|
const { spawnSync } = require('node:child_process');
|
|
25
25
|
const { saveSession, loadStoredSession, listStoredSessions } = require('./cli-lib');
|
|
26
|
+
// The device-flow refusal detector + its plain-language cure (task 1003400). A leaf
|
|
27
|
+
// module, NOT setup.js: setup requires this file for runDeviceFlow, so requiring it
|
|
28
|
+
// back would close a require cycle. Both callers depend on the leaf instead.
|
|
29
|
+
const { isDeviceFlowDisabled, deviceFlowFallbackLines } = require('./device-flow-help');
|
|
26
30
|
const { branding } = require('../../src/branding');
|
|
27
31
|
|
|
28
32
|
// --- pure helpers (unit-tested) ---------------------------------------------
|
|
@@ -102,6 +106,20 @@ async function runDeviceFlow(base, { log, idp = null }) {
|
|
|
102
106
|
if (start.status === 503 || (start.data && (start.data.error === 'auth_not_configured' || (start.data.error && start.data.error.code === 'idp_signing_unconfigured')))) {
|
|
103
107
|
throw new Error('that instance has no sign-in configured yet — ask its Archon to set it up.');
|
|
104
108
|
}
|
|
109
|
+
// Device Flow is OFF on every GitHub App born from a manifest, and GitHub exposes no
|
|
110
|
+
// API to enable it — so terminal sign-in is broken from birth on a fresh instance,
|
|
111
|
+
// for everyone, until its owner ticks one checkbox. This threw a bare
|
|
112
|
+
// "could not start sign-in (HTTP 424)", which is where the CLI's OWN wrong-instance
|
|
113
|
+
// refusal sends every multi-project user: a dead end at the end of our own advice.
|
|
114
|
+
// In federated mode the device flow runs against the HUB, so the cure is on the hub's
|
|
115
|
+
// app — point the guidance at whichever origin actually refused.
|
|
116
|
+
if (isDeviceFlowDisabled(start)) {
|
|
117
|
+
const refusedBy = idp ? idp.origin : base;
|
|
118
|
+
throw new Error([
|
|
119
|
+
'GitHub refused the device-code request for this project.',
|
|
120
|
+
...deviceFlowFallbackLines(refusedBy, { finish: 'login' }),
|
|
121
|
+
].join('\n'));
|
|
122
|
+
}
|
|
105
123
|
if (!start.ok || !start.data || !start.data.device_code) {
|
|
106
124
|
throw new Error(`could not start sign-in (HTTP ${start.status}).`);
|
|
107
125
|
}
|
package/scripts/gds/setup.js
CHANGED
|
@@ -274,45 +274,11 @@ async function promptOnboardingConsent() {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
// ── W1: the manifest-born App has Device Flow OFF (task 1003051) ─────────────
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
// CLI-token path so setup still completes. Pure; exported for tests.
|
|
283
|
-
function isDeviceFlowDisabled(start) {
|
|
284
|
-
if (!start || start.ok) return false;
|
|
285
|
-
const err = start.data && start.data.error;
|
|
286
|
-
const text = [
|
|
287
|
-
typeof err === 'string' ? err : '',
|
|
288
|
-
err && typeof err === 'object' ? `${err.code || ''} ${err.message || ''}` : '',
|
|
289
|
-
start.data && start.data.message ? String(start.data.message) : '',
|
|
290
|
-
].join(' ');
|
|
291
|
-
return /device_flow_disabled/i.test(text);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// The plain-language fallback guidance (task 1003051): what happened, the web
|
|
295
|
-
// path that works right now, and the one-checkbox owner fix. Pure; exported.
|
|
296
|
-
function deviceFlowFallbackLines(base) {
|
|
297
|
-
const settings = `${base}/builders/settings`;
|
|
298
|
-
return [
|
|
299
|
-
'',
|
|
300
|
-
"This project's GitHub App has Device Flow switched off, so terminal sign-in",
|
|
301
|
-
'cannot start. Apps created by the one-click setup begin this way — GitHub',
|
|
302
|
-
'only offers the switch as a checkbox in the App settings; no setup flow can',
|
|
303
|
-
'tick it automatically.',
|
|
304
|
-
'',
|
|
305
|
-
'Sign in through the web instead (works right now):',
|
|
306
|
-
` 1. Open ${settings} and sign in with GitHub.`,
|
|
307
|
-
' 2. Under "CLI Access", click "Re-issue CLI token".',
|
|
308
|
-
' 3. Copy the token it shows and paste it below (or run the printed',
|
|
309
|
-
' one-liner in another shell, then re-run setup).',
|
|
310
|
-
'',
|
|
311
|
-
'To repair terminal sign-in for everyone (the project owner, once):',
|
|
312
|
-
' GitHub → Settings → Developer settings → GitHub Apps → your app →',
|
|
313
|
-
' check "Enable Device Flow" → Save. Web sign-in is unaffected either way.',
|
|
314
|
-
];
|
|
315
|
-
}
|
|
277
|
+
// The detector + the guidance moved to ./device-flow-help (task 1003400) so
|
|
278
|
+
// `bongos login` can use the same words without closing a require cycle back
|
|
279
|
+
// through this file (setup requires login for runDeviceFlow). Re-exported here
|
|
280
|
+
// because both are part of setup.js's tested public surface.
|
|
281
|
+
const { isDeviceFlowDisabled, deviceFlowFallbackLines } = require('./device-flow-help');
|
|
316
282
|
|
|
317
283
|
// Verify a hall-minted CLI token against /me and save the session (the
|
|
318
284
|
// paste-token.js contract: nothing is written unless the token verifies).
|
package/src/bongos/repo-info.js
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
// transfers, the deploy's git remote changes and the served links follow,
|
|
13
13
|
// with zero code edits required.
|
|
14
14
|
//
|
|
15
|
-
// Cached on first call.
|
|
16
|
-
// OTB_GITHUB_REPO / OTB_GITHUB_BRANCH
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// Cached on first call. Three sources, in order: env vars (OTB_GITHUB_OWNER /
|
|
16
|
+
// OTB_GITHUB_REPO / OTB_GITHUB_BRANCH — the operator's explicit override), the git
|
|
17
|
+
// remote when this package IS the repo, then the instance's committed branding pack
|
|
18
|
+
// (config/branding.json `repo`) — the source a standalone instance actually has.
|
|
19
|
+
// Returns { error } when none resolves, so callers can degrade gracefully.
|
|
19
20
|
|
|
20
21
|
const { execSync } = require('node:child_process');
|
|
21
22
|
const path = require('node:path');
|
|
@@ -44,6 +45,34 @@ function loadFromEnv() {
|
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
// The instance's OWN declared GitHub binding, read from the committed branding pack
|
|
49
|
+
// (`config/branding.json` -> `repo: { owner, name }` — a first-class key in the
|
|
50
|
+
// branding contract). `bongos init` writes it at scaffold time, so a standalone
|
|
51
|
+
// instance carries the correct answer on disk from birth.
|
|
52
|
+
//
|
|
53
|
+
// WHY it exists: an installed core (ADR 0140) lives in node_modules/@bongos/core, so
|
|
54
|
+
// loadFromGit's anchor rule correctly returns null — and nothing sets
|
|
55
|
+
// <PREFIX>_GITHUB_OWNER/REPO on a provisioned instance. EVERY standalone instance
|
|
56
|
+
// therefore resolved no_repo_info and served a 503 from /public/repo-info, with public
|
|
57
|
+
// build status and every repo-linked surface dead. The identity was one file away the
|
|
58
|
+
// whole time, simply never read.
|
|
59
|
+
//
|
|
60
|
+
// Placed LAST in the chain on purpose: the env pin stays the operator's explicit
|
|
61
|
+
// override and git stays authoritative for the core dev checkout, so this source can
|
|
62
|
+
// only ever turn an ERROR into an answer — it can never change one that already
|
|
63
|
+
// resolved. Required lazily so module load order never depends on branding, and
|
|
64
|
+
// fail-soft: a malformed or absent pack must degrade to no_repo_info, not crash boot.
|
|
65
|
+
function loadFromBranding(deps = {}) {
|
|
66
|
+
try {
|
|
67
|
+
const read = deps.branding || require('../branding').branding;
|
|
68
|
+
const pack = read() || {};
|
|
69
|
+
const owner = pack.repo && String(pack.repo.owner || '').trim();
|
|
70
|
+
const name = pack.repo && String(pack.repo.name || '').trim();
|
|
71
|
+
if (owner && name) return { owner, repo: name };
|
|
72
|
+
} catch (_) { /* fall through — an unreadable pack is not an answer */ }
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
47
76
|
// The directory this file's package occupies: <root>/src/bongos -> <root>. In the core
|
|
48
77
|
// dev checkout that is the repo root; installed, it is node_modules/@bongos/core.
|
|
49
78
|
const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
|
|
@@ -92,12 +121,13 @@ function loadFromGit(deps = {}) {
|
|
|
92
121
|
function loadRepoInfo(deps = {}) {
|
|
93
122
|
if (cached) return cached;
|
|
94
123
|
const branch = ic.resolveEnv('GITHUB_BRANCH') || 'main';
|
|
95
|
-
const parsed = loadFromEnv() || loadFromGit(deps);
|
|
124
|
+
const parsed = loadFromEnv() || loadFromGit(deps) || loadFromBranding(deps);
|
|
96
125
|
if (!parsed) {
|
|
97
126
|
cached = {
|
|
98
127
|
error: 'no_repo_info',
|
|
99
128
|
detail:
|
|
100
|
-
'git remote origin not resolvable from this package (an installed core does not sit inside the repo it publishes to)
|
|
129
|
+
'git remote origin not resolvable from this package (an installed core does not sit inside the repo it publishes to), '
|
|
130
|
+
+ 'config/branding.json has no repo.owner/repo.name, and <PREFIX>_GITHUB_OWNER/REPO unset',
|
|
101
131
|
};
|
|
102
132
|
return cached;
|
|
103
133
|
}
|
|
@@ -123,5 +153,6 @@ module.exports = {
|
|
|
123
153
|
loadRepoInfo,
|
|
124
154
|
parseRemote,
|
|
125
155
|
loadFromGit, // exported for unit tests — the anchor rule is the whole fix
|
|
156
|
+
loadFromBranding, // exported for unit tests — the standalone-instance source
|
|
126
157
|
_resetCacheForTests,
|
|
127
158
|
};
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.725'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -427,6 +427,13 @@ module.exports = {
|
|
|
427
427
|
// directly. Resolved lazily so requiring the doorway never forces a config
|
|
428
428
|
// read at module-load (BV1.R81 / ADR 0093 §1).
|
|
429
429
|
enabledDisciplines: (...a) => require('./modules').enabledDisciplines(...a),
|
|
430
|
+
// isModuleEnabled(name) — is THIS module on for this instance? A module needs
|
|
431
|
+
// it when its own behaviour depends on ANOTHER module being present: onboarding
|
|
432
|
+
// posts a Discord approve/decline control whose 👍 is only ever applied by the
|
|
433
|
+
// `discord` module's reaction handler, so with discord off the control is a lie
|
|
434
|
+
// and must not be posted at all. Throws on an unknown name (a typo is a bug, not
|
|
435
|
+
// a silent false). Lazy, like its sibling above.
|
|
436
|
+
isModuleEnabled: (...a) => require('./modules').isModuleEnabled(...a),
|
|
430
437
|
|
|
431
438
|
// --- the seam registry — how modules cooperate without importing each other.
|
|
432
439
|
// Three primitives: PORTS (one required provider), EVENTS (many optional
|
|
@@ -101,5 +101,77 @@ t('preflightAdopt: a clean repo → no blocking; every category reports', () =>
|
|
|
101
101
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
// ---- ignored-incoming: the check that was missing (task 1003400) ----------
|
|
105
|
+
//
|
|
106
|
+
// The failure it exists to stop: adopt onto a repo whose .gitignore already carries
|
|
107
|
+
// `.claude/`. The skills/hooks/settings are written to disk, `git add` drops them, the
|
|
108
|
+
// commit looks clean, and every clone of that repo is missing its /builder-* commands
|
|
109
|
+
// with nothing anywhere saying why.
|
|
110
|
+
|
|
111
|
+
t('checkIgnoredIncoming: an ignored incoming file BLOCKS and names the rule to change', () => {
|
|
112
|
+
const f = pf.checkIgnoredIncoming([{ path: '.claude/skills', rule: '.gitignore:7:.claude/' }]);
|
|
113
|
+
assert.equal(f.length, 1);
|
|
114
|
+
assert.equal(f[0].severity, pf.SEV.BLOCK, 'silent data loss at git add is not a warning');
|
|
115
|
+
assert.match(f[0].message, /\.claude\/skills/, 'names the swallowed path');
|
|
116
|
+
assert.match(f[0].message, /\.gitignore:7/, 'names the exact rule line so the owner need not hunt');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
t('checkIgnoredIncoming: nothing ignored → INFO, not blocking', () => {
|
|
120
|
+
const f = pf.checkIgnoredIncoming([]);
|
|
121
|
+
assert.equal(f[0].severity, pf.SEV.INFO);
|
|
122
|
+
assert.equal(f.filter((x) => x.severity === pf.SEV.BLOCK).length, 0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
t('checkIgnoredIncoming: probe could not run → INFO that says so, never a false all-clear', () => {
|
|
126
|
+
const f = pf.checkIgnoredIncoming([], { checked: false });
|
|
127
|
+
assert.equal(f[0].severity, pf.SEV.INFO);
|
|
128
|
+
assert.match(f[0].message, /could not run/, 'unknown must not be reported as clean');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
t('probeIgnoredIncoming: parses git check-ignore -v into path + rule', () => {
|
|
132
|
+
const execFileSync = () => '.gitignore:7:.claude/\t.claude/skills\n.gitignore:7:.claude/\t.claude/hooks\n';
|
|
133
|
+
const { ignored, checked } = pf.probeIgnoredIncoming({ dir: '/x', execFileSync });
|
|
134
|
+
assert.equal(checked, true);
|
|
135
|
+
assert.deepEqual(ignored, [
|
|
136
|
+
{ path: '.claude/skills', rule: '.gitignore:7:.claude/' },
|
|
137
|
+
{ path: '.claude/hooks', rule: '.gitignore:7:.claude/' },
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
t('probeIgnoredIncoming: git exit 1 means NOTHING ignored — a real answer, not a failure', () => {
|
|
142
|
+
const execFileSync = () => { const e = new Error('exit 1'); e.status = 1; throw e; };
|
|
143
|
+
assert.deepEqual(pf.probeIgnoredIncoming({ dir: '/x', execFileSync }), { ignored: [], checked: true });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
t('probeIgnoredIncoming: no git / not a repo → checked:false, distinct from "clean"', () => {
|
|
147
|
+
const execFileSync = () => { const e = new Error('not a git repository'); e.status = 128; throw e; };
|
|
148
|
+
assert.deepEqual(pf.probeIgnoredIncoming({ dir: '/x', execFileSync }), { ignored: [], checked: false });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
t('probeIgnoredIncoming: strips our trailing dir slashes before handing paths to git', () => {
|
|
152
|
+
let input = null;
|
|
153
|
+
const execFileSync = (_cmd, _args, opts) => { input = opts.input; return ''; };
|
|
154
|
+
pf.probeIgnoredIncoming({ dir: '/x', incoming: ['.claude/hooks/', 'package.json'], execFileSync });
|
|
155
|
+
assert.equal(input, '.claude/hooks\npackage.json', 'git check-ignore wants plain pathnames');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
t('preflightAdopt (end to end): mercury\'s .gitignore would have BLOCKED the adopt', () => {
|
|
159
|
+
const dir = mkdtempSync(join(tmpdir(), 'pf-ignored-'));
|
|
160
|
+
try {
|
|
161
|
+
// The exact rule that swallowed the methodology surface on the real repo.
|
|
162
|
+
writeFileSync(join(dir, '.gitignore'), '.env\nvenv/\n\n#claude code\n.claude/\n');
|
|
163
|
+
const report = pf.preflightAdopt({
|
|
164
|
+
dir,
|
|
165
|
+
stack: { migrationsDirs: [], topLevelDirs: [], ci: [] },
|
|
166
|
+
// Stand in for git so the case is hermetic (no repo needed in tmp).
|
|
167
|
+
execFileSync: () => '.gitignore:5:.claude/\t.claude/settings.json\n.gitignore:5:.claude/\t.claude/skills\n',
|
|
168
|
+
});
|
|
169
|
+
assert.ok(report.hasBlocking, 'adopt must stop rather than produce a silently broken instance');
|
|
170
|
+
const found = cats(report.findings, 'ignored-incoming');
|
|
171
|
+
assert.equal(found.length, 1);
|
|
172
|
+
assert.equal(found[0].severity, pf.SEV.BLOCK);
|
|
173
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
174
|
+
});
|
|
175
|
+
|
|
104
176
|
console.log(`\nadopt_preflight: ${passed} passed, ${failed} failed`);
|
|
105
177
|
process.exit(failed ? 1 : 0);
|
package/tests/bongos_login.mjs
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
// tests/bongos_login.mjs —
|
|
2
|
-
// (V4.R83 / task 1250). DB-free
|
|
1
|
+
// tests/bongos_login.mjs — the helpers and the failure surface behind
|
|
2
|
+
// `bongos login <instance>` (V4.R83 / task 1250). DB-free; the one end-to-end case
|
|
3
|
+
// drives the real CLI against a 127.0.0.1 stub, so it needs no network either.
|
|
4
|
+
// Picked up by run-unit-tests.js.
|
|
3
5
|
import assert from 'node:assert/strict';
|
|
4
6
|
import { test } from 'node:test';
|
|
5
7
|
import { createRequire } from 'node:module';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import http from 'node:http';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
6
14
|
|
|
7
15
|
const require = createRequire(import.meta.url);
|
|
8
16
|
const { normalizeInstanceBase, pollDecision } = require('../scripts/gds/login.js');
|
|
17
|
+
const LOGIN_JS = path.join(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'), 'scripts', 'gds', 'login.js');
|
|
9
18
|
|
|
10
19
|
test('normalizeInstanceBase adds https, strips trailing slashes + a pasted /api/gds', () => {
|
|
11
20
|
assert.equal(normalizeInstanceBase('amazonprimea.com'), 'https://amazonprimea.com');
|
|
@@ -32,3 +41,61 @@ test('pollDecision maps the /access-requests/status body to a loop action', () =
|
|
|
32
41
|
assert.equal(pollDecision(null), 'wait');
|
|
33
42
|
assert.equal(pollDecision({}), 'wait');
|
|
34
43
|
});
|
|
44
|
+
|
|
45
|
+
// ---- the Device-Flow dead end (task 1003400) -------------------------------
|
|
46
|
+
//
|
|
47
|
+
// Device Flow is OFF on every GitHub App created from a manifest and GitHub offers no
|
|
48
|
+
// API to switch it on, so terminal sign-in is broken from birth on a fresh instance —
|
|
49
|
+
// for every builder. `bongos login` used to answer that with a bare
|
|
50
|
+
// "could not start sign-in (HTTP 424)": no cause, no cure, and sitting directly
|
|
51
|
+
// downstream of the CLI's own wrong-instance advice to run `bongos login`.
|
|
52
|
+
//
|
|
53
|
+
// Drives the REAL CLI against a stub replaying the exact body a live instance returned.
|
|
54
|
+
|
|
55
|
+
test('login on a Device-Flow-disabled instance explains the cause and the cure', async () => {
|
|
56
|
+
const server = http.createServer((req, res) => {
|
|
57
|
+
const send = (code, body) => {
|
|
58
|
+
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
59
|
+
res.end(JSON.stringify(body));
|
|
60
|
+
};
|
|
61
|
+
if (req.url.endsWith('/api/gds/instance')) {
|
|
62
|
+
return send(200, { schema: 'instance-manifest/v1', name: 'Stub', admission: { policy: 'invite-only' } });
|
|
63
|
+
}
|
|
64
|
+
if (req.url.endsWith('/api/gds/auth/device/start')) {
|
|
65
|
+
// Byte-for-byte what a real instance returned when its App had the box unticked.
|
|
66
|
+
return send(424, { error: { code: 'device_start_failed', message: 'device/code failed (400): {"error":"device_flow_disabled","error_description":"Device Flow must be explicitly enabled for this App"}' } });
|
|
67
|
+
}
|
|
68
|
+
return send(404, { error: { code: 'not_found' } });
|
|
69
|
+
});
|
|
70
|
+
server.listen(0, '127.0.0.1');
|
|
71
|
+
await new Promise((r) => server.once('listening', r));
|
|
72
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
73
|
+
// A throwaway HOME so the run can never read or write the real session.
|
|
74
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'login-df-'));
|
|
75
|
+
|
|
76
|
+
// spawn, not execFileSync: the stub server lives in THIS process, so a synchronous
|
|
77
|
+
// child would block the event loop that has to answer it — a deadlock, not a failure.
|
|
78
|
+
let out = '';
|
|
79
|
+
try {
|
|
80
|
+
out = await new Promise((resolve, reject) => {
|
|
81
|
+
const child = spawn(process.execPath, [LOGIN_JS, base], {
|
|
82
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
83
|
+
env: { ...process.env, HOME: home, USERPROFILE: home },
|
|
84
|
+
});
|
|
85
|
+
let buf = '';
|
|
86
|
+
child.stdout.on('data', (d) => { buf += d; });
|
|
87
|
+
child.stderr.on('data', (d) => { buf += d; });
|
|
88
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('login did not exit')); }, 30000);
|
|
89
|
+
child.on('close', () => { clearTimeout(timer); resolve(buf); });
|
|
90
|
+
child.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
91
|
+
});
|
|
92
|
+
} finally {
|
|
93
|
+
server.close();
|
|
94
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
assert.doesNotMatch(out, /could not start sign-in \(HTTP 424\)/, 'the bare dead end is gone');
|
|
98
|
+
assert.match(out, /Device Flow switched off/, 'names the actual cause');
|
|
99
|
+
assert.match(out, /Enable Device Flow/, 'names the owner\'s one-checkbox repair');
|
|
100
|
+
assert.match(out, /works right now/, 'names a route that works while the owner fixes it');
|
|
101
|
+
});
|
|
@@ -510,6 +510,15 @@ t('renderInstanceClaudeMd: brand-substituted — names the instance + its hall/c
|
|
|
510
510
|
assert.ok(!md.toLowerCase().includes('amazonprimea') && !md.includes('~/.config/otb'), 'no founder literal in the starter');
|
|
511
511
|
});
|
|
512
512
|
|
|
513
|
+
t('renderInstanceClaudeMd: the first-run bootstrap precedes every bongos instruction', () => {
|
|
514
|
+
const md = tmpl.renderInstanceClaudeMd({ identity: { productName: 'Acme' } });
|
|
515
|
+
const iNpm = md.indexOf('npm install');
|
|
516
|
+
const iStart = md.indexOf('bongos start');
|
|
517
|
+
assert.ok(iNpm > 0, 'names npm install at all — without it a fresh clone has no bongos command');
|
|
518
|
+
assert.ok(iStart > iNpm, 'npm install comes BEFORE the first bongos instruction, or the agent hits command not found');
|
|
519
|
+
assert.ok(/@bongos\/core/.test(md), 'explains WHY: the CLI ships inside the dependency');
|
|
520
|
+
});
|
|
521
|
+
|
|
513
522
|
t('renderInstanceClaudeMd: degrades cleanly on a minimal branding (no world dup, defaults currency)', () => {
|
|
514
523
|
const md = tmpl.renderInstanceClaudeMd({ identity: { productName: 'Solo', worldName: 'Solo' } });
|
|
515
524
|
assert.ok(md.startsWith('# Solo — Project Memory'));
|
|
@@ -170,5 +170,50 @@ await t('a DB error returns {error}, never throws', async () => {
|
|
|
170
170
|
assert.equal(r.outcome, 'error');
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
// ---- the outbound half must not outlive the inbound half (task 1003400) ----
|
|
174
|
+
//
|
|
175
|
+
// The 👍 this line invites is applied ONLY by discord-approvals.js, which lives in the
|
|
176
|
+
// `discord` module. onboarding (which posts) is default-ON; discord is default-OFF; and
|
|
177
|
+
// nothing gated one on the other. A default instance therefore posted a working-looking
|
|
178
|
+
// approve/decline control into a channel where no handler was listening — an Archon
|
|
179
|
+
// reacts, sees it land, and believes they admitted a builder who is still locked out.
|
|
180
|
+
|
|
181
|
+
await t('discord module OFF → the control is NOT posted', async () => {
|
|
182
|
+
let posted = false;
|
|
183
|
+
const r = await bcast.postMemberApprovalRequest(
|
|
184
|
+
{ id: 7, github_login: 'carol' },
|
|
185
|
+
{ postWebhook: async () => { posted = true; }, log: () => {}, isModuleEnabled: () => false },
|
|
186
|
+
);
|
|
187
|
+
assert.equal(posted, false, 'a 👍 nobody applies must never be invited');
|
|
188
|
+
assert.equal(r.skipped, 'discord_module_off');
|
|
189
|
+
assert.equal(r.posted, false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await t('discord module ON → the control posts exactly as before', async () => {
|
|
193
|
+
let line = null;
|
|
194
|
+
const r = await bcast.postMemberApprovalRequest(
|
|
195
|
+
{ id: 7, github_login: 'carol' },
|
|
196
|
+
{ postWebhook: async (_ch, l) => { line = l; }, log: () => {}, isModuleEnabled: () => true },
|
|
197
|
+
);
|
|
198
|
+
assert.equal(r.posted, true, 'the working path is untouched');
|
|
199
|
+
assert.deepEqual(ap.parseApprovalMarker(line), { kind: 'member', id: 7 }, 'and still round-trips');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await t('enablement unresolvable → fails CLOSED (no post)', async () => {
|
|
203
|
+
let posted = false;
|
|
204
|
+
const r = await bcast.postMemberApprovalRequest(
|
|
205
|
+
{ id: 7, github_login: 'carol' },
|
|
206
|
+
{ postWebhook: async () => { posted = true; }, log: () => {}, isModuleEnabled: () => { throw new Error('config unreadable'); } },
|
|
207
|
+
);
|
|
208
|
+
assert.equal(posted, false, 'a state we cannot vouch for is not a state we post into');
|
|
209
|
+
assert.equal(r.skipped, 'discord_module_off');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await t('the gate is reachable through the published doorway, not a direct core require', async () => {
|
|
213
|
+
const doorway = require('../src/module-api.js');
|
|
214
|
+
assert.equal(typeof doorway.isModuleEnabled, 'function', 'ADR 0083: modules read enablement here');
|
|
215
|
+
assert.throws(() => doorway.isModuleEnabled('no-such-module'), /unknown module/, 'a typo must be a bug, not a silent false');
|
|
216
|
+
});
|
|
217
|
+
|
|
173
218
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
174
219
|
process.exit(failed ? 1 : 0);
|
package/tests/init.mjs
CHANGED
|
@@ -535,10 +535,60 @@ test('writeConfigs (greenfield): writes a root README START-HERE, never clobberi
|
|
|
535
535
|
const md = readFileSync(res.readmePath, 'utf8');
|
|
536
536
|
assert.ok(/## Start here/.test(md) && md.includes('npm install'), 'is the START-HERE guide');
|
|
537
537
|
|
|
538
|
-
// A --force re-init must NOT clobber a README the owner has since edited.
|
|
538
|
+
// A --force re-init must NOT clobber a README the owner has since edited. It APPENDS
|
|
539
|
+
// the start-here section instead of skipping (the old behaviour silently owed the
|
|
540
|
+
// `npm install` instruction to every repo that already had a README).
|
|
539
541
|
writeFileSync(res.readmePath, '# my own readme\n');
|
|
540
542
|
init.writeConfigs(fullSpec(), { dir, force: true, dryRun: false, log: () => {} });
|
|
541
|
-
|
|
543
|
+
const after = readFileSync(res.readmePath, 'utf8');
|
|
544
|
+
assert.ok(after.startsWith('# my own readme\n'), 'the owner\'s own README content is never clobbered');
|
|
545
|
+
assert.ok(after.includes('npm install'), 'and the first-run instruction is still delivered');
|
|
546
|
+
} finally {
|
|
547
|
+
rmSync(dir, { recursive: true, force: true });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test('appendReadmeSection: appends once, then is idempotent', () => {
|
|
552
|
+
const section = init.buildReadmeSection({ identity: { productName: 'Mercury' } });
|
|
553
|
+
const own = '# hermes_inventory_management\nInventory Management\n';
|
|
554
|
+
const first = init.appendReadmeSection(own, section);
|
|
555
|
+
assert.equal(first.action, 'appended');
|
|
556
|
+
assert.ok(first.text.startsWith(own), 'the owner\'s content stays at the top, byte-identical');
|
|
557
|
+
assert.ok(first.text.includes('npm install'), 'delivers the first-run instruction');
|
|
558
|
+
|
|
559
|
+
const second = init.appendReadmeSection(first.text, section);
|
|
560
|
+
assert.equal(second.action, 'present', 're-running init must not stack duplicate sections');
|
|
561
|
+
assert.equal(second.text, first.text, 'and must not rewrite the file at all');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test('buildReadmeSection: nests under the owner\'s README — no second H1', () => {
|
|
565
|
+
const section = init.buildReadmeSection({ identity: { productName: 'Mercury' } });
|
|
566
|
+
assert.ok(!/^# /m.test(section), 'no H1 that would compete with the owner\'s title');
|
|
567
|
+
assert.ok(section.includes(init.README_MARKER), 'carries the marker that makes re-runs idempotent');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test('layerReadme (adopt): an existing README GETS the start-here section, not a skip', () => {
|
|
571
|
+
const dir = mkdtempSync(join(tmpdir(), 'adopt-readme-'));
|
|
572
|
+
try {
|
|
573
|
+
// The exact shape that broke: a brownfield repo whose own README predates adoption.
|
|
574
|
+
writeFileSync(join(dir, 'README.md'), '# hermes_inventory_management\nInventory Management\n');
|
|
575
|
+
const action = init.layerReadme({ identity: { productName: 'Mercury' } }, { dir, dryRun: false, log: () => {} });
|
|
576
|
+
assert.equal(action, 'appended');
|
|
577
|
+
const md = readFileSync(join(dir, 'README.md'), 'utf8');
|
|
578
|
+
assert.ok(md.startsWith('# hermes_inventory_management'), 'owner content preserved');
|
|
579
|
+
assert.ok(md.includes('npm install'), 'THE FIX: adopt no longer silently owes the first-run guide');
|
|
580
|
+
} finally {
|
|
581
|
+
rmSync(dir, { recursive: true, force: true });
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test('layerReadme (dry-run): reports the append but writes nothing', () => {
|
|
586
|
+
const dir = mkdtempSync(join(tmpdir(), 'adopt-readme-dry-'));
|
|
587
|
+
try {
|
|
588
|
+
writeFileSync(join(dir, 'README.md'), '# mine\n');
|
|
589
|
+
const action = init.layerReadme({ identity: { productName: 'Mercury' } }, { dir, dryRun: true, log: () => {} });
|
|
590
|
+
assert.equal(action, 'appended');
|
|
591
|
+
assert.equal(readFileSync(join(dir, 'README.md'), 'utf8'), '# mine\n', 'dry-run writes nothing');
|
|
542
592
|
} finally {
|
|
543
593
|
rmSync(dir, { recursive: true, force: true });
|
|
544
594
|
}
|
package/tests/module_api.mjs
CHANGED
|
@@ -48,6 +48,7 @@ const PUBLISHED_SURFACE = [
|
|
|
48
48
|
'artistGate', // added by task 1003575 (ADR 0241): the artist gate — off | advisory | strict, plus the pure verdict helpers over it. Two modules ask it two different questions ("file this review?" and "does an open one hold the deploy?") and both must resolve the knob through this one reader, never branding().project
|
|
49
49
|
'readApplicantProfileFromHub', // added by task 1002972 (privacy spec D7): the reviewer queue's LIVE per-render read of one applicant's hub profile view. NARROW by design — the underlying call is authenticated with this instance's hub client secret, and loadIdpConfig stays OFF the doorway so no module ever holds the credential that speaks for the whole project
|
|
50
50
|
'enabledDisciplines', // BV1.R81: instance offered-disciplines (1.9.0; onboarding restock backstop)
|
|
51
|
+
'isModuleEnabled', // task 1003400: is ANOTHER module present? onboarding's Discord approve/decline control is only actionable when the `discord` module's reaction handler is loaded, so the posting half has to be able to ask
|
|
51
52
|
'registerProvider', 'hasProvider', 'resolve', 'resolveOptional', 'listPorts', 'verifyPortsSatisfied',
|
|
52
53
|
'on', 'emit', 'emitAsync', 'listEvents', 'seamSnapshot',
|
|
53
54
|
'contribute', 'contributions', 'collectContributions', 'listContributionPoints',
|
|
@@ -208,6 +208,88 @@ test('a good bind still produces the blob/tree bases callers rely on', () => {
|
|
|
208
208
|
}
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
+
// ---- loadFromBranding: the source a standalone instance actually has -------
|
|
212
|
+
//
|
|
213
|
+
// An installed core sits in node_modules/@bongos/core, so loadFromGit correctly refuses
|
|
214
|
+
// and nothing sets the env pin — which meant EVERY standalone instance served a 503 from
|
|
215
|
+
// /public/repo-info while `config/branding.json` held the answer. These cases pin the new
|
|
216
|
+
// source AND, just as importantly, pin that it did not disturb the two above it.
|
|
217
|
+
|
|
218
|
+
const PACK = { repo: { owner: FIXTURE_OWNER, name: FIXTURE_REPO } };
|
|
219
|
+
|
|
220
|
+
test('a branding pack with owner + name binds (repo.name maps to repo)', () => {
|
|
221
|
+
assert.deepEqual(ri.loadFromBranding({ branding: () => PACK }), {
|
|
222
|
+
owner: FIXTURE_OWNER,
|
|
223
|
+
repo: FIXTURE_REPO,
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('the neutral pack (empty strings) is NOT an answer', () => {
|
|
228
|
+
assert.equal(ri.loadFromBranding({ branding: () => ({ repo: { owner: '', name: '' } }) }), null);
|
|
229
|
+
assert.equal(ri.loadFromBranding({ branding: () => ({ repo: { owner: ' ', name: FIXTURE_REPO } }) }), null);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('a pack with no repo key at all → null', () => {
|
|
233
|
+
assert.equal(ri.loadFromBranding({ branding: () => ({ identity: { productName: 'x' } }) }), null);
|
|
234
|
+
assert.equal(ri.loadFromBranding({ branding: () => null }), null);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('a THROWING branding loader degrades to null, never a crash', () => {
|
|
238
|
+
assert.equal(ri.loadFromBranding({ branding: () => { throw new Error('malformed pack'); } }), null);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('THE FIX: packaged instance, no env, no git → branding answers instead of 503', () => {
|
|
242
|
+
ri._resetCacheForTests();
|
|
243
|
+
try {
|
|
244
|
+
const root = path.resolve('/srv/instance/node_modules/@bongos/core');
|
|
245
|
+
const info = ri.loadRepoInfo({
|
|
246
|
+
execSync: mkExec({ toplevel: path.resolve('/srv/instance') }), // git walks up → refused
|
|
247
|
+
packageRoot: root,
|
|
248
|
+
branding: () => PACK,
|
|
249
|
+
});
|
|
250
|
+
assert.equal(info.error, undefined, 'the instance declares its own repo — that is not an error case');
|
|
251
|
+
assert.equal(info.owner, FIXTURE_OWNER);
|
|
252
|
+
assert.equal(info.repo, FIXTURE_REPO);
|
|
253
|
+
assert.equal(info.blob_base, `https://github.com/${FIXTURE_OWNER}/${FIXTURE_REPO}/blob/main`);
|
|
254
|
+
} finally {
|
|
255
|
+
ri._resetCacheForTests();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('git still WINS over branding (the core dev checkout is unchanged)', () => {
|
|
260
|
+
ri._resetCacheForTests();
|
|
261
|
+
try {
|
|
262
|
+
const root = path.resolve('/srv/cloud-bongos');
|
|
263
|
+
const info = ri.loadRepoInfo({
|
|
264
|
+
execSync: mkExec({ toplevel: root }), // the remote resolves → git answers
|
|
265
|
+
packageRoot: root,
|
|
266
|
+
branding: () => ({ repo: { owner: 'packowner', name: 'packrepo' } }),
|
|
267
|
+
});
|
|
268
|
+
assert.equal(info.owner, FIXTURE_OWNER, 'branding must never override a resolved git remote');
|
|
269
|
+
} finally {
|
|
270
|
+
ri._resetCacheForTests();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('the env pin still WINS over branding (the operator override stays on top)', () => {
|
|
275
|
+
ri._resetCacheForTests();
|
|
276
|
+
process.env.OTB_GITHUB_OWNER = 'envowner';
|
|
277
|
+
process.env.OTB_GITHUB_REPO = 'envrepo';
|
|
278
|
+
try {
|
|
279
|
+
const root = path.resolve('/srv/instance/node_modules/@bongos/core');
|
|
280
|
+
const info = ri.loadRepoInfo({
|
|
281
|
+
execSync: mkExec({ toplevel: path.resolve('/srv/instance') }),
|
|
282
|
+
packageRoot: root,
|
|
283
|
+
branding: () => PACK,
|
|
284
|
+
});
|
|
285
|
+
assert.equal(info.owner, 'envowner');
|
|
286
|
+
} finally {
|
|
287
|
+
delete process.env.OTB_GITHUB_OWNER;
|
|
288
|
+
delete process.env.OTB_GITHUB_REPO;
|
|
289
|
+
ri._resetCacheForTests();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
211
293
|
// ---- end-to-end canary: the regression this must never cause ---------------
|
|
212
294
|
//
|
|
213
295
|
// The only case that touches real git. It earns its place — an anchor rule that is too
|
|
@@ -91,6 +91,22 @@ t('names the Settings page, CLI Access, and the owner checkbox repair', () => {
|
|
|
91
91
|
assert.match(text, /Web sign-in is unaffected/i, 'says what still works');
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
t('the closing step adapts to the caller — setup pastes, login runs a one-liner', () => {
|
|
95
|
+
const forSetup = setup.deviceFlowFallbackLines('https://spike.cloudbongos.com').join('\n');
|
|
96
|
+
const forLogin = setup.deviceFlowFallbackLines('https://spike.cloudbongos.com', { finish: 'login' }).join('\n');
|
|
97
|
+
|
|
98
|
+
// The default must be byte-stable — setup's own prompt still says "paste it below".
|
|
99
|
+
assert.match(forSetup, /paste it below/, 'setup keeps its inline-paste wording');
|
|
100
|
+
assert.doesNotMatch(forLogin, /paste it below/, '`bongos login` has no paste prompt to point at');
|
|
101
|
+
assert.match(forLogin, /run it in this shell/, 'login tells you what actually works there');
|
|
102
|
+
|
|
103
|
+
// Everything that is genuinely shared stays shared — one source of truth.
|
|
104
|
+
for (const shared of [/Enable Device Flow/, /CLI Access/, /builders\/settings/]) {
|
|
105
|
+
assert.match(forSetup, shared);
|
|
106
|
+
assert.match(forLogin, shared);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
94
110
|
// ── redeemCliToken in a hermetic child against a stubbed instance API ────────
|
|
95
111
|
// A child process because API_BASE + the session path resolve at module load:
|
|
96
112
|
// the child gets GDS_API_BASE → the stub and HOME/USERPROFILE → a throwaway
|