355 lines
8.7 KiB
Markdown
355 lines
8.7 KiB
Markdown
# 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:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
### MJS Format (`.nopysession.mjs`) - **Recommended**
|
|
|
|
JavaScript module format with full ES Module support:
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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:**
|
|
```javascript
|
|
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:**
|
|
```javascript
|
|
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:**
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
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):**
|
|
```json
|
|
{
|
|
"version": "1.0.0",
|
|
"cubes": [...]
|
|
}
|
|
```
|
|
|
|
**After (MJS):**
|
|
```javascript
|
|
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:
|
|
|
|
```typescript
|
|
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.
|