diff --git a/.gitignore b/.gitignore index f9e7a8c..1ca61a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ coverage/ .DS_Store +package-lock.json diff --git a/dist/index.js b/dist/index.js index f2b88de..60afb9e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -4,6 +4,9 @@ const VALID_STATES = new Set(['off', 'running', 'warning', 'error']); function asString(value) { return typeof value === 'string' && value.length > 0 ? value : undefined; } +function asNumber(value) { + return typeof value === 'number' && Number.isFinite(value) ? value : undefined; +} function asState(value) { return typeof value === 'string' && VALID_STATES.has(value) ? value @@ -18,6 +21,45 @@ function runtimePid() { const pid = globalThis.process?.pid; return typeof pid === 'number' ? String(pid) : 'unknown'; } +function extractPortFromUrl(url) { + if (!url) + return undefined; + try { + const parsed = new URL(url); + return parsed.port ? parsed.port : undefined; + } + catch { + const match = url.match(/:(\d+)(?:\/|$)/); + return match?.[1]; + } +} +function stateLabel(state, locale) { + if (locale === 'fr') { + if (state === 'running') + return 'En cours'; + if (state === 'warning') + return 'Avertissement'; + return 'Erreur'; + } + if (state === 'running') + return 'Running'; + if (state === 'warning') + return 'Warning'; + return 'Error'; +} +function relativeAge(updatedAt, now, locale) { + const seconds = Math.max(0, Math.round((now - updatedAt) / 1000)); + if (locale === 'fr') { + if (seconds < 60) + return `Maj : il y a ${seconds}s`; + const minutes = Math.round(seconds / 60); + return `Maj : il y a ${minutes}min`; + } + if (seconds < 60) + return `Updated ${seconds}s ago`; + const minutes = Math.round(seconds / 60); + return `Updated ${minutes}min ago`; +} function parseTrackedStatus(value) { const record = asRecord(value); if (!record) @@ -25,11 +67,18 @@ function parseTrackedStatus(value) { const state = asState(record.state); if (!state) return undefined; + const updatedAt = asNumber(record.updatedAt); + if (updatedAt === undefined) + return undefined; const errorMessage = asString(record.errorMessage); + const command = asString(record.command); + const url = asString(record.url) ?? null; return { state, - url: asString(record.url) ?? null, + url, + updatedAt, ...(errorMessage ? { errorMessage } : {}), + ...(command ? { command } : {}), }; } function loadRuntimeState(context) { @@ -62,38 +111,81 @@ function loadRuntimeState(context) { function persistRuntimeState(context, states) { context.storage.set(STORAGE_STATES_KEY, JSON.stringify(Object.fromEntries(states))); } -function toBadgeState(status) { +function buildTooltip(segments) { + return segments.filter((s) => typeof s === 'string' && s.length > 0).join(' · '); +} +function toBadgeState(status, now) { if (!status || status.state === 'off') return { visible: false }; + const port = extractPortFromUrl(status.url); + const value = port; if (status.state === 'running') { - const suffix = status.url ? ` — ${status.url}` : ''; + const urlSegment = status.url ?? undefined; return { + ...(value ? { value } : {}), visible: true, tone: 'success', tooltip: { - en: `Dev server running${suffix}`, - fr: `Serveur de développement actif${suffix}`, + en: buildTooltip([ + stateLabel('running', 'en'), + urlSegment, + relativeAge(status.updatedAt, now, 'en'), + ]), + fr: buildTooltip([ + stateLabel('running', 'fr'), + urlSegment, + relativeAge(status.updatedAt, now, 'fr'), + ]), }, }; } if (status.state === 'warning') { - const suffix = status.errorMessage ? ` — ${status.errorMessage}` : ''; + const urlSegment = status.url ?? undefined; + const errorSegment = status.errorMessage; return { + ...(value ? { value } : {}), visible: true, tone: 'warning', tooltip: { - en: `Dev server warning${suffix}`, - fr: `Avertissement serveur de développement${suffix}`, + en: buildTooltip([ + stateLabel('warning', 'en'), + urlSegment, + errorSegment, + relativeAge(status.updatedAt, now, 'en'), + ]), + fr: buildTooltip([ + stateLabel('warning', 'fr'), + urlSegment, + errorSegment, + relativeAge(status.updatedAt, now, 'fr'), + ]), }, }; } - const suffix = status.errorMessage ? ` — ${status.errorMessage}` : ''; + const urlSegment = status.url ?? undefined; + const errorSegment = status.errorMessage; + const commandSegment = status.command; return { + ...(value ? { value } : {}), visible: true, tone: 'danger', tooltip: { - en: `Dev server error${suffix}`, - fr: `Erreur serveur de développement${suffix}`, + en: buildTooltip([ + stateLabel('error', 'en'), + urlSegment, + errorSegment, + commandSegment, + 'URL may not be reachable', + relativeAge(status.updatedAt, now, 'en'), + ]), + fr: buildTooltip([ + stateLabel('error', 'fr'), + urlSegment, + errorSegment, + commandSegment, + 'URL peut ne pas être joignable', + relativeAge(status.updatedAt, now, 'fr'), + ]), }, }; } @@ -122,7 +214,7 @@ export function register(registry) { const workdir = asString(rpcContext.workdir); if (!workdir) return { visible: false }; - return toBadgeState(states.get(workdir)); + return toBadgeState(states.get(workdir), Date.now()); }); registry.registerHook('devserver.state.changed', (payload) => { const workdir = asString(payload.data.workdir); @@ -130,9 +222,12 @@ export function register(registry) { if (!workdir || !state) return; const errorMessage = asString(payload.data.errorMessage); + const previous = states.get(workdir); update(workdir, { state, url: asString(payload.data.url) ?? null, + updatedAt: Date.now(), + ...(previous?.command ? { command: previous.command } : {}), ...(errorMessage ? { errorMessage } : {}), }); }); @@ -140,9 +235,12 @@ export function register(registry) { const workdir = asString(payload.data.workdir); if (!workdir) return; + const command = asString(payload.data.command); update(workdir, { state: 'running', url: asString(payload.data.url) ?? null, + updatedAt: Date.now(), + ...(command ? { command } : {}), }); }); registry.registerHook('devserver.stopped', (payload) => { @@ -150,11 +248,15 @@ export function register(registry) { if (!workdir) return; const reason = asString(payload.data.reason); + const previous = states.get(workdir); + const command = previous?.command; if (reason === 'error') { const errorMessage = asString(payload.data.error); update(workdir, { state: 'error', url: asString(payload.data.url) ?? null, + updatedAt: Date.now(), + ...(command ? { command } : {}), ...(errorMessage ? { errorMessage } : {}), }); return; @@ -162,6 +264,8 @@ export function register(registry) { update(workdir, { state: 'off', url: asString(payload.data.url) ?? null, + updatedAt: Date.now(), + ...(command ? { command } : {}), }); }); } diff --git a/dist/index.js.map b/dist/index.js.map index 4e990ec..28fd360 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index d6a82c1..d808df9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,8 @@ interface TrackedStatus { state: DevServerState url?: string | null errorMessage?: string + command?: string + updatedAt: number } const STORAGE_PID_KEY = 'runtimePid' @@ -20,6 +22,10 @@ function asString(value: unknown): string | undefined { return typeof value === 'string' && value.length > 0 ? value : undefined } +function asNumber(value: unknown): number | undefined { + return typeof value === 'number' && Number.isFinite(value) ? value : undefined +} + function asState(value: unknown): DevServerState | undefined { return typeof value === 'string' && VALID_STATES.has(value as DevServerState) ? (value as DevServerState) @@ -37,6 +43,43 @@ function runtimePid(): string { return typeof pid === 'number' ? String(pid) : 'unknown' } +function extractPortFromUrl(url: string | null | undefined): string | undefined { + if (!url) return undefined + try { + const parsed = new URL(url) + return parsed.port ? parsed.port : undefined + } catch { + const match = url.match(/:(\d+)(?:\/|$)/) + return match?.[1] + } +} + +function stateLabel( + state: DevServerState, + locale: 'en' | 'fr', +): string { + if (locale === 'fr') { + if (state === 'running') return 'En cours' + if (state === 'warning') return 'Avertissement' + return 'Erreur' + } + if (state === 'running') return 'Running' + if (state === 'warning') return 'Warning' + return 'Error' +} + +function relativeAge(updatedAt: number, now: number, locale: 'en' | 'fr'): string { + const seconds = Math.max(0, Math.round((now - updatedAt) / 1000)) + if (locale === 'fr') { + if (seconds < 60) return `Maj : il y a ${seconds}s` + const minutes = Math.round(seconds / 60) + return `Maj : il y a ${minutes}min` + } + if (seconds < 60) return `Updated ${seconds}s ago` + const minutes = Math.round(seconds / 60) + return `Updated ${minutes}min ago` +} + function parseTrackedStatus(value: unknown): TrackedStatus | undefined { const record = asRecord(value) if (!record) return undefined @@ -44,11 +87,19 @@ function parseTrackedStatus(value: unknown): TrackedStatus | undefined { const state = asState(record.state) if (!state) return undefined + const updatedAt = asNumber(record.updatedAt) + if (updatedAt === undefined) return undefined + const errorMessage = asString(record.errorMessage) + const command = asString(record.command) + const url = asString(record.url) ?? null + return { state, - url: asString(record.url) ?? null, + url, + updatedAt, ...(errorMessage ? { errorMessage } : {}), + ...(command ? { command } : {}), } } @@ -84,40 +135,88 @@ function persistRuntimeState(context: PluginContext, states: Map typeof s === 'string' && s.length > 0).join(' · ') +} + +function toBadgeState( + status: TrackedStatus | undefined, + now: number, +): PluginUiBadgeDynamicState { if (!status || status.state === 'off') return { visible: false } + const port = extractPortFromUrl(status.url) + const value = port + if (status.state === 'running') { - const suffix = status.url ? ` — ${status.url}` : '' + const urlSegment = status.url ?? undefined return { + ...(value ? { value } : {}), visible: true, tone: 'success', tooltip: { - en: `Dev server running${suffix}`, - fr: `Serveur de développement actif${suffix}`, + en: buildTooltip([ + stateLabel('running', 'en'), + urlSegment, + relativeAge(status.updatedAt, now, 'en'), + ]), + fr: buildTooltip([ + stateLabel('running', 'fr'), + urlSegment, + relativeAge(status.updatedAt, now, 'fr'), + ]), }, } } if (status.state === 'warning') { - const suffix = status.errorMessage ? ` — ${status.errorMessage}` : '' + const urlSegment = status.url ?? undefined + const errorSegment = status.errorMessage return { + ...(value ? { value } : {}), visible: true, tone: 'warning', tooltip: { - en: `Dev server warning${suffix}`, - fr: `Avertissement serveur de développement${suffix}`, + en: buildTooltip([ + stateLabel('warning', 'en'), + urlSegment, + errorSegment, + relativeAge(status.updatedAt, now, 'en'), + ]), + fr: buildTooltip([ + stateLabel('warning', 'fr'), + urlSegment, + errorSegment, + relativeAge(status.updatedAt, now, 'fr'), + ]), }, } } - const suffix = status.errorMessage ? ` — ${status.errorMessage}` : '' + const urlSegment = status.url ?? undefined + const errorSegment = status.errorMessage + const commandSegment = status.command return { + ...(value ? { value } : {}), visible: true, tone: 'danger', tooltip: { - en: `Dev server error${suffix}`, - fr: `Erreur serveur de développement${suffix}`, + en: buildTooltip([ + stateLabel('error', 'en'), + urlSegment, + errorSegment, + commandSegment, + 'URL may not be reachable', + relativeAge(status.updatedAt, now, 'en'), + ]), + fr: buildTooltip([ + stateLabel('error', 'fr'), + urlSegment, + errorSegment, + commandSegment, + 'URL peut ne pas être joignable', + relativeAge(status.updatedAt, now, 'fr'), + ]), }, } } @@ -149,7 +248,7 @@ export function register(registry: PluginRegistry): void { registry.registerRpc('status', async (_params, rpcContext) => { const workdir = asString(rpcContext.workdir) if (!workdir) return { visible: false } - return toBadgeState(states.get(workdir)) + return toBadgeState(states.get(workdir), Date.now()) }) registry.registerHook('devserver.state.changed', (payload) => { @@ -158,9 +257,12 @@ export function register(registry: PluginRegistry): void { if (!workdir || !state) return const errorMessage = asString(payload.data.errorMessage) + const previous = states.get(workdir) update(workdir, { state, url: asString(payload.data.url) ?? null, + updatedAt: Date.now(), + ...(previous?.command ? { command: previous.command } : {}), ...(errorMessage ? { errorMessage } : {}), }) }) @@ -169,9 +271,12 @@ export function register(registry: PluginRegistry): void { const workdir = asString(payload.data.workdir) if (!workdir) return + const command = asString(payload.data.command) update(workdir, { state: 'running', url: asString(payload.data.url) ?? null, + updatedAt: Date.now(), + ...(command ? { command } : {}), }) }) @@ -180,11 +285,16 @@ export function register(registry: PluginRegistry): void { if (!workdir) return const reason = asString(payload.data.reason) + const previous = states.get(workdir) + const command = previous?.command + if (reason === 'error') { const errorMessage = asString(payload.data.error) update(workdir, { state: 'error', url: asString(payload.data.url) ?? null, + updatedAt: Date.now(), + ...(command ? { command } : {}), ...(errorMessage ? { errorMessage } : {}), }) return @@ -193,6 +303,8 @@ export function register(registry: PluginRegistry): void { update(workdir, { state: 'off', url: asString(payload.data.url) ?? null, + updatedAt: Date.now(), + ...(command ? { command } : {}), }) }) } diff --git a/test/index.test.ts b/test/index.test.ts index a498902..eb09d5e 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -184,4 +184,154 @@ describe('openfox-devserver-status', () => { assert.equal(result.tone, 'danger') assert.match(result.tooltip.en, /spawn failed/) }) + + it('packs tooltip as a single dot-separated line including state, URL and relative age (CA-1, CA-4)', async () => { + const { registry, calls } = createRegistry() + register(registry) + + await handler(calls, 'hook:devserver.started')({ + data: { + workdir: '/tmp/a', + url: 'http://localhost:4173', + command: 'vite', + }, + }) + + const status = handler(calls, 'rpc:status') + const result = await status({}, { workdir: '/tmp/a' }) as { + visible: boolean + tone: string + tooltip: { en: string; fr: string } + } + assert.equal(result.visible, true) + assert.equal(result.tone, 'success') + assert.ok(!result.tooltip.en.includes('\n'), 'English tooltip must be single-line') + assert.ok(!result.tooltip.fr.includes('\n'), 'French tooltip must be single-line') + assert.match(result.tooltip.en, /Running · http:\/\/localhost:4173 · Updated \d+s ago/) + assert.match(result.tooltip.fr, /En cours · http:\/\/localhost:4173 · Maj : il y a \d+s/) + }) + + it('refreshes the relative age on each RPC call without re-emitting a hook (CA-4)', async () => { + const { registry, calls } = createRegistry() + register(registry) + + const startedAt = Date.now() + await handler(calls, 'hook:devserver.started')({ + data: { workdir: '/tmp/a', url: 'http://localhost:4173' }, + }) + + const status = handler(calls, 'rpc:status') + + const first = await status({}, { workdir: '/tmp/a' }) as { + tooltip: { fr: string } + } + assert.match(first.tooltip.fr, /Maj : il y a 0s/) + + await new Promise((resolve) => setTimeout(resolve, 1100)) + + const second = await status({}, { workdir: '/tmp/a' }) as { + tooltip: { fr: string } + } + const ageMatch = second.tooltip.fr.match(/Maj : il y a (\d+)s/) + assert.ok(ageMatch, 'should still include a relative age segment') + assert.ok(Number(ageMatch![1]) >= 1, `expected at least 1s elapsed, got ${ageMatch![1]}`) + void startedAt + }) + + it('surfaces the configured port as the badge value when running (CA-3)', async () => { + const { registry, calls } = createRegistry() + register(registry) + + await handler(calls, 'hook:devserver.started')({ + data: { workdir: '/tmp/a', url: 'http://localhost:5173' }, + }) + + const result = await handler(calls, 'rpc:status')({}, { workdir: '/tmp/a' }) as { + visible: boolean + value?: string | number + } + assert.equal(result.visible, true) + assert.equal(result.value, '5173') + }) + + it('omits the badge value when the URL has no explicit port (CA-3)', async () => { + const { registry, calls } = createRegistry() + register(registry) + + await handler(calls, 'hook:devserver.started')({ + data: { workdir: '/tmp/a', url: 'http://localhost/' }, + }) + + const result = await handler(calls, 'rpc:status')({}, { workdir: '/tmp/a' }) as { + visible: boolean + value?: string | number + } + assert.equal(result.visible, true) + assert.equal(result.value, undefined) + }) + + it('flags the URL as potentially unreachable on error and exposes the last command (CA-2, CA-5)', async () => { + const { registry, calls } = createRegistry() + register(registry) + + await handler(calls, 'hook:devserver.started')({ + data: { + workdir: '/tmp/a', + url: 'http://localhost:8000', + command: 'uvicorn app:app --port 8000', + }, + }) + + await handler(calls, 'hook:devserver.stopped')({ + data: { + workdir: '/tmp/a', + reason: 'error', + url: 'http://localhost:8000', + error: 'spawn failed', + }, + }) + + const result = await handler(calls, 'rpc:status')({}, { workdir: '/tmp/a' }) as { + visible: boolean + tone: string + tooltip: { en: string; fr: string } + } + assert.equal(result.visible, true) + assert.equal(result.tone, 'danger') + assert.match(result.tooltip.en, /URL may not be reachable/) + assert.match(result.tooltip.fr, /URL peut ne pas être joignable/) + assert.match(result.tooltip.en, /uvicorn app:app --port 8000/) + assert.match(result.tooltip.en, /spawn failed/) + }) + + it('localizes the age segment in the French error tooltip (regression for CA-1/CA-2)', async () => { + const { registry, calls } = createRegistry() + register(registry) + + await handler(calls, 'hook:devserver.started')({ + data: { + workdir: '/tmp/a', + url: 'http://localhost:8000', + command: 'uvicorn app:app --port 8000', + }, + }) + + await handler(calls, 'hook:devserver.stopped')({ + data: { + workdir: '/tmp/a', + reason: 'error', + url: 'http://localhost:8000', + error: 'spawn failed', + }, + }) + + await new Promise((resolve) => setTimeout(resolve, 1100)) + + const result = await handler(calls, 'rpc:status')({}, { workdir: '/tmp/a' }) as { + tooltip: { en: string; fr: string } + } + assert.match(result.tooltip.fr, /Maj : il y a \d+s/) + assert.doesNotMatch(result.tooltip.fr, /Updated/) + assert.match(result.tooltip.en, /Updated \d+s ago/) + }) })