plugsync-cli 0.1.1__tar.gz → 0.4.0__tar.gz

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.
Files changed (51) hide show
  1. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/PKG-INFO +20 -14
  2. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/README.md +19 -13
  3. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/__init__.py +1 -1
  4. plugsync_cli-0.4.0/plugsync_cli/client.py +351 -0
  5. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/commands/auth.py +4 -0
  6. plugsync_cli-0.4.0/plugsync_cli/commands/diff.py +439 -0
  7. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/commands/plugin.py +94 -7
  8. plugsync_cli-0.4.0/plugsync_cli/commands/preview.py +68 -0
  9. plugsync_cli-0.4.0/plugsync_cli/commands/pull.py +70 -0
  10. plugsync_cli-0.4.0/plugsync_cli/commands/push.py +140 -0
  11. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/commands/rollback.py +7 -7
  12. plugsync_cli-0.4.0/plugsync_cli/commands/validate.py +54 -0
  13. plugsync_cli-0.4.0/plugsync_cli/compat.py +96 -0
  14. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/config.py +12 -2
  15. plugsync_cli-0.4.0/plugsync_cli/context.py +205 -0
  16. plugsync_cli-0.4.0/plugsync_cli/findings.py +119 -0
  17. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/main.py +46 -3
  18. plugsync_cli-0.4.0/plugsync_cli/schema_defaults.py +183 -0
  19. plugsync_cli-0.4.0/plugsync_cli/serializer.py +426 -0
  20. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli.egg-info/PKG-INFO +20 -14
  21. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli.egg-info/SOURCES.txt +8 -0
  22. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/pyproject.toml +1 -1
  23. plugsync_cli-0.4.0/tests/test_api_contract.py +274 -0
  24. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/tests/test_auth_commands.py +26 -0
  25. plugsync_cli-0.4.0/tests/test_commands.py +1482 -0
  26. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/tests/test_compat.py +25 -13
  27. plugsync_cli-0.4.0/tests/test_config.py +93 -0
  28. plugsync_cli-0.4.0/tests/test_e2e_draft_api.py +593 -0
  29. plugsync_cli-0.4.0/tests/test_hand_edited_yaml.py +341 -0
  30. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/tests/test_plugin_commands.py +328 -0
  31. plugsync_cli-0.4.0/tests/test_schema_defaults.py +179 -0
  32. plugsync_cli-0.4.0/tests/test_serializer.py +239 -0
  33. plugsync_cli-0.1.1/plugsync_cli/client.py +0 -201
  34. plugsync_cli-0.1.1/plugsync_cli/commands/diff.py +0 -169
  35. plugsync_cli-0.1.1/plugsync_cli/commands/preview.py +0 -107
  36. plugsync_cli-0.1.1/plugsync_cli/commands/pull.py +0 -121
  37. plugsync_cli-0.1.1/plugsync_cli/commands/push.py +0 -101
  38. plugsync_cli-0.1.1/plugsync_cli/commands/validate.py +0 -66
  39. plugsync_cli-0.1.1/plugsync_cli/compat.py +0 -73
  40. plugsync_cli-0.1.1/plugsync_cli/serializer.py +0 -205
  41. plugsync_cli-0.1.1/tests/test_commands.py +0 -339
  42. plugsync_cli-0.1.1/tests/test_serializer.py +0 -160
  43. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/bundler.py +0 -0
  44. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/commands/__init__.py +0 -0
  45. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/commands/log.py +0 -0
  46. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli.egg-info/dependency_links.txt +0 -0
  47. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli.egg-info/entry_points.txt +0 -0
  48. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli.egg-info/requires.txt +0 -0
  49. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli.egg-info/top_level.txt +0 -0
  50. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/setup.cfg +0 -0
  51. {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/tests/test_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugsync-cli
3
- Version: 0.1.1
3
+ Version: 0.4.0
4
4
  Summary: Command-line client for plugsync HubSpot connectors -- manage connectors and plugins like code, no repo checkout required
5
5
  Author-email: Exelab <hello@plugsync.com>
6
6
  License: Proprietary
@@ -33,8 +33,12 @@ Requires-Dist: respx>=0.21; extra == "dev"
33
33
 
34
34
  Command-line client for [plugsync](https://plugsync.com), managing HubSpot
35
35
  connectors like code: pull a connector's config to a local directory, edit
36
- it, push it back, diff against the remote, validate, preview a publish, and
37
- manage plugins -- all without cloning the plugsync backend repo.
36
+ it, push it back to the draft, diff against a published version, validate,
37
+ preview a publish, and manage plugins -- all without cloning the plugsync
38
+ backend repo.
39
+
40
+ `push` writes the draft and stops. Publishing is opt-in (`push --publish`),
41
+ so a config never reaches production as a side effect of saving a file.
38
42
 
39
43
  ## Install
40
44
 
@@ -47,14 +51,16 @@ Requires Python 3.12+.
47
51
  ## Quickstart
48
52
 
49
53
  ```bash
50
- plugsync config set api_url https://app.plugsync.com
51
54
  plugsync auth login -e you@example.com
52
55
  plugsync pull my-connector
53
56
  ```
54
57
 
55
- `plugsync auth login` saves a bearer credential to `~/.plugsync/config.yaml`
56
- that every subsequent command reuses. `PLUGSYNC_API_URL` / `PLUGSYNC_API_KEY`
57
- environment variables override the config file, handy for CI.
58
+ `api_url` defaults to the hosted platform (`https://app.plugsync.com`), so
59
+ there's nothing to configure for the common case. Point it elsewhere with
60
+ `plugsync config set api_url <url>`. `plugsync auth login` saves a bearer
61
+ credential to `~/.plugsync/config.yaml` that every subsequent command
62
+ reuses. `PLUGSYNC_API_URL` / `PLUGSYNC_API_KEY` environment variables
63
+ override the config file, handy for CI.
58
64
 
59
65
  ## Commands
60
66
 
@@ -62,13 +68,13 @@ environment variables override the config file, handy for CI.
62
68
  |---|---|
63
69
  | `plugsync auth login` / `logout` | Authenticate (email/password JWT, or a long-lived org API key with `--token`) |
64
70
  | `plugsync config set/get/show` | Manage `~/.plugsync/config.yaml` |
65
- | `plugsync pull <name>` | Download a connector as a local directory |
66
- | `plugsync push <name>` | Upload a local connector directory |
67
- | `plugsync diff <name>` | Diff local vs. remote working state |
68
- | `plugsync validate <name>` | Validate a local connector config |
69
- | `plugsync preview <name>` | Preview a publish's effects |
70
- | `plugsync log <name>` | Show a connector's revision history |
71
- | `plugsync rollback <name>` | Restore a previous revision |
71
+ | `plugsync pull <name>` | Download a connector's draft as a local directory (`--revision N` for a published version) |
72
+ | `plugsync push <name>` | Upload a local connector directory to the draft (`--publish` to also publish) |
73
+ | `plugsync diff <name>` | Diff local files vs. a published version |
74
+ | `plugsync validate <name>` | Preflight the draft; non-zero exit on an error |
75
+ | `plugsync preview <name>` | Preview a publish's effects and its preflight findings |
76
+ | `plugsync log <name>` | Show a connector's published version history |
77
+ | `plugsync rollback <name>` | Restore a published version into the draft |
72
78
  | `plugsync plugin init/push/list/status/logs/metrics/invoke/promote/publish/rollback` | Author and manage plugins (Enterprise tier) |
73
79
 
74
80
  Run `plugsync --help` or `plugsync <command> --help` for the full option list.
@@ -2,8 +2,12 @@
2
2
 
3
3
  Command-line client for [plugsync](https://plugsync.com), managing HubSpot
4
4
  connectors like code: pull a connector's config to a local directory, edit
5
- it, push it back, diff against the remote, validate, preview a publish, and
6
- manage plugins -- all without cloning the plugsync backend repo.
5
+ it, push it back to the draft, diff against a published version, validate,
6
+ preview a publish, and manage plugins -- all without cloning the plugsync
7
+ backend repo.
8
+
9
+ `push` writes the draft and stops. Publishing is opt-in (`push --publish`),
10
+ so a config never reaches production as a side effect of saving a file.
7
11
 
8
12
  ## Install
9
13
 
@@ -16,14 +20,16 @@ Requires Python 3.12+.
16
20
  ## Quickstart
17
21
 
18
22
  ```bash
19
- plugsync config set api_url https://app.plugsync.com
20
23
  plugsync auth login -e you@example.com
21
24
  plugsync pull my-connector
22
25
  ```
23
26
 
24
- `plugsync auth login` saves a bearer credential to `~/.plugsync/config.yaml`
25
- that every subsequent command reuses. `PLUGSYNC_API_URL` / `PLUGSYNC_API_KEY`
26
- environment variables override the config file, handy for CI.
27
+ `api_url` defaults to the hosted platform (`https://app.plugsync.com`), so
28
+ there's nothing to configure for the common case. Point it elsewhere with
29
+ `plugsync config set api_url <url>`. `plugsync auth login` saves a bearer
30
+ credential to `~/.plugsync/config.yaml` that every subsequent command
31
+ reuses. `PLUGSYNC_API_URL` / `PLUGSYNC_API_KEY` environment variables
32
+ override the config file, handy for CI.
27
33
 
28
34
  ## Commands
29
35
 
@@ -31,13 +37,13 @@ environment variables override the config file, handy for CI.
31
37
  |---|---|
32
38
  | `plugsync auth login` / `logout` | Authenticate (email/password JWT, or a long-lived org API key with `--token`) |
33
39
  | `plugsync config set/get/show` | Manage `~/.plugsync/config.yaml` |
34
- | `plugsync pull <name>` | Download a connector as a local directory |
35
- | `plugsync push <name>` | Upload a local connector directory |
36
- | `plugsync diff <name>` | Diff local vs. remote working state |
37
- | `plugsync validate <name>` | Validate a local connector config |
38
- | `plugsync preview <name>` | Preview a publish's effects |
39
- | `plugsync log <name>` | Show a connector's revision history |
40
- | `plugsync rollback <name>` | Restore a previous revision |
40
+ | `plugsync pull <name>` | Download a connector's draft as a local directory (`--revision N` for a published version) |
41
+ | `plugsync push <name>` | Upload a local connector directory to the draft (`--publish` to also publish) |
42
+ | `plugsync diff <name>` | Diff local files vs. a published version |
43
+ | `plugsync validate <name>` | Preflight the draft; non-zero exit on an error |
44
+ | `plugsync preview <name>` | Preview a publish's effects and its preflight findings |
45
+ | `plugsync log <name>` | Show a connector's published version history |
46
+ | `plugsync rollback <name>` | Restore a published version into the draft |
41
47
  | `plugsync plugin init/push/list/status/logs/metrics/invoke/promote/publish/rollback` | Author and manage plugins (Enterprise tier) |
42
48
 
43
49
  Run `plugsync --help` or `plugsync <command> --help` for the full option list.
@@ -1,2 +1,2 @@
1
1
  """PluSync CLI — git-like connector management."""
2
- __version__ = "0.1.0"
2
+ __version__ = "0.4.0"
@@ -0,0 +1,351 @@
1
+ """HTTP client wrapper for PluSync REST API."""
2
+ import json
3
+
4
+ import httpx
5
+
6
+ from plugsync_cli.compat import check_api_compat
7
+ from plugsync_cli.config import get_api_key, get_api_url
8
+
9
+
10
+ class DraftConflict(Exception):
11
+ """The draft moved on since the version the caller read.
12
+
13
+ Carries the draft version the server currently holds, so a command can tell
14
+ the user what to re-pull instead of surfacing a bare 409.
15
+ """
16
+
17
+ def __init__(self, current_version: int | None):
18
+ self.current_version = current_version
19
+ super().__init__(
20
+ "the connector draft changed since it was pulled"
21
+ + (f" (server is at draft v{current_version})" if current_version else "")
22
+ )
23
+
24
+
25
+ def _render_detail(detail) -> str:
26
+ """The backend's `detail` as one readable block.
27
+
28
+ FastAPI answers a string for a hand-raised HTTPException, a dict for the
29
+ structured ones (draft conflicts, upgrade payloads), and a list of
30
+ per-field entries for a request-validation failure. The last one carries
31
+ the precise Pydantic error, which is exactly what an operator who mistyped
32
+ a config key needs to see.
33
+ """
34
+ if isinstance(detail, str):
35
+ return detail
36
+ if isinstance(detail, list):
37
+ lines = []
38
+ for entry in detail:
39
+ if isinstance(entry, dict) and "msg" in entry:
40
+ location = ".".join(str(part) for part in entry.get("loc", []))
41
+ lines.append(f"{location}: {entry['msg']}" if location else entry["msg"])
42
+ else:
43
+ lines.append(json.dumps(entry, ensure_ascii=False))
44
+ return "\n ".join([""] + lines)
45
+ return json.dumps(detail, ensure_ascii=False, indent=2)
46
+
47
+
48
+ def describe_http_error(exc: httpx.HTTPStatusError) -> str:
49
+ """A non-2xx turned into something an operator can act on.
50
+
51
+ Without this the CLI dies on the raw `raise_for_status()` traceback and
52
+ throws away the response body, which is where the backend says what is
53
+ actually wrong.
54
+ """
55
+ response, request = exc.response, exc.request
56
+ summary = (
57
+ f"{request.method} {request.url.path} failed "
58
+ f"(HTTP {response.status_code} {response.reason_phrase})"
59
+ )
60
+ try:
61
+ payload = response.json()
62
+ except ValueError:
63
+ body = (response.text or "").strip()
64
+ return f"{summary}: {body}" if body else summary
65
+
66
+ detail = payload.get("detail", payload) if isinstance(payload, dict) else payload
67
+ return f"{summary}: {_render_detail(detail)}"
68
+
69
+
70
+ def _conflict_version(response: httpx.Response) -> int | None:
71
+ """The `current_version` the draft API reports in its 409 detail."""
72
+ try:
73
+ detail = response.json().get("detail")
74
+ except ValueError:
75
+ return None
76
+ return detail.get("current_version") if isinstance(detail, dict) else None
77
+
78
+
79
+ class PlugSyncClient:
80
+ """Thin wrapper around httpx for PluSync API calls."""
81
+
82
+ def __init__(self, api_url: str | None = None, api_key: str | None = None):
83
+ self.api_url = api_url or get_api_url()
84
+ self.api_key = api_key or get_api_key()
85
+ if not self.api_key:
86
+ raise RuntimeError(
87
+ "No API key configured. Run 'plugsync config set api_key <key>' "
88
+ "or set PLUGSYNC_API_KEY environment variable."
89
+ )
90
+ try:
91
+ check_api_compat(self.api_url)
92
+ except Exception:
93
+ # Compat check is advisory only (issue #957): never let it
94
+ # block a command from running, even if it misbehaves.
95
+ pass
96
+ self.headers = {"Authorization": f"Bearer {self.api_key}"}
97
+
98
+ def _url(self, path: str) -> str:
99
+ return f"{self.api_url}/api{path}"
100
+
101
+ def _client(self) -> httpx.Client:
102
+ return httpx.Client(headers=self.headers, timeout=60.0)
103
+
104
+ def list_connectors(self) -> list[dict]:
105
+ with self._client() as c:
106
+ r = c.get(self._url("/connectors"))
107
+ r.raise_for_status()
108
+ return r.json()
109
+
110
+ def find_connector(self, name: str) -> dict | None:
111
+ connectors = self.list_connectors()
112
+ for conn in connectors:
113
+ if conn["name"] == name:
114
+ return conn
115
+ return None
116
+
117
+ def get_connector(self, connector_id: str) -> dict:
118
+ with self._client() as c:
119
+ r = c.get(self._url(f"/connectors/{connector_id}"))
120
+ r.raise_for_status()
121
+ return r.json()
122
+
123
+ def get_draft(self, connector_id: str) -> dict:
124
+ """The connector's editable config (backend DraftResponse).
125
+
126
+ The draft is the CLI's read AND write surface for the current config:
127
+ there is no server-side export, the config travels as JSON and the
128
+ directory format is produced locally by `serializer.py` (#1241).
129
+ """
130
+ with self._client() as c:
131
+ r = c.get(self._url(f"/connectors/{connector_id}/draft"))
132
+ r.raise_for_status()
133
+ return r.json()
134
+
135
+ def put_draft(
136
+ self, connector_id: str, config: dict, expected_version: int | None = None
137
+ ) -> dict:
138
+ """Replace the whole draft config. Never publishes.
139
+
140
+ `expected_version` is the `draft_version` the caller last read: the
141
+ backend rejects the write with 409 if the draft moved on since, which
142
+ is what keeps a `pull` -> edit -> `push` cycle from silently discarding
143
+ someone else's dashboard edit. Raises `DraftConflict` on that 409.
144
+ """
145
+ params = {} if expected_version is None else {"expected_version": expected_version}
146
+ with self._client() as c:
147
+ r = c.put(
148
+ self._url(f"/connectors/{connector_id}/draft"),
149
+ json=config,
150
+ params=params,
151
+ )
152
+ if r.status_code == 409:
153
+ raise DraftConflict(_conflict_version(r))
154
+ r.raise_for_status()
155
+ return r.json()
156
+
157
+ def config_surface(self) -> dict:
158
+ """The config catalogue: actions, transforms, when, sources, json_schema.
159
+
160
+ `json_schema` is `ConnectorConfigV3.model_json_schema()`, defaults
161
+ included: it is how `plugsync diff` knows which values the server wrote
162
+ from a schema default rather than the operator (#1259). Org-scoped
163
+ auth only -- the catalogue itself is the same for every org.
164
+ """
165
+ with self._client() as c:
166
+ r = c.get(self._url("/config-surface"))
167
+ r.raise_for_status()
168
+ return r.json()
169
+
170
+ def get_version(self, connector_id: str, version: int) -> dict:
171
+ """One published config version, config included (backend VersionResponse)."""
172
+ with self._client() as c:
173
+ r = c.get(self._url(f"/connectors/{connector_id}/versions/{version}"))
174
+ r.raise_for_status()
175
+ return r.json()
176
+
177
+ def preflight(self, connector_id: str) -> dict:
178
+ """Preflight findings on the draft, split into errors/warnings/infos."""
179
+ with self._client() as c:
180
+ r = c.get(self._url(f"/connectors/{connector_id}/preflight"))
181
+ r.raise_for_status()
182
+ return r.json()
183
+
184
+ def preview(self, connector_id: str) -> dict:
185
+ """What publishing the draft would change against the active version."""
186
+ with self._client() as c:
187
+ r = c.get(self._url(f"/connectors/{connector_id}/preview"))
188
+ r.raise_for_status()
189
+ return r.json()
190
+
191
+ def publish(
192
+ self, connector_id: str, change_summary: str | None = None, force: bool = False,
193
+ ) -> dict:
194
+ """`force` (#1367, DX-61): publish a new version even when the draft
195
+ is identical to the active one - the values a publish resolves (plugin
196
+ ARN/version, HubSpot schema) can drift without any draft edit. Unrelated
197
+ to `plugsync push --force`, which discards a concurrent DRAFT edit."""
198
+ with self._client() as c:
199
+ body = {}
200
+ if change_summary:
201
+ body["change_summary"] = change_summary
202
+ if force:
203
+ body["force"] = True
204
+ r = c.post(self._url(f"/connectors/{connector_id}/publish"), json=body)
205
+ r.raise_for_status()
206
+ return r.json()
207
+
208
+ def list_revisions(self, connector_id: str) -> list[dict]:
209
+ """Published config versions, newest first (backend: app/api/versions.py).
210
+
211
+ The endpoint is `/versions`: Plan 6a replaced the `/revisions` API the
212
+ CLI used to call, which is why `plugsync log` answered 404 (#1244).
213
+ """
214
+ with self._client() as c:
215
+ r = c.get(self._url(f"/connectors/{connector_id}/versions"))
216
+ r.raise_for_status()
217
+ return r.json()
218
+
219
+ def restore_revision(self, connector_id: str, version: int) -> dict:
220
+ """Copy a published version back into the draft, returning the draft.
221
+
222
+ Does not publish: the restored config becomes the connector's draft and
223
+ needs an explicit publish to go live.
224
+ """
225
+ with self._client() as c:
226
+ r = c.post(self._url(f"/connectors/{connector_id}/versions/{version}/restore"))
227
+ r.raise_for_status()
228
+ return r.json()
229
+
230
+ # ------------------------------------------------------------------
231
+ # Plugin API (T10, issue #190)
232
+ # ------------------------------------------------------------------
233
+
234
+ def plugin_create(self, name: str, capabilities: list[str] | None = None) -> dict:
235
+ """Create a new plugin (status=draft)."""
236
+ body: dict = {"name": name}
237
+ if capabilities is not None:
238
+ body["capabilities"] = capabilities
239
+ with self._client() as c:
240
+ r = c.post(self._url("/plugins"), json=body)
241
+ r.raise_for_status()
242
+ return r.json()
243
+
244
+ def plugin_list(self) -> list[dict]:
245
+ """List all plugins for the current org."""
246
+ with self._client() as c:
247
+ r = c.get(self._url("/plugins"))
248
+ r.raise_for_status()
249
+ return r.json()
250
+
251
+ def plugin_get(self, plugin_id: str) -> dict:
252
+ """Get a single plugin by ID."""
253
+ with self._client() as c:
254
+ r = c.get(self._url(f"/plugins/{plugin_id}"))
255
+ r.raise_for_status()
256
+ return r.json()
257
+
258
+ def plugin_push(self, plugin_id: str, bundle_bytes: bytes, source_code: str) -> dict:
259
+ """Upload a new bundle + source_code for a plugin (multipart/form-data)."""
260
+ with self._client() as c:
261
+ r = c.post(
262
+ self._url(f"/plugins/{plugin_id}/push"),
263
+ files={"bundle": ("plugin.js", bundle_bytes, "application/javascript")},
264
+ data={"source_code": source_code},
265
+ )
266
+ r.raise_for_status()
267
+ return r.json()
268
+
269
+ def plugin_promote(self, plugin_id: str, target: str) -> dict:
270
+ """Promote a plugin to staging or live."""
271
+ with self._client() as c:
272
+ r = c.post(
273
+ self._url(f"/plugins/{plugin_id}/promote"),
274
+ json={"target": target},
275
+ )
276
+ r.raise_for_status()
277
+ return r.json()
278
+
279
+ def plugin_invoke(
280
+ self,
281
+ plugin_id: str,
282
+ payload: dict,
283
+ config: dict | None = None,
284
+ auth_credential: str | None = None,
285
+ auth_key: str | None = None,
286
+ ) -> dict:
287
+ """Proxy an invocation to the plugin's Lambda function.
288
+
289
+ `payload` is the event BODY only -- it lands at `event.payload` on
290
+ the Lambda side, not the whole PluginPayload envelope (issue #1365).
291
+ `config` mirrors `event.config`.
292
+
293
+ `auth_credential` is a name, never a secret in the clear: the server
294
+ reads it as an event source name and resolves that source's outbound
295
+ auth exactly as a real flow does (its credential, auth_type and
296
+ auth_header), falling back to a stored credential name when the org
297
+ has no source called that. The event.auth KEY those headers land
298
+ under is `auth_key` when given, else `auth_credential` itself; that
299
+ key is also the `auth` capability grant the server requires on the
300
+ plugin, mirroring what the runtime hands a plugin.
301
+ """
302
+ with self._client() as c:
303
+ r = c.post(
304
+ self._url(f"/plugins/{plugin_id}/invoke"),
305
+ json={
306
+ "payload": payload,
307
+ "config": config or {},
308
+ "auth_credential": auth_credential,
309
+ "auth_key": auth_key,
310
+ },
311
+ )
312
+ r.raise_for_status()
313
+ return r.json()
314
+
315
+ def plugin_logs(
316
+ self,
317
+ plugin_id: str,
318
+ connector_id: str | None = None,
319
+ since_minutes: int | None = None,
320
+ limit: int | None = None,
321
+ ) -> dict:
322
+ """Tail the most recent CloudWatch log lines for a plugin.
323
+
324
+ Optional filters (#293): connector_id narrows to one connector's
325
+ lines, since_minutes widens the lookback, limit caps the line count.
326
+ """
327
+ params: dict = {}
328
+ if connector_id is not None:
329
+ params["connector_id"] = connector_id
330
+ if since_minutes is not None:
331
+ params["since_minutes"] = since_minutes
332
+ if limit is not None:
333
+ params["limit"] = limit
334
+ with self._client() as c:
335
+ r = c.get(self._url(f"/plugins/{plugin_id}/logs"), params=params)
336
+ r.raise_for_status()
337
+ return r.json()
338
+
339
+ def plugin_metrics(self, plugin_id: str, window: str = "24h") -> dict:
340
+ """CloudWatch metrics (invocations, errors, latency) for a plugin (#293)."""
341
+ with self._client() as c:
342
+ r = c.get(
343
+ self._url(f"/plugins/{plugin_id}/metrics"),
344
+ params={"window": window},
345
+ )
346
+ r.raise_for_status()
347
+ return r.json()
348
+
349
+ def plugin_status(self, plugin_id: str) -> dict:
350
+ """Get the current status of a plugin."""
351
+ return self.plugin_get(plugin_id)
@@ -125,6 +125,10 @@ def auth_login(email: str | None, password: str | None, token: str | None):
125
125
  raise SystemExit(1)
126
126
  except httpx.RequestError as exc:
127
127
  console.print(f"[red]Could not reach {api_url}: {exc}[/red]")
128
+ console.print(
129
+ "[yellow]Is this the right API URL? Set it with "
130
+ "'plugsync config set api_url <url>'.[/yellow]"
131
+ )
128
132
  raise SystemExit(1)
129
133
 
130
134
  data = response.json()