@pingroom/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 +93 -0
- package/bin/pingroom.js +175 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @pingroom/cli
|
|
2
|
+
|
|
3
|
+
Send PingRoom pings from CI, scripts, and agents — deploy notifications in one line,
|
|
4
|
+
delivered as push straight to your phone.
|
|
5
|
+
|
|
6
|
+
Zero dependencies. Works anywhere Node ≥ 20 runs.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx @pingroom/cli ping -w "$PINGROOM_WEBHOOK_URL" -m "Deploy succeeded ✅"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Getting a webhook URL
|
|
13
|
+
|
|
14
|
+
In the PingRoom app, open a room → **Connections → Incoming webhooks → Add**. Copy the
|
|
15
|
+
URL (it embeds its own secret — treat it like a password and store it as a CI secret).
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pingroom ping [options]
|
|
21
|
+
|
|
22
|
+
-m, --message <text> Ping body text (required)
|
|
23
|
+
-t, --title <text> Ping title (<= 40 chars)
|
|
24
|
+
-a, --action <1-4> Quick-action slot to attribute the ping to
|
|
25
|
+
-d, --data <json> Extra JSON data, e.g. '{"commit":"abc123"}'
|
|
26
|
+
-w, --webhook <url> Room webhook URL (or env PINGROOM_WEBHOOK_URL)
|
|
27
|
+
--token <token> Agent access token (or env PINGROOM_TOKEN)
|
|
28
|
+
--room <code> Room invite code (used with --token)
|
|
29
|
+
--api <url> API base URL (env PINGROOM_API_URL)
|
|
30
|
+
--json Print the raw JSON response
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Exit codes: `0` success · `1` delivery failed · `2` bad usage. So CI fails loudly if a
|
|
34
|
+
ping doesn't land.
|
|
35
|
+
|
|
36
|
+
## GitHub Actions
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
# Notify on deploy
|
|
40
|
+
- uses: pingroom/cli@v0
|
|
41
|
+
with:
|
|
42
|
+
webhook-url: ${{ secrets.PINGROOM_WEBHOOK_URL }}
|
|
43
|
+
title: 'Deploy'
|
|
44
|
+
message: '🚀 ${{ github.repository }} deployed (${{ github.sha }})'
|
|
45
|
+
data: '{"ref":"${{ github.ref_name }}","run":"${{ github.run_id }}"}'
|
|
46
|
+
|
|
47
|
+
# Notify only on failure
|
|
48
|
+
- if: failure()
|
|
49
|
+
uses: pingroom/cli@v0
|
|
50
|
+
with:
|
|
51
|
+
webhook-url: ${{ secrets.PINGROOM_WEBHOOK_URL }}
|
|
52
|
+
title: 'CI failed'
|
|
53
|
+
message: '❌ ${{ github.workflow }} failed on ${{ github.ref_name }}'
|
|
54
|
+
action: '2'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## GitLab CI
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
notify:
|
|
61
|
+
stage: .post
|
|
62
|
+
image: node:20-alpine
|
|
63
|
+
script:
|
|
64
|
+
- npx --yes @pingroom/cli ping -t "Deploy" -m "🚀 $CI_PROJECT_NAME @ $CI_COMMIT_SHORT_SHA"
|
|
65
|
+
variables:
|
|
66
|
+
PINGROOM_WEBHOOK_URL: $PINGROOM_WEBHOOK_URL # set as a masked CI/CD variable
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Plain shell / curl
|
|
70
|
+
|
|
71
|
+
The webhook is just an HTTP POST, so you don't even need this CLI:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
curl -fsS -X POST "$PINGROOM_WEBHOOK_URL" \
|
|
75
|
+
-H 'Content-Type: application/json' \
|
|
76
|
+
-d '{"title":"Deploy","message":"🚀 shipped"}'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Agent token mode
|
|
80
|
+
|
|
81
|
+
For an agent acting as a user (e.g. an OAuth/auth.md credential), send to a room the
|
|
82
|
+
agent belongs to instead of a webhook:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pingroom ping --token "$PINGROOM_TOKEN" --room ab12cd -m "Release shipped" \
|
|
86
|
+
-d '{"version":"1.4.0"}'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
See <https://pingroom.io/connect-mcp.md> to connect Cursor, Claude Desktop, or Claude Code.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/bin/pingroom.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @pingroom/cli — send a PingRoom ping from CI, a script, or an agent.
|
|
3
|
+
// Zero dependencies: uses Node's built-in fetch (Node >= 20).
|
|
4
|
+
//
|
|
5
|
+
// Two delivery modes:
|
|
6
|
+
// 1. Webhook (best for CI) — a room webhook URL carries its own secret, so no
|
|
7
|
+
// browser auth is needed. `pingroom ping -w <url> -m "deploy ok"`.
|
|
8
|
+
// 2. Agent token — a Bearer credential + room invite code hits the agent API.
|
|
9
|
+
// `pingroom ping --token <t> --room <code> -m "deploy ok"`.
|
|
10
|
+
|
|
11
|
+
const DEFAULT_API = process.env.PINGROOM_API_URL || 'https://api.pingroom.io';
|
|
12
|
+
|
|
13
|
+
const HELP = `pingroom — send a ping to a PingRoom room
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
pingroom ping [options]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
-m, --message <text> Ping body text (required)
|
|
20
|
+
-t, --title <text> Ping title (<= 40 chars)
|
|
21
|
+
-a, --action <1-4> Quick-action slot to attribute the ping to
|
|
22
|
+
-d, --data <json> Extra JSON data object, e.g. '{"commit":"abc123"}'
|
|
23
|
+
-w, --webhook <url> Room webhook URL (or env PINGROOM_WEBHOOK_URL)
|
|
24
|
+
--token <token> Agent access token (or env PINGROOM_TOKEN)
|
|
25
|
+
--room <code> Room invite code (used with --token)
|
|
26
|
+
--api <url> API base URL (default ${DEFAULT_API}; env PINGROOM_API_URL)
|
|
27
|
+
--json Print the raw JSON response
|
|
28
|
+
-h, --help Show this help
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
pingroom ping -w "$PINGROOM_WEBHOOK_URL" -m "Deploy succeeded ✅"
|
|
32
|
+
pingroom ping -w "$PINGROOM_WEBHOOK_URL" -t "CI" -m "Build #42 failed" -a 2
|
|
33
|
+
pingroom ping --token "$PINGROOM_TOKEN" --room ab12cd -m "Release shipped" \\
|
|
34
|
+
-d '{"version":"1.4.0","env":"prod"}'
|
|
35
|
+
|
|
36
|
+
Exit codes: 0 on success, 1 on delivery failure, 2 on bad usage.`;
|
|
37
|
+
|
|
38
|
+
function parseArgs(argv) {
|
|
39
|
+
const args = { _: [] };
|
|
40
|
+
const alias = {
|
|
41
|
+
'-m': 'message', '--message': 'message',
|
|
42
|
+
'-t': 'title', '--title': 'title',
|
|
43
|
+
'-a': 'action', '--action': 'action',
|
|
44
|
+
'-d': 'data', '--data': 'data',
|
|
45
|
+
'-w': 'webhook', '--webhook': 'webhook',
|
|
46
|
+
'--token': 'token',
|
|
47
|
+
'--room': 'room',
|
|
48
|
+
'--api': 'api',
|
|
49
|
+
'--json': 'json',
|
|
50
|
+
'-h': 'help', '--help': 'help',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < argv.length; i++) {
|
|
54
|
+
const token = argv[i];
|
|
55
|
+
if (token === '--json' || token === '-h' || token === '--help') {
|
|
56
|
+
args[alias[token]] = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const key = alias[token];
|
|
60
|
+
if (key) {
|
|
61
|
+
args[key] = argv[++i];
|
|
62
|
+
} else if (token.startsWith('-')) {
|
|
63
|
+
fail(`Unknown option: ${token}`, 2);
|
|
64
|
+
} else {
|
|
65
|
+
args._.push(token);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return args;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fail(message, code = 1) {
|
|
72
|
+
process.stderr.write(`pingroom: ${message}\n`);
|
|
73
|
+
process.exit(code);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function postJson(url, body, headers = {}) {
|
|
77
|
+
let res;
|
|
78
|
+
try {
|
|
79
|
+
res = await fetch(url, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...headers },
|
|
82
|
+
body: JSON.stringify(body),
|
|
83
|
+
});
|
|
84
|
+
} catch (err) {
|
|
85
|
+
fail(`network error: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const text = await res.text();
|
|
89
|
+
let json = null;
|
|
90
|
+
try { json = text ? JSON.parse(text) : null; } catch { /* non-JSON response */ }
|
|
91
|
+
|
|
92
|
+
return { res, text, json };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function ping(args) {
|
|
96
|
+
if (args.help) { process.stdout.write(`${HELP}\n`); return 0; }
|
|
97
|
+
|
|
98
|
+
const message = args.message;
|
|
99
|
+
if (!message) fail('a --message is required', 2);
|
|
100
|
+
|
|
101
|
+
if (args.action !== undefined && !/^[1-4]$/.test(String(args.action))) {
|
|
102
|
+
fail('--action must be an integer 1–4', 2);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let data;
|
|
106
|
+
if (args.data !== undefined) {
|
|
107
|
+
try {
|
|
108
|
+
data = JSON.parse(args.data);
|
|
109
|
+
} catch {
|
|
110
|
+
fail('--data must be valid JSON', 2);
|
|
111
|
+
}
|
|
112
|
+
if (typeof data !== 'object' || Array.isArray(data) || data === null) {
|
|
113
|
+
fail('--data must be a JSON object', 2);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const webhook = args.webhook || process.env.PINGROOM_WEBHOOK_URL;
|
|
118
|
+
const token = args.token || process.env.PINGROOM_TOKEN;
|
|
119
|
+
const apiBase = (args.api || DEFAULT_API).replace(/\/$/, '');
|
|
120
|
+
|
|
121
|
+
let result;
|
|
122
|
+
|
|
123
|
+
if (webhook) {
|
|
124
|
+
const body = { message };
|
|
125
|
+
if (args.title) body.title = args.title;
|
|
126
|
+
if (args.action !== undefined) body.action = Number(args.action);
|
|
127
|
+
if (data) body.data = data;
|
|
128
|
+
result = await postJson(webhook, body);
|
|
129
|
+
} else if (token) {
|
|
130
|
+
if (!args.room) fail('--room is required when using --token', 2);
|
|
131
|
+
const url = `${apiBase}/api/agent/rooms/${encodeURIComponent(args.room)}/notifications`;
|
|
132
|
+
const body = { message };
|
|
133
|
+
if (args.title) body.title = args.title;
|
|
134
|
+
if (args.action !== undefined) body.action_number = Number(args.action);
|
|
135
|
+
if (data) body.data = data;
|
|
136
|
+
result = await postJson(url, body, { Authorization: `Bearer ${token}` });
|
|
137
|
+
} else {
|
|
138
|
+
fail('provide a webhook (--webhook / PINGROOM_WEBHOOK_URL) or an agent token (--token / PINGROOM_TOKEN)', 2);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { res, text, json } = result;
|
|
142
|
+
|
|
143
|
+
if (args.json) {
|
|
144
|
+
process.stdout.write(`${text || '{}'}\n`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const ok = res.ok && !(json && json.success === false);
|
|
148
|
+
|
|
149
|
+
if (!ok) {
|
|
150
|
+
const detail = (json && (json.message || json.error)) || `HTTP ${res.status}`;
|
|
151
|
+
fail(`delivery failed: ${detail}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!args.json) process.stdout.write('ping sent ✅\n');
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function main() {
|
|
159
|
+
const argv = process.argv.slice(2);
|
|
160
|
+
const command = argv[0];
|
|
161
|
+
|
|
162
|
+
if (!command || command === '-h' || command === '--help' || command === 'help') {
|
|
163
|
+
process.stdout.write(`${HELP}\n`);
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (command === 'ping') {
|
|
168
|
+
const code = await ping(parseArgs(argv.slice(1)));
|
|
169
|
+
process.exit(code);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fail(`unknown command: ${command}\nRun "pingroom --help".`, 2);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pingroom/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Send PingRoom pings from CI, scripts, and agents — deploy notifications in one line.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pingroom": "bin/pingroom.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pingroom",
|
|
17
|
+
"notifications",
|
|
18
|
+
"push",
|
|
19
|
+
"ci",
|
|
20
|
+
"deploy",
|
|
21
|
+
"webhook",
|
|
22
|
+
"agent",
|
|
23
|
+
"mcp"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"homepage": "https://pingroom.io",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://gitlab.com/pingroom/cli.git"
|
|
30
|
+
}
|
|
31
|
+
}
|