Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@

## Development

## Version 1.2.1

### Fixed

- The update notice no longer announces the version you are already running:
a cached answer is re-checked against the running version, so `self update`
is not followed by "a newer version is available (1.2.0 -> 1.2.0)".

## Version 1.2.0

### Added
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ailoud",
"version": "1.2.0",
"version": "1.2.1",
"type": "module",
"bin": {
"ailoud": "./dist/bin/ailoud.js"
Expand Down
61 changes: 61 additions & 0 deletions apps/cli/src/updateNotice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,67 @@ describe('startUpdateCheck', () => {
expect(await notice.finish()).toBeNull();
});

/**
* Reported from a real terminal: `self update` from 1.1.0 to 1.2.0, and the
* line right after it said "a newer version is available (1.2.0 -> 1.2.0)".
* The cache is keyed on nothing but time, so the answer written while 1.1.0
* was running stayed authoritative for a day after the version it named
* became the version in use.
*/
describe('a cached answer this run has already caught up with', () => {
const cacheWith = (target: string): MemFs =>
new MemFs({
[updateCachePath(DATA_DIR)]: JSON.stringify({
checkedAt: '2026-01-01T00:00:00.000Z',
target,
}),
});

it('says nothing when the cached target is the running version', async () => {
const deps = baseDeps({
fs: cacheWith('1.2.0'),
currentVersion: '1.2.0',
published: hangingPublished(),
});

expect(await startUpdateCheck(deps).finish()).toBeNull();
});

it('says nothing when the running version is newer than the cached target', async () => {
// A local build, or an install from a tarball ahead of the registry.
const deps = baseDeps({
fs: cacheWith('1.2.0'),
currentVersion: '1.3.0',
published: hangingPublished(),
});

expect(await startUpdateCheck(deps).finish()).toBeNull();
});

it('still speaks up when the cached target really is newer', async () => {
const deps = baseDeps({
fs: cacheWith('1.3.0'),
currentVersion: '1.2.0',
published: hangingPublished(),
});

expect(await startUpdateCheck(deps).finish()).toBe('1.3.0');
});

it('says nothing, rather than throwing, when it cannot read its own version', async () => {
// `chooseUpdateTarget` throws on a version it cannot parse, and this
// path runs outside the try that guards the fetch. A notice must never
// be the reason a command fails.
const deps = baseDeps({
fs: cacheWith('1.3.0'),
currentVersion: 'not-a-version',
published: hangingPublished(),
});

expect(await startUpdateCheck(deps).finish()).toBeNull();
});
});

it('prints from the cache when this run could not refresh it', async () => {
const fs = new MemFs({
[updateCachePath(DATA_DIR)]: JSON.stringify({
Expand Down
31 changes: 30 additions & 1 deletion apps/cli/src/updateNotice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,14 +257,43 @@ async function writeCache(deps: NoticeDeps, target: string | null): Promise<void
* the fetch is bounded, never the fetch's own right to keep running and to
* report a genuine answer whenever it actually settles.
*/
/**
* A cached answer, re-judged against the version running NOW.
*
* The cache is keyed on time alone, and a day is long enough to install the
* very version it names. Reported from a real terminal: `self update` moved
* 1.1.0 to 1.2.0 and the next line announced "a newer version is available
* (1.2.0 -> 1.2.0)", because the entry written while 1.1.0 ran stayed
* authoritative afterwards. The same applies to any install that happens
* outside ailoud -- `npm i -g ailoud` leaves no trace here to invalidate.
*
* `chooseUpdateTarget` again rather than a fresh comparison: whether a
* version counts as an upgrade is one policy (it also decides what a
* pre-release may move to), and asking the same function twice is what keeps
* the answer from drifting from the one the fetch path gives. `deprecated`
* is false because a deprecated release was already excluded when this
* target was chosen; the cache stores only the string that survived.
*
* Throwing is not an option: this runs outside the try that guards the fetch,
* and a notice must never be why a command failed.
*/
function stillNewer(deps: NoticeDeps, target: string | null): string | null {
if (target === null) return null;
try {
return chooseUpdateTarget(deps.currentVersion, [{ version: target, deprecated: false }]);
} catch {
return null;
}
}

export function startUpdateCheck(deps: NoticeDeps): UpdateCheck {
if (suppressed(deps)) return { finish: async () => null };

const controller = new AbortController();

const inflight = (async (): Promise<string | null> => {
const cached = await readCache(deps);
if (cached !== null) return cached.target;
if (cached !== null) return stillNewer(deps, cached.target);
try {
const versions = await deps.published(controller.signal);
const target = chooseUpdateTarget(deps.currentVersion, versions);
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ailoud-workspace",
"version": "1.2.0",
"version": "1.2.1",
"private": true,
"type": "module",
"description": "Multilingual audio-to-text CLI with a recording library and LLM summaries",
Expand Down Expand Up @@ -31,17 +31,17 @@
"devDependencies": {
"@eslint/js": "10.0.1",
"@types/jest": "30.0.0",
"@types/node": "26.2.0",
"@types/node": "26.3.0",
"@vitest/coverage-v8": "4.1.11",
"eslint": "10.9.0",
"eslint": "10.9.1",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-typescript": "4.4.5",
"eslint-plugin-boundaries": "7.2.0",
"jest": "30.4.2",
"prettier": "3.9.6",
"ts-jest": "29.4.12",
"typescript": "6.0.3",
"typescript-eslint": "8.67.0",
"typescript-eslint": "8.68.0",
"vitest": "4.1.11"
}
}
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ailoud/core",
"version": "1.2.0",
"version": "1.2.1",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/providers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ailoud/providers",
"version": "1.2.0",
"version": "1.2.1",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
Loading