peakflow-instance-control 0.1.1 → 0.1.3
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/CHANGELOG.md +4 -0
- package/README.md +15 -0
- package/package.json +3 -2
- package/scripts/release-patch.sh +10 -0
- package/spec/cli-arguments-spec.js +39 -0
- package/spec/terminal-relay-spec.js +129 -0
- package/src/bug-reporter.js +50 -0
- package/src/cli-arguments.js +2 -0
- package/src/index.js +22 -0
- package/src/terminal-relay.js +30 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
- 2026-02-18: Added an npm authentication precheck to `npm run release:patch` so unauthenticated runs fail early with a clear `npm login` instruction instead of a publish 404.
|
|
4
|
+
- 2026-02-18: Updated `npm run release:patch` to automatically run `npm login` when unauthenticated, then continue with publish only after a successful login.
|
package/README.md
CHANGED
|
@@ -16,11 +16,20 @@ npm install
|
|
|
16
16
|
npm test
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Release patch version
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm run release:patch
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`release:patch` now checks npm authentication first and runs `npm login` automatically when needed before continuing.
|
|
26
|
+
|
|
19
27
|
## Run
|
|
20
28
|
|
|
21
29
|
```bash
|
|
22
30
|
node src/index.js \
|
|
23
31
|
--action-cable-url ws://example.com/cable \
|
|
32
|
+
--bug-report-auth-token BUG_REPORT_AUTH_TOKEN \
|
|
24
33
|
--instance-control-token TOKEN \
|
|
25
34
|
--project-id PROJECT_ID \
|
|
26
35
|
--project-environment-instance-id INSTANCE_ID \
|
|
@@ -28,6 +37,12 @@ node src/index.js \
|
|
|
28
37
|
--tmux-session-name TMUX_SESSION
|
|
29
38
|
```
|
|
30
39
|
|
|
40
|
+
You can also provide the bug reporting auth token through:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
PEAKFLOW_INSTANCE_CONTROL_BUG_REPORT_AUTH_TOKEN=BUG_REPORT_AUTH_TOKEN
|
|
44
|
+
```
|
|
45
|
+
|
|
31
46
|
## Channel contract
|
|
32
47
|
|
|
33
48
|
The process subscribes to `ProjectEnvironmentTerminalSessionsChannel` with `instance_control: true` and handles `request_snapshot`, `send_input`, and `send_keys` command payloads.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peakflow-instance-control",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Instance-side terminal relay for Peakflow project environment sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"test": "node scripts/velocious-cli.js test spec"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@rails/actioncable": "^8.1.200"
|
|
15
|
+
"@rails/actioncable": "^8.1.200",
|
|
16
|
+
"peakflow-api": "^0.0.9"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
19
|
"glob": "^10.5.0",
|
package/scripts/release-patch.sh
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
+
if ! npm whoami >/dev/null 2>&1; then
|
|
5
|
+
echo "You are not signed in to npm. Running 'npm login'..." >&2
|
|
6
|
+
npm login
|
|
7
|
+
|
|
8
|
+
if ! npm whoami >/dev/null 2>&1; then
|
|
9
|
+
echo "npm login did not complete successfully. Aborting release." >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
fi
|
|
13
|
+
|
|
4
14
|
npm version patch
|
|
5
15
|
npm publish
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {describe, expect, it} from "velocious/build/src/testing/test.js"
|
|
2
|
+
import {parseCliArguments} from "../src/cli-arguments.js"
|
|
3
|
+
|
|
4
|
+
describe("parseCliArguments", () => {
|
|
5
|
+
it("uses bug report auth token from environment when option is not passed", async () => {
|
|
6
|
+
process.env.PEAKFLOW_INSTANCE_CONTROL_BUG_REPORT_AUTH_TOKEN = "env-token"
|
|
7
|
+
|
|
8
|
+
const parsed = parseCliArguments([
|
|
9
|
+
"--action-cable-url", "ws://example.com/cable",
|
|
10
|
+
"--instance-control-token", "instance-token",
|
|
11
|
+
"--project-id", "project-id",
|
|
12
|
+
"--project-environment-instance-id", "instance-id",
|
|
13
|
+
"--project-environment-terminal-session-id", "session-id",
|
|
14
|
+
"--tmux-session-name", "tmux-name"
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
expect(parsed.bugReportAuthToken).toEqual("env-token")
|
|
18
|
+
|
|
19
|
+
delete process.env.PEAKFLOW_INSTANCE_CONTROL_BUG_REPORT_AUTH_TOKEN
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("uses bug report auth token from cli option", async () => {
|
|
23
|
+
process.env.PEAKFLOW_INSTANCE_CONTROL_BUG_REPORT_AUTH_TOKEN = "env-token"
|
|
24
|
+
|
|
25
|
+
const parsed = parseCliArguments([
|
|
26
|
+
"--action-cable-url", "ws://example.com/cable",
|
|
27
|
+
"--bug-report-auth-token", "cli-token",
|
|
28
|
+
"--instance-control-token", "instance-token",
|
|
29
|
+
"--project-id", "project-id",
|
|
30
|
+
"--project-environment-instance-id", "instance-id",
|
|
31
|
+
"--project-environment-terminal-session-id", "session-id",
|
|
32
|
+
"--tmux-session-name", "tmux-name"
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
expect(parsed.bugReportAuthToken).toEqual("cli-token")
|
|
36
|
+
|
|
37
|
+
delete process.env.PEAKFLOW_INSTANCE_CONTROL_BUG_REPORT_AUTH_TOKEN
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {describe, expect, it} from "velocious/build/src/testing/test.js"
|
|
2
|
+
import TerminalRelay from "../src/terminal-relay.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {{captureOutputPayload?: () => Promise<{terminal_output: string}>}} [options]
|
|
6
|
+
* @returns {typeof import("../src/tmux-client.js").default}
|
|
7
|
+
*/
|
|
8
|
+
function buildTmuxClientClass(options = {}) {
|
|
9
|
+
return class FakeTmuxClient {
|
|
10
|
+
/** @returns {Promise<void>} */
|
|
11
|
+
async ensureSession() {
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @returns {Promise<{terminal_output: string}>} */
|
|
15
|
+
async captureOutputPayload() {
|
|
16
|
+
if (options.captureOutputPayload) {
|
|
17
|
+
return options.captureOutputPayload()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {terminal_output: "snapshot"}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @returns {Promise<void>} */
|
|
24
|
+
async sendInput() {
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @returns {Promise<void>} */
|
|
28
|
+
async sendKeys() {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("TerminalRelay", () => {
|
|
34
|
+
it("reports connected callback errors", async () => {
|
|
35
|
+
const reportCalls = []
|
|
36
|
+
let callbacks = null
|
|
37
|
+
const relay = new TerminalRelay(
|
|
38
|
+
{
|
|
39
|
+
actionCableUrl: "ws://example.com/cable",
|
|
40
|
+
bugReportAuthToken: undefined,
|
|
41
|
+
instanceControlToken: "instance-token",
|
|
42
|
+
projectEnvironmentInstanceId: "instance-id",
|
|
43
|
+
projectEnvironmentTerminalSessionId: "session-id",
|
|
44
|
+
projectId: "project-id",
|
|
45
|
+
tmuxSessionName: "tmux-name"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
bugReporter: {
|
|
49
|
+
reportError: async (options) => {
|
|
50
|
+
reportCalls.push(options)
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
TmuxClientClass: buildTmuxClientClass({
|
|
54
|
+
captureOutputPayload: async () => {
|
|
55
|
+
throw new Error("could not capture")
|
|
56
|
+
}
|
|
57
|
+
}),
|
|
58
|
+
createConsumerFunction: () => ({
|
|
59
|
+
subscriptions: {
|
|
60
|
+
create: (_identifier, subscriptionCallbacks) => {
|
|
61
|
+
callbacks = subscriptionCallbacks
|
|
62
|
+
return {perform: () => {}}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
const originalConsoleError = console.error
|
|
69
|
+
|
|
70
|
+
console.error = () => {}
|
|
71
|
+
await relay.start()
|
|
72
|
+
callbacks.connected()
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
74
|
+
console.error = originalConsoleError
|
|
75
|
+
|
|
76
|
+
expect(reportCalls.length).toEqual(1)
|
|
77
|
+
expect(reportCalls[0].context).toEqual("connected")
|
|
78
|
+
expect(reportCalls[0].error.message).toEqual("could not capture")
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("reports rejected subscriptions", async () => {
|
|
82
|
+
const reportCalls = []
|
|
83
|
+
let callbacks = null
|
|
84
|
+
const relay = new TerminalRelay(
|
|
85
|
+
{
|
|
86
|
+
actionCableUrl: "ws://example.com/cable",
|
|
87
|
+
bugReportAuthToken: undefined,
|
|
88
|
+
instanceControlToken: "instance-token",
|
|
89
|
+
projectEnvironmentInstanceId: "instance-id",
|
|
90
|
+
projectEnvironmentTerminalSessionId: "session-id",
|
|
91
|
+
projectId: "project-id",
|
|
92
|
+
tmuxSessionName: "tmux-name"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
bugReporter: {
|
|
96
|
+
reportError: async (options) => {
|
|
97
|
+
reportCalls.push(options)
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
TmuxClientClass: buildTmuxClientClass(),
|
|
101
|
+
createConsumerFunction: () => ({
|
|
102
|
+
subscriptions: {
|
|
103
|
+
create: (_identifier, subscriptionCallbacks) => {
|
|
104
|
+
callbacks = subscriptionCallbacks
|
|
105
|
+
return {perform: () => {}}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
const originalConsoleError = console.error
|
|
112
|
+
let raisedError = null
|
|
113
|
+
|
|
114
|
+
console.error = () => {}
|
|
115
|
+
await relay.start()
|
|
116
|
+
try {
|
|
117
|
+
callbacks.rejected()
|
|
118
|
+
} catch (error) {
|
|
119
|
+
raisedError = error
|
|
120
|
+
}
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
122
|
+
console.error = originalConsoleError
|
|
123
|
+
|
|
124
|
+
expect(raisedError.message).toEqual("ProjectEnvironmentTerminalSessionsChannel subscription rejected")
|
|
125
|
+
expect(reportCalls.length).toEqual(1)
|
|
126
|
+
expect(reportCalls[0].context).toEqual("rejected")
|
|
127
|
+
expect(reportCalls[0].error.message).toEqual("ProjectEnvironmentTerminalSessionsChannel subscription rejected")
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {BugReporting} from "peakflow-api"
|
|
2
|
+
|
|
3
|
+
/** Reports instance-control runtime errors to Peakflow. */
|
|
4
|
+
export default class BugReporter {
|
|
5
|
+
/**
|
|
6
|
+
* @param {{bugReportAuthToken?: string}} options
|
|
7
|
+
*/
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.options = options
|
|
10
|
+
this.bugReporting = this.buildBugReporting()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {{error: Error, context: string}} options
|
|
15
|
+
* @returns {Promise<void>}
|
|
16
|
+
*/
|
|
17
|
+
async reportError(options) {
|
|
18
|
+
const {error, context} = options
|
|
19
|
+
|
|
20
|
+
if (!this.bugReporting) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await this.bugReporting.handleError({
|
|
26
|
+
error,
|
|
27
|
+
errorClass: error.name || "Error",
|
|
28
|
+
line: null,
|
|
29
|
+
file: null,
|
|
30
|
+
message: error.message,
|
|
31
|
+
url: null
|
|
32
|
+
})
|
|
33
|
+
} catch (reportError) {
|
|
34
|
+
console.error(`Could not report ${context} error to Peakflow`)
|
|
35
|
+
console.error(reportError)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @returns {BugReporting|undefined}
|
|
41
|
+
*/
|
|
42
|
+
buildBugReporting() {
|
|
43
|
+
const authToken = this.options.bugReportAuthToken
|
|
44
|
+
if (!authToken) {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new BugReporting({authToken})
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/cli-arguments.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @param {string[]} argv
|
|
4
4
|
* @returns {{
|
|
5
5
|
* actionCableUrl: string,
|
|
6
|
+
* bugReportAuthToken: string|undefined,
|
|
6
7
|
* instanceControlToken: string,
|
|
7
8
|
* projectEnvironmentInstanceId: string,
|
|
8
9
|
* projectEnvironmentTerminalSessionId: string,
|
|
@@ -40,6 +41,7 @@ export function parseCliArguments(argv) {
|
|
|
40
41
|
|
|
41
42
|
return {
|
|
42
43
|
actionCableUrl: options["action-cable-url"],
|
|
44
|
+
bugReportAuthToken: options["bug-report-auth-token"] || process.env.PEAKFLOW_INSTANCE_CONTROL_BUG_REPORT_AUTH_TOKEN,
|
|
43
45
|
instanceControlToken: options["instance-control-token"],
|
|
44
46
|
projectEnvironmentInstanceId: options["project-environment-instance-id"],
|
|
45
47
|
projectEnvironmentTerminalSessionId: options["project-environment-terminal-session-id"],
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import BugReporter from "./bug-reporter.js"
|
|
3
4
|
import {parseCliArguments} from "./cli-arguments.js"
|
|
4
5
|
import TerminalRelay from "./terminal-relay.js"
|
|
5
6
|
|
|
@@ -12,6 +13,27 @@ async function main() {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
main().catch((error) => {
|
|
16
|
+
const bugReporter = new BugReporter({
|
|
17
|
+
bugReportAuthToken: bugReportAuthTokenFromArgv(process.argv.slice(2))
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
bugReporter.reportError({context: "startup", error}).catch((reportError) => {
|
|
21
|
+
console.error(reportError)
|
|
22
|
+
})
|
|
15
23
|
console.error(error)
|
|
16
24
|
process.exit(1)
|
|
17
25
|
})
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string[]} argv
|
|
29
|
+
* @returns {string|undefined}
|
|
30
|
+
*/
|
|
31
|
+
function bugReportAuthTokenFromArgv(argv) {
|
|
32
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
33
|
+
if (argv[index] == "--bug-report-auth-token") {
|
|
34
|
+
return argv[index + 1]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return process.env.PEAKFLOW_INSTANCE_CONTROL_BUG_REPORT_AUTH_TOKEN
|
|
39
|
+
}
|
package/src/terminal-relay.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {createConsumer} from "@rails/actioncable"
|
|
2
|
+
import BugReporter from "./bug-reporter.js"
|
|
2
3
|
import TmuxClient from "./tmux-client.js"
|
|
3
4
|
|
|
4
5
|
/** Instance-side terminal relay over ActionCable. */
|
|
@@ -7,17 +8,24 @@ export default class TerminalRelay {
|
|
|
7
8
|
* Build relay with options.
|
|
8
9
|
* @param {{
|
|
9
10
|
* actionCableUrl: string,
|
|
11
|
+
* bugReportAuthToken: string|undefined,
|
|
10
12
|
* instanceControlToken: string,
|
|
11
13
|
* projectEnvironmentInstanceId: string,
|
|
12
14
|
* projectEnvironmentTerminalSessionId: string,
|
|
13
15
|
* projectId: string,
|
|
14
16
|
* tmuxSessionName: string
|
|
15
17
|
* }} options
|
|
18
|
+
* @param {{
|
|
19
|
+
* bugReporter?: BugReporter,
|
|
20
|
+
* createConsumerFunction?: (url: string) => ReturnType<typeof createConsumer>,
|
|
21
|
+
* TmuxClientClass?: typeof TmuxClient
|
|
22
|
+
* }} [dependencies]
|
|
16
23
|
*/
|
|
17
|
-
constructor(options) {
|
|
24
|
+
constructor(options, dependencies = {}) {
|
|
18
25
|
this.options = options
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
26
|
+
this.bugReporter = dependencies.bugReporter || new BugReporter({bugReportAuthToken: options.bugReportAuthToken})
|
|
27
|
+
this.tmuxClient = new (dependencies.TmuxClientClass || TmuxClient)({tmuxSessionName: options.tmuxSessionName})
|
|
28
|
+
this.consumer = (dependencies.createConsumerFunction || createConsumer)(options.actionCableUrl)
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
/** @returns {Promise<void>} */
|
|
@@ -36,21 +44,37 @@ export default class TerminalRelay {
|
|
|
36
44
|
{
|
|
37
45
|
connected: () => {
|
|
38
46
|
this.publishSnapshot().catch((error) => {
|
|
39
|
-
|
|
47
|
+
this.logAndReportError({context: "connected", error})
|
|
40
48
|
})
|
|
41
49
|
},
|
|
42
50
|
received: (data) => {
|
|
43
51
|
this.onMessage(data).catch((error) => {
|
|
44
|
-
|
|
52
|
+
this.logAndReportError({context: "received", error})
|
|
45
53
|
})
|
|
46
54
|
},
|
|
47
55
|
rejected: () => {
|
|
48
|
-
|
|
56
|
+
const error = new Error("ProjectEnvironmentTerminalSessionsChannel subscription rejected")
|
|
57
|
+
|
|
58
|
+
this.logAndReportError({context: "rejected", error})
|
|
59
|
+
throw error
|
|
49
60
|
}
|
|
50
61
|
}
|
|
51
62
|
)
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
/**
|
|
66
|
+
* @param {{context: string, error: Error}} options
|
|
67
|
+
* @returns {void}
|
|
68
|
+
*/
|
|
69
|
+
logAndReportError(options) {
|
|
70
|
+
const {context, error} = options
|
|
71
|
+
|
|
72
|
+
console.error(error)
|
|
73
|
+
this.bugReporter.reportError({context, error}).catch((reportError) => {
|
|
74
|
+
console.error(reportError)
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
/**
|
|
55
79
|
* Handle command payload from ActionCable command stream.
|
|
56
80
|
* @param {Record<string, unknown>} data
|