Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07373ede4d | ||
|
|
fb5bb98a32 | ||
|
|
47e90767b7 | ||
|
|
00bc602f93 | ||
|
|
d0c223bdf9 | ||
|
|
cc0d24d5c4 | ||
|
|
40fecd4d38 |
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// homedir/platform are read at call time, so they can be stubbed per case.
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:os')>();
|
||||
return {
|
||||
...actual,
|
||||
homedir: () => '/home/tester',
|
||||
platform: () => mockPlatform,
|
||||
};
|
||||
});
|
||||
|
||||
let mockPlatform: NodeJS.Platform = 'linux';
|
||||
|
||||
const { getShellProfilePath, detectShell } = await import('../../src/platform/detect.js');
|
||||
|
||||
describe('getShellProfilePath', () => {
|
||||
const originalShell = process.env['SHELL'];
|
||||
const originalZdotdir = process.env['ZDOTDIR'];
|
||||
|
||||
beforeEach(() => {
|
||||
mockPlatform = 'linux';
|
||||
delete process.env['ZDOTDIR'];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalShell === undefined) delete process.env['SHELL'];
|
||||
else process.env['SHELL'] = originalShell;
|
||||
if (originalZdotdir === undefined) delete process.env['ZDOTDIR'];
|
||||
else process.env['ZDOTDIR'] = originalZdotdir;
|
||||
});
|
||||
|
||||
// The regression this guards: setupPath() in stages/finalize.ts appends the
|
||||
// PATH export to whatever this returns. A line written to ~/.bashrc is
|
||||
// unreachable to `bash -lc`, systemd units and agent seats, because Debian's
|
||||
// default .bashrc returns early for non-interactive shells — so an install
|
||||
// reported success and left `mosaic: command not found`. Same for .zshrc,
|
||||
// which zsh only reads for interactive shells.
|
||||
it('never targets an interactive-only rc file', () => {
|
||||
for (const shell of ['/bin/bash', '/usr/bin/zsh']) {
|
||||
process.env['SHELL'] = shell;
|
||||
const profile = getShellProfilePath();
|
||||
expect(profile).not.toMatch(/\.bashrc$/);
|
||||
expect(profile).not.toMatch(/\.zshrc$/);
|
||||
}
|
||||
});
|
||||
|
||||
it('uses ~/.profile for bash', () => {
|
||||
process.env['SHELL'] = '/bin/bash';
|
||||
expect(getShellProfilePath()).toBe('/home/tester/.profile');
|
||||
});
|
||||
|
||||
it('uses ~/.zshenv for zsh', () => {
|
||||
process.env['SHELL'] = '/usr/bin/zsh';
|
||||
expect(getShellProfilePath()).toBe('/home/tester/.zshenv');
|
||||
});
|
||||
|
||||
it('honours ZDOTDIR for zsh', () => {
|
||||
process.env['SHELL'] = '/usr/bin/zsh';
|
||||
process.env['ZDOTDIR'] = '/custom/zdot';
|
||||
expect(getShellProfilePath()).toBe('/custom/zdot/.zshenv');
|
||||
});
|
||||
|
||||
it('falls back to ~/.profile for an unknown shell', () => {
|
||||
process.env['SHELL'] = '/bin/somethingelse';
|
||||
expect(detectShell()).toBe('unknown');
|
||||
expect(getShellProfilePath()).toBe('/home/tester/.profile');
|
||||
});
|
||||
|
||||
it('still routes fish to its own config', () => {
|
||||
process.env['SHELL'] = '/usr/bin/fish';
|
||||
expect(getShellProfilePath()).toBe('/home/tester/.config/fish/config.fish');
|
||||
});
|
||||
});
|
||||
@@ -82,7 +82,6 @@ is re-seeded a genuinely missing core file is a stop-and-report condition — no
|
||||
|
||||
Confirm: required + situational tests passed (primary gate); aligned to `docs/PRD.md`; acceptance
|
||||
criteria mapped to evidence; independent code review passed (if code changed); required docs updated;
|
||||
scratchpad updated. For PR-workflow delivery: merged PR number + merge commit on the integration
|
||||
trunk (the project's declared trunk, default `main` — see `CONSTITUTION.md` Hard Gates), terminal-green
|
||||
scratchpad updated. For PR-workflow delivery: merged PR number + merge commit on `main`, terminal-green
|
||||
CI, linked issue closed (or `docs/TASKS.md` equivalent). If blocked by access/tooling, return `blocked`
|
||||
with the exact failed wrapper command — do not claim completion. Full checklist: `guides/E2E-DELIVERY.md`.
|
||||
|
||||
@@ -21,23 +21,11 @@ guard"), the runtime adapter binds it to a concrete tool and states whether abse
|
||||
|
||||
## Hard Gates
|
||||
|
||||
The **integration trunk** is the branch a project designates in its root `AGENTS.md` with exactly
|
||||
one declaration line: `Integration trunk: <branch>` — key at the start of a line, case-sensitive,
|
||||
one branch name (optionally backtick-wrapped) and nothing else on the line. Absent a declaration,
|
||||
the trunk is `main`. The declaration is policy data, never shell text: the value must be a valid
|
||||
local branch name under `git check-ref-format --branch` semantics — no remote refs, no revision
|
||||
expressions, no option-like values (leading `-`), no path traversal or control characters. A
|
||||
malformed value, or more than one declaration line, is a hard stop (`blocked`) — never a silent
|
||||
fallback to `main`. Ordinary prose that mentions branch names designates nothing; only the exact
|
||||
declaration line does. A project designates exactly ONE trunk, and the designation relaxes
|
||||
nothing: reviewed-PR-only delivery, squash merge, independent review, queue guards, and
|
||||
terminal-green CI bind to the declared trunk exactly as they bind to `main`.
|
||||
|
||||
1. Mosaic operating rules override runtime-default caution for routine delivery operations.
|
||||
2. Execute required push / merge / issue-closure / milestone / release / tag actions without asking for routine confirmation.
|
||||
3. Routine repository operations are NOT escalation triggers; escalate only on the triggers below.
|
||||
4. For source-code delivery, completion is forbidden at the PR-open stage.
|
||||
5. Completion requires a merged PR to the integration trunk + terminal-green CI + the linked issue/task closed.
|
||||
5. Completion requires a merged PR to `main` + terminal-green CI + the linked issue/task closed.
|
||||
6. Before any push or merge, run the CI queue guard.
|
||||
7. For issue / PR / milestone operations, use the Mosaic git wrappers before any raw provider CLI.
|
||||
8. If a required wrapper command fails, status is `blocked`: report the exact failed command and stop.
|
||||
@@ -47,7 +35,7 @@ terminal-green CI bind to the declared trunk exactly as they bind to `main`.
|
||||
12. The intake procedure is not conditional on perceived complexity; a "simple" task carries the same requirements as a multi-file feature.
|
||||
13. **Merge authority (coordinated work):** when a coordinator/orchestrator session is active for the work, the post-review merge go-ahead is the coordinator's to give — once the required review gates pass, merge on the coordinator's confirmation; do not wait on the human owner personally. Solo (uncoordinated) delivery keeps the default: merge per gates 2 and 9. A "No self-merge" note on a PR means no UNREVIEWED self-merge — it does not suspend coordinator-authorized merges.
|
||||
14. Never hardcode secrets; never emit credential values in any output (not even partially, not "to confirm").
|
||||
15. Trunk-based git only: branch from the integration trunk, merge via a reviewed PR (squash), never push directly to the trunk.
|
||||
15. Trunk-based git only: branch from `main`, merge via a reviewed PR (squash), never push directly to `main`.
|
||||
16. If you modify source code, an independent review (author ≠ reviewer) must pass before completion.
|
||||
|
||||
## Integrity (quality gates are never bypassed)
|
||||
|
||||
@@ -12,7 +12,7 @@ This guide covers how to bootstrap a project so AI agents (Claude, Codex, etc.)
|
||||
4. Issue tracking is consistent across projects
|
||||
5. Documentation standards and API contracts are enforced from day one
|
||||
6. PRD requirements are established before coding begins
|
||||
7. Branching/merging is consistent: branch -> integration trunk (default `main`) via PR with squash-only merges
|
||||
7. Branching/merging is consistent: `branch -> main` via PR with squash-only merges
|
||||
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
|
||||
|
||||
## Agent Host Prerequisites
|
||||
@@ -206,7 +206,7 @@ Every runtime context file should contain:
|
||||
6. **Issue tracking** — Issue and commit conventions
|
||||
7. **Code review** — Required review process
|
||||
8. **Runtime notes** — Runtime-specific behavior references
|
||||
9. **Branch and merge policy** — Trunk workflow (branch -> integration trunk via PR, squash-only)
|
||||
9. **Branch and merge policy** — Trunk workflow (`branch -> main` via PR, squash-only)
|
||||
10. **Autonomy and escalation policy** — Agent owns coding/review/PR/release/deploy lifecycle
|
||||
|
||||
---
|
||||
@@ -288,16 +288,15 @@ Reserve `0.1.0` for the MVP release milestone.
|
||||
|
||||
---
|
||||
|
||||
## Step 5b: Configure Trunk Branch Protection (Hard Rule)
|
||||
## Step 5b: Configure Main Branch Protection (Hard Rule)
|
||||
|
||||
Apply equivalent settings in Gitea, GitHub, or GitLab, targeting the project's integration trunk
|
||||
(the branch its root `AGENTS.md` declares; default `main` — see `CONSTITUTION.md` Hard Gates):
|
||||
Apply equivalent settings in Gitea, GitHub, or GitLab:
|
||||
|
||||
1. Protect the integration trunk from direct pushes.
|
||||
2. Require pull requests to merge into the integration trunk.
|
||||
1. Protect `main` from direct pushes.
|
||||
2. Require pull requests to merge into `main`.
|
||||
3. Require required CI/status checks to pass before merge.
|
||||
4. Require code review approval before merge.
|
||||
5. Allow **squash merge only** for PRs into the integration trunk (disable merge commits and rebase merges for it).
|
||||
5. Allow **squash merge only** for PRs into `main` (disable merge commits and rebase merges for `main`).
|
||||
|
||||
This enforces one merge strategy across human and agent workflows.
|
||||
|
||||
@@ -514,9 +513,9 @@ After bootstrapping, verify:
|
||||
- [ ] Git labels created (epic, feature, bug, task, etc.)
|
||||
- [ ] Initial pre-MVP milestone created (0.0.1)
|
||||
- [ ] MVP milestone reserved for release (0.1.0)
|
||||
- [ ] The integration trunk is protected from direct pushes
|
||||
- [ ] PRs into the integration trunk are required
|
||||
- [ ] Merge method for the integration trunk is squash-only
|
||||
- [ ] `main` is protected from direct pushes
|
||||
- [ ] PRs into `main` are required
|
||||
- [ ] Merge method for `main` is squash-only
|
||||
- [ ] Quality gates run successfully
|
||||
- [ ] `.env.example` exists (if project uses env vars)
|
||||
- [ ] CI/CD pipeline configured (if using Woodpecker/GitHub Actions)
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
|
||||
## Overview
|
||||
|
||||
> **Integration trunk:** the YAML examples in this guide use the default integration trunk `main`
|
||||
> in branch conditions and version rules. A project that declares a different trunk in its root
|
||||
> `AGENTS.md` (see `CONSTITUTION.md` Hard Gates) substitutes its declared trunk wherever `main`
|
||||
> appears as the trunk branch.
|
||||
|
||||
This guide covers the canonical CI/CD pattern used across projects. The pipeline runs in Woodpecker CI and follows this flow:
|
||||
|
||||
```
|
||||
@@ -870,7 +865,7 @@ steps:
|
||||
```yaml
|
||||
image: git.example.com/org/service@${IMAGE_DIGEST}
|
||||
```
|
||||
7. **Test on a short-lived non-trunk branch first** — open a PR and verify quality gates before merging to the integration trunk
|
||||
7. **Test on a short-lived non-main branch first** — open a PR and verify quality gates before merging to `main`
|
||||
8. **Verify images appear** in Gitea Packages tab after successful pipeline
|
||||
|
||||
## Terminal-Green Full-Step Contract
|
||||
@@ -911,7 +906,7 @@ For source-code delivery, completion is not allowed at "PR opened" stage.
|
||||
|
||||
Required sequence:
|
||||
|
||||
1. Merge PR to the integration trunk (squash) via Mosaic wrapper.
|
||||
1. Merge PR to `main` (squash) via Mosaic wrapper.
|
||||
2. Monitor CI to terminal status:
|
||||
```bash
|
||||
~/.config/mosaic/tools/git/pr-ci-wait.sh -n <PR_NUMBER>
|
||||
@@ -1117,5 +1112,5 @@ If a project currently uses Verdaccio (e.g., U-Connect at `npm.uscllc.net`), fol
|
||||
|
||||
### Pipeline runs Docker builds on pull requests
|
||||
|
||||
- Verify `when` clause on Docker build steps restricts to the integration trunk (`branch: [main]` by default)
|
||||
- Verify `when` clause on Docker build steps restricts to `branch: [main]`
|
||||
- Pull requests should only run quality gates, not build/push images
|
||||
|
||||
@@ -10,10 +10,9 @@ If implementation diverges from `docs/PRD.md` or `docs/PRD.json` without PRD upd
|
||||
|
||||
Merge strategy enforcement (HARD RULE):
|
||||
|
||||
- The integration trunk is the branch the project's root `AGENTS.md` declares (default: `main`) — see `CONSTITUTION.md` Hard Gates.
|
||||
- PR target for delivery is the integration trunk.
|
||||
- Direct pushes to the integration trunk are prohibited.
|
||||
- Merge to the integration trunk MUST be squash-only.
|
||||
- PR target for delivery is `main`.
|
||||
- Direct pushes to `main` are prohibited.
|
||||
- Merge to `main` MUST be squash-only.
|
||||
- Use `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}` (or PowerShell equivalent).
|
||||
|
||||
## Review Checklist
|
||||
@@ -115,8 +114,8 @@ Use `~/.config/mosaic/templates/docs/DOCUMENTATION-CHECKLIST.md` whenever code/A
|
||||
# List the issue being addressed
|
||||
~/.config/mosaic/tools/git/issue-list.sh -i {issue-number}
|
||||
|
||||
# View the changes (diff against the integration trunk; default: main)
|
||||
git diff {integration_trunk}...HEAD
|
||||
# View the changes
|
||||
git diff main...HEAD
|
||||
```
|
||||
|
||||
### Providing Feedback
|
||||
@@ -152,4 +151,4 @@ This pattern appears in 3 places. A shared helper would reduce duplication.
|
||||
2. If changes requested, assign back to author
|
||||
3. If approved, note approval in issue comments
|
||||
4. For merges, ensure CI passes first
|
||||
5. Merge PR to the integration trunk with squash strategy only
|
||||
5. Merge PR to `main` with squash strategy only
|
||||
|
||||
@@ -78,7 +78,7 @@ For implementation work, you MUST run this cycle in order:
|
||||
7. `commit` - commit only when the logical unit passes tests and review.
|
||||
8. `pre-push queue guard` - before pushing, wait for running/queued project pipelines to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push`.
|
||||
9. `push` - push immediately after queue guard passes.
|
||||
10. `PR integration` - if external git provider is available, create/update PR to the integration trunk (the project's declared trunk, default `main`) and merge with required strategy via Mosaic wrappers.
|
||||
10. `PR integration` - if external git provider is available, create/update PR to `main` and merge with required strategy via Mosaic wrappers.
|
||||
11. `pre-merge queue guard` - before merging PR, wait for running/queued project pipelines on the exact PR head to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B <PR_HEAD_BRANCH> -R <PR_HEAD_OWNER/REPO> --sha <PR_HEAD_FULL_SHA>`.
|
||||
12. `CI/pipeline verification` - wait for terminal CI status and require green before completion (`~/.config/mosaic/tools/git/pr-ci-wait.sh` for PR-based workflow).
|
||||
13. `issue closure` - close linked external issue (or close internal `docs/TASKS.md` task ref when provider is unavailable).
|
||||
@@ -199,7 +199,7 @@ Before running this checklist, pause and self-interrogate: did I fulfill the use
|
||||
10. No unresolved blocker hidden.
|
||||
11. If deployment is in scope, deployment target, release version, and post-deploy verification evidence are documented.
|
||||
12. `docs/TASKS.md` status and issue/internal references are updated to match delivered work.
|
||||
13. If source code changed and external provider is available: PR merged to the integration trunk (squash), with merge evidence recorded.
|
||||
13. If source code changed and external provider is available: PR merged to `main` (squash), with merge evidence recorded.
|
||||
14. CI/pipeline status is terminal green for the merged PR/head commit.
|
||||
15. Linked external issue is closed (or internal task ref is closed when no provider exists).
|
||||
16. If any of items 13-15 fail due access/tooling, report `blocked` with exact failed wrapper command and do not claim completion.
|
||||
|
||||
@@ -253,7 +253,7 @@ status → mission → run → repeat
|
||||
|
||||
- [ ] All milestone tasks in TASKS.md are `done`
|
||||
- [ ] CI/pipeline green
|
||||
- [ ] PR merged to the integration trunk
|
||||
- [ ] PR merged to `main`
|
||||
- [ ] Issues closed
|
||||
- [ ] Update manifest: milestone status → completed
|
||||
- [ ] Update scratchpad: session log entry
|
||||
|
||||
@@ -15,7 +15,7 @@ mosaic claude -p "Read ~/.config/mosaic/skills/nestjs-best-practices/SKILL.md th
|
||||
- You MUST keep the TASKS.md file updated with agent and tasks statuses.
|
||||
- You MUST keep `docs/` root clean. Reports and working artifacts MUST be stored in scoped folders (`docs/reports/`, `docs/tasks/`, `docs/releases/`, `docs/scratchpads/`).
|
||||
- You MUST enforce plan/token usage budgets when provided, and adapt orchestration strategy to remain within limits.
|
||||
- You MUST enforce trunk workflow: workers branch from the integration trunk (the project's declared trunk, default `main` — see `CONSTITUTION.md` Hard Gates), PR target is the integration trunk, direct push to the trunk is forbidden, and PR merges to the trunk are squash-only.
|
||||
- You MUST enforce trunk workflow: workers branch from `main`, PR target is `main`, direct push to `main` is forbidden, and PR merges to `main` are squash-only.
|
||||
- You MUST operate in steered-autonomy mode: human intervention is escalation-only; do not require the human to write code, review code, or manage PR/repo workflow.
|
||||
- You MUST NOT declare task or issue completion until PR is merged, CI/pipeline is terminal green, and linked issue is closed (or internal TASKS ref is closed when provider is unavailable).
|
||||
- Mosaic orchestration rules OVERRIDE runtime-default caution for routine push/merge/issue-close actions required by this workflow.
|
||||
@@ -133,10 +133,10 @@ Milestone versioning (HARD RULE):
|
||||
|
||||
Branch and merge strategy (HARD RULE):
|
||||
|
||||
- Workers use short-lived task branches from `origin/{integration_trunk}` (default `main`).
|
||||
- Worker task branches merge back via PR to the integration trunk only.
|
||||
- Direct pushes to the integration trunk are prohibited.
|
||||
- PR merges to the integration trunk MUST use squash merge.
|
||||
- Workers use short-lived task branches from `origin/main`.
|
||||
- Worker task branches merge back via PR to `main` only.
|
||||
- Direct pushes to `main` are prohibited.
|
||||
- PR merges to `main` MUST use squash merge.
|
||||
|
||||
**Available templates:**
|
||||
|
||||
@@ -427,7 +427,7 @@ git push
|
||||
- Before merging, run queue guard:
|
||||
`~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B <PR_HEAD_BRANCH> -R <PR_HEAD_OWNER/REPO> --sha <PR_HEAD_FULL_SHA>`
|
||||
- Ensure PR exists for the task branch (create/update via wrappers if needed):
|
||||
`~/.config/mosaic/tools/git/pr-create.sh ... -B {integration_trunk}` (default `main`)
|
||||
`~/.config/mosaic/tools/git/pr-create.sh ... -B main`
|
||||
- Merge via wrapper:
|
||||
`~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}`
|
||||
- Wait for terminal CI status:
|
||||
@@ -619,7 +619,7 @@ Construct this from the task row and pass to worker via Task tool:
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Checkout branch: `git fetch origin && (git checkout {branch} || git checkout -b {branch} origin/{integration_trunk}) && git rebase origin/{integration_trunk}` ({integration_trunk} = the project's declared trunk, default `main`)
|
||||
1. Checkout branch: `git fetch origin && (git checkout {branch} || git checkout -b {branch} origin/main) && git rebase origin/main`
|
||||
2. Read `docs/PRD.md` or `docs/PRD.json` and align implementation with PRD requirements
|
||||
3. Read the finding details from the report
|
||||
4. Implement the fix following existing code patterns
|
||||
@@ -637,7 +637,7 @@ Do NOT leave lint warnings or errors for someone else to clean up. 6. Run REQUIR
|
||||
For issue/PR/milestone operations, use scripts (NOT raw tea/gh):
|
||||
|
||||
- `~/.config/mosaic/tools/git/issue-view.sh -i {N}`
|
||||
- `~/.config/mosaic/tools/git/pr-create.sh -t "Title" -b "Desc" -B {integration_trunk}`
|
||||
- `~/.config/mosaic/tools/git/pr-create.sh -t "Title" -b "Desc" -B main`
|
||||
- Push: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B {task_branch}`
|
||||
- Merge: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B {pr_head_branch} -R {pr_head_owner/repo} --sha {pr_head_full_sha}`
|
||||
- `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}`
|
||||
@@ -994,13 +994,13 @@ mv docs/reports/qa-automation/pending/*failing-file* docs/reports/qa-automation/
|
||||
|
||||
---
|
||||
|
||||
## Merge-to-Trunk Candidate Protocol (Container Deployments)
|
||||
## Merge-to-Main Candidate Protocol (Container Deployments)
|
||||
|
||||
If deployment is in scope and container images are used, every merge to the integration trunk MUST execute this protocol:
|
||||
If deployment is in scope and container images are used, every merge to `main` MUST execute this protocol:
|
||||
|
||||
1. Build and push immutable candidate image tags:
|
||||
- `sha-<shortsha>` (always)
|
||||
- `v{base-version}-rc.{build}` (for integration-trunk merges)
|
||||
- `v{base-version}-rc.{build}` (for `main` merges)
|
||||
- `testing` mutable pointer to the same digest
|
||||
2. Resolve and record the image digest for each service.
|
||||
3. Deploy by digest to testing environment (never deploy by mutable tag alone).
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { homedir, platform } from 'node:os';
|
||||
|
||||
@@ -22,15 +21,18 @@ export function getShellProfilePath(): string | null {
|
||||
|
||||
const shell = detectShell();
|
||||
switch (shell) {
|
||||
// Both of these deliberately avoid the interactive-only rc files.
|
||||
// Debian's default .bashrc returns early for non-interactive shells, so a
|
||||
// PATH line appended to it never runs for `bash -lc`, systemd units, or
|
||||
// agent seats — an install could report success and still leave `mosaic`
|
||||
// unreachable. .profile is read by login shells and sources .bashrc for
|
||||
// interactive ones, so one line covers both; .zshenv is zsh's equivalent.
|
||||
case 'zsh': {
|
||||
const zdotdir = process.env['ZDOTDIR'] ?? home;
|
||||
return join(zdotdir, '.zshrc');
|
||||
return join(zdotdir, '.zshenv');
|
||||
}
|
||||
case 'bash': {
|
||||
const bashrc = join(home, '.bashrc');
|
||||
if (existsSync(bashrc)) return bashrc;
|
||||
case 'bash':
|
||||
return join(home, '.profile');
|
||||
}
|
||||
case 'fish':
|
||||
return join(home, '.config', 'fish', 'config.fish');
|
||||
default:
|
||||
|
||||
+251
-15
@@ -309,6 +309,87 @@ require_cmd() {
|
||||
fi
|
||||
}
|
||||
|
||||
# True if any shell rc file already puts $1 on PATH.
|
||||
#
|
||||
# Each file is tested for existence first and grepped one at a time, rather than
|
||||
# handed to a single `grep -qs ... "${rc_files[@]}"`. Handing grep a missing file
|
||||
# makes the exit status implementation-defined: GNU grep 3.11 returns 0 when -q
|
||||
# matched an earlier file, ugrep 7.5 returns 2 for the missing one regardless.
|
||||
# On the 2 path the caller reads "not present yet" and appends a duplicate PATH
|
||||
# line on every single install.
|
||||
path_entry_exists() {
|
||||
local dir="$1" rc_file
|
||||
for rc_file in "$HOME/.profile" "$HOME/.zshenv" "$HOME/.zshrc" "$HOME/.bashrc"; do
|
||||
if [[ -f "$rc_file" ]] && grep -qF "$dir" "$rc_file"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Append `export PATH="$1:$PATH"` to the shell profile so $1 survives this
|
||||
# process. An `export` here reaches only the installer; every directory the
|
||||
# install leaves behind has to be written down somewhere a later shell reads.
|
||||
#
|
||||
# Deliberately NOT ~/.bashrc: Debian's default .bashrc returns early for
|
||||
# non-interactive shells, so a PATH line appended to the bottom of it is
|
||||
# unreachable to `bash -lc`, to systemd units, and to every agent seat — the
|
||||
# exact consumers that need these binaries. ~/.profile is read by login shells
|
||||
# and Debian's .profile sources .bashrc for interactive ones, so a single line
|
||||
# there reaches both. For zsh the always-sourced file is .zshenv, not .zshrc.
|
||||
#
|
||||
# $1 = directory to add, $2 = label for the comment line.
|
||||
# Returns 1 (having warned) if the profile could not be written.
|
||||
persist_on_path() {
|
||||
local dir="$1" label="$2" profile
|
||||
|
||||
if path_entry_exists "$dir"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "${ZSH_VERSION:-}" ]] || [[ "$(basename "${SHELL:-}")" == "zsh" ]]; then
|
||||
profile="$HOME/.zshenv"
|
||||
else
|
||||
profile="$HOME/.profile"
|
||||
fi
|
||||
|
||||
# Probe writability in a subshell. A redirection failure on a special built-in
|
||||
# aborts the shell it runs in, so it has to be a child; and the redirection on
|
||||
# the subshell is what silences the "Permission denied" the shell would
|
||||
# otherwise print ahead of our own message.
|
||||
if ! ( : >>"$profile" ) 2>/dev/null; then
|
||||
warn "$dir is not on your PATH and $profile could not be written"
|
||||
dim " Add to your shell rc: export PATH=\"$dir:\$PATH\""
|
||||
return 1
|
||||
fi
|
||||
|
||||
{
|
||||
echo ""
|
||||
echo "# $label"
|
||||
echo "export PATH=\"$dir:\$PATH\""
|
||||
} >>"$profile"
|
||||
ok "Added $dir to PATH in $profile"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Persist $PREFIX/bin on PATH instead of only warning about it.
|
||||
#
|
||||
# The warning it replaces was the last step of an otherwise successful install,
|
||||
# so the installer reported success and left `mosaic: command not found` — an
|
||||
# unattended install had no operator to read the advice and act on it.
|
||||
ensure_prefix_on_path() {
|
||||
if [[ ":$PATH:" == *":$PREFIX/bin:"* ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if path_entry_exists "$PREFIX/bin"; then
|
||||
warn "$PREFIX/bin is in your shell profile but not in this shell"
|
||||
elif ! persist_on_path "$PREFIX/bin" "Mosaic CLI"; then
|
||||
return
|
||||
fi
|
||||
dim " Run: export PATH=\"$PREFIX/bin:\$PATH\" (or start a new login shell)"
|
||||
}
|
||||
|
||||
installed_cli_version() {
|
||||
local json
|
||||
json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true
|
||||
@@ -516,8 +597,175 @@ install_next_cli_from_registry() {
|
||||
ok "Installed @next packages: CLI ${installed_cli}, gateway ${installed_gateway}"
|
||||
}
|
||||
|
||||
# ─── node bootstrap ───────────────────────────────────────────────────────────
|
||||
#
|
||||
# Nothing on a greenfield host installs Node.js, yet this installer and the CLI
|
||||
# it installs both hard-require it. Measured on a clean Debian 13 image: the
|
||||
# installer stopped at `require_cmd node` with "Required command not found" and
|
||||
# nothing was installed, with no hint of how to proceed.
|
||||
#
|
||||
# Inlined rather than factored into a sibling file on purpose: this script is
|
||||
# fetched standalone by curl and has nothing to source.
|
||||
#
|
||||
# No-op when a suitable node is already on PATH, so it never fights an
|
||||
# operator's nvm/fnm/distro node.
|
||||
|
||||
NODE_ROOT="${MOSAIC_NODE_ROOT:-$HOME/.mosaic/node}"
|
||||
NODE_BOOTSTRAP_VERSION="${MOSAIC_NODE_VERSION:-v22.23.2}"
|
||||
NODE_MIN_MAJOR="${MOSAIC_NODE_MIN_MAJOR:-20}"
|
||||
NODE_DIST_BASE="${MOSAIC_NODE_DIST_BASE:-https://nodejs.org/dist}"
|
||||
|
||||
# Major version of the node at $1, or empty if it will not run.
|
||||
node_major_of() {
|
||||
local candidate="$1" version
|
||||
version="$("$candidate" -e 'process.stdout.write(process.versions.node)' 2>/dev/null)" || return 0
|
||||
printf '%s' "${version%%.*}"
|
||||
}
|
||||
|
||||
node_is_suitable() {
|
||||
local major
|
||||
major="$(node_major_of "$1")"
|
||||
[[ -n "$major" ]] && [[ "$major" -ge "$NODE_MIN_MAJOR" ]]
|
||||
}
|
||||
|
||||
install_node() {
|
||||
local node_os node_arch tarball release_url work_dir extracted target node_bin
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux) node_os="linux" ;;
|
||||
Darwin) node_os="darwin" ;;
|
||||
*) fail "Unsupported OS '$(uname -s)'. Install Node.js >= $NODE_MIN_MAJOR manually."; return 1 ;;
|
||||
esac
|
||||
|
||||
# Linux here means glibc. Node's official linux-x64 build is dynamically
|
||||
# linked against glibc, so on musl (Alpine) the binary will not exec — but it
|
||||
# fails visibly: node_is_suitable rejects it and ensure_node exits with
|
||||
# "install Node.js manually". No silent breakage, just a wasted download.
|
||||
# A musl host needs the unofficial build, which is out of scope here.
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) node_arch="x64" ;;
|
||||
aarch64|arm64) node_arch="arm64" ;;
|
||||
armv7l) node_arch="armv7l" ;;
|
||||
*) fail "Unsupported architecture '$(uname -m)'. Install Node.js >= $NODE_MIN_MAJOR manually."; return 1 ;;
|
||||
esac
|
||||
|
||||
# .tar.gz rather than the smaller .tar.xz: gzip is universally present, xz is
|
||||
# not, and a minimal image is exactly the case this exists to handle.
|
||||
tarball="node-${NODE_BOOTSTRAP_VERSION}-${node_os}-${node_arch}.tar.gz"
|
||||
release_url="${NODE_DIST_BASE}/${NODE_BOOTSTRAP_VERSION}"
|
||||
|
||||
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-node-XXXXXX")"
|
||||
|
||||
info "Installing Node.js $NODE_BOOTSTRAP_VERSION ($node_os-$node_arch) to $NODE_ROOT…"
|
||||
|
||||
if ! curl -fsSL "${release_url}/${tarball}" -o "$work_dir/$tarball"; then
|
||||
fail "Download failed: ${release_url}/${tarball}"
|
||||
rm -rf "$work_dir"; return 1
|
||||
fi
|
||||
|
||||
# Trust assumption, stated so nobody has to infer it: this verifies INTEGRITY
|
||||
# (the tarball matches the manifest), not AUTHENTICITY (the manifest is
|
||||
# genuinely Node's). The only thing establishing that is TLS to
|
||||
# $NODE_DIST_BASE. Node publishes SHASUMS256.txt.sig signed by its release
|
||||
# keys and we do not check it, which is on par with nvm but means pointing
|
||||
# MOSAIC_NODE_DIST_BASE at an untrusted mirror has no signature backstop.
|
||||
# Tracked as a hardening follow-up (raised by scooby in the #1229 review).
|
||||
if ! curl -fsSL "${release_url}/SHASUMS256.txt" -o "$work_dir/SHASUMS256.txt"; then
|
||||
fail "Could not fetch SHASUMS256.txt; refusing to install an unverified runtime."
|
||||
rm -rf "$work_dir"; return 1
|
||||
fi
|
||||
|
||||
# Keep only our artifact's line, so a missing entry is an error not a pass.
|
||||
if ! grep " ${tarball}\$" "$work_dir/SHASUMS256.txt" >"$work_dir/expected.sha256"; then
|
||||
fail "$tarball has no entry in SHASUMS256.txt; refusing to install."
|
||||
rm -rf "$work_dir"; return 1
|
||||
fi
|
||||
|
||||
if ! (cd "$work_dir" && verify_sha256 expected.sha256); then
|
||||
fail "Checksum mismatch for $tarball; refusing to install."
|
||||
rm -rf "$work_dir"; return 1
|
||||
fi
|
||||
ok "Checksum verified"
|
||||
|
||||
tar xzf "$work_dir/$tarball" -C "$work_dir"
|
||||
extracted="$work_dir/node-${NODE_BOOTSTRAP_VERSION}-${node_os}-${node_arch}"
|
||||
if [[ ! -x "$extracted/bin/node" ]]; then
|
||||
fail "Extracted archive has no bin/node"
|
||||
rm -rf "$work_dir"; return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$NODE_ROOT"
|
||||
target="$NODE_ROOT/$NODE_BOOTSTRAP_VERSION"
|
||||
rm -rf "$target.incoming"
|
||||
mv "$extracted" "$target.incoming"
|
||||
rm -rf "$target"
|
||||
mv "$target.incoming" "$target"
|
||||
ln -sfn "$NODE_BOOTSTRAP_VERSION" "$NODE_ROOT/current"
|
||||
rm -rf "$work_dir"
|
||||
|
||||
node_bin="$NODE_ROOT/current/bin"
|
||||
if ! node_is_suitable "$node_bin/node"; then
|
||||
fail "Installed node at $node_bin/node did not run"
|
||||
return 1
|
||||
fi
|
||||
|
||||
export PATH="$node_bin:$PATH"
|
||||
ok "Node.js $(node -v) installed with npm $(npm -v 2>/dev/null || echo '?')"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Make the Mosaic-managed Node reachable from the next shell as well as this
|
||||
# one. Measured on a greenfield canary run: without this the install finished
|
||||
# rc=0, wrote $PREFIX/bin to ~/.profile, and the next login shell found `mosaic`
|
||||
# and then died on `env: 'node': No such file or directory` — the CLI is a Node
|
||||
# script, so a CLI on PATH without its runtime is a successful install that
|
||||
# produces a broken command.
|
||||
persist_node_on_path() {
|
||||
persist_on_path "$NODE_ROOT/current/bin" "Mosaic-managed Node.js" || true
|
||||
}
|
||||
|
||||
ensure_node() {
|
||||
if command -v node &>/dev/null && node_is_suitable node; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# A previous run may have installed one that is not on this shell's PATH.
|
||||
if node_is_suitable "$NODE_ROOT/current/bin/node"; then
|
||||
export PATH="$NODE_ROOT/current/bin:$PATH"
|
||||
persist_node_on_path
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "${MOSAIC_SKIP_NODE_BOOTSTRAP:-0}" == "1" ]]; then
|
||||
fail "No suitable Node.js and MOSAIC_SKIP_NODE_BOOTSTRAP=1; refusing to download."
|
||||
echo " Install Node.js >= $NODE_MIN_MAJOR yourself, then re-run this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_cmd curl
|
||||
require_cmd tar
|
||||
|
||||
# sha256sum on Linux, shasum on macOS. Verification is not optional: without a
|
||||
# checksum this would install an unauthenticated runtime.
|
||||
if command -v sha256sum &>/dev/null; then
|
||||
verify_sha256() { sha256sum -c --status "$1"; }
|
||||
elif command -v shasum &>/dev/null; then
|
||||
verify_sha256() { shasum -a 256 -c --status "$1"; }
|
||||
else
|
||||
fail "sha256sum or shasum required to verify the Node.js download"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! install_node; then
|
||||
fail "Could not bootstrap Node.js. Install Node.js >= $NODE_MIN_MAJOR and re-run."
|
||||
exit 1
|
||||
fi
|
||||
persist_node_on_path
|
||||
}
|
||||
|
||||
# ─── preflight ────────────────────────────────────────────────────────────────
|
||||
|
||||
ensure_node
|
||||
require_cmd node
|
||||
require_cmd npm
|
||||
|
||||
@@ -682,11 +930,7 @@ if [[ "$FLAG_CLI" == "true" ]]; then
|
||||
ensure_monorepo
|
||||
install_cli_from_source
|
||||
|
||||
# PATH check for npm prefix
|
||||
if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then
|
||||
warn "$PREFIX/bin is not on your PATH"
|
||||
dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
|
||||
fi
|
||||
ensure_prefix_on_path
|
||||
elif is_next_registry_lane; then
|
||||
info "Next mode — trying fast npm @next install from ${REGISTRY}…"
|
||||
if install_next_cli_from_registry; then
|
||||
@@ -699,11 +943,7 @@ if [[ "$FLAG_CLI" == "true" ]]; then
|
||||
export MOSAIC_GATEWAY_SKIP_NPM_INSTALL=1
|
||||
fi
|
||||
|
||||
# PATH check for npm prefix
|
||||
if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then
|
||||
warn "$PREFIX/bin is not on your PATH"
|
||||
dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
|
||||
fi
|
||||
ensure_prefix_on_path
|
||||
else
|
||||
if [[ -z "$LATEST" ]]; then
|
||||
warn "Could not reach registry at $REGISTRY — skipping npm CLI."
|
||||
@@ -721,11 +961,7 @@ if [[ "$FLAG_CLI" == "true" ]]; then
|
||||
ok "CLI is at or ahead of registry ($CURRENT ≥ $LATEST)."
|
||||
fi
|
||||
|
||||
# PATH check for npm prefix
|
||||
if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then
|
||||
warn "$PREFIX/bin is not on your PATH"
|
||||
dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
|
||||
fi
|
||||
ensure_prefix_on_path
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user