@bookedsolid/rea 0.1.0 → 0.2.1
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/.husky/commit-msg +130 -0
- package/.husky/pre-push +128 -0
- package/README.md +5 -5
- package/agents/codex-adversarial.md +23 -8
- package/commands/codex-review.md +2 -2
- package/dist/audit/append.d.ts +62 -0
- package/dist/audit/append.js +189 -0
- package/dist/audit/codex-event.d.ts +28 -0
- package/dist/audit/codex-event.js +15 -0
- package/dist/cli/doctor.d.ts +60 -1
- package/dist/cli/doctor.js +459 -20
- package/dist/cli/index.js +35 -5
- package/dist/cli/init.d.ts +13 -0
- package/dist/cli/init.js +278 -67
- package/dist/cli/install/canonical.d.ts +43 -0
- package/dist/cli/install/canonical.js +101 -0
- package/dist/cli/install/claude-md.d.ts +48 -0
- package/dist/cli/install/claude-md.js +93 -0
- package/dist/cli/install/commit-msg.d.ts +30 -0
- package/dist/cli/install/commit-msg.js +102 -0
- package/dist/cli/install/copy.d.ts +169 -0
- package/dist/cli/install/copy.js +455 -0
- package/dist/cli/install/fs-safe.d.ts +91 -0
- package/dist/cli/install/fs-safe.js +347 -0
- package/dist/cli/install/manifest-io.d.ts +12 -0
- package/dist/cli/install/manifest-io.js +44 -0
- package/dist/cli/install/manifest-schema.d.ts +83 -0
- package/dist/cli/install/manifest-schema.js +80 -0
- package/dist/cli/install/reagent.d.ts +59 -0
- package/dist/cli/install/reagent.js +160 -0
- package/dist/cli/install/settings-merge.d.ts +91 -0
- package/dist/cli/install/settings-merge.js +239 -0
- package/dist/cli/install/sha.d.ts +9 -0
- package/dist/cli/install/sha.js +21 -0
- package/dist/cli/serve.d.ts +11 -0
- package/dist/cli/serve.js +72 -6
- package/dist/cli/upgrade.d.ts +67 -0
- package/dist/cli/upgrade.js +509 -0
- package/dist/gateway/downstream-pool.d.ts +39 -0
- package/dist/gateway/downstream-pool.js +93 -0
- package/dist/gateway/downstream.d.ts +80 -0
- package/dist/gateway/downstream.js +196 -0
- package/dist/gateway/middleware/audit-types.d.ts +10 -0
- package/dist/gateway/middleware/audit.js +14 -0
- package/dist/gateway/middleware/injection.d.ts +59 -2
- package/dist/gateway/middleware/injection.js +91 -14
- package/dist/gateway/middleware/kill-switch.d.ts +20 -5
- package/dist/gateway/middleware/kill-switch.js +57 -35
- package/dist/gateway/middleware/redact.d.ts +83 -6
- package/dist/gateway/middleware/redact.js +133 -46
- package/dist/gateway/observability/codex-probe.d.ts +110 -0
- package/dist/gateway/observability/codex-probe.js +234 -0
- package/dist/gateway/observability/codex-telemetry.d.ts +93 -0
- package/dist/gateway/observability/codex-telemetry.js +221 -0
- package/dist/gateway/redact-safe/match-timeout.d.ts +83 -0
- package/dist/gateway/redact-safe/match-timeout.js +179 -0
- package/dist/gateway/reviewers/claude-self.d.ts +99 -0
- package/dist/gateway/reviewers/claude-self.js +316 -0
- package/dist/gateway/reviewers/codex.d.ts +64 -0
- package/dist/gateway/reviewers/codex.js +80 -0
- package/dist/gateway/reviewers/select.d.ts +64 -0
- package/dist/gateway/reviewers/select.js +102 -0
- package/dist/gateway/reviewers/types.d.ts +85 -0
- package/dist/gateway/reviewers/types.js +14 -0
- package/dist/gateway/server.d.ts +51 -0
- package/dist/gateway/server.js +258 -0
- package/dist/gateway/session.d.ts +9 -0
- package/dist/gateway/session.js +17 -0
- package/dist/policy/loader.d.ts +59 -0
- package/dist/policy/loader.js +65 -0
- package/dist/policy/profiles.d.ts +80 -0
- package/dist/policy/profiles.js +94 -0
- package/dist/policy/types.d.ts +38 -0
- package/dist/registry/loader.d.ts +98 -0
- package/dist/registry/loader.js +153 -0
- package/dist/registry/types.d.ts +44 -0
- package/dist/registry/types.js +6 -0
- package/dist/scripts/read-policy-field.d.ts +36 -0
- package/dist/scripts/read-policy-field.js +96 -0
- package/hooks/push-review-gate.sh +627 -17
- package/package.json +13 -2
- package/profiles/bst-internal-no-codex.yaml +40 -0
- package/profiles/bst-internal.yaml +23 -0
- package/profiles/client-engagement.yaml +23 -0
- package/profiles/lit-wc.yaml +17 -0
- package/profiles/minimal.yaml +11 -0
- package/profiles/open-source-no-codex.yaml +33 -0
- package/profiles/open-source.yaml +18 -0
- package/scripts/lint-safe-regex.mjs +78 -0
- package/scripts/postinstall.mjs +131 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"./middleware": {
|
|
29
29
|
"types": "./dist/gateway/middleware/chain.d.ts",
|
|
30
30
|
"import": "./dist/gateway/middleware/chain.js"
|
|
31
|
+
},
|
|
32
|
+
"./audit": {
|
|
33
|
+
"types": "./dist/audit/append.d.ts",
|
|
34
|
+
"import": "./dist/audit/append.js"
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
"sideEffects": false,
|
|
@@ -41,6 +45,8 @@
|
|
|
41
45
|
"agents/",
|
|
42
46
|
"profiles/",
|
|
43
47
|
"templates/",
|
|
48
|
+
"scripts/",
|
|
49
|
+
".husky/",
|
|
44
50
|
"LICENSE",
|
|
45
51
|
"README.md",
|
|
46
52
|
"SECURITY.md",
|
|
@@ -60,15 +66,18 @@
|
|
|
60
66
|
"adversarial-review"
|
|
61
67
|
],
|
|
62
68
|
"dependencies": {
|
|
69
|
+
"@anthropic-ai/sdk": "^0.90.0",
|
|
63
70
|
"@clack/prompts": "^1.2.0",
|
|
64
71
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
65
72
|
"commander": "^14.0.3",
|
|
73
|
+
"safe-regex": "^2.1.1",
|
|
66
74
|
"yaml": "^2.7.0",
|
|
67
75
|
"zod": "^3.23.0"
|
|
68
76
|
},
|
|
69
77
|
"devDependencies": {
|
|
70
78
|
"@changesets/cli": "^2.30.0",
|
|
71
79
|
"@types/node": "^25.5.2",
|
|
80
|
+
"@types/safe-regex": "^1.1.6",
|
|
72
81
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
73
82
|
"@typescript-eslint/parser": "^8.0.0",
|
|
74
83
|
"@vitest/coverage-v8": "^3.2.4",
|
|
@@ -79,7 +88,9 @@
|
|
|
79
88
|
},
|
|
80
89
|
"scripts": {
|
|
81
90
|
"build": "tsc -p tsconfig.build.json",
|
|
82
|
-
"
|
|
91
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
92
|
+
"lint": "pnpm run lint:regex && eslint .",
|
|
93
|
+
"lint:regex": "node scripts/lint-safe-regex.mjs",
|
|
83
94
|
"format": "prettier --write .",
|
|
84
95
|
"format:check": "prettier --check .",
|
|
85
96
|
"test": "vitest run",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# profiles/bst-internal-no-codex.yaml
|
|
2
|
+
#
|
|
3
|
+
# bst-internal variant with Codex adversarial review disabled.
|
|
4
|
+
#
|
|
5
|
+
# Same governance settings as `bst-internal.yaml` — L1 autonomy with an L2
|
|
6
|
+
# ceiling, the same blocked-paths list, the same context-protection rules.
|
|
7
|
+
# The only difference: the installed `.rea/policy.yaml` will carry
|
|
8
|
+
# `review.codex_required: false`, and `rea init` surfaces that choice as
|
|
9
|
+
# the wizard default.
|
|
10
|
+
#
|
|
11
|
+
# Pick this variant when:
|
|
12
|
+
# - The team does not have an OpenAI account / Codex CLI on the bench.
|
|
13
|
+
# - A BST internal project is scoped such that Claude self-review is
|
|
14
|
+
# considered sufficient.
|
|
15
|
+
# - Codex is temporarily unavailable (rate limits, outage) and you want
|
|
16
|
+
# a durable opt-out rather than relying on REA_SKIP_CODEX_REVIEW.
|
|
17
|
+
#
|
|
18
|
+
# To re-enable Codex later, edit `.rea/policy.yaml` and set
|
|
19
|
+
# `review.codex_required: true` (or remove the field entirely — true is
|
|
20
|
+
# the documented default).
|
|
21
|
+
autonomy_level: L1
|
|
22
|
+
max_autonomy_level: L2
|
|
23
|
+
promotion_requires_human_approval: true
|
|
24
|
+
block_ai_attribution: true
|
|
25
|
+
blocked_paths:
|
|
26
|
+
- .env
|
|
27
|
+
- .env.*
|
|
28
|
+
- .rea/policy.yaml
|
|
29
|
+
- .rea/HALT
|
|
30
|
+
- CODEOWNERS
|
|
31
|
+
- .github/workflows/release.yml
|
|
32
|
+
- SECURITY.md
|
|
33
|
+
- THREAT_MODEL.md
|
|
34
|
+
notification_channel: ""
|
|
35
|
+
context_protection:
|
|
36
|
+
delegate_to_subagent:
|
|
37
|
+
- pnpm run build
|
|
38
|
+
- pnpm run test
|
|
39
|
+
- pnpm run lint
|
|
40
|
+
max_bash_output_lines: 100
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# profiles/bst-internal.yaml
|
|
2
|
+
# Booked Solid internal projects. Mirrors the current .rea/policy.yaml in the
|
|
3
|
+
# rea repository itself — this is the canonical dogfood profile.
|
|
4
|
+
autonomy_level: L1
|
|
5
|
+
max_autonomy_level: L2
|
|
6
|
+
promotion_requires_human_approval: true
|
|
7
|
+
block_ai_attribution: true
|
|
8
|
+
blocked_paths:
|
|
9
|
+
- .env
|
|
10
|
+
- .env.*
|
|
11
|
+
- .rea/policy.yaml
|
|
12
|
+
- .rea/HALT
|
|
13
|
+
- CODEOWNERS
|
|
14
|
+
- .github/workflows/release.yml
|
|
15
|
+
- SECURITY.md
|
|
16
|
+
- THREAT_MODEL.md
|
|
17
|
+
notification_channel: ""
|
|
18
|
+
context_protection:
|
|
19
|
+
delegate_to_subagent:
|
|
20
|
+
- pnpm run build
|
|
21
|
+
- pnpm run test
|
|
22
|
+
- pnpm run lint
|
|
23
|
+
max_bash_output_lines: 100
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# profiles/client-engagement.yaml
|
|
2
|
+
# Zero-trust client projects. Starts at the lowest meaningful autonomy level,
|
|
3
|
+
# max ceiling held at L1 — promotion requires explicit maintainer approval.
|
|
4
|
+
autonomy_level: L0
|
|
5
|
+
max_autonomy_level: L1
|
|
6
|
+
promotion_requires_human_approval: true
|
|
7
|
+
block_ai_attribution: true
|
|
8
|
+
blocked_paths:
|
|
9
|
+
- .env
|
|
10
|
+
- .env.*
|
|
11
|
+
- .rea/policy.yaml
|
|
12
|
+
- .rea/HALT
|
|
13
|
+
- CODEOWNERS
|
|
14
|
+
- SECURITY.md
|
|
15
|
+
- THREAT_MODEL.md
|
|
16
|
+
- secrets/
|
|
17
|
+
- credentials/
|
|
18
|
+
notification_channel: ""
|
|
19
|
+
context_protection:
|
|
20
|
+
delegate_to_subagent:
|
|
21
|
+
- pnpm run build
|
|
22
|
+
- pnpm run test
|
|
23
|
+
max_bash_output_lines: 100
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# profiles/lit-wc.yaml
|
|
2
|
+
# Lit / web component libraries. Typical dev ergonomics; protects publish paths
|
|
3
|
+
# and the design-system contract files.
|
|
4
|
+
autonomy_level: L1
|
|
5
|
+
max_autonomy_level: L2
|
|
6
|
+
promotion_requires_human_approval: true
|
|
7
|
+
block_ai_attribution: true
|
|
8
|
+
blocked_paths:
|
|
9
|
+
- .env
|
|
10
|
+
- .env.*
|
|
11
|
+
- .rea/policy.yaml
|
|
12
|
+
- .rea/HALT
|
|
13
|
+
- CODEOWNERS
|
|
14
|
+
- .github/workflows/release.yml
|
|
15
|
+
- .github/workflows/publish.yml
|
|
16
|
+
- tokens/
|
|
17
|
+
notification_channel: ""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# profiles/minimal.yaml
|
|
2
|
+
# Bare policy defaults — nothing profile-specific. Suitable as a starting point
|
|
3
|
+
# for a project that wants rea's governance but will tune the rest by hand.
|
|
4
|
+
autonomy_level: L1
|
|
5
|
+
max_autonomy_level: L2
|
|
6
|
+
promotion_requires_human_approval: true
|
|
7
|
+
block_ai_attribution: true
|
|
8
|
+
blocked_paths:
|
|
9
|
+
- .env
|
|
10
|
+
- .env.*
|
|
11
|
+
notification_channel: ""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# profiles/open-source-no-codex.yaml
|
|
2
|
+
#
|
|
3
|
+
# open-source variant with Codex adversarial review disabled.
|
|
4
|
+
#
|
|
5
|
+
# Same governance settings as `open-source.yaml` — L1 autonomy with an L2
|
|
6
|
+
# ceiling, the same blocked-paths list (sensitive release and disclosure
|
|
7
|
+
# files flagged for human-only edits). The only difference: the installed
|
|
8
|
+
# `.rea/policy.yaml` will carry `review.codex_required: false`.
|
|
9
|
+
#
|
|
10
|
+
# Pick this variant when:
|
|
11
|
+
# - Your public OSS project does not have a Codex account or does not
|
|
12
|
+
# want to require one from every contributor.
|
|
13
|
+
# - Contributors are expected to run a locally-hosted adversarial review
|
|
14
|
+
# instead (Claude self-review is the shipped fallback).
|
|
15
|
+
#
|
|
16
|
+
# To re-enable Codex later, edit `.rea/policy.yaml` and set
|
|
17
|
+
# `review.codex_required: true` (or remove the field entirely — true is
|
|
18
|
+
# the documented default).
|
|
19
|
+
autonomy_level: L1
|
|
20
|
+
max_autonomy_level: L2
|
|
21
|
+
promotion_requires_human_approval: true
|
|
22
|
+
block_ai_attribution: true
|
|
23
|
+
blocked_paths:
|
|
24
|
+
- .env
|
|
25
|
+
- .env.*
|
|
26
|
+
- .rea/policy.yaml
|
|
27
|
+
- .rea/HALT
|
|
28
|
+
- CODEOWNERS
|
|
29
|
+
- LICENSE
|
|
30
|
+
- SECURITY.md
|
|
31
|
+
- .github/workflows/release.yml
|
|
32
|
+
- .github/workflows/publish.yml
|
|
33
|
+
notification_channel: ""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# profiles/open-source.yaml
|
|
2
|
+
# Public OSS repositories. Slightly looser autonomy, strict attribution rules,
|
|
3
|
+
# and sensitive files flagged for human-only edits.
|
|
4
|
+
autonomy_level: L1
|
|
5
|
+
max_autonomy_level: L2
|
|
6
|
+
promotion_requires_human_approval: true
|
|
7
|
+
block_ai_attribution: true
|
|
8
|
+
blocked_paths:
|
|
9
|
+
- .env
|
|
10
|
+
- .env.*
|
|
11
|
+
- .rea/policy.yaml
|
|
12
|
+
- .rea/HALT
|
|
13
|
+
- CODEOWNERS
|
|
14
|
+
- LICENSE
|
|
15
|
+
- SECURITY.md
|
|
16
|
+
- .github/workflows/release.yml
|
|
17
|
+
- .github/workflows/publish.yml
|
|
18
|
+
notification_channel: ""
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// G3 — Static ReDoS lint.
|
|
3
|
+
//
|
|
4
|
+
// Every default pattern built into rea's middleware is passed through
|
|
5
|
+
// `safe-regex`. Any pattern flagged as unsafe fails the build.
|
|
6
|
+
//
|
|
7
|
+
// Run as part of `pnpm lint` (wired in package.json#scripts), BEFORE eslint —
|
|
8
|
+
// a bad regex is a security defect and must short-circuit the pipeline.
|
|
9
|
+
//
|
|
10
|
+
// This script reads the compiled `dist/**` output rather than importing the TS
|
|
11
|
+
// source directly: `lint` runs in CI after `build`, so `dist/` is authoritative.
|
|
12
|
+
// If `dist/` does not exist, we exit non-zero with a build hint.
|
|
13
|
+
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import safeRegex from 'safe-regex';
|
|
18
|
+
|
|
19
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const repoRoot = path.resolve(here, '..');
|
|
21
|
+
const distRoot = path.join(repoRoot, 'dist');
|
|
22
|
+
|
|
23
|
+
if (!existsSync(distRoot)) {
|
|
24
|
+
console.error('[lint:regex] dist/ not found — run `pnpm build` first.');
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const redactModulePath = path.join(distRoot, 'gateway', 'middleware', 'redact.js');
|
|
29
|
+
const injectionModulePath = path.join(distRoot, 'gateway', 'middleware', 'injection.js');
|
|
30
|
+
|
|
31
|
+
if (!existsSync(redactModulePath) || !existsSync(injectionModulePath)) {
|
|
32
|
+
console.error(
|
|
33
|
+
'[lint:regex] Expected dist outputs not found:\n' +
|
|
34
|
+
` - ${redactModulePath}\n` +
|
|
35
|
+
` - ${injectionModulePath}\n` +
|
|
36
|
+
'Run `pnpm build` first.',
|
|
37
|
+
);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const redact = await import(redactModulePath);
|
|
42
|
+
const injection = await import(injectionModulePath);
|
|
43
|
+
|
|
44
|
+
const offenders = [];
|
|
45
|
+
|
|
46
|
+
// Check every SECRET_PATTERN.
|
|
47
|
+
for (const { name, pattern } of redact.SECRET_PATTERNS) {
|
|
48
|
+
if (!safeRegex(pattern)) {
|
|
49
|
+
offenders.push({ layer: 'redact.SECRET_PATTERNS', name, pattern: pattern.toString() });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check injection regex constants.
|
|
54
|
+
const injectionPatterns = [
|
|
55
|
+
{ name: 'INJECTION_BASE64_PATTERN', pattern: injection.INJECTION_BASE64_PATTERN },
|
|
56
|
+
{ name: 'INJECTION_BASE64_SHAPE', pattern: injection.INJECTION_BASE64_SHAPE },
|
|
57
|
+
];
|
|
58
|
+
for (const { name, pattern } of injectionPatterns) {
|
|
59
|
+
if (!safeRegex(pattern)) {
|
|
60
|
+
offenders.push({ layer: 'injection', name, pattern: pattern.toString() });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (offenders.length > 0) {
|
|
65
|
+
console.error('[lint:regex] UNSAFE REGEX DETECTED:');
|
|
66
|
+
for (const o of offenders) {
|
|
67
|
+
console.error(` - ${o.layer} / ${o.name}: ${o.pattern}`);
|
|
68
|
+
}
|
|
69
|
+
console.error(
|
|
70
|
+
'\nSafe-regex flagged these patterns as potentially ReDoS-vulnerable. Rewrite them with' +
|
|
71
|
+
' bounded quantifiers / no nested repetition / no disjoint alternation, then re-run.',
|
|
72
|
+
);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(
|
|
77
|
+
`[lint:regex] OK — ${redact.SECRET_PATTERNS.length} redact patterns, ${injectionPatterns.length} injection patterns cleared.`,
|
|
78
|
+
);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* G12 — postinstall version-drift nudge.
|
|
4
|
+
*
|
|
5
|
+
* Printed exactly once when a consumer's `@bookedsolid/rea` installed version
|
|
6
|
+
* disagrees with the version recorded in their `.rea/install-manifest.json`.
|
|
7
|
+
* The message points at `rea upgrade`. NEVER fails the install — this script
|
|
8
|
+
* returns 0 under every code path.
|
|
9
|
+
*
|
|
10
|
+
* Silence rules:
|
|
11
|
+
* - Only runs when invoked from a consumer project (i.e. when this script
|
|
12
|
+
* lives inside `node_modules/@bookedsolid/rea/scripts/`). When running
|
|
13
|
+
* from the rea repo itself, the INIT_CWD/workspace check skips.
|
|
14
|
+
* - No output when CI (any common CI var set) — we don't want to spam build
|
|
15
|
+
* logs with governance reminders.
|
|
16
|
+
* - No output when a manifest does not exist (pre-G12 install) — `rea init`
|
|
17
|
+
* will write the first manifest; nothing to reconcile.
|
|
18
|
+
* - No output when versions match.
|
|
19
|
+
*
|
|
20
|
+
* Everything else is best-effort. Any error is swallowed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import process from 'node:process';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const NOTE = (lines) => {
|
|
29
|
+
process.stderr.write('\n');
|
|
30
|
+
for (const line of lines) process.stderr.write(`rea: ${line}\n`);
|
|
31
|
+
process.stderr.write('\n');
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** CI detection: check the union of vars that package managers and runners set.
|
|
35
|
+
* `CI=true` covers GitHub Actions / CircleCI / Travis / GitLab / BuildKite /
|
|
36
|
+
* Netlify / Vercel; the rest cover Jenkins, Buildkite agent, and npm's own
|
|
37
|
+
* `npm_config_ci`. A conservative default: silent if any is set. */
|
|
38
|
+
function isCI() {
|
|
39
|
+
const env = process.env;
|
|
40
|
+
if (env.CI === 'true' || env.CI === '1') return true;
|
|
41
|
+
if (env.CONTINUOUS_INTEGRATION === 'true' || env.CONTINUOUS_INTEGRATION === '1') return true;
|
|
42
|
+
if (env.BUILD_NUMBER !== undefined && env.BUILD_NUMBER !== '') return true; // Jenkins
|
|
43
|
+
if (env.RUN_ID !== undefined && env.RUN_ID !== '') return true;
|
|
44
|
+
if (env.GITHUB_ACTIONS === 'true') return true;
|
|
45
|
+
if (env.npm_config_ci === 'true') return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Resolve this script's directory without relying on `new URL().pathname`,
|
|
50
|
+
* which is broken on Windows (leading `/` + backslash decoding). */
|
|
51
|
+
function selfDir() {
|
|
52
|
+
return path.dirname(fileURLToPath(import.meta.url));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Rea-repo self-detection. Any of these means we're running inside the rea
|
|
56
|
+
* source tree (not a consumer install) and should be silent. */
|
|
57
|
+
function isOwnRepo(consumerRoot, selfPkgPath) {
|
|
58
|
+
try {
|
|
59
|
+
const consumerPkgPath = path.join(consumerRoot, 'package.json');
|
|
60
|
+
if (fs.existsSync(consumerPkgPath)) {
|
|
61
|
+
const consumerPkg = JSON.parse(fs.readFileSync(consumerPkgPath, 'utf8'));
|
|
62
|
+
if (consumerPkg?.name === '@bookedsolid/rea') return true;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
/* consumer package.json unreadable — fall through */
|
|
66
|
+
}
|
|
67
|
+
// If our own package.json lives directly under consumerRoot, the consumer
|
|
68
|
+
// IS the rea repo (no node_modules nesting). This covers `pnpm install` at
|
|
69
|
+
// the top of the rea repo where INIT_CWD and selfDir's parent match.
|
|
70
|
+
try {
|
|
71
|
+
const selfPkgDir = path.dirname(selfPkgPath);
|
|
72
|
+
if (path.resolve(selfPkgDir) === path.resolve(consumerRoot)) return true;
|
|
73
|
+
} catch {
|
|
74
|
+
/* fall through */
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (isCI()) process.exit(0);
|
|
81
|
+
|
|
82
|
+
// INIT_CWD is the directory where `npm/pnpm/yarn install` was invoked — i.e.
|
|
83
|
+
// the consumer project root. If it's unset, npm is either very old or we're
|
|
84
|
+
// running outside a package manager; either way, nothing to do.
|
|
85
|
+
const consumerRoot = process.env.INIT_CWD;
|
|
86
|
+
if (typeof consumerRoot !== 'string' || consumerRoot.length === 0) process.exit(0);
|
|
87
|
+
|
|
88
|
+
// `fileURLToPath` handles Windows (`file:///C:/...`) correctly, unlike the
|
|
89
|
+
// old `new URL(import.meta.url).pathname` which left a leading `/` on Win32.
|
|
90
|
+
const selfPkgPath = path.join(selfDir(), '..', 'package.json');
|
|
91
|
+
|
|
92
|
+
if (isOwnRepo(consumerRoot, selfPkgPath)) process.exit(0);
|
|
93
|
+
|
|
94
|
+
const manifestPath = path.join(consumerRoot, '.rea', 'install-manifest.json');
|
|
95
|
+
if (!fs.existsSync(manifestPath)) process.exit(0);
|
|
96
|
+
|
|
97
|
+
let installedVersion = null;
|
|
98
|
+
if (fs.existsSync(selfPkgPath)) {
|
|
99
|
+
try {
|
|
100
|
+
const selfPkg = JSON.parse(fs.readFileSync(selfPkgPath, 'utf8'));
|
|
101
|
+
if (typeof selfPkg?.version === 'string') installedVersion = selfPkg.version;
|
|
102
|
+
} catch {
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (installedVersion === null) process.exit(0);
|
|
107
|
+
|
|
108
|
+
let manifestVersion = null;
|
|
109
|
+
try {
|
|
110
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
111
|
+
if (typeof manifest?.version === 'string') manifestVersion = manifest.version;
|
|
112
|
+
} catch {
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
if (manifestVersion === null) process.exit(0);
|
|
116
|
+
|
|
117
|
+
if (manifestVersion === installedVersion) process.exit(0);
|
|
118
|
+
|
|
119
|
+
// Package-manager-agnostic message. Any of `npx rea upgrade`,
|
|
120
|
+
// `pnpm exec rea upgrade`, or `yarn rea upgrade` works; recommending `npx`
|
|
121
|
+
// covers the widest audience without privileging pnpm in error output.
|
|
122
|
+
NOTE([
|
|
123
|
+
`@bookedsolid/rea v${installedVersion} installed; manifest at v${manifestVersion}.`,
|
|
124
|
+
`Run \`npx rea upgrade\` to sync .claude/, .husky/, and managed fragments.`,
|
|
125
|
+
`(Or \`npx rea doctor --drift\` to preview without changes.)`,
|
|
126
|
+
]);
|
|
127
|
+
} catch {
|
|
128
|
+
// Any uncaught failure → silent success. Never break the consumer's install.
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
process.exit(0);
|