Skip to content

Commit 1befed4

Browse files
refactor(update): move notifier policy to services
1 parent 84a5c30 commit 1befed4

13 files changed

Lines changed: 182 additions & 156 deletions

File tree

docs/intentional-architecture-rewrite-2026-06-27/status.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Branch: `codex/intentional-architecture-rewrite`
55

66
## Current State
77

8-
The branch has more than 250 committed rewrite commits past `dev`. The worklog records 211 production slices so far.
8+
The branch has more than 250 committed rewrite commits past `dev`. The worklog records 212 production slices so far.
99

1010
The diff is broad: more than 490 files changed, with tens of thousands of lines reshaped.
1111

@@ -53,6 +53,7 @@ This is no longer a small cleanup branch. It is a real ownership rewrite.
5353
- Moved update state and install-lock persistence into `src/stores/update/`; platform update modules now own registry/npm/version behavior, not JSON state-file mechanics.
5454
- Moved update registry/version/install mechanics behind a service-owned `UpdateRuntime` contract, with the real runtime composed in the CLI edge.
5555
- Moved update check/cache workflow into `src/services/update/check.ts`, registry fetch mechanics into `src/platform/update/check.ts`, and concrete runtime composition into `src/app/update-runtime.ts`.
56+
- Moved update notifier/banner eligibility into `src/services/update/notifier.ts`, CLI banner rendering into `src/edges/cli/update-announcement.ts`, and detached update-check process spawning into `src/platform/update/notifier-worker.ts`.
5657
- Moved GitHub source resolution mechanics into `src/platform/github/`.
5758
- Moved Absorb source resolver composition into `src/platform/sources/absorb.ts` and the CLI edge, so lifecycle Absorb services no longer import platform GitHub mechanics.
5859
- Moved Absorb source-ref and resolved-source contracts into `src/shared/absorb-sources.ts`, so platform source resolvers no longer import lifecycle service-internal Absorb files.
@@ -86,23 +87,23 @@ This is no longer a small cleanup branch. It is a real ownership rewrite.
8687

8788
## Latest Checkpoint
8889

89-
The latest slice moved update cache/check workflow out of platform and into `src/services/update/check.ts`, added `src/shared/update-runtime.ts` for update runtime contracts, replaced `src/platform/update/check.ts` with a registry-only latest-version fetcher, and added `src/app/update-runtime.ts` as the concrete composition point. Platform update modules no longer own update-state cache policy.
90+
The latest slice moved update notifier/banner eligibility and background-check scheduling policy out of platform and into `src/services/update/notifier.ts`, added a config-store sync read for startup-time notifier checks, moved banner rendering into `src/edges/cli/update-announcement.ts`, and reduced `src/platform/update/notifier-worker.ts` to detached process spawning.
9091

9192
Verification passed:
9293

9394
- `git diff --check`
9495
- `npm run lint`
95-
- `npx vitest run test/architecture-boundaries.test.ts test/update-check.test.ts test/update.test.ts test/update-install.test.ts test/update-store.test.ts test/update-announce.test.ts test/cli.test.ts`
96+
- `npx vitest run test/architecture-boundaries.test.ts test/update-announce.test.ts test/update.test.ts test/update-check.test.ts test/cli.test.ts test/config-command.test.ts test/doctor.test.ts`
9697
- `npm test`
9798
- `npm run build`
98-
- `node dist/launcher.js update --help | head -50`
99-
- `node dist/launcher.js doctor --json --install-only`
100-
- `node dist/launcher.js --help | head -25`
99+
- `node dist/launcher.js --help`
100+
- `node dist/launcher.js update --help`
101101
- `node dist/launcher.js update --check`
102+
- `node dist/launcher.js doctor --json --install-only`
102103

103104
## Immediate Next Work
104105

105-
Continue top-down subsystem passes before small leak cleanup. The major loose source buckets for jobs, init, config, wiki, viewer read models, worker entrypoints, serve process lifetime, setup/uninstall terminal UI, wiki file mechanics, automation scheduler mechanics, automation scheduler app composition, setup instruction runtime composition, provider setup-view ownership, job provider-runner composition, job-worker process spawning, Absorb source resolver composition, Absorb source contract ownership, prompt loader mechanics, update runtime composition, setup runtime composition, sync transcript runtime composition, sync-to-job session lookup, CLI app composition, diagnostic fact contracts, provider-neutral agent runtime contracts, lock process-liveness contracts, operation-spec type ownership, init prompt-context ownership, config command validation ownership, store atomic-write ownership, review command markdown ownership, and lifecycle workflow type ownership have now been removed or assigned. Remaining candidates include command files that still own workflow decisions, remaining platform modules that read config/store state directly, lifecycle/job boundary duplication that remains after the big moves, and large files whose size may still reflect mixed ownership.
106+
Continue top-down subsystem passes before small leak cleanup. The major loose source buckets for jobs, init, config, wiki, viewer read models, worker entrypoints, serve process lifetime, setup/uninstall terminal UI, wiki file mechanics, automation scheduler mechanics, automation scheduler app composition, setup instruction runtime composition, provider setup-view ownership, job provider-runner composition, job-worker process spawning, Absorb source resolver composition, Absorb source contract ownership, prompt loader mechanics, update runtime composition, update notifier ownership, setup runtime composition, sync transcript runtime composition, sync-to-job session lookup, CLI app composition, diagnostic fact contracts, provider-neutral agent runtime contracts, lock process-liveness contracts, operation-spec type ownership, init prompt-context ownership, config command validation ownership, store atomic-write ownership, review command markdown ownership, and lifecycle workflow type ownership have now been removed or assigned. Remaining candidates include command files that still own workflow decisions, remaining platform modules that read config/store state directly, lifecycle/job boundary duplication that remains after the big moves, and large files whose size may still reflect mixed ownership.
106107

107108
## Decision Log
108109

docs/intentional-architecture-rewrite-2026-06-27/worklog.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,3 +1472,12 @@ Two-hundred-eleventh production slice:
14721472
- Added `src/app/update-runtime.ts` as the concrete composition point for service check/cache workflow plus platform registry fetch, installed-version reads, and npm install mechanics.
14731473
- Deleted `src/platform/update/runtime.ts` and rewired CLI setup/update paths to use app update runtime composition.
14741474
- Strengthened architecture-boundary tests so platform update check no longer imports update stores and service update check no longer imports platform registry mechanics.
1475+
1476+
Two-hundred-twelfth production slice:
1477+
1478+
- Added `src/services/update/notifier.ts` as the service owner for update banner eligibility, notifier-enabled config reads, and background update-check scheduling policy.
1479+
- Added `readConfigSync` to `src/stores/config/store.ts` so startup-time config reads stay in store ownership instead of platform code parsing config files directly.
1480+
- Moved update banner rendering from `src/platform/update/announce.ts` into the CLI edge at `src/edges/cli/update-announcement.ts`.
1481+
- Added `src/edges/cli/update-check-scheduler.ts` to compose service scheduling policy with platform detached-process spawning.
1482+
- Reduced `src/platform/update/notifier-worker.ts` to process mechanics only and deleted `src/platform/update/announce.ts`.
1483+
- Strengthened architecture-boundary tests so platform update modules no longer import config or update stores for notifier behavior.

src/edges/cli/run.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import { runCodealmanacBootstrap } from "../../platform/install/global.js";
1616
import { isLocalPidAlive } from "../../platform/process.js";
1717
import type { runDoctor } from "../../cli/commands/doctor/index.js";
1818
import { runInternalUpdateCheck } from "../../app/update-runtime.js";
19-
import { announceUpdateIfAvailable } from "../../platform/update/announce.js";
20-
import { scheduleBackgroundUpdateCheck } from "../../platform/update/notifier-worker.js";
19+
import { announceUpdateIfAvailable } from "./update-announcement.js";
20+
import { scheduleBackgroundUpdateCheck } from "./update-check-scheduler.js";
2121

2222
/**
2323
* Optional dependency overrides for `run`. Tests use these to avoid
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { makeAnsiTheme } from "../../ansi-theme.js";
2+
import { readUpdateAnnouncement } from "../../services/update/index.js";
3+
import { readInstalledVersion } from "../../platform/update/version.js";
4+
5+
export interface AnnounceUpdateOptions {
6+
statePath?: string;
7+
configPath?: string;
8+
installedVersion?: string;
9+
color?: boolean;
10+
}
11+
12+
export function announceUpdateIfAvailable(
13+
stderr: NodeJS.WritableStream,
14+
opts: AnnounceUpdateOptions = {},
15+
): void {
16+
const announcement = readUpdateAnnouncement({
17+
statePath: opts.statePath,
18+
configPath: opts.configPath,
19+
installedVersion: opts.installedVersion ?? readInstalledVersion(),
20+
});
21+
if (announcement === null) return;
22+
23+
const useColor = opts.color ?? shouldUseStreamColor(stderr);
24+
const theme = makeAnsiTheme(useColor);
25+
const warn = useColor ? `${theme.YELLOW}${theme.BOLD}\u26a0${theme.RST}` : "!";
26+
const cmd = `${theme.BOLD}almanac update${theme.RST}`;
27+
stderr.write(
28+
`${warn} Almanac ${announcement.latestVersion} available ` +
29+
`(you're on ${announcement.installedVersion}) — run: ${cmd}\n`,
30+
);
31+
}
32+
33+
function shouldUseStreamColor(stream: NodeJS.WritableStream): boolean {
34+
return streamIsTTY(stream) && !("NO_COLOR" in process.env);
35+
}
36+
37+
function streamIsTTY(stream: NodeJS.WritableStream): boolean {
38+
return (stream as NodeJS.WritableStream & { isTTY?: boolean }).isTTY === true;
39+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { shouldScheduleUpdateCheck } from "../../services/update/index.js";
2+
import { spawnBackgroundUpdateCheck } from "../../platform/update/notifier-worker.js";
3+
4+
export function scheduleBackgroundUpdateCheck(argv: string[]): void {
5+
if (!shouldScheduleUpdateCheck({ argv, environment: process.env })) return;
6+
spawnBackgroundUpdateCheck(argv);
7+
}

src/platform/update/announce.ts

Lines changed: 0 additions & 106 deletions
This file was deleted.
Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
11
import { spawn } from "node:child_process";
2-
import { readFileSync } from "node:fs";
3-
4-
import {
5-
getConfigPath,
6-
getLegacyConfigPath,
7-
parseConfigText,
8-
} from "../../stores/config/index.js";
92

103
/**
114
* Post-command worker for the update-notifier cache.
@@ -16,9 +9,7 @@ import {
169
* live under `src/platform/automation/`.
1710
*/
1811

19-
export function scheduleBackgroundUpdateCheck(argv: string[]): void {
20-
if (!shouldSchedule(argv)) return;
21-
12+
export function spawnBackgroundUpdateCheck(argv: string[]): void {
2213
const scriptPath = argv[1];
2314
const nodeBin = process.execPath;
2415
if (scriptPath === undefined || scriptPath.length === 0) return;
@@ -38,28 +29,3 @@ export function scheduleBackgroundUpdateCheck(argv: string[]): void {
3829
// Background checks are best-effort.
3930
}
4031
}
41-
42-
function shouldSchedule(argv: string[]): boolean {
43-
if (process.env.CODEALMANAC_SKIP_UPDATE_CHECK === "1") return false;
44-
if (process.env.NODE_ENV === "test") return false;
45-
if (process.env.VITEST !== undefined) return false;
46-
if (argv.slice(2).includes("--internal-check-updates")) return false;
47-
if (!notifierEnabled()) return false;
48-
return true;
49-
}
50-
51-
function notifierEnabled(): boolean {
52-
const parsed = readConfigSync(getConfigPath());
53-
if (parsed !== null) return parsed.update_notifier !== false;
54-
const legacy = readConfigSync(getLegacyConfigPath());
55-
return legacy?.update_notifier !== false;
56-
}
57-
58-
function readConfigSync(path: string): { update_notifier?: unknown } | null {
59-
try {
60-
const raw = readFileSync(path, "utf8");
61-
return parseConfigText(raw, path) as { update_notifier?: unknown };
62-
} catch {
63-
return null;
64-
}
65-
}

src/services/update/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
export { runUpdateWorkflow } from "./update.js";
22
export { checkForUpdate, type CheckForUpdateOptions } from "./check.js";
3+
export {
4+
readUpdateAnnouncement,
5+
readUpdateNotifierEnabled,
6+
shouldScheduleUpdateCheck,
7+
type UpdateAnnouncement,
8+
type UpdateAnnouncementOptions,
9+
type UpdateCheckScheduleOptions,
10+
} from "./notifier.js";
311
export type {
412
UpdateCheckFn,
513
UpdateInstallFn,

src/services/update/notifier.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { isNewerVersion } from "../../shared/version.js";
2+
import { readConfigSync } from "../../stores/config/index.js";
3+
import { readStateSync } from "../../stores/update/index.js";
4+
5+
export interface UpdateAnnouncementOptions {
6+
installedVersion: string;
7+
statePath?: string;
8+
configPath?: string;
9+
}
10+
11+
export interface UpdateAnnouncement {
12+
installedVersion: string;
13+
latestVersion: string;
14+
}
15+
16+
export interface UpdateCheckScheduleOptions {
17+
argv: readonly string[];
18+
environment: NodeJS.ProcessEnv;
19+
configPath?: string;
20+
}
21+
22+
export function readUpdateAnnouncement(
23+
options: UpdateAnnouncementOptions,
24+
): UpdateAnnouncement | null {
25+
if (!readUpdateNotifierEnabled(options.configPath)) return null;
26+
27+
const state = readStateSync(options.statePath);
28+
if (state === null) return null;
29+
if (state.latest_version.length === 0) return null;
30+
if (!isNewerVersion(state.latest_version, options.installedVersion)) return null;
31+
if (state.dismissed_versions.includes(state.latest_version)) return null;
32+
33+
return {
34+
installedVersion: options.installedVersion,
35+
latestVersion: state.latest_version,
36+
};
37+
}
38+
39+
export function readUpdateNotifierEnabled(configPath?: string): boolean {
40+
return readConfigSync(configPath).update_notifier !== false;
41+
}
42+
43+
export function shouldScheduleUpdateCheck(
44+
options: UpdateCheckScheduleOptions,
45+
): boolean {
46+
if (options.environment.CODEALMANAC_SKIP_UPDATE_CHECK === "1") return false;
47+
if (options.environment.NODE_ENV === "test") return false;
48+
if (options.environment.VITEST !== undefined) return false;
49+
if (options.argv.slice(2).includes("--internal-check-updates")) return false;
50+
return readUpdateNotifierEnabled(options.configPath);
51+
}

src/stores/config/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export {
2525
export {
2626
ensureAutomationSyncSince,
2727
readConfig,
28+
readConfigSync,
2829
readConfigWithOrigins,
2930
writeConfig,
3031
type ConfigReadOptions,

0 commit comments

Comments
 (0)