Files
ansiblings/packages/nopy/docs/SESSION_FORMAT.md
T

8.7 KiB

Nopy Session Format

Nopy supports two session file formats: JSON and MJS (ES Module JavaScript).

The extension is what picks the loader, so a session file has to end in .json or .mjs; anything else is refused by name. The .nopysession.* names used throughout are the convention listSessions() looks for — -s and -l accept any path you give them.

Supported Formats

JSON Format (.nopysession.json)

Traditional JSON format for session files:

{
  "version": "1.0.0",
  "timestamp": "2025-10-15T00:00:00.000Z",
  "cubes": [
    {
      "key": "runtime:nodevm",
      "variables": {
        "VERSION": "22",
        "USER": "myuser"
      }
    }
  ],
  "hosts": ["@ssh/myhost.local"],
  "auth": {
    "method": "password",
    "username": "admin"
  },
  "env": {}
}

Limitations:

  • No comments allowed (pure JSON)
  • Cannot use dynamic values or computation
  • No code reuse or imports

JavaScript module format with full ES Module support:

// Nopy Session Configuration
// Comments are fully supported!

// You can import values from other files
import { commonHosts } from './common-config.mjs';

// You can use dynamic values
const timestamp = new Date().toISOString();
const nodeVersion = process.env.NODE_VERSION || "22";

export default {
  version: "1.0.0",
  timestamp,

  cubes: [
    // Inline comments for each cube
    {
      key: "runtime:nodevm",
      variables: {
        VERSION: nodeVersion,  // Dynamic value
        USER: "myuser",
        ALIAS: "nodelts",
        GLOBAL_PACKAGES: "pm2 yarn"
      }
    },

    // Add more cubes...
  ],

  hosts: commonHosts,  // Imported from another file

  auth: {
    method: "password",
    username: "admin"
  },

  env: {
    NODE_ENV: process.env.NODE_ENV || "production"
  }
};

Advantages:

  • ✅ Comments - Document your configuration inline
  • ✅ Dynamic values - Use environment variables, compute values
  • ✅ Code reuse - Import common configurations from other files
  • ✅ Parameterization - Easily parameterize sessions from external tools
  • ✅ Type safety - Use JSDoc or TypeScript for validation
  • ✅ Computation - Calculate values, filter arrays, etc.

Advanced MJS Examples

Using Environment Variables

export default {
  version: "1.0.0",
  timestamp: new Date().toISOString(),

  cubes: [
    {
      key: "typestack-install",
      variables: {
        REPO: process.env.GIT_REPO || "git@github.com:org/repo.git",
        USER: process.env.DEPLOY_USER || "admin",
        APP: process.env.APP_NAME || "myapp",
        ENV: process.env.NODE_ENV || "production"
      }
    }
  ],

  hosts: [process.env.TARGET_HOST || "@ssh/localhost"],

  auth: {
    method: "password",
    username: process.env.SSH_USER || "admin"
  }
};

Conditional Cube Inclusion

const isDevelopment = process.env.NODE_ENV === 'development';

export default {
  version: "1.0.0",
  timestamp: new Date().toISOString(),

  cubes: [
    {
      key: "runtime:docker",
      variables: { DISTRO: "debian" }
    },

    // Only include in development
    ...(isDevelopment ? [{
      key: "debug-tools",
      variables: { INSTALL_GDB: true }
    }] : [])
  ],

  hosts: ["@ssh/myhost.local"],
  auth: { method: "ssh" }
};

Importing Common Configuration

common-config.mjs:

export const productionHosts = [
  "@ssh/prod-server-1.local",
  "@ssh/prod-server-2.local"
];

export const stagingHosts = [
  "@ssh/staging.local"
];

export const commonCubes = [
  {
    key: "apt:essentials",
    variables: { UPDATE: true }
  },
  {
    key: "runtime:docker",
    variables: { DISTRO: "debian" }
  }
];

my-session.nopysession.mjs:

import { productionHosts, commonCubes } from './common-config.mjs';

export default {
  version: "1.0.0",
  timestamp: new Date().toISOString(),

  cubes: [
    ...commonCubes,  // Include common cubes
    {
      key: "typestack-install",
      variables: {
        REPO: "git@github.com:myorg/myapp.git",
        USER: "appuser",
        APP: "myapp"
      }
    }
  ],

  hosts: productionHosts,  // Use imported hosts

  auth: {
    method: "password",
    username: "admin"
  }
};

Programmatic Generation

You can even generate sessions programmatically from other tools:

generate-session.mjs:

import fs from 'fs';

function generateSession(config) {
  const cubes = config.services.map(service => ({
    key: "typestack-install",
    variables: {
      REPO: service.repo,
      USER: config.user,
      APP: service.name,
      ENV: config.environment
    }
  }));

  const session = {
    version: "1.0.0",
    timestamp: new Date().toISOString(),
    cubes,
    hosts: config.hosts,
    auth: {
      method: "password",
      username: config.user
    }
  };

  const content = `export default ${JSON.stringify(session, null, 2)};`;
  fs.writeFileSync('generated.nopysession.mjs', content);
}

// Generate from external configuration
generateSession({
  user: "deploy",
  environment: "production",
  services: [
    { name: "api", repo: "git@github.com:org/api.git" },
    { name: "web", repo: "git@github.com:org/web.git" }
  ],
  hosts: ["@ssh/prod.local"]
});

Loading Sessions

Both formats are loaded the same way:

import { loadSession } from '@bitsquare/nopy';

// Load JSON
const jsonSession = await loadSession('./my-session.nopysession.json');

// Load MJS
const mjsSession = await loadSession('./my-session.nopysession.mjs');

The file extension determines which loader to use.

Migration from JSON to MJS

To convert an existing JSON session to MJS:

  1. Rename the file from .nopysession.json to .nopysession.mjs
  2. Add export default before the configuration object
  3. Remove quotes from property keys (optional)
  4. Add comments and dynamic values as needed

Before (JSON):

{
  "version": "1.0.0",
  "cubes": [...]
}

After (MJS):

export default {
  version: "1.0.0",
  cubes: [...]
};

Best Practices

  1. Use MJS for new sessions - Take advantage of comments and flexibility
  2. Document your cubes - Add comments explaining what each cube does
  3. Use environment variables - Make sessions reusable across environments
  4. Extract common config - Share configuration across multiple sessions
  5. Version control - Both formats work well with git, but a recorded session holds every value its run used; read one before committing it
  6. Validate at runtime - The loader validates the structure regardless of format
  7. Leave secrets out - Declare them in the manifest instead, and let the replay ask

Session Schema

Both formats must export/contain an object with this structure:

interface NopySession {
  cubes: CubeSession[];        // Array of cube configurations — required
  auth: AuthSession;           // Authentication configuration — required
  version?: string;            // Session format version, currently "1.0.0"
  timestamp?: string;          // ISO timestamp
  name?: string;               // One-line description
  hosts?: string[];            // Target hosts
  env?: Record<string, any>;   // Global environment variables
}

interface CubeSession {
  key: string;                 // Cube identifier
  variables: Record<string, any>; // Cube-specific variables
}

interface AuthSession {
  method: 'ssh-key' | 'password' | 'ssh';
  username?: string;
}

Only cubes and auth are demanded of a session being read — the loader requires what it cannot work without and nothing else, so the sessions in these examples are all valid, and one written before version existed still loads. A session nopy writes always carries version, timestamp and name; a version this build does not recognise produces a warning on stderr and loads anyway.

method: 'ssh' is the third value and the one no prompt produces: it means the connector handles authentication and nopy supplies no credential. Every @vagrant/ and @docker/ host gets it.

A session nopy writes holds, per cube, every value that cube ran with — what was typed, what came from .nopyrc.json, what a dependency supplied, and what fell through to the schema's .default(). Two things are deliberately absent and are asked for again on replay: the SSH password, and any key the cube's manifest listed under secrets.

A session you write by hand is under no such obligation — variables may hold as few keys as you like, and anything missing resolves the usual way. Note that a key declared a secret is prompted for whether or not the session carries a value: writing one in only pre-fills the prompt, it does not skip it. The variable form shows what it is editing, so a secret you put in a session file appears on screen as well as on disk.