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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
run: npm ci

- name: Build
run: npm run build
run: POLYLANE_CLI_VERSION="${GITHUB_REF_NAME#v}" npm run build
env:
POLYLANE_API_DOMAIN: ${{ secrets.POLYLANE_API_DOMAIN }}
POLYLANE_OAUTH_CLIENT_ID: ${{ secrets.POLYLANE_OAUTH_CLIENT_ID }}
Expand Down
6 changes: 4 additions & 2 deletions build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ async function main(): Promise<void> {

await generateAll();

// Step 2: read version
// Step 2: resolve version — an explicit POLYLANE_CLI_VERSION (e.g. the tag
// in the release workflow) wins over package.json.
const pkg = JSON.parse(readFileSync('package.json', 'utf-8')) as { version: string };
const version = process.env.POLYLANE_CLI_VERSION ?? pkg.version;

// Step 3: bundle with esbuild
mkdirSync('dist', { recursive: true });
const outfile = join('dist', 'polylane.mjs');

const define: Record<string, string> = {
'process.env.POLYLANE_CLI_VERSION': JSON.stringify(pkg.version),
'process.env.POLYLANE_CLI_VERSION': JSON.stringify(version),
};
// Bake every POLYLANE_* env var visible at build time into the bundle, so the
// produced binary works without needing those vars set at runtime.
Expand Down
23 changes: 3 additions & 20 deletions src/client/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import { ExitCode } from '../errors/codes';
import { maskToken } from '../utils/token';
import { showStatusBar } from '../output/status-bar';
import { recordHttpRequest } from '../telemetry/http-counter';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { getCliVersion } from '../version';

export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

Expand All @@ -32,21 +30,6 @@ interface ApiEnvelope<T> {
result: T;
}

let cachedVersion: string | null = null;

function getVersion(): string {
if (cachedVersion !== null) return cachedVersion;
try {
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkgPath = join(__dirname, '..', '..', 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version: string };
cachedVersion = pkg.version;
} catch {
cachedVersion = '0.0.0';
}
return cachedVersion;
}

function buildQueryString(query: Query): string {
const params: string[] = [];
for (const [key, value] of Object.entries(query)) {
Expand Down Expand Up @@ -78,9 +61,9 @@ export async function request(config: Config, opts: RequestOpts): Promise<Respon
const fullUrl = path + query;

const headers: Record<string, string> = {
'User-Agent': `polylane-cli/${getVersion()}`,
'User-Agent': `polylane-cli/${getCliVersion()}`,
'x-nominal-client': 'cli',
'x-nominal-client-version': getVersion(),
'x-nominal-client-version': getCliVersion(),
...opts.headers,
};

Expand Down
3 changes: 2 additions & 1 deletion src/client/thread-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Config } from '../config/schema';
import { resolveCredential, getAuthHeader } from '../auth/resolver';
import { CLIError } from '../errors/base';
import { ExitCode } from '../errors/codes';
import { getCliVersion } from '../version';

export interface UIMessagePart {
type: string;
Expand Down Expand Up @@ -91,7 +92,7 @@ async function buildConnection(
const headers: Record<string, string> = {
...getAuthHeader(cred),
'x-nominal-client': 'cli',
'x-nominal-client-version': process.env.POLYLANE_CLI_VERSION ?? '0.0.0',
'x-nominal-client-version': getCliVersion(),
};
return { url, headers };
}
Expand Down
21 changes: 4 additions & 17 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import type { Config } from '../config/schema';
import { UPDATE_STATE_FILE } from '../config/paths';
import { readJsonFile, writeJsonFile } from '../utils/fs';
import { isCI } from '../utils/env';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { getCliVersion } from '../version';

interface UpdateState {
lastCheck: number;
Expand All @@ -14,17 +12,6 @@ interface UpdateState {

const CACHE_TTL_MS = 24 * 60 * 60 * 1000;

function getCurrentVersion(): string {
try {
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkgPath = join(__dirname, '..', '..', 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version: string };
return pkg.version;
} catch {
return '0.0.0';
}
}

async function fetchLatest(): Promise<string | null> {
try {
const res = await fetch('https://registry.npmjs.org/@coreplane/polylane/latest', {
Expand Down Expand Up @@ -53,7 +40,7 @@ export const updateCommand: Command = {
name: 'update',
description: 'Check for CLI updates',
async execute(config: Config): Promise<void> {
const current = getCurrentVersion();
const current = getCliVersion();
process.stderr.write(`Current version: ${current}\n`);

if (isCI() && !config.verbose) {
Expand Down Expand Up @@ -83,15 +70,15 @@ export async function checkForUpdateAsync(): Promise<string | null> {
if (isCI()) return null;
const state = readJsonFile<UpdateState>(UPDATE_STATE_FILE);
if (state && Date.now() - state.lastCheck < CACHE_TTL_MS) {
const current = getCurrentVersion();
const current = getCliVersion();
if (state.latest && compareVersions(state.latest, current) > 0) {
return state.latest;
}
return null;
}
const latest = await fetchLatest();
writeJsonFile(UPDATE_STATE_FILE, { lastCheck: Date.now(), latest });
const current = getCurrentVersion();
const current = getCliVersion();
if (latest && compareVersions(latest, current) > 0) return latest;
return null;
}
18 changes: 2 additions & 16 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

import { parseFlags, scanCommandPath } from './args';
import { GLOBAL_OPTIONS } from './command';
import type { GlobalFlags } from './types/flags';
Expand All @@ -20,6 +16,7 @@ import {
import type { Config } from './config/schema';
import type { Credential } from './auth/types';
import { buildEvent, dispatch, hasShownFirstRunNotice, markFirstRunNoticeShown } from './telemetry';
import { getCliVersion } from './version';

const NO_AUTH_COMMANDS = new Set([
'auth login',
Expand All @@ -40,17 +37,6 @@ const NO_AUTH_COMMANDS = new Set([
'api describe',
]);

function readVersion(): string {
try {
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkgPath = join(__dirname, '..', 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version: string };
return pkg.version;
} catch {
return '0.0.0';
}
}

function setupSignalHandlers(): void {
process.on('SIGINT', () => {
process.stderr.write('\n');
Expand Down Expand Up @@ -85,7 +71,7 @@ async function run(): Promise<void> {
const argv = process.argv.slice(2);

if (argv[0] === '--version' || argv[0] === '-v') {
process.stdout.write(`polylane ${readVersion()}\n`);
process.stdout.write(`polylane ${getCliVersion()}\n`);
return;
}

Expand Down
5 changes: 1 addition & 4 deletions src/telemetry/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isCI, getCIName, isStdoutTTY, isStderrTTY } from '../utils/env';
import { getInstallId, recordRun } from './state';
import { detectInstallSource, detectEnvironment, type InstallSource } from './environment';
import { getHttpStats } from './http-counter';
import { getCliVersion } from '../version';

// The set of fields shipped to the telemetry endpoint. Everything here is
// intentionally non-sensitive — see PRIVACY.md for the full inventory.
Expand Down Expand Up @@ -82,10 +83,6 @@ export interface CliTelemetryEvent {

const SESSION_ID = randomUUID();

function getCliVersion(): string {
return process.env.POLYLANE_CLI_VERSION ?? '0.0.0';
}

function authMethodOf(cred: Credential | null): CliTelemetryEvent['authMethod'] {
if (!cred) return null;
return cred.type === 'oauth' ? 'oauth' : 'api-key';
Expand Down
26 changes: 26 additions & 0 deletions src/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

let cachedVersion: string | null = null;

// The released bundle ships as a standalone dist/polylane.mjs with no
// package.json next to it, so build.ts bakes the version in via an esbuild
// define on POLYLANE_CLI_VERSION. Reading package.json is the dev-mode
// fallback (tsx src/main.ts from a checkout).
export function resolveVersion(baked = process.env.POLYLANE_CLI_VERSION): string {
if (baked) return baked;
try {
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkgPath = join(__dirname, '..', 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version: string };
return pkg.version;
} catch {
return '0.0.0';
}
}

export function getCliVersion(): string {
cachedVersion ??= resolveVersion();
return cachedVersion;
}
26 changes: 26 additions & 0 deletions test/version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveVersion } from '../src/version';

const __dirname = dirname(fileURLToPath(import.meta.url));

describe('resolveVersion', () => {
it('prefers the baked build-time version', () => {
assert.equal(resolveVersion('1.2.3'), '1.2.3');
});

it('falls back to package.json in dev mode', () => {
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')) as {
version: string;
};
assert.equal(resolveVersion(undefined), pkg.version);
assert.notEqual(resolveVersion(undefined), '0.0.0');
});

it('treats an empty baked value as unset', () => {
assert.notEqual(resolveVersion(''), '');
});
});
Loading