Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Upload IPFS files directly to Filecoin via the command line. Perfect for develop
- Run `filecoin-pin --help` to see all available commands and options.
- [CLI Walkthrough](https://docs.filecoin.io/builder-cookbook/filecoin-pin/filecoin-pin-cli)
- **Installation**: `npm install -g filecoin-pin`
- **Update notice**: Every command quickly checks npm for a newer version and prints a reminder when one is available. Disable with `--no-update-check`.

### GitHub Action
Automatically publish websites or build artifacts to IPFS and Filecoin as part of your CI/CD pipeline. Ideal for static websites, documentation sites, and automated deployment workflows.
Expand Down Expand Up @@ -158,7 +159,8 @@ The Pinning Server requires the use of environment variables, as detailed below.
### Common CLI Arguments

* `-h`, `--help`: Display help information for each command
* `-v`, `--version`: Output the version number
* `-V`, `--version`: Output the version number
* `-v`, `--verbose`: Verbose output
* `--private-key`: Ethereum-style (`0x`) private key, funded with USDFC (required)
* `--rpc-url`: Filecoin RPC endpoint (default: Calibration testnet)

Expand Down
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
"./core/utils": {
"types": "./dist/core/utils/index.d.ts",
"default": "./dist/core/utils/index.js"
},
"./version-check": {
"types": "./dist/common/version-check.d.ts",
"default": "./dist/common/version-check.js"
}
},
"bin": {
Expand All @@ -68,7 +72,7 @@
"LICENSE.md"
],
"scripts": {
"build": "tsc",
"build": "tsc && node scripts/write-version.mjs",
"dev": "tsx watch src/cli.ts server",
"start": "node dist/cli.js server",
"test": "npm run lint && npm run typecheck && npm run test:unit && npm run test:integration",
Expand Down Expand Up @@ -109,12 +113,14 @@
"it-to-buffer": "^4.0.10",
"multiformats": "^13.4.1",
"picocolors": "^1.1.1",
"pino": "^10.0.0"
"pino": "^10.0.0",
"semver": "^7.6.3"
},
"devDependencies": {
"@biomejs/biome": "2.3.4",
"@ipld/dag-cbor": "^9.2.5",
"@types/node": "^24.5.1",
"@types/semver": "^7.5.8",
"@vitest/coverage-v8": "^3.2.4",
"tsx": "^4.20.5",
"typescript": "^5.9.2",
Expand Down
10 changes: 10 additions & 0 deletions scripts/write-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { readFile, writeFile } from 'fs/promises'

const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url)))

await writeFile(
new URL('../dist/core/utils/version.js', import.meta.url),
`export const version = '${pkg.version}'
export const name = '${pkg.name}'
`
)
50 changes: 41 additions & 9 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
#!/usr/bin/env node
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'

import { Command } from 'commander'
import pc from 'picocolors'

import { addCommand } from './commands/add.js'
import { dataSetCommand } from './commands/data-set.js'
import { importCommand } from './commands/import.js'
import { paymentsCommand } from './commands/payments.js'
import { serverCommand } from './commands/server.js'

// Get package.json for version
const __dirname = dirname(fileURLToPath(import.meta.url))
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'))
import { checkForUpdate } from './common/version-check.js'
import { version as packageVersion } from './core/utils/version.js'

// Create the main program
const program = new Command()
.name('filecoin-pin')
.description('IPFS Pinning Service with Filecoin storage via Synapse SDK')
.version(packageJson.version)
.version(packageVersion)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: this adds a --version option via Commander, automatically.

.option('-v, --verbose', 'verbose output')
.option('--no-update-check', 'skip check for updates')

// Add subcommands
program.addCommand(serverCommand)
Expand All @@ -33,6 +30,41 @@ program.action(() => {
program.help()
})

let updateCheckPromise: ReturnType<typeof checkForUpdate> | null = null

program.hook('preAction', () => {
if (updateCheckPromise) {
return
}

const options = program.optsWithGlobals<{ updateCheck?: boolean }>()
if (options.updateCheck === false) {
updateCheckPromise = null
return
}

updateCheckPromise = checkForUpdate({ currentVersion: packageVersion })
})

program.hook('postAction', async () => {
const promise = updateCheckPromise
updateCheckPromise = null

if (!promise) {
return
}

const result = await promise

if (result.status === 'update-available') {
const header = `${pc.yellow(`Update available: filecoin-pin ${result.currentVersion} → ${result.latestVersion}`)}. Upgrade with ${pc.cyan('npm i -g filecoin-pin@latest')}`
const releasesLink = 'https://github.com/filecoin-project/filecoin-pin/releases'
const instruction = `Visit ${releasesLink} to view release notes or download the latest version.`
console.log(header)
console.log(instruction)
}
})

// Parse arguments and run
program.parseAsync(process.argv).catch((error) => {
console.error('Error:', error.message)
Expand Down
116 changes: 116 additions & 0 deletions src/common/version-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { compare } from 'semver'
import { name as packageName, version as packageVersion } from '../core/utils/version.js'

type UpdateCheckStatus =
| {
status: 'disabled'
reason: string
}
| {
status: 'up-to-date'
currentVersion: string
latestVersion: string
}
| {
status: 'update-available'
currentVersion: string
latestVersion: string
}
| {
status: 'error'
currentVersion: string
message: string
}

type CheckForUpdateOptions = {
packageName?: string
currentVersion?: string
timeoutMs?: number
disableCheck?: boolean
}

const DEFAULT_PACKAGE_NAME = packageName
const DEFAULT_TIMEOUT_MS = 1500
export async function checkForUpdate(options: CheckForUpdateOptions = {}): Promise<UpdateCheckStatus> {
const { packageName = DEFAULT_PACKAGE_NAME, timeoutMs = DEFAULT_TIMEOUT_MS } = options
const disableCheck = options.disableCheck === true

if (disableCheck) {
return {
status: 'disabled',
reason: 'Update check disabled by configuration',
}
}

const currentVersion = options.currentVersion ?? getLocalPackageVersion()

const controller = new AbortController()
const timeoutHandle = setTimeout(() => {
controller.abort()
}, timeoutMs)

try {
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
signal: controller.signal,
headers: {
accept: 'application/json',
},
})

if (!response.ok) {
return {
status: 'error',
currentVersion,
message: `Received ${response.status} from npm registry`,
}
}

const data = (await response.json()) as { version?: string }

if (typeof data.version !== 'string') {
return {
status: 'error',
currentVersion,
message: 'Response missing version field',
}
}

const latestVersion = data.version

if (compare(latestVersion, currentVersion) > 0) {
return {
status: 'update-available',
currentVersion,
latestVersion,
}
}

return {
status: 'up-to-date',
currentVersion,
latestVersion,
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return {
status: 'error',
currentVersion,
message: 'Update check timed out',
}
}

return {
status: 'error',
currentVersion,
message: error instanceof Error ? error.message : 'Unknown error during update check',
}
} finally {
clearTimeout(timeoutHandle)
}
}

function getLocalPackageVersion(): string {
return packageVersion
}

export type { UpdateCheckStatus }
6 changes: 3 additions & 3 deletions src/core/synapse/telemetry-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { TelemetryConfig } from '@filoz/synapse-sdk'
// biome-ignore lint/correctness/useImportExtensions: package.json is bundled for browser and node
import packageJson from '../../../package.json' with { type: 'json' }

import { name as packageName, version as packageVersion } from '../utils/version.js'

export const getTelemetryConfig = (config?: TelemetryConfig | undefined): TelemetryConfig => {
return {
Expand All @@ -12,7 +12,7 @@ export const getTelemetryConfig = (config?: TelemetryConfig | undefined): Teleme
},
sentrySetTags: {
...config?.sentrySetTags,
filecoinPinVersion: `${packageJson.name}@v${packageJson.version}`,
filecoinPinVersion: `${packageName}@v${packageVersion}`,
},
}
}
2 changes: 2 additions & 0 deletions src/core/utils/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const version = '0.0.0-dev'
export const name = 'filecoin-pin'
11 changes: 3 additions & 8 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'

import { createConfig } from './config.js'
import { name as packageName, version as packageVersion } from './core/utils/version.js'
import { createFilecoinPinningServer } from './filecoin-pinning-server.js'
import { createLogger } from './logger.js'

Expand All @@ -12,11 +9,9 @@ export interface ServiceInfo {
}

function getServiceInfo(): ServiceInfo {
const __dirname = dirname(fileURLToPath(import.meta.url))
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'))
return {
service: packageJson.name,
version: packageJson.version,
service: packageName,
version: packageVersion,
}
}

Expand Down
63 changes: 63 additions & 0 deletions src/test/unit/version-check.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { afterEach, describe, expect, it, vi } from 'vitest'

import { checkForUpdate } from '../../common/version-check.js'

afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
describe('version check', () => {
it('detects when a newer version is available', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ version: '0.12.0' }),
})
vi.stubGlobal('fetch', fetchMock)

const result = await checkForUpdate({ currentVersion: '0.11.0' })

expect(result).toMatchObject({
status: 'update-available',
currentVersion: '0.11.0',
latestVersion: '0.12.0',
})
expect(fetchMock).toHaveBeenCalled()
})

it('returns up-to-date when versions match', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ version: '0.11.0' }),
})
vi.stubGlobal('fetch', fetchMock)

const result = await checkForUpdate({ currentVersion: '0.11.0' })

expect(result).toEqual({
status: 'up-to-date',
currentVersion: '0.11.0',
latestVersion: '0.11.0',
})
})

it('returns error when fetch fails', async () => {
const fetchMock = vi.fn().mockRejectedValue(new Error('network down'))
vi.stubGlobal('fetch', fetchMock)

const result = await checkForUpdate({ currentVersion: '0.11.0' })

expect(result).toMatchObject({
status: 'error',
currentVersion: '0.11.0',
message: 'network down',
})
})
})

it('supports opting out via options', async () => {
const result = await checkForUpdate({ currentVersion: '0.11.0', disableCheck: true })
expect(result).toEqual({
status: 'disabled',
reason: 'Update check disabled by configuration',
})
})
2 changes: 2 additions & 0 deletions upload-action/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This GitHub Action is provided to illustrate how to use filecoin-pin, a new IPFS

See the two-workflow approach in the [examples directory](./examples/) for complete workflow files and setup instructions.

**Heads-up on updates:** the action checks npm for a newer `filecoin-pin` release at the start of each run and posts a GitHub Actions notice when one is available.

## Inputs & Outputs

See [action.yml](./action.yml) for complete input documentation including:
Expand Down
Loading
Loading