propr-cli 0.8.4 → 0.8.6

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.
@@ -6,7 +6,7 @@
6
6
  * In all cases the containers run detached, so the stack outlives this process.
7
7
  */
8
8
  import { getHostConfig } from "../orchestrator/index.js";
9
- import { renderStatusTable } from "../orchestrator/format.js";
9
+ import { renderStatusTable, renderTunnelEndpointSummary } from "../orchestrator/format.js";
10
10
  import { ensureVibePromptCacheDir } from "../commands/initStack.js";
11
11
  import { createInterface } from "node:readline/promises";
12
12
  async function confirmRestart() {
@@ -71,8 +71,23 @@ export async function runStart(configManager, options) {
71
71
  }
72
72
  const ui = configManager.getUiEnabled();
73
73
  const docs = cfg.docsEnabled;
74
+ // cfg.uiTunnelEnabled already reflects a persisted `propr tunnel on|off`
75
+ // toggle (forwarded as an override in getHostConfig), falling back to the
76
+ // env-derived default when the toggle has never been set.
77
+ const tunnel = cfg.uiTunnelEnabled;
74
78
  orch.ensureNetwork(cfg, (l) => console.log(l));
75
- const status = orch.startStack(cfg, { ui, docs, onLog: (l) => console.log(l) });
79
+ const status = orch.startStack(cfg, { ui, docs, tunnel, onLog: (l) => console.log(l) });
80
+ // When the tunnel is on, surface the concrete routed endpoints (not the base
81
+ // URL as a health target) so the operator can see where the hosted UI reaches
82
+ // this stack.
83
+ if (tunnel) {
84
+ const summary = renderTunnelEndpointSummary(cfg.uiPublicApiUrl);
85
+ if (summary.length > 0) {
86
+ console.log("");
87
+ for (const line of summary)
88
+ console.log(line);
89
+ }
90
+ }
76
91
  const interactive = options.tui !== false && Boolean(process.stdout.isTTY);
77
92
  if (!interactive) {
78
93
  console.log("");
@@ -10,7 +10,7 @@ export { validateRelayUrl } from './validateRelayUrl.js';
10
10
  // Export the hosted propr-routing service default URLs (one source of truth for
11
11
  // the webhook.propr.dev host shared by the CLI, the daemon dialer, and the
12
12
  // boot/check prerequisite validators)
13
- export { DEFAULT_PROPR_ROUTING_URL, DEFAULT_PROPR_GH_RELAY_URL, } from './proprServiceUrls.js';
13
+ export { DEFAULT_PROPR_ROUTING_URL, DEFAULT_PROPR_GH_RELAY_URL, DEFAULT_PROPR_UI_ORIGIN, PROPR_UI_PROXY_SUFFIX, PROPR_UI_PROXY_LABEL_PREFIX, DEFAULT_CLOUDFLARED_IMAGE, proprInstanceProxyUrl, isValidProprInstanceId, isProprProxyUrl, proprTunnelEndpoints, } from './proprServiceUrls.js';
14
14
  // Export routing URL validation (shared by intake prerequisites and the daemon
15
15
  // routing service so the boot/CLI checks and the dialer agree on one policy)
16
16
  export { validateRoutingUrl } from './validateRoutingUrl.js';
@@ -25,6 +25,7 @@ export { validateIntakeModePrerequisites, } from './intakeModePrerequisites.js';
25
25
  // Export shared Redis status keys (one source of truth for cross-process status
26
26
  // keys so the daemon publisher, API status route, and CLI cannot drift)
27
27
  export { ROUTING_STATUS_REDIS_KEY } from './statusKeys.js';
28
+ export { PROPR_VERSION, PROPR_API_COMPATIBILITY, PROPR_UI_COMPATIBILITY, PROPR_UI_SUPPORTED_API_COMPATIBILITY, getProprCompatibilityMetadata, evaluateProprApiCompatibility, } from './proprCompatibility.js';
28
29
  export { shortHash, buildDynamicLlmLabel, MAX_GITHUB_LABEL_LENGTH } from './labelUtils.js';
29
30
  // Export the default review guidance (the overridable part of the /review prompt)
30
31
  export { DEFAULT_REVIEW_GUIDANCE } from './reviewPrompt.js';
@@ -0,0 +1,70 @@
1
+ // Public ProPR version surfaced to the hosted UI via `/api/compatibility`. This
2
+ // must track the release version. The shared package is bundled for the browser
3
+ // (no fs/JSON-import of package.json available within rootDir), so it is kept as
4
+ // a constant rather than read from package.json at runtime. A release bump that
5
+ // updates packages/shared/package.json or docker/launcher/manifest.json but
6
+ // forgets this constant is caught by the drift test in
7
+ // test/orchestratorProprUrlsDrift.test.ts, which asserts all three agree.
8
+ export const PROPR_VERSION = '0.8.6';
9
+ // Bump this only when the API/UI contract changes in a way the hosted UI must
10
+ // account for. Patch releases that do not change the browser-facing contract can
11
+ // keep the same compatibility version.
12
+ export const PROPR_API_COMPATIBILITY = '2026-06-27';
13
+ export const PROPR_UI_COMPATIBILITY = PROPR_API_COMPATIBILITY;
14
+ export const PROPR_UI_SUPPORTED_API_COMPATIBILITY = [PROPR_API_COMPATIBILITY];
15
+ export function getProprCompatibilityMetadata() {
16
+ return {
17
+ version: PROPR_VERSION,
18
+ apiCompatibility: PROPR_API_COMPATIBILITY,
19
+ uiCompatibility: PROPR_UI_COMPATIBILITY,
20
+ };
21
+ }
22
+ export function evaluateProprApiCompatibility(input) {
23
+ const apiCompatibility = input.apiCompatibility?.trim() || null;
24
+ const apiVersion = input.version?.trim() || null;
25
+ if (!apiCompatibility) {
26
+ return {
27
+ compatible: false,
28
+ apiCompatibility,
29
+ apiVersion,
30
+ reason: 'missing',
31
+ message: 'This ProPR instance does not publish API compatibility metadata. Update the local ProPR stack before using the hosted UI.',
32
+ };
33
+ }
34
+ if (PROPR_UI_SUPPORTED_API_COMPATIBILITY.includes(apiCompatibility)) {
35
+ return { compatible: true, apiCompatibility, apiVersion };
36
+ }
37
+ // PROPR_UI_SUPPORTED_API_COMPATIBILITY currently holds a single value, so
38
+ // oldest === newest and only the too_old / too_new branches below can fire (the
39
+ // final `unsupported` branch is unreachable today). These are forward-looking:
40
+ // once the UI supports a range with gaps, an in-range-but-unsupported value can
41
+ // occur and the `unsupported` branch covers it.
42
+ const oldestSupported = PROPR_UI_SUPPORTED_API_COMPATIBILITY[0];
43
+ const newestSupported = PROPR_UI_SUPPORTED_API_COMPATIBILITY[PROPR_UI_SUPPORTED_API_COMPATIBILITY.length - 1];
44
+ if (apiCompatibility < oldestSupported) {
45
+ return {
46
+ compatible: false,
47
+ apiCompatibility,
48
+ apiVersion,
49
+ reason: 'too_old',
50
+ message: `This ProPR instance is too old for the hosted UI. Update the local ProPR stack to API compatibility ${oldestSupported} or newer.`,
51
+ };
52
+ }
53
+ if (apiCompatibility > newestSupported) {
54
+ return {
55
+ compatible: false,
56
+ apiCompatibility,
57
+ apiVersion,
58
+ reason: 'too_new',
59
+ message: `This ProPR instance is newer than the hosted UI supports. Update the hosted UI or use the matching local UI for API compatibility ${apiCompatibility}.`,
60
+ };
61
+ }
62
+ return {
63
+ compatible: false,
64
+ apiCompatibility,
65
+ apiVersion,
66
+ reason: 'unsupported',
67
+ message: `This hosted UI supports API compatibility ${PROPR_UI_SUPPORTED_API_COMPATIBILITY.join(', ')}, but the local ProPR instance reports ${apiCompatibility}.`,
68
+ };
69
+ }
70
+ //# sourceMappingURL=proprCompatibility.js.map
@@ -24,4 +24,113 @@ export const DEFAULT_PROPR_ROUTING_URL = 'wss://webhook.propr.dev';
24
24
  * directly to this value.
25
25
  */
26
26
  export const DEFAULT_PROPR_GH_RELAY_URL = 'https://webhook.propr.dev/v1';
27
+ /**
28
+ * Origin of the hosted Propr UI (https://app.propr.dev). This is where the
29
+ * managed control plane is served from; a local stack exposes its own UI on a
30
+ * tunnel under a {@link PROPR_UI_PROXY_LABEL_PREFIX} host on
31
+ * {@link PROPR_UI_PROXY_SUFFIX} so the hosted UI can reach it.
32
+ */
33
+ export const DEFAULT_PROPR_UI_ORIGIN = 'https://app.propr.dev';
34
+ /**
35
+ * DNS suffix and label prefix for per-instance UI/API tunnel hostnames. Each
36
+ * local stack with an instance id is reachable at
37
+ * `https://t-<instanceId>.propr.dev`, so the hosted UI at
38
+ * {@link DEFAULT_PROPR_UI_ORIGIN} can discover and address it.
39
+ */
40
+ export const PROPR_UI_PROXY_SUFFIX = 'propr.dev';
41
+ export const PROPR_UI_PROXY_LABEL_PREFIX = 't-';
42
+ /**
43
+ * Default Cloudflare Tunnel image used to expose the local stack's UI/API to
44
+ * the hosted control plane when a UI tunnel is enabled. This is only a fallback:
45
+ * the launcher prefers the `cloudflared` entry pinned in the stack manifest
46
+ * (docker/launcher/manifest.json). Keep this tag in sync with that manifest pin
47
+ * so the effective default is the same regardless of which source supplies it.
48
+ */
49
+ export const DEFAULT_CLOUDFLARED_IMAGE = 'cloudflare/cloudflared:2024.12.2';
50
+ /**
51
+ * Whether an instance id is usable as a single DNS label in the per-instance
52
+ * proxy hostname (`t-<id>.propr.dev`). Enforces the standard label rules:
53
+ * 1–63 characters, ASCII letters/digits/hyphens only, and no leading or
54
+ * trailing hyphen. This rejects spaces, slashes, dots, underscores, and other
55
+ * characters that would produce an invalid or ambiguous hostname.
56
+ */
57
+ export function isValidProprInstanceId(instanceId) {
58
+ const id = (instanceId ?? '').trim();
59
+ return /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i.test(id);
60
+ }
61
+ /**
62
+ * Derive the public API/UI URL for a local stack from its instance id, using
63
+ * the shared public tunnel host pattern. Returns `https://t-abc123.propr.dev`
64
+ * for instance id `abc123`. A caller may pass either the bare instance id or the
65
+ * public DNS label (`t-abc123`); the returned URL is canonicalized. Returns
66
+ * `undefined` for a missing/blank id — or an id that is not a valid DNS label
67
+ * (see {@link isValidProprInstanceId}) — so callers can fall back to an explicit
68
+ * URL or a local-development default rather than emitting a malformed hostname.
69
+ * The id is lowercased so a mixed-case instance id yields a canonical hostname
70
+ * (DNS is case-insensitive).
71
+ */
72
+ export function proprInstanceProxyUrl(instanceId) {
73
+ const id = normalizeProprInstanceId(instanceId);
74
+ if (!isValidProprInstanceId(id))
75
+ return undefined;
76
+ return `https://${PROPR_UI_PROXY_LABEL_PREFIX}${id.toLowerCase()}.${PROPR_UI_PROXY_SUFFIX}`;
77
+ }
78
+ /**
79
+ * Whether a URL is a hosted per-instance proxy URL (`https://t-<id>.propr.dev`).
80
+ * propr-routing only forwards `/api/*` and `/socket.io/*` on these hosts, so the
81
+ * tunnel base URL must be one of them. Requires https and *exactly one* valid
82
+ * `t-<instance-id>` label in front of the shared {@link PROPR_UI_PROXY_SUFFIX}.
83
+ * Other propr.dev hosts like `app.propr.dev` and nested hosts are rejected. It
84
+ * must also be a bare origin: a non-root path, query, or fragment (e.g.
85
+ * `https://t-abc.propr.dev/api`) is rejected because
86
+ * {@link proprTunnelEndpoints} appends `/api/...` itself and a base path would
87
+ * double it up (`.../api/api/status`). Returns false for a malformed URL.
88
+ */
89
+ export function isProprProxyUrl(url) {
90
+ if (!url)
91
+ return false;
92
+ try {
93
+ const { protocol, hostname, pathname, search, hash } = new URL(url);
94
+ if (protocol !== 'https:')
95
+ return false;
96
+ // Must be a bare origin — the tunnel endpoint helpers own the path suffix.
97
+ // Trailing slashes (`/`, `//`) are tolerated (callers trim them); any real
98
+ // path segment, query, or fragment is rejected so a base path can't double
99
+ // up the appended `/api/...`.
100
+ if (/[^/]/.test(pathname) || search || hash)
101
+ return false;
102
+ const suffix = `.${PROPR_UI_PROXY_SUFFIX}`;
103
+ if (!hostname.endsWith(suffix))
104
+ return false;
105
+ const label = hostname.slice(0, -suffix.length);
106
+ if (label.includes('.') || !label.startsWith(PROPR_UI_PROXY_LABEL_PREFIX)) {
107
+ return false;
108
+ }
109
+ return isValidProprInstanceId(label.slice(PROPR_UI_PROXY_LABEL_PREFIX.length));
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ }
115
+ function normalizeProprInstanceId(instanceId) {
116
+ const id = (instanceId ?? '').trim();
117
+ return id.startsWith(PROPR_UI_PROXY_LABEL_PREFIX)
118
+ ? id.slice(PROPR_UI_PROXY_LABEL_PREFIX.length)
119
+ : id;
120
+ }
121
+ /**
122
+ * The concrete endpoints the hosted UI reaches through the tunnel base URL.
123
+ * propr-routing only allows `/api/*` and `/socket.io/*`, so the base (root) URL
124
+ * itself intentionally returns 404 — it is NOT a health target. Use `apiStatus`
125
+ * to probe liveness. The base is normalized (trailing slashes trimmed) so the
126
+ * derived paths never double up a slash.
127
+ */
128
+ export function proprTunnelEndpoints(baseUrl) {
129
+ const base = baseUrl.replace(/\/+$/, '');
130
+ return {
131
+ apiStatus: `${base}/api/status`,
132
+ socketIo: `${base}/socket.io/`,
133
+ root: `${base}/`,
134
+ };
135
+ }
27
136
  //# sourceMappingURL=proprServiceUrls.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "propr-cli",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "CLI for interacting with the ProPR backend",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,5 +27,5 @@
27
27
  "code-review",
28
28
  "automation"
29
29
  ],
30
- "license": "ISC"
30
+ "license": "Apache-2.0"
31
31
  }