@royaltyport/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -0
- package/bin/royaltyport.js +19 -0
- package/package.json +27 -0
- package/skills/royaltyport-sandbox/SKILL.md +164 -0
- package/src/commands/login.js +54 -0
- package/src/commands/logout.js +12 -0
- package/src/commands/project.js +60 -0
- package/src/commands/projects.js +36 -0
- package/src/lib/api.js +79 -0
- package/src/lib/config.js +33 -0
- package/src/lib/output.js +31 -0
- package/src/lib/theme.js +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# @royaltyport/cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for Royaltyport. Authenticate, browse projects, and execute commands in project sandboxes.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js >= 18.0.0
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @royaltyport/cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install from source:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/royaltyport/royaltyport-cli.git
|
|
19
|
+
cd royaltyport-cli
|
|
20
|
+
npm install
|
|
21
|
+
npm link
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Authentication
|
|
25
|
+
|
|
26
|
+
### Interactive login
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
royaltyport login
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You'll be prompted for your API token (`rp_...`). The token is validated against the API and stored locally.
|
|
33
|
+
|
|
34
|
+
### Token flag
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
royaltyport login --token rp_your_token_here
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Environment variables
|
|
41
|
+
|
|
42
|
+
For CI/CD or AI agent integrations, set these environment variables instead of running `login`:
|
|
43
|
+
|
|
44
|
+
| Variable | Description |
|
|
45
|
+
| -------------------- | -------------------------------------------------------- |
|
|
46
|
+
| `ROYALTYPORT_TOKEN` | API token — overrides the stored token |
|
|
47
|
+
| `ROYALTYPORT_API_URL`| Custom API base URL (default: `https://api.royaltyport.com`) |
|
|
48
|
+
|
|
49
|
+
### Custom API URL
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
royaltyport login --api-url https://your-api-url.com
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Logout
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
royaltyport logout
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Commands
|
|
62
|
+
|
|
63
|
+
### `royaltyport projects`
|
|
64
|
+
|
|
65
|
+
List all accessible projects.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
royaltyport projects
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
ID Name Created
|
|
73
|
+
───────────────────────────────────── ────────────────── ──────────
|
|
74
|
+
a1b2c3d4-... Record Label Ltd 1/15/2025
|
|
75
|
+
e5f6g7h8-... Publishing Co 3/22/2025
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `royaltyport project info <project_id>`
|
|
79
|
+
|
|
80
|
+
Display the AGENTS.md from the project sandbox — a filesystem overview with instructions for navigating project data.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
royaltyport project info a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `royaltyport project exec <project_id> "<command>"`
|
|
87
|
+
|
|
88
|
+
Execute a single bash command in the project sandbox. Commands run with the sandbox workspace root as the working directory.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# List all contract folders
|
|
92
|
+
royaltyport project exec $PROJECT_ID "ls contracts/"
|
|
93
|
+
|
|
94
|
+
# Read project stats
|
|
95
|
+
royaltyport project exec $PROJECT_ID "cat stats.yaml"
|
|
96
|
+
|
|
97
|
+
# Search for an entity by name
|
|
98
|
+
royaltyport project exec $PROJECT_ID "grep -rl 'Sony Music' entities/"
|
|
99
|
+
|
|
100
|
+
# Read a contract's extracted royalties
|
|
101
|
+
royaltyport project exec $PROJECT_ID "cat contracts/contract_123/extracted/royalties.yaml"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
stdout and stderr are written to their respective streams, and the process exits with the command's exit code — making it suitable for scripting and AI agent tool use.
|
|
105
|
+
|
|
106
|
+
## Agent Skill
|
|
107
|
+
|
|
108
|
+
This repo includes a [skills.sh](https://skills.sh/)-compatible skill that teaches AI agents how to use the CLI to explore and query Royaltyport project data.
|
|
109
|
+
|
|
110
|
+
Install it into your agent:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npx skills add royaltyport/royaltyport-cli
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The skill covers authentication, project discovery, filesystem layout, and common data access patterns — everything an agent needs to send the right `project exec` commands.
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
Credentials and settings are stored at `~/.config/royaltyport/config.json` (managed by [conf](https://github.com/sindresorhus/conf)). Running `royaltyport logout` clears this file.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
UNLICENSED
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { registerLoginCommand } from '../src/commands/login.js';
|
|
5
|
+
import { registerLogoutCommand } from '../src/commands/logout.js';
|
|
6
|
+
import { registerProjectsCommand } from '../src/commands/projects.js';
|
|
7
|
+
import { registerProjectCommand } from '../src/commands/project.js';
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('royaltyport')
|
|
11
|
+
.description('Royaltyport CLI — authenticate, list projects, and execute commands in a sandboxed project filesystem')
|
|
12
|
+
.version('0.1.0');
|
|
13
|
+
|
|
14
|
+
registerLoginCommand(program);
|
|
15
|
+
registerLogoutCommand(program);
|
|
16
|
+
registerProjectsCommand(program);
|
|
17
|
+
registerProjectCommand(program);
|
|
18
|
+
|
|
19
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@royaltyport/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Royaltyport CLI — authenticate, list projects, and connect to project file systems",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"royaltyport": "bin/royaltyport.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/royaltyport.js",
|
|
11
|
+
"lint": "eslint"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"royaltyport",
|
|
15
|
+
"cli"
|
|
16
|
+
],
|
|
17
|
+
"license": "UNLICENSED",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chalk": "^5.4.1",
|
|
23
|
+
"commander": "^13.1.0",
|
|
24
|
+
"conf": "^13.1.0",
|
|
25
|
+
"ora": "^8.2.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: royaltyport-sandbox
|
|
3
|
+
description: Access Royaltyport project data via the CLI sandbox. Use when working with music royalty contracts, entities, artists, writers, relations, recordings, compositions, or statements.
|
|
4
|
+
metadata:
|
|
5
|
+
author: royaltyport
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill: Royaltyport Sandbox
|
|
10
|
+
|
|
11
|
+
Access Royaltyport project data through the CLI. All project data is stored as YAML files in a sandboxed filesystem. You execute bash commands against the sandbox — no SDK or API calls needed.
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
Install the CLI and authenticate:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @royaltyport/cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Set your API token as an environment variable (preferred for agents):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export ROYALTYPORT_TOKEN=rp_your_token_here
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or authenticate interactively:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
royaltyport login
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Discovery
|
|
34
|
+
|
|
35
|
+
### List available projects
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
royaltyport projects
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Get the full filesystem overview for a project
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
royaltyport project info <project_id>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This prints the project's AGENTS.md — a detailed breakdown of every directory, file type, and YAML field available in the sandbox.
|
|
48
|
+
|
|
49
|
+
## Executing Commands
|
|
50
|
+
|
|
51
|
+
Run any bash command in the sandbox with `project exec`. Commands run with the workspace root as the working directory. Use relative paths directly.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
royaltyport project exec <project_id> "<command>"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
stdout and stderr stream to their respective outputs. The process exits with the command's exit code.
|
|
58
|
+
|
|
59
|
+
## Filesystem Layout
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
stats.yaml # Record counts for all resource types
|
|
63
|
+
contracts/contract_{id}/ # Per-contract data
|
|
64
|
+
uploaded.yaml # File metadata (id, file_name, file_type)
|
|
65
|
+
extracted/ # AI-extracted contract terms
|
|
66
|
+
royalties.yaml, entities.yaml, dates.yaml, control_areas.yaml,
|
|
67
|
+
compensations.yaml, costs.yaml, artists.yaml, writers.yaml,
|
|
68
|
+
recordings.yaml, compositions.yaml, signatures.yaml, splits.yaml,
|
|
69
|
+
accounting_period.yaml, languages.yaml, types.yaml,
|
|
70
|
+
creative_approvals.yaml, targets.yaml, commitments.yaml
|
|
71
|
+
relationships/ # Parent/sibling/child links
|
|
72
|
+
statements.yaml # Linked statements
|
|
73
|
+
entities/entity_{id}/ # metadata.yaml, merged.yaml, relations.yaml, artists.yaml, writers.yaml
|
|
74
|
+
artists/artist_{id}/ # metadata.yaml, merged.yaml, entities.yaml
|
|
75
|
+
writers/writer_{id}/ # metadata.yaml, merged.yaml, entities.yaml
|
|
76
|
+
relations/relation_{id}/ # metadata.yaml, merged.yaml, entities.yaml
|
|
77
|
+
recordings/recording_{id}/ # metadata.yaml, products.yaml, tracks.yaml
|
|
78
|
+
compositions/composition_{id}/ # metadata.yaml, products.yaml, tracks.yaml
|
|
79
|
+
statements/
|
|
80
|
+
statement_{id}/ # metadata.yaml, contracts.yaml
|
|
81
|
+
recordings/statement_{id}/assets.yaml # Recording assets (isrc, upc, matched)
|
|
82
|
+
compositions/statement_{id}/assets.yaml # Composition assets (iswc, work_id, matched)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Common Data Access Patterns
|
|
86
|
+
|
|
87
|
+
### Project overview
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
royaltyport project exec $PROJECT_ID "cat stats.yaml"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Find contracts
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# List all contracts
|
|
97
|
+
royaltyport project exec $PROJECT_ID "ls contracts/"
|
|
98
|
+
|
|
99
|
+
# Find contract by name
|
|
100
|
+
royaltyport project exec $PROJECT_ID "grep -ril 'CONTRACT_NAME' contracts/contract_*/uploaded.yaml"
|
|
101
|
+
|
|
102
|
+
# Find contracts involving an entity
|
|
103
|
+
royaltyport project exec $PROJECT_ID "grep -ril 'ENTITY_NAME' contracts/contract_*/extracted/entities.yaml"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Read contract details
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# File metadata
|
|
110
|
+
royaltyport project exec $PROJECT_ID "cat contracts/contract_{id}/uploaded.yaml"
|
|
111
|
+
|
|
112
|
+
# Extracted terms
|
|
113
|
+
royaltyport project exec $PROJECT_ID "cat contracts/contract_{id}/extracted/royalties.yaml"
|
|
114
|
+
royaltyport project exec $PROJECT_ID "cat contracts/contract_{id}/extracted/splits.yaml"
|
|
115
|
+
royaltyport project exec $PROJECT_ID "ls contracts/contract_{id}/extracted/"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Find artists, writers, entities
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Search by name
|
|
122
|
+
royaltyport project exec $PROJECT_ID "grep -rl 'SEARCH_TERM' artists/"
|
|
123
|
+
royaltyport project exec $PROJECT_ID "grep -rl 'SEARCH_TERM' writers/"
|
|
124
|
+
royaltyport project exec $PROJECT_ID "grep -rl 'SEARCH_TERM' entities/"
|
|
125
|
+
|
|
126
|
+
# List all artist names
|
|
127
|
+
royaltyport project exec $PROJECT_ID "grep -rh '^name:' artists/artist_*/metadata.yaml | sort"
|
|
128
|
+
|
|
129
|
+
# Read artist details
|
|
130
|
+
royaltyport project exec $PROJECT_ID "cat artists/artist_{id}/metadata.yaml"
|
|
131
|
+
royaltyport project exec $PROJECT_ID "cat artists/artist_{id}/merged.yaml"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Find recordings and compositions
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Search by name or ISRC/ISWC
|
|
138
|
+
royaltyport project exec $PROJECT_ID "grep -rl 'SEARCH_TERM' recordings/"
|
|
139
|
+
royaltyport project exec $PROJECT_ID "grep -rl 'ISWC_CODE' compositions/"
|
|
140
|
+
|
|
141
|
+
# Read recording details
|
|
142
|
+
royaltyport project exec $PROJECT_ID "cat recordings/recording_{id}/metadata.yaml"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Browse statements
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# List statements
|
|
149
|
+
royaltyport project exec $PROJECT_ID "ls statements/ | head -20"
|
|
150
|
+
|
|
151
|
+
# Read statement metadata
|
|
152
|
+
royaltyport project exec $PROJECT_ID "cat statements/statement_{id}/metadata.yaml"
|
|
153
|
+
|
|
154
|
+
# Read matched recording assets on a statement
|
|
155
|
+
royaltyport project exec $PROJECT_ID "cat statements/recordings/statement_{id}/assets.yaml"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Tips
|
|
159
|
+
|
|
160
|
+
- **Always start with `stats.yaml`** to understand how much data the project has.
|
|
161
|
+
- **Use `grep -rl`** for searching across files, `grep -rh` for extracting values.
|
|
162
|
+
- **Check `merged.yaml`** — artists, writers, and entities may have duplicates merged into a root record.
|
|
163
|
+
- **All data is plain-text YAML** — standard Unix tools (`grep`, `cat`, `find`, `wc`, `sort`, `head`) all work.
|
|
164
|
+
- **Contract extractions** under `extracted/` are AI-extracted terms. One YAML file per category.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { apiGet } from '../lib/api.js';
|
|
4
|
+
import { setToken, setApiUrl, getConfigPath } from '../lib/config.js';
|
|
5
|
+
import { printError, printSuccess, printInfo } from '../lib/output.js';
|
|
6
|
+
import { spinnerColor, dim } from '../lib/theme.js';
|
|
7
|
+
|
|
8
|
+
export function registerLoginCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('login')
|
|
11
|
+
.description('Authenticate with your Royaltyport API token')
|
|
12
|
+
.option('-t, --token <token>', 'API token (rp_...)')
|
|
13
|
+
.option('--api-url <url>', 'Custom API URL (default: https://api.royaltyport.com)')
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
try {
|
|
16
|
+
let token = options.token;
|
|
17
|
+
|
|
18
|
+
if (!token) {
|
|
19
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
20
|
+
token = await rl.question('Enter your API token (rp_...): ');
|
|
21
|
+
rl.close();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
token = token.trim();
|
|
25
|
+
if (!token) {
|
|
26
|
+
printError('Token cannot be empty.');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (options.apiUrl) {
|
|
31
|
+
setApiUrl(options.apiUrl);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const spinner = ora({ text: 'Validating token...', color: spinnerColor }).start();
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await apiGet('/v1/projects', token);
|
|
38
|
+
const projectCount = Array.isArray(response.data) ? response.data.length : 0;
|
|
39
|
+
spinner.succeed(`Authenticated successfully (${projectCount} project${projectCount !== 1 ? 's' : ''} accessible)`);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
spinner.fail('Token validation failed');
|
|
42
|
+
printError(err.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setToken(token);
|
|
47
|
+
printSuccess('Token saved.');
|
|
48
|
+
printInfo(`Config stored at ${dim(getConfigPath())}`);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
printError(err.message);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { clearConfig } from '../lib/config.js';
|
|
2
|
+
import { printSuccess } from '../lib/output.js';
|
|
3
|
+
|
|
4
|
+
export function registerLogoutCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command('logout')
|
|
7
|
+
.description('Clear stored credentials')
|
|
8
|
+
.action(() => {
|
|
9
|
+
clearConfig();
|
|
10
|
+
printSuccess('Logged out. Credentials cleared.');
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import { apiPost, apiGet, requireAuth } from '../lib/api.js';
|
|
3
|
+
import { printError, printInfo } from '../lib/output.js';
|
|
4
|
+
import { warning, spinnerColor } from '../lib/theme.js';
|
|
5
|
+
|
|
6
|
+
export function registerProjectCommand(program) {
|
|
7
|
+
const project = program
|
|
8
|
+
.command('project')
|
|
9
|
+
.description('Use bash commands to explore a project filesystem');
|
|
10
|
+
|
|
11
|
+
project
|
|
12
|
+
.command('info')
|
|
13
|
+
.description('Show the AGENTS.md for a project sandbox (filesystem overview and instructions)')
|
|
14
|
+
.argument('<project_id>', 'Project ID')
|
|
15
|
+
.action(async (projectId) => {
|
|
16
|
+
try {
|
|
17
|
+
requireAuth();
|
|
18
|
+
|
|
19
|
+
const spinner = ora({ text: 'Connecting to sandbox...', color: spinnerColor }).start();
|
|
20
|
+
await apiPost(`/v1/projects/${projectId}/sandbox/connect`, {});
|
|
21
|
+
spinner.text = 'Reading AGENTS.md...';
|
|
22
|
+
|
|
23
|
+
const response = await apiGet(`/v1/projects/${projectId}/sandbox/files?path=${encodeURIComponent('AGENTS.md')}`);
|
|
24
|
+
spinner.stop();
|
|
25
|
+
|
|
26
|
+
if (response.data?.type === 'file' && response.data.content) {
|
|
27
|
+
console.log(response.data.content);
|
|
28
|
+
} else {
|
|
29
|
+
printInfo('No AGENTS.md found in this project sandbox.');
|
|
30
|
+
}
|
|
31
|
+
} catch (err) {
|
|
32
|
+
printError(err.message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
project
|
|
38
|
+
.command('exec')
|
|
39
|
+
.description('Execute a bash command in a project sandbox')
|
|
40
|
+
.argument('<project_id>', 'Project ID')
|
|
41
|
+
.argument('<command>', 'Bash command to execute')
|
|
42
|
+
.action(async (projectId, command) => {
|
|
43
|
+
try {
|
|
44
|
+
requireAuth();
|
|
45
|
+
|
|
46
|
+
await apiPost(`/v1/projects/${projectId}/sandbox/connect`, {});
|
|
47
|
+
|
|
48
|
+
const response = await apiPost(`/v1/projects/${projectId}/sandbox/exec`, { command });
|
|
49
|
+
const { stdout, stderr, exitCode } = response.data;
|
|
50
|
+
|
|
51
|
+
if (stdout) process.stdout.write(stdout);
|
|
52
|
+
if (stderr) process.stderr.write(warning(stderr));
|
|
53
|
+
|
|
54
|
+
process.exit(exitCode);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
printError(err.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import { apiGet, requireAuth } from '../lib/api.js';
|
|
3
|
+
import { printTable, printError, printInfo } from '../lib/output.js';
|
|
4
|
+
import { spinnerColor } from '../lib/theme.js';
|
|
5
|
+
|
|
6
|
+
export function registerProjectsCommand(program) {
|
|
7
|
+
program
|
|
8
|
+
.command('projects')
|
|
9
|
+
.description('List available projects')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
try {
|
|
12
|
+
requireAuth();
|
|
13
|
+
|
|
14
|
+
const spinner = ora({ text: 'Fetching projects...', color: spinnerColor }).start();
|
|
15
|
+
const response = await apiGet('/v1/projects');
|
|
16
|
+
spinner.stop();
|
|
17
|
+
|
|
18
|
+
const projects = response.data;
|
|
19
|
+
if (!projects || projects.length === 0) {
|
|
20
|
+
printInfo('No projects found.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const rows = projects.map((p) => [
|
|
25
|
+
p.id,
|
|
26
|
+
p.name,
|
|
27
|
+
new Date(p.created_at).toLocaleDateString(),
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
printTable(['ID', 'Name', 'Created'], rows);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
printError(err.message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
package/src/lib/api.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getToken, getApiUrl } from './config.js';
|
|
2
|
+
|
|
3
|
+
class ApiError extends Error {
|
|
4
|
+
constructor(message, status, body) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'ApiError';
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.body = body;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildHeaders(token) {
|
|
13
|
+
return {
|
|
14
|
+
'Authorization': `Bearer ${token}`,
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function parseResponse(res) {
|
|
20
|
+
const text = await res.text();
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(text);
|
|
23
|
+
} catch {
|
|
24
|
+
return { message: text };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function requireAuth() {
|
|
29
|
+
const token = getToken();
|
|
30
|
+
if (!token) {
|
|
31
|
+
throw new ApiError('Not authenticated. Run `royaltyport login` first.', 401);
|
|
32
|
+
}
|
|
33
|
+
return token;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function apiGet(path, token) {
|
|
37
|
+
const baseUrl = getApiUrl();
|
|
38
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
39
|
+
method: 'GET',
|
|
40
|
+
headers: buildHeaders(token || requireAuth()),
|
|
41
|
+
});
|
|
42
|
+
const body = await parseResponse(res);
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const msg = body?.error?.message || body?.message || `Request failed with status ${res.status}`;
|
|
45
|
+
throw new ApiError(msg, res.status, body);
|
|
46
|
+
}
|
|
47
|
+
return body;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function apiPost(path, data, token) {
|
|
51
|
+
const baseUrl = getApiUrl();
|
|
52
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: buildHeaders(token || requireAuth()),
|
|
55
|
+
body: JSON.stringify(data),
|
|
56
|
+
});
|
|
57
|
+
const body = await parseResponse(res);
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
const msg = body?.error?.message || body?.message || `Request failed with status ${res.status}`;
|
|
60
|
+
throw new ApiError(msg, res.status, body);
|
|
61
|
+
}
|
|
62
|
+
return body;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function apiDelete(path, token) {
|
|
66
|
+
const baseUrl = getApiUrl();
|
|
67
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
68
|
+
method: 'DELETE',
|
|
69
|
+
headers: buildHeaders(token || requireAuth()),
|
|
70
|
+
});
|
|
71
|
+
const body = await parseResponse(res);
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
const msg = body?.error?.message || body?.message || `Request failed with status ${res.status}`;
|
|
74
|
+
throw new ApiError(msg, res.status, body);
|
|
75
|
+
}
|
|
76
|
+
return body;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { ApiError };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
|
|
3
|
+
const config = new Conf({
|
|
4
|
+
projectName: 'royaltyport',
|
|
5
|
+
schema: {
|
|
6
|
+
token: { type: 'string', default: '' },
|
|
7
|
+
apiUrl: { type: 'string', default: 'https://api.royaltyport.com' },
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function getToken() {
|
|
12
|
+
return process.env.ROYALTYPORT_TOKEN || config.get('token');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function setToken(token) {
|
|
16
|
+
config.set('token', token);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getApiUrl() {
|
|
20
|
+
return process.env.ROYALTYPORT_API_URL || config.get('apiUrl');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function setApiUrl(url) {
|
|
24
|
+
config.set('apiUrl', url);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function clearConfig() {
|
|
28
|
+
config.clear();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getConfigPath() {
|
|
32
|
+
return config.path;
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { brand, brandBold, dim, error, accent } from './theme.js';
|
|
2
|
+
|
|
3
|
+
export function printTable(columns, rows) {
|
|
4
|
+
const widths = columns.map((col, i) => {
|
|
5
|
+
const maxData = rows.reduce((max, row) => Math.max(max, String(row[i] ?? '').length), 0);
|
|
6
|
+
return Math.max(col.length, maxData);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const header = columns.map((col, i) => col.padEnd(widths[i])).join(' ');
|
|
10
|
+
const separator = widths.map(w => '─'.repeat(w)).join('──');
|
|
11
|
+
|
|
12
|
+
console.log(brandBold(header));
|
|
13
|
+
console.log(dim(separator));
|
|
14
|
+
|
|
15
|
+
for (const row of rows) {
|
|
16
|
+
const line = row.map((cell, i) => String(cell ?? '').padEnd(widths[i])).join(' ');
|
|
17
|
+
console.log(line);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function printError(message) {
|
|
22
|
+
console.error(error(`Error: ${message}`));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function printSuccess(message) {
|
|
26
|
+
console.log(brand(message));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function printInfo(message) {
|
|
30
|
+
console.log(accent(message));
|
|
31
|
+
}
|
package/src/lib/theme.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export const brand = chalk.green;
|
|
4
|
+
export const accent = chalk.cyan;
|
|
5
|
+
export const dim = chalk.dim;
|
|
6
|
+
export const bold = chalk.bold;
|
|
7
|
+
export const error = chalk.red;
|
|
8
|
+
export const warning = chalk.yellow;
|
|
9
|
+
|
|
10
|
+
export const spinnerColor = 'white';
|
|
11
|
+
|
|
12
|
+
export function brandBold(text) {
|
|
13
|
+
return chalk.bold(text);
|
|
14
|
+
}
|