Add v9.1.1: Device registry and remote blocking system

- Implement DeviceRegistry for remote device management
- Add dashboard for device tracking and blocking
- Remote device blocking capability with admin control
- Support for device aliasing and notes
- Enhanced device management interface
- Dashboard with real-time device status
- Configurable registry URL in build config

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
renato97
2025-11-23 21:37:11 +01:00
parent cf11aa04bc
commit 8fc8991efa
925 changed files with 97082 additions and 2 deletions

View File

@@ -0,0 +1,12 @@
const borderedText = (text: string) => {
const lines = text.split('\n');
const width = Math.max(...lines.map((l) => l.length));
const res = [`${'─'.repeat(width + 2)}`];
for (const line of lines) {
res.push(`${line.padEnd(width)}`);
}
res.push(`${'─'.repeat(width + 2)}`);
return res.join('\n');
};
export default borderedText;

View File

@@ -0,0 +1,17 @@
import { createConfigDir, getLastUpdate, saveLastUpdate } from './cache';
createConfigDir();
jest.useFakeTimers().setSystemTime(new Date('2022-01-01'));
const fakeTime = new Date('2022-01-01').getTime();
test('can save update then get the update details', () => {
saveLastUpdate('test');
expect(getLastUpdate('test')).toBe(fakeTime);
});
test('prefixed module can save update then get the update details', () => {
saveLastUpdate('@alexbrazier/test');
expect(getLastUpdate('@alexbrazier/test')).toBe(fakeTime);
});

View File

@@ -0,0 +1,44 @@
import os from 'os';
import path from 'path';
import fs from 'fs';
const homeDirectory = os.homedir();
const configDir =
process.env.XDG_CONFIG_HOME ||
path.join(homeDirectory, '.config', 'simple-update-notifier');
const getConfigFile = (packageName: string) => {
return path.join(
configDir,
`${packageName.replace('@', '').replace('/', '__')}.json`
);
};
export const createConfigDir = () => {
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
};
export const getLastUpdate = (packageName: string) => {
const configFile = getConfigFile(packageName);
try {
if (!fs.existsSync(configFile)) {
return undefined;
}
const file = JSON.parse(fs.readFileSync(configFile, 'utf8'));
return file.lastUpdateCheck as number;
} catch {
return undefined;
}
};
export const saveLastUpdate = (packageName: string) => {
const configFile = getConfigFile(packageName);
fs.writeFileSync(
configFile,
JSON.stringify({ lastUpdateCheck: new Date().getTime() })
);
};

View File

@@ -0,0 +1,35 @@
import Stream from 'stream';
import https from 'https';
import getDistVersion from './getDistVersion';
jest.mock('https', () => ({
get: jest.fn(),
}));
test('Valid response returns version', async () => {
const st = new Stream();
(https.get as jest.Mock).mockImplementation((url, cb) => {
cb(st);
st.emit('data', '{"latest":"1.0.0"}');
st.emit('end');
});
const version = await getDistVersion('test', 'latest');
expect(version).toEqual('1.0.0');
});
test('Invalid response throws error', async () => {
const st = new Stream();
(https.get as jest.Mock).mockImplementation((url, cb) => {
cb(st);
st.emit('data', 'some invalid json');
st.emit('end');
});
expect(getDistVersion('test', 'latest')).rejects.toThrow(
'Could not parse version response'
);
});

View File

@@ -0,0 +1,29 @@
import https from 'https';
const getDistVersion = async (packageName: string, distTag: string) => {
const url = `https://registry.npmjs.org/-/package/${packageName}/dist-tags`;
return new Promise<string>((resolve, reject) => {
https
.get(url, (res) => {
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => {
try {
const json = JSON.parse(body);
const version = json[distTag];
if (!version) {
reject(new Error('Error getting version'));
}
resolve(version);
} catch {
reject(new Error('Could not parse version response'));
}
});
})
.on('error', (err) => reject(err));
});
};
export default getDistVersion;

View File

@@ -0,0 +1,82 @@
import hasNewVersion from './hasNewVersion';
import { getLastUpdate } from './cache';
import getDistVersion from './getDistVersion';
jest.mock('./getDistVersion', () => jest.fn().mockReturnValue('1.0.0'));
jest.mock('./cache', () => ({
getLastUpdate: jest.fn().mockReturnValue(undefined),
createConfigDir: jest.fn(),
saveLastUpdate: jest.fn(),
}));
const pkg = { name: 'test', version: '1.0.0' };
afterEach(() => jest.clearAllMocks());
const defaultArgs = {
pkg,
shouldNotifyInNpmScript: true,
alwaysRun: true,
};
test('it should not trigger update for same version', async () => {
const newVersion = await hasNewVersion(defaultArgs);
expect(newVersion).toBe(false);
});
test('it should trigger update for patch version bump', async () => {
(getDistVersion as jest.Mock).mockReturnValue('1.0.1');
const newVersion = await hasNewVersion(defaultArgs);
expect(newVersion).toBe('1.0.1');
});
test('it should trigger update for minor version bump', async () => {
(getDistVersion as jest.Mock).mockReturnValue('1.1.0');
const newVersion = await hasNewVersion(defaultArgs);
expect(newVersion).toBe('1.1.0');
});
test('it should trigger update for major version bump', async () => {
(getDistVersion as jest.Mock).mockReturnValue('2.0.0');
const newVersion = await hasNewVersion(defaultArgs);
expect(newVersion).toBe('2.0.0');
});
test('it should not trigger update if version is lower', async () => {
(getDistVersion as jest.Mock).mockReturnValue('0.0.9');
const newVersion = await hasNewVersion(defaultArgs);
expect(newVersion).toBe(false);
});
it('should trigger update check if last update older than config', async () => {
const TWO_WEEKS = new Date().getTime() - 1000 * 60 * 60 * 24 * 14;
(getLastUpdate as jest.Mock).mockReturnValue(TWO_WEEKS);
const newVersion = await hasNewVersion({
pkg,
shouldNotifyInNpmScript: true,
});
expect(newVersion).toBe(false);
expect(getDistVersion).toHaveBeenCalled();
});
it('should not trigger update check if last update is too recent', async () => {
const TWELVE_HOURS = new Date().getTime() - 1000 * 60 * 60 * 12;
(getLastUpdate as jest.Mock).mockReturnValue(TWELVE_HOURS);
const newVersion = await hasNewVersion({
pkg,
shouldNotifyInNpmScript: true,
});
expect(newVersion).toBe(false);
expect(getDistVersion).not.toHaveBeenCalled();
});

View File

@@ -0,0 +1,40 @@
import semver from 'semver';
import { createConfigDir, getLastUpdate, saveLastUpdate } from './cache';
import getDistVersion from './getDistVersion';
import { IUpdate } from './types';
const hasNewVersion = async ({
pkg,
updateCheckInterval = 1000 * 60 * 60 * 24,
distTag = 'latest',
alwaysRun,
debug,
}: IUpdate) => {
createConfigDir();
const lastUpdateCheck = getLastUpdate(pkg.name);
if (
alwaysRun ||
!lastUpdateCheck ||
lastUpdateCheck < new Date().getTime() - updateCheckInterval
) {
const latestVersion = await getDistVersion(pkg.name, distTag);
saveLastUpdate(pkg.name);
if (semver.gt(latestVersion, pkg.version)) {
return latestVersion;
} else if (debug) {
console.error(
`Latest version (${latestVersion}) not newer than current version (${pkg.version})`
);
}
} else if (debug) {
console.error(
`Too recent to check for a new update. simpleUpdateNotifier() interval set to ${updateCheckInterval}ms but only ${
new Date().getTime() - lastUpdateCheck
}ms since last check.`
);
}
return false;
};
export default hasNewVersion;

View File

@@ -0,0 +1,27 @@
import simpleUpdateNotifier from '.';
import hasNewVersion from './hasNewVersion';
const consoleSpy = jest.spyOn(console, 'error');
jest.mock('./hasNewVersion', () => jest.fn().mockResolvedValue('2.0.0'));
beforeEach(jest.clearAllMocks);
test('it logs message if update is available', async () => {
await simpleUpdateNotifier({
pkg: { name: 'test', version: '1.0.0' },
alwaysRun: true,
});
expect(consoleSpy).toHaveBeenCalledTimes(1);
});
test('it does not log message if update is not available', async () => {
(hasNewVersion as jest.Mock).mockResolvedValue(false);
await simpleUpdateNotifier({
pkg: { name: 'test', version: '2.0.0' },
alwaysRun: true,
});
expect(consoleSpy).toHaveBeenCalledTimes(0);
});

View File

@@ -0,0 +1,34 @@
import isNpmOrYarn from './isNpmOrYarn';
import hasNewVersion from './hasNewVersion';
import { IUpdate } from './types';
import borderedText from './borderedText';
const simpleUpdateNotifier = async (args: IUpdate) => {
if (
!args.alwaysRun &&
(!process.stdout.isTTY || (isNpmOrYarn && !args.shouldNotifyInNpmScript))
) {
if (args.debug) {
console.error('Opting out of running simpleUpdateNotifier()');
}
return;
}
try {
const latestVersion = await hasNewVersion(args);
if (latestVersion) {
console.error(
borderedText(`New version of ${args.pkg.name} available!
Current Version: ${args.pkg.version}
Latest Version: ${latestVersion}`)
);
}
} catch (err) {
// Catch any network errors or cache writing errors so module doesn't cause a crash
if (args.debug && err instanceof Error) {
console.error('Unexpected error in simpleUpdateNotifier():', err);
}
}
};
export default simpleUpdateNotifier;

View File

@@ -0,0 +1,12 @@
import process from 'process';
const packageJson = process.env.npm_package_json;
const userAgent = process.env.npm_config_user_agent;
const isNpm6 = Boolean(userAgent && userAgent.startsWith('npm'));
const isNpm7 = Boolean(packageJson && packageJson.endsWith('package.json'));
const isNpm = isNpm6 || isNpm7;
const isYarn = Boolean(userAgent && userAgent.startsWith('yarn'));
const isNpmOrYarn = isNpm || isYarn;
export default isNpmOrYarn;

View File

@@ -0,0 +1,8 @@
export interface IUpdate {
pkg: { name: string; version: string };
updateCheckInterval?: number;
shouldNotifyInNpmScript?: boolean;
distTag?: string;
alwaysRun?: boolean;
debug?: boolean;
}