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 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.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",
@@ -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
+ }
@@ -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
+ }
@@ -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.tmuxClient = new TmuxClient({tmuxSessionName: options.tmuxSessionName})
20
- this.consumer = createConsumer(options.actionCableUrl)
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
- console.error(error)
47
+ this.logAndReportError({context: "connected", error})
40
48
  })
41
49
  },
42
50
  received: (data) => {
43
51
  this.onMessage(data).catch((error) => {
44
- console.error(error)
52
+ this.logAndReportError({context: "received", error})
45
53
  })
46
54
  },
47
55
  rejected: () => {
48
- throw new Error("ProjectEnvironmentTerminalSessionsChannel subscription rejected")
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