diff --git a/npm/.npmignore b/npm/.npmignore new file mode 100644 index 0000000..b474811 --- /dev/null +++ b/npm/.npmignore @@ -0,0 +1,21 @@ +# Test files +test.js +*.test.js + +# Development files +.gitignore +.editorconfig + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log + +# Temporary files +*.tmp +temp/ + +# Don't include pre-downloaded binaries +bin/ \ No newline at end of file diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 0000000..3a41b39 --- /dev/null +++ b/npm/README.md @@ -0,0 +1,80 @@ +# @clickup/cli + +Command-line interface for ClickUp - manage tasks, lists, and spaces from your terminal. + +## Installation + +```bash +npm install -g @clickup/cli +``` + +Or using yarn: +```bash +yarn global add @clickup/cli +``` + +## Usage + +After installation, the `cu` command will be available globally: + +```bash +# Authenticate with ClickUp +cu auth login + +# List tasks +cu task list + +# Create a new task +cu task create + +# View help +cu --help +``` + +## Features + +- **Task Management**: Create, view, update, and manage tasks +- **Bulk Operations**: Efficiently handle multiple tasks at once +- **Multiple Output Formats**: Table, JSON, YAML, and CSV +- **Interactive Mode**: User-friendly prompts for complex operations +- **Cross-Platform**: Works on macOS, Linux, and Windows + +## Documentation + +For full documentation, visit: https://github.com/timimsms/cu + +## Binary Distribution + +This npm package automatically downloads the appropriate ClickUp CLI binary for your platform during installation. The binary is downloaded from the official GitHub releases. + +### Supported Platforms + +- macOS (Intel & Apple Silicon) +- Linux (x64, ARM64, i386) +- Windows (x64, i386) + +### Skip Binary Download + +If you want to skip the automatic binary download (e.g., in CI environments), set the environment variable: + +```bash +CLICKUP_CLI_SKIP_DOWNLOAD=1 npm install -g @clickup/cli +``` + +## Troubleshooting + +If you encounter issues during installation: + +1. **Permission errors**: Try using `sudo` (not recommended) or configure npm to use a different directory +2. **Download failures**: Check your internet connection and GitHub access +3. **Platform not supported**: Download the binary manually from [releases](https://github.com/timimsms/cu/releases) + +## License + +MIT © Tim Timmerman + +## Links + +- [GitHub Repository](https://github.com/timimsms/cu) +- [Issue Tracker](https://github.com/timimsms/cu/issues) +- [Releases](https://github.com/timimsms/cu/releases) \ No newline at end of file diff --git a/npm/bin/cu b/npm/bin/cu new file mode 100755 index 0000000..4eb23b7 --- /dev/null +++ b/npm/bin/cu @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../index.js'); \ No newline at end of file diff --git a/npm/index.js b/npm/index.js new file mode 100644 index 0000000..7027f6d --- /dev/null +++ b/npm/index.js @@ -0,0 +1,70 @@ +#!/usr/bin/env node +/** + * ClickUp CLI npm package entry point + * This file handles the execution of the cu binary + */ + +const { spawn } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +// Determine the binary name based on platform +const getBinaryName = () => { + const platform = process.platform; + return platform === 'win32' ? 'cu.exe' : 'cu'; +}; + +// Get the path to the binary +const getBinaryPath = () => { + const binaryName = getBinaryName(); + const binPath = path.join(__dirname, 'bin', binaryName); + + if (!fs.existsSync(binPath)) { + console.error('ClickUp CLI binary not found!'); + console.error('Please run: npm install -g @clickup/cli'); + console.error(''); + console.error('If the problem persists, please report it at:'); + console.error('https://github.com/timimsms/cu/issues'); + process.exit(1); + } + + return binPath; +}; + +// Main execution +const main = () => { + const binaryPath = getBinaryPath(); + const args = process.argv.slice(2); + + // Spawn the binary with inherited stdio + const child = spawn(binaryPath, args, { + stdio: 'inherit', + shell: false + }); + + // Handle exit + child.on('exit', (code) => { + process.exit(code); + }); + + // Handle errors + child.on('error', (err) => { + if (err.code === 'ENOENT') { + console.error('ClickUp CLI binary not found!'); + console.error('Path:', binaryPath); + } else if (err.code === 'EACCES') { + console.error('Permission denied when trying to execute ClickUp CLI'); + console.error('Try running: chmod +x', binaryPath); + } else { + console.error('Failed to start ClickUp CLI:', err.message); + } + process.exit(1); + }); +}; + +// Run if called directly +if (require.main === module) { + main(); +} + +module.exports = { getBinaryPath, getBinaryName }; \ No newline at end of file diff --git a/npm/package.json b/npm/package.json new file mode 100644 index 0000000..2acbbd9 --- /dev/null +++ b/npm/package.json @@ -0,0 +1,54 @@ +{ + "name": "@clickup/cli", + "version": "0.0.0-development", + "description": "Command-line interface for ClickUp", + "keywords": [ + "clickup", + "cli", + "task-management", + "productivity", + "command-line" + ], + "homepage": "https://github.com/timimsms/cu", + "bugs": { + "url": "https://github.com/timimsms/cu/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/timimsms/cu.git" + }, + "license": "MIT", + "author": "Tim Timmerman ", + "main": "index.js", + "bin": { + "cu": "./bin/cu", + "clickup": "./bin/cu" + }, + "files": [ + "bin/", + "index.js", + "postinstall.js", + "README.md" + ], + "scripts": { + "postinstall": "node postinstall.js", + "test": "node test.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "os": [ + "darwin", + "linux", + "win32" + ], + "cpu": [ + "x64", + "arm64", + "ia32" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} \ No newline at end of file diff --git a/npm/postinstall.js b/npm/postinstall.js new file mode 100644 index 0000000..ec80a60 --- /dev/null +++ b/npm/postinstall.js @@ -0,0 +1,287 @@ +#!/usr/bin/env node +/** + * ClickUp CLI npm package post-install script + * Downloads the appropriate binary for the current platform + */ + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const { createHash } = require('crypto'); +const zlib = require('zlib'); +const { execSync } = require('child_process'); + +// Configuration +const REPO_OWNER = 'timimsms'; +const REPO_NAME = 'cu'; +const BINARY_NAME = 'cu'; + +// Get platform details +const getPlatform = () => { + const platform = process.platform; + const arch = process.arch; + + // Map Node.js platform/arch to our naming convention + const platformMap = { + 'darwin': 'darwin', + 'linux': 'linux', + 'win32': 'windows' + }; + + const archMap = { + 'x64': 'x86_64', + 'arm64': 'arm64', + 'ia32': 'i386' + }; + + const mappedPlatform = platformMap[platform]; + const mappedArch = archMap[arch]; + + if (!mappedPlatform || !mappedArch) { + throw new Error(`Unsupported platform: ${platform} ${arch}`); + } + + return `${mappedPlatform}_${mappedArch}`; +}; + +// Download file with progress +const downloadFile = (url) => { + return new Promise((resolve, reject) => { + https.get(url, (response) => { + if (response.statusCode === 302 || response.statusCode === 301) { + // Handle redirect + downloadFile(response.headers.location) + .then(resolve) + .catch(reject); + return; + } + + if (response.statusCode !== 200) { + reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`)); + return; + } + + const chunks = []; + const totalSize = parseInt(response.headers['content-length'], 10); + let downloadedSize = 0; + + response.on('data', (chunk) => { + chunks.push(chunk); + downloadedSize += chunk.length; + + // Simple progress indicator + if (totalSize) { + const percentage = Math.round((downloadedSize / totalSize) * 100); + process.stdout.write(`\rDownloading: ${percentage}%`); + } + }); + + response.on('end', () => { + process.stdout.write('\n'); + resolve(Buffer.concat(chunks)); + }); + + response.on('error', reject); + }).on('error', reject); + }); +}; + +// Get latest version from GitHub +const getLatestVersion = async () => { + const apiUrl = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`; + + const options = { + headers: { + 'User-Agent': 'clickup-cli-npm' + } + }; + + return new Promise((resolve, reject) => { + https.get(apiUrl, options, (response) => { + let data = ''; + + response.on('data', (chunk) => { + data += chunk; + }); + + response.on('end', () => { + try { + const release = JSON.parse(data); + resolve(release.tag_name); + } catch (err) { + reject(new Error('Failed to parse GitHub API response')); + } + }); + + response.on('error', reject); + }).on('error', reject); + }); +}; + +// Verify checksum +const verifyChecksum = async (buffer, checksumsUrl, fileName) => { + console.log('Verifying checksum...'); + + try { + const checksumsData = await downloadFile(checksumsUrl); + const checksums = checksumsData.toString('utf-8').split('\n'); + + // Find checksum for our file + const checksumLine = checksums.find(line => line.includes(fileName)); + if (!checksumLine) { + console.warn('Warning: Could not find checksum for', fileName); + return true; // Continue anyway + } + + const expectedChecksum = checksumLine.split(/\s+/)[0]; + const actualChecksum = createHash('sha256').update(buffer).digest('hex'); + + if (expectedChecksum !== actualChecksum) { + throw new Error('Checksum verification failed!'); + } + + console.log('✓ Checksum verified'); + } catch (err) { + console.warn('Warning: Checksum verification skipped:', err.message); + } + + return true; +}; + +// Extract binary using native tools +const extractBinary = async (archiveBuffer, platform, archivePath) => { + const binDir = path.join(__dirname, 'bin'); + + // Create bin directory + if (!fs.existsSync(binDir)) { + fs.mkdirSync(binDir, { recursive: true }); + } + + // Save archive to temp file + const tempDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'cu-')); + const tempArchive = path.join(tempDir, path.basename(archivePath)); + fs.writeFileSync(tempArchive, archiveBuffer); + + try { + if (platform.includes('windows')) { + // Windows: Use PowerShell to extract + execSync(`powershell -Command "Expand-Archive -Path '${tempArchive}' -DestinationPath '${tempDir}' -Force"`, { stdio: 'ignore' }); + } else { + // Unix: Use tar + execSync(`tar -xzf "${tempArchive}" -C "${tempDir}"`, { stdio: 'ignore' }); + } + + // Find and move the binary + const isWindows = platform.includes('windows'); + const binaryName = isWindows ? `${BINARY_NAME}.exe` : BINARY_NAME; + const sourcePath = path.join(tempDir, binaryName); + const destPath = path.join(binDir, binaryName); + + if (fs.existsSync(sourcePath)) { + fs.copyFileSync(sourcePath, destPath); + if (!isWindows) { + fs.chmodSync(destPath, 0o755); + } + } else { + // Try to find the binary in subdirectories + const files = fs.readdirSync(tempDir, { recursive: true }); + const binaryFile = files.find(f => f === binaryName || f.endsWith(`/${binaryName}`)); + if (binaryFile) { + const fullPath = path.join(tempDir, binaryFile); + fs.copyFileSync(fullPath, destPath); + if (!isWindows) { + fs.chmodSync(destPath, 0o755); + } + } else { + throw new Error(`Binary ${binaryName} not found in archive`); + } + } + + return destPath; + } finally { + // Cleanup + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch (err) { + // Ignore cleanup errors + } + } +}; + +// Main installation function +const install = async () => { + console.log('Installing ClickUp CLI...\n'); + + try { + // Skip in CI environment + if (process.env.CI || process.env.CLICKUP_CLI_SKIP_DOWNLOAD) { + console.log('Skipping binary download (CI environment detected)'); + return; + } + + // Skip if binary already exists + const binPath = path.join(__dirname, 'bin', process.platform === 'win32' ? 'cu.exe' : 'cu'); + if (fs.existsSync(binPath)) { + console.log('ClickUp CLI binary already exists, skipping download.'); + return; + } + + // Detect platform + const platform = getPlatform(); + console.log(`Platform: ${platform}`); + + // Get latest version + console.log('Fetching latest version...'); + const version = await getLatestVersion(); + console.log(`Version: ${version}`); + + // Construct URLs + const archiveName = platform.includes('windows') + ? `${BINARY_NAME}_${platform}.zip` + : `${BINARY_NAME}_${platform}.tar.gz`; + + const downloadUrl = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${version}/${archiveName}`; + const checksumsUrl = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${version}/checksums.txt`; + + // Download archive + console.log(`\nDownloading ${archiveName}...`); + const archiveBuffer = await downloadFile(downloadUrl); + console.log('✓ Download complete'); + + // Verify checksum + await verifyChecksum(archiveBuffer, checksumsUrl, archiveName); + + // Extract binary + console.log('Extracting binary...'); + const binaryPath = await extractBinary(archiveBuffer, platform, archiveName); + console.log('✓ Extraction complete'); + + // Verify installation + try { + const output = execSync(`"${binaryPath}" --version`, { encoding: 'utf-8' }).trim(); + console.log(`\n✓ Successfully installed: ${output}`); + } catch (err) { + console.warn('\nWarning: Could not verify installation'); + } + + console.log('\nGet started with: cu --help'); + + } catch (err) { + console.error('\n✗ Installation failed:', err.message); + console.error('\nYou can try:'); + console.error('1. Installing directly: https://github.com/timimsms/cu/releases'); + console.error('2. Reporting the issue: https://github.com/timimsms/cu/issues'); + + // Don't fail npm install + process.exit(0); + } +}; + +// Run installation +if (require.main === module) { + install().catch(err => { + console.error('Unexpected error:', err); + // Don't fail npm install + process.exit(0); + }); +} \ No newline at end of file diff --git a/npm/test.js b/npm/test.js new file mode 100644 index 0000000..b0cfdcf --- /dev/null +++ b/npm/test.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node +/** + * Simple test script for the npm package + */ + +const { getBinaryPath, getBinaryName } = require('./index.js'); +const fs = require('fs'); +const path = require('path'); + +console.log('Testing @clickup/cli npm package...\n'); + +// Test 1: Check binary name +console.log('Test 1: Binary name detection'); +const binaryName = getBinaryName(); +console.log(` Binary name: ${binaryName}`); +console.log(` ✓ Platform detection works\n`); + +// Test 2: Check if postinstall would run +console.log('Test 2: Post-install readiness'); +const binDir = path.join(__dirname, 'bin'); +if (fs.existsSync(binDir)) { + console.log(' ✓ bin directory exists'); +} else { + console.log(' ✓ bin directory will be created during install'); +} + +// Test 3: Check package.json +console.log('\nTest 3: Package configuration'); +const pkg = require('./package.json'); +console.log(` Name: ${pkg.name}`); +console.log(` Version: ${pkg.version}`); +console.log(` ✓ Package.json is valid\n`); + +console.log('All tests passed! ✓'); +console.log('\nTo test installation:'); +console.log(' 1. Run: npm pack'); +console.log(' 2. Run: npm install -g clickup-cli-*.tgz'); +console.log(' 3. Test: cu --version'); \ No newline at end of file diff --git a/scripts/publish-npm.sh b/scripts/publish-npm.sh new file mode 100755 index 0000000..6a5a9f3 --- /dev/null +++ b/scripts/publish-npm.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Script to publish the npm package after a GitHub release +# This should be called by the release workflow + +set -euo pipefail + +# Configuration +NPM_DIR="npm" +PACKAGE_NAME="@clickup/cli" + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +# Helper functions +info() { + echo -e "${GREEN}$1${NC}" +} + +error() { + echo -e "${RED}Error: $1${NC}" >&2 + exit 1 +} + +# Check if npm directory exists +if [ ! -d "$NPM_DIR" ]; then + error "npm directory not found" +fi + +cd "$NPM_DIR" + +# Get the version from the latest GitHub release +if [ -z "${GITHUB_REF_NAME:-}" ]; then + error "GITHUB_REF_NAME not set. This script should be run in GitHub Actions." +fi + +VERSION="${GITHUB_REF_NAME#v}" +info "Publishing version: $VERSION" + +# Update package.json version +if command -v jq >/dev/null 2>&1; then + jq ".version = \"$VERSION\"" package.json > package.json.tmp + mv package.json.tmp package.json +else + # Fallback to sed if jq is not available + sed -i.bak "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" package.json + rm -f package.json.bak +fi + +# Ensure we're logged in to npm +if [ -z "${NPM_TOKEN:-}" ]; then + error "NPM_TOKEN not set" +fi + +# Create .npmrc with auth token +echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc + +# Publish to npm +info "Publishing to npm..." +npm publish --access public + +# Cleanup +rm -f .npmrc + +info "✓ Successfully published $PACKAGE_NAME@$VERSION to npm" + +# Tag the release +npm dist-tag add "$PACKAGE_NAME@$VERSION" latest + +info "✓ Tagged as latest" \ No newline at end of file