Skip to content

Feature: Enhanced CLI Setup Wizard & Developer Experience #98

Description

@melvincarvalho

Summary

Enhance the jss init command and add complementary CLI tools to provide a polished, professional setup experience. This covers interactive setup improvements, validation, diagnostics, and output generation.

Difficulty: 35/100
Estimated Effort: 3-5 days
Dependencies: #97 (presets)


Current State

The existing jss init command (bin/jss.js:181-244) is functional but basic:

$ jss init

  JavaScript Solid Server Setup

Port (3000): 
Data directory (./data): 
Enable content negotiation (Turtle support)? [y/N]: 
Enable WebSocket notifications? [y/N]: 
Configure SSL? [y/N]: 
Enable built-in Identity Provider? [y/N]: 

Configuration saved to: ./config.json
Data directory created: ./data

Run `jss start` to start the server.

Current Limitations

Issue Impact
No preset selection Users must configure everything manually
No input validation Invalid ports, non-existent paths accepted
No visual feedback Plain text, no colors or progress indication
No post-setup verification No way to verify config works
Limited feature coverage Only covers ~5 of 30+ options
No output generation Can't generate docker-compose, systemd, nginx configs
Basic readline prompts No autocomplete, no list selection

Proposed Enhancements

1. Preset-First Flow

$ jss init

  ╭──────────────────────────────────────────╮
  │   JavaScript Solid Server Setup          │
  │   v0.0.81                                 │
  ╰──────────────────────────────────────────╯

? Choose a configuration preset:
  ❯ personal    - Single-user production server
    community   - Multi-user with open registration
    private     - Multi-user with invite-only access
    federation  - Full ActivityPub + Nostr support
    minimal     - Development and testing
    developer   - All features enabled
    custom      - Configure everything manually

Using preset: personal ✓

? Customize settings? (y/N): y

2. Improved Prompts with Validation

? Port (443): 80
  ⚠ Port 80 requires root privileges. Continue? (y/N): n
? Port (443): 8443 ✓

? Data directory (./data): /var/jss/data
  ✓ Directory exists and is writable

? Domain name: example.com
  ✓ Valid domain format

? SSL certificate path: ./ssl/cert.pem
  ✗ File not found: ./ssl/cert.pem
  ? Create self-signed certificate? (y/N): y
  ✓ Generated self-signed certificate (valid 365 days)
  ⚠ Self-signed certificates will show browser warnings

3. Feature Selection with Descriptions

? Select additional features: (space to toggle, enter to confirm)
  ◉ Identity Provider (IdP)
    └─ Email/password authentication, account management
  ◯ WebSocket Notifications  
    └─ Real-time updates for Solid apps
  ◉ Content Negotiation
    └─ Turtle/RDF support for Linked Data
  ◯ Mashlib Data Browser
    └─ Built-in file manager UI
  ◯ ActivityPub Federation
    └─ Connect with Mastodon, Plesk, etc.
  ◯ Nostr Relay
    └─ Decentralized social protocol support
  ◯ Git HTTP Backend
    └─ Host git repositories in pods

4. Summary & Confirmation

  ╭──────────────────────────────────────────╮
  │   Configuration Summary                   │
  ╰──────────────────────────────────────────╯

  Preset:        personal
  Port:          8443
  Data Dir:      /var/jss/data
  Domain:        example.com
  SSL:           ✓ Enabled (self-signed)
  
  Features:
    ✓ Identity Provider
    ✓ Content Negotiation
    ✗ WebSocket Notifications
    ✗ ActivityPub
    ✗ Nostr

  Quota:         10GB per pod

? Save configuration? (Y/n): y

  ✓ Configuration saved to: ./config.json
  ✓ Data directory created: /var/jss/data
  ✓ SSL certificates generated

  Next steps:
    1. Start the server:  jss start -c ./config.json
    2. Verify setup:      jss doctor -c ./config.json
    3. View your server:  https://example.com:8443

New Commands

jss doctor - Configuration Diagnostics

Verify configuration and system readiness.

$ jss doctor -c ./config.json

  ╭──────────────────────────────────────────╮
  │   JSS Doctor - Configuration Check       │
  ╰──────────────────────────────────────────╯

  Configuration
  ✓ Config file found: ./config.json
  ✓ Config syntax valid
  ✓ Using preset: personal

  Network
  ✓ Port 8443 is available
  ✓ Host 0.0.0.0 is valid
  ⚠ Port 8443 may require firewall configuration

  Storage
  ✓ Data directory exists: /var/jss/data
  ✓ Directory is writable
  ✓ 45GB free disk space

  SSL/TLS
  ✓ Certificate found: ./ssl/cert.pem
  ✓ Private key found: ./ssl/key.pem
  ✓ Certificate valid until: 2025-12-01
  ⚠ Self-signed certificate detected

  Features
  ✓ IdP: Enabled
  ✓ Content Negotiation: Enabled
  ✗ Notifications: Disabled
  ✗ Federation: Disabled

  ──────────────────────────────────────────
  Result: 12 checks passed, 2 warnings, 0 errors
  
  Your configuration looks good! Run `jss start` to begin.

Doctor Checks

Category Check
Config File exists, valid JSON, no unknown keys
Network Port available, valid host, firewall hints
Storage Directory exists, writable, disk space
SSL Files exist, valid format, expiration date, chain valid
Features Required dependencies met, valid combinations
System Node.js version, memory available

jss generate - Output File Generation

Generate deployment configuration files.

$ jss generate docker -c ./config.json

  Generated: docker-compose.yml
  
  Usage:
    docker-compose up -d
    docker-compose logs -f

Supported Outputs

Docker Compose

$ jss generate docker -c ./config.json -o docker-compose.yml
version: '3.8'
services:
  jss:
    image: ghcr.io/javascriptsolidserver/jss:latest
    ports:
      - "8443:8443"
    volumes:
      - ./data:/app/data
      - ./ssl:/app/ssl:ro
    environment:
      - JSS_PORT=8443
      - JSS_ROOT=/app/data
      - JSS_SSL_KEY=/app/ssl/key.pem
      - JSS_SSL_CERT=/app/ssl/cert.pem
      - JSS_IDP=true
      - JSS_CONNEG=true
    restart: unless-stopped

Systemd Service

$ jss generate systemd -c ./config.json -o jss.service
[Unit]
Description=JavaScript Solid Server
After=network.target

[Service]
Type=simple
User=jss
WorkingDirectory=/opt/jss
ExecStart=/usr/bin/jss start -c /etc/jss/config.json
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

Nginx Reverse Proxy

$ jss generate nginx -c ./config.json --domain example.com
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Caddy

$ jss generate caddy -c ./config.json --domain example.com
example.com {
    reverse_proxy localhost:3000
}

Environment File

$ jss generate env -c ./config.json -o .env
JSS_PORT=8443
JSS_ROOT=./data
JSS_SSL_KEY=./ssl/key.pem
JSS_SSL_CERT=./ssl/cert.pem
JSS_IDP=true
JSS_CONNEG=true

jss migrate - Configuration Migration

Migrate configuration from older versions or other servers.

$ jss migrate --from node-solid-server --input .solid/config.json

  ╭──────────────────────────────────────────╮
  │   Configuration Migration                │
  ╰──────────────────────────────────────────╯

  Source: Node Solid Server config
  
  Migrating settings:
    ✓ port: 8443
    ✓ serverUri → computed from port
    ✓ root → root
    ✓ sslKey → sslKey
    ✓ sslCert → sslCert
    ⚠ webid: true → idp: true (similar feature)
    ✗ multiuser: skipped (different implementation)
  
  Saved to: ./config.json
  
  ⚠ Manual review recommended for:
    - User accounts (not migrated)
    - ACL files (may need updates)

Implementation Details

Library Recommendations

Library Purpose Size
@inquirer/prompts Modern interactive prompts ~50KB
chalk Terminal colors ~20KB
ora Spinners for async operations ~15KB
boxen Boxes for headers ~10KB
cli-table3 Tables for summaries ~15KB

Alternative: Zero-dependency approach
Use built-in readline with ANSI escape codes for colors. Larger code but no dependencies.

Prompt Types Needed

import {
  select,      // Preset selection
  checkbox,    // Feature toggles
  input,       // Text input with validation
  confirm,     // Yes/no questions
  password     // Hidden input (for tokens)
} from '@inquirer/prompts';

Validation Functions

const validators = {
  port: (value) => {
    const port = parseInt(value);
    if (isNaN(port) || port < 1 || port > 65535) {
      return 'Port must be between 1 and 65535';
    }
    if (port < 1024) {
      return 'warning:Port below 1024 requires root privileges';
    }
    return true;
  },
  
  directory: async (value) => {
    const dir = path.resolve(value);
    try {
      await fs.access(path.dirname(dir), fs.constants.W_OK);
      return true;
    } catch {
      return 'Parent directory is not writable';
    }
  },
  
  domain: (value) => {
    const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/;
    if (!domainRegex.test(value) && value !== 'localhost') {
      return 'Invalid domain format';
    }
    return true;
  },
  
  sslCert: async (value) => {
    try {
      const cert = await fs.readFile(value, 'utf8');
      if (!cert.includes('BEGIN CERTIFICATE')) {
        return 'File does not appear to be a PEM certificate';
      }
      return true;
    } catch {
      return 'File not found or not readable';
    }
  }
};

Error Handling

// Graceful Ctrl+C handling
process.on('SIGINT', () => {
  console.log('\n\nSetup cancelled.');
  process.exit(0);
});

// Wrap prompts in try/catch
try {
  const answers = await runWizard();
} catch (error) {
  if (error.name === 'ExitPromptError') {
    console.log('\nSetup cancelled.');
    process.exit(0);
  }
  throw error;
}

Industry Comparison

Tool Setup Experience
npm init Simple prompts, -y for defaults, generates package.json
yarn create Runs create-* packages, delegates to framework CLIs
vite create Select framework → variant → done, very fast
create-next-app Name → TypeScript? → ESLint? → Tailwind? → src/? → App Router?
create-react-app Just project name, everything else automatic
Prisma init Datasource selection, generates schema file
Docker init Detect project type, generate Dockerfile + compose
Mastodon setup rake mastodon:setup - domain, DB, SMTP, admin account

Best Practices Observed

  1. Preset/template first (Vite, Next.js) - Offer starting points
  2. Sensible defaults (CRA) - Work out of box with minimal input
  3. Validation with recovery (Inquirer) - Don't fail, offer alternatives
  4. Summary before commit (Prisma) - Show what will be created
  5. Next steps guidance (all) - Tell user what to do after
  6. Skip option (-y flag) - Allow non-interactive mode

Command Summary

Command Purpose Priority
jss init Interactive setup wizard (enhanced) P0
jss doctor Validate configuration and system P1
jss generate docker Generate docker-compose.yml P1
jss generate systemd Generate systemd service file P2
jss generate nginx Generate nginx config P2
jss generate caddy Generate Caddyfile P2
jss generate env Export config as .env P2
jss migrate Migrate from other configs P3
jss presets List/show presets (from #97) P1

Difficulty Breakdown

Component Difficulty Notes
Enhanced prompts (inquirer) 20/100 Library does heavy lifting
Input validation 15/100 Straightforward checks
Visual improvements (chalk, boxen) 10/100 Simple additions
jss doctor command 30/100 Many checks to implement
jss generate docker 20/100 Template + variable substitution
jss generate systemd 15/100 Simple template
jss generate nginx/caddy 20/100 Need to handle edge cases
jss migrate 40/100 Need to understand other formats
Total 35/100

Testing Plan

  1. Interactive tests (manual): Run through wizard with various inputs
  2. Non-interactive tests: jss init -y with env vars
  3. Validation tests: Invalid inputs, edge cases
  4. Generation tests: Compare output against expected templates
  5. Doctor tests: Mock various system states
describe('jss init', () => {
  it('should accept preset flag', async () => {
    const config = await runInit({ preset: 'personal', yes: true });
    expect(config.singleUser).toBe(true);
    expect(config.idp).toBe(true);
  });

  it('should validate port range', async () => {
    const result = validators.port('70000');
    expect(result).toContain('between 1 and 65535');
  });
});

describe('jss doctor', () => {
  it('should detect unavailable port', async () => {
    // Start something on port 3000
    const checks = await runDoctor({ port: 3000 });
    expect(checks.network.portAvailable).toBe(false);
  });
});

describe('jss generate', () => {
  it('should generate valid docker-compose', async () => {
    const output = await generateDocker(sampleConfig);
    const parsed = yaml.parse(output);
    expect(parsed.services.jss).toBeDefined();
  });
});

Related Issues


Open Questions

  1. Should we use @inquirer/prompts (modern, ESM) or inquirer (legacy, CJS compatible)?
  2. Should jss doctor attempt fixes or just report issues?
  3. Should generated files include comments explaining each setting?
  4. Should jss init offer to run jss doctor automatically after setup?
  5. How to handle jss init in Docker environments (non-interactive)?

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions