impel-cli 0.20.63-canary.6 → 0.20.63

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 CHANGED
@@ -219,7 +219,7 @@ skill-catalog prompt context; the selected Eve agent still performs the
219
219
  substantive work. Model, profile,
220
220
  developer-instruction, MCP, approval, sandbox, and feature overrides are
221
221
  rejected for its managed path. Ordinary `impel claude`, user-authored Claude
222
- agent, and `impel codex` passthrough are unchanged.
222
+ agent, and `impel codex` are unchanged.
223
223
 
224
224
  ## Remote Fargate sessions
225
225
 
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.63 — Route isolated Codex through the typed gateway surface
4
+
5
+ - Moves the `impel codex` and `impel use gateway codex` model provider from the
6
+ raw `/chatgpt_passthrough/backend-api/codex` route to `/openai/v1`, the typed
7
+ OpenAI-compatible surface the managed ChatGPT desktop profile already uses.
8
+ The raw route forwards bytes to chatgpt.com and can only serve Codex-native
9
+ models; every OpenAI-compatible subscription model (MiniMax, GLM, Qwen)
10
+ answered `400 model is not available on this client protocol` there.
11
+ Cross-app mode no longer switches routes; it only changes the catalog.
12
+ - Pins `supports_websockets = false` on every generated Codex provider block.
13
+ Without it Codex dials `wss://…/responses` first and retries the handshake
14
+ five times per turn, showing each gateway refusal to the user, before falling
15
+ back to HTTPS. Bumps the managed config version so existing profiles rewrite.
16
+
3
17
  ## 0.20.62 — Silence the Codex apps connector in desktop profiles
4
18
 
5
19
  - Persists `features.apps = false` in the managed ChatGPT desktop `config.toml`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.63-canary.6",
3
+ "version": "0.20.63",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -34,7 +34,6 @@ import {
34
34
  TELEMETRY_FLUSH_COMMAND,
35
35
  } from "./posthog.js";
36
36
  import { maybePrintTelemetryNotice } from "./telemetryNotice.js";
37
- import { versionLine } from "./runtimeBrand.js";
38
37
 
39
38
  const HELP = `impel — isolated Impel workspaces for every tenant
40
39
 
@@ -94,7 +93,7 @@ Config file:
94
93
  function printVersion() {
95
94
  const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
96
95
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
97
- console.log(versionLine(pkg.version));
96
+ console.log(pkg.version);
98
97
  }
99
98
 
100
99
  // Accepts "claude" | "codex" | "all" | undefined; returns a normalized target
@@ -208,13 +208,16 @@ function codexManagedBlock(gatewayUrl, tenantId, { crossAppModels = false } = {}
208
208
  `# Generated for \`${RUNTIME_BRAND.cli.command} codex\`. Other profile settings outside this block are preserved.`,
209
209
  `[model_providers.${providerId}]`,
210
210
  `name = ${JSON.stringify(`${RUNTIME_BRAND.product.displayName} Gateway`)}`,
211
- // The experimental OpenAI-compatible route can dispatch both provider
212
- // families (the desktop ChatGPT profile's cross-app path); the default
213
- // route is the byte-preserving Codex passthrough.
214
- `base_url = ${JSON.stringify(crossAppModels
215
- ? `${gatewayUrl}/experimental/openai/v1`
216
- : impelCodexBaseUrl(gatewayUrl))}`,
211
+ // The typed OpenAI-compatible route serves Codex-native and every
212
+ // OpenAI-compatible subscription model, and is what the managed ChatGPT
213
+ // desktop profile already uses. Cross-app mode only changes which models
214
+ // the catalog lists, not the route. The old byte-preserving Codex
215
+ // passthrough could serve Codex-native models only.
216
+ `base_url = ${JSON.stringify(impelCodexBaseUrl(gatewayUrl))}`,
217
217
  'wire_api = "responses"',
218
+ // HTTPS only: without this Codex retries a wss:// handshake five times
219
+ // per turn before falling back.
220
+ "supports_websockets = false",
218
221
  `env_key = ${JSON.stringify(CODEX_GATEWAY_TOKEN_ENV)}`,
219
222
  ];
220
223
  if (RUNTIME_BRAND.features.mcp) {
package/src/codexSetup.js CHANGED
@@ -39,11 +39,16 @@ export const CODEX_CONFIG_PATH = path.join(CODEX_HOME, "config.toml");
39
39
 
40
40
  export const PROVIDER_ID = RUNTIME_BRAND.cli.providerId;
41
41
 
42
- // Genuine Codex CLI traffic has its own byte-preserving compatibility route.
43
- // With wire_api = "responses", Codex POSTs to `${base_url}/responses`, so this
44
- // base must stop immediately before that suffix. Do not point the CLI at the
45
- // separate `/v1/responses` SDK front door; the gateway may shape that body.
46
- export const CODEX_CLI_BASE_PATH = "/chatgpt_passthrough/backend-api/codex";
42
+ // Codex CLI traffic uses the gateway's typed OpenAI-compatible surface, the
43
+ // same route the managed ChatGPT desktop profile uses. With
44
+ // wire_api = "responses", Codex POSTs to `${base_url}/responses`, so this base
45
+ // must stop immediately before that suffix. The old raw
46
+ // `/chatgpt_passthrough/backend-api/codex` route forwards bytes to chatgpt.com
47
+ // and can only serve Codex-native models: every OpenAI-compatible subscription
48
+ // model (MiniMax, GLM, Qwen) answered "model is not available on this client
49
+ // protocol" there. Do not point the CLI at the separate `/v1/responses` SDK
50
+ // front door either; the gateway may shape that body differently.
51
+ export const CODEX_CLI_BASE_PATH = "/openai/v1";
47
52
  export const impelCodexBaseUrl = (gatewayUrl) => `${gatewayUrl}${CODEX_CLI_BASE_PATH}`;
48
53
 
49
54
  const START_MARK = `# >>> ${RUNTIME_BRAND.cli.managedMarker} managed block (model_providers.${PROVIDER_ID}) >>>`;
@@ -79,6 +84,10 @@ function providerTablesBlock(
79
84
  `name = ${JSON.stringify(`${RUNTIME_BRAND.product.displayName} Gateway`)}`,
80
85
  `base_url = "${baseUrl}"`,
81
86
  `wire_api = "responses"`,
87
+ // The gateway serves Responses over HTTPS only. Without this pin Codex
88
+ // dials wss://.../responses first and retries the handshake five times
89
+ // (surfacing each refusal to the user) before falling back to HTTPS.
90
+ `supports_websockets = false`,
82
91
  "",
83
92
  `[model_providers.${PROVIDER_ID}.auth]`,
84
93
  `command = ${JSON.stringify(auth.command)}`,
@@ -3,7 +3,6 @@ import {
3
3
  loadConfig,
4
4
  CONFIG_PATH,
5
5
  saveConfig,
6
- foreignChannelOrigin,
7
6
  normalizeGatewayUrl,
8
7
  resolveDefaultGateway,
9
8
  resolveDefaultAppUrl,
@@ -13,8 +12,7 @@ import { promptSecret } from "../prompt.js";
13
12
  import { applyCliUser, fetchTenants, normalizeTenantId } from "../tenants.js";
14
13
  import { RUNTIME_BRAND } from "../runtimeBrand.js";
15
14
 
16
- export async function cmdAuth(argv, overrides = {}) {
17
- const io = { fetchTenants, promptSecret, ...overrides };
15
+ export async function cmdAuth(argv) {
18
16
  const { flags } = parseFlags(argv, {
19
17
  pat: { type: "string" },
20
18
  gateway: { type: "string" },
@@ -22,16 +20,11 @@ export async function cmdAuth(argv, overrides = {}) {
22
20
  tenant: { type: "string" },
23
21
  });
24
22
 
25
- // `auth` is the one command that must run against a config whose origins
26
- // belong to another channel: it is how a device that carried a production
27
- // config is moved onto the canary origins (and the message `loadConfig`
28
- // prints elsewhere tells the tester to run it).
29
- const canary = RUNTIME_BRAND.channel === "canary";
30
- const existing = loadConfig({ rejectForeignOrigins: !canary });
23
+ const existing = loadConfig();
31
24
 
32
25
  let pat = flags.pat;
33
26
  if (!pat) {
34
- pat = await io.promptSecret(`${RUNTIME_BRAND.product.displayName} Personal Access Token (${RUNTIME_BRAND.auth.patPrefix}...): `);
27
+ pat = await promptSecret(`${RUNTIME_BRAND.product.displayName} Personal Access Token (${RUNTIME_BRAND.auth.patPrefix}...): `);
35
28
  }
36
29
  if (!pat) {
37
30
  console.error("impel: no PAT provided, aborting.");
@@ -44,21 +37,12 @@ export async function cmdAuth(argv, overrides = {}) {
44
37
  );
45
38
  }
46
39
 
47
- // Under the canary brand the stored origins are ignored in favour of the
48
- // brand defaults; a production origin can only survive here if it is asked
49
- // for explicitly, and then it is refused below rather than written.
50
- const gatewayUrl = normalizeGatewayUrl(flags.gateway || (canary ? null : existing?.gatewayUrl) || resolveDefaultGateway());
51
- const appUrl = normalizeGatewayUrl(flags.app || (canary ? null : existing?.appUrl) || resolveDefaultAppUrl());
52
- for (const [key, value] of [["gatewayUrl", gatewayUrl], ["appUrl", appUrl]]) {
53
- const expected = foreignChannelOrigin(key, value);
54
- if (expected) {
55
- throw new Error(`${key} ${value} is not a ${RUNTIME_BRAND.channel} origin; this ${RUNTIME_BRAND.channel} build talks only to ${expected}.`);
56
- }
57
- }
40
+ const gatewayUrl = normalizeGatewayUrl(flags.gateway || existing?.gatewayUrl || resolveDefaultGateway());
41
+ const appUrl = normalizeGatewayUrl(flags.app || existing?.appUrl || resolveDefaultAppUrl());
58
42
 
59
43
  const config = { ...(existing || {}), pat, gatewayUrl, appUrl, updatedAt: new Date().toISOString() };
60
44
  try {
61
- const listing = await io.fetchTenants(config);
45
+ const listing = await fetchTenants(config);
62
46
  const requestedTenant = flags.tenant ? normalizeTenantId(flags.tenant) : null;
63
47
  const selected = requestedTenant
64
48
  ? listing.tenants.find((tenant) => tenant.id === requestedTenant || tenant.slug === requestedTenant)
@@ -15,7 +15,7 @@ import {
15
15
  convergenceSummaryDiagnostics,
16
16
  readRecentConvergenceSummary,
17
17
  } from "../convergenceSummary.js";
18
- import { brandedEnvironmentName, brandedText, RUNTIME_BRAND, versionLine } from "../runtimeBrand.js";
18
+ import { brandedEnvironmentName, brandedText, RUNTIME_BRAND } from "../runtimeBrand.js";
19
19
  import { IMPEL_CLI_ENTRYPOINT } from "../selfInvocation.js";
20
20
  import {
21
21
  fetchRemoteVersion,
@@ -68,9 +68,7 @@ export function postInstallCliRunnable(expectedVersion, { spawn = spawnSync, exe
68
68
  windowsHide: true,
69
69
  });
70
70
  if (run.status !== 0 || run.error) return false;
71
- // The freshly installed build is the same package on the same channel, so
72
- // its `--version` line carries the same channel suffix this build prints.
73
- return String(run.stdout || "").trim() === versionLine(expectedVersion.trim());
71
+ return String(run.stdout || "").trim() === expectedVersion.trim();
74
72
  } catch {
75
73
  return false;
76
74
  }
package/src/config.js CHANGED
@@ -47,39 +47,8 @@ export function normalizeGatewayUrl(url) {
47
47
  return LEGACY_GATEWAY_URLS.has(normalized) ? DEFAULT_GATEWAY_URL : normalized;
48
48
  }
49
49
 
50
- const CHANNEL_ORIGIN_KEYS = Object.freeze({
51
- gatewayUrl: Object.freeze({ brandDefault: DEFAULT_GATEWAY_URL, environmentSuffix: "GATEWAY_URL" }),
52
- appUrl: Object.freeze({ brandDefault: DEFAULT_APP_URL, environmentSuffix: "APP_URL" }),
53
- });
54
-
55
- /**
56
- * Under the canary brand, an origin the CLI would use must be the canary row
57
- * the brand carries, or the explicit branded env override (how an engineer
58
- * points a build at a local service). Returns the origin `key` is required to
59
- * be when `value` is foreign, else null. The stable brand accepts every
60
- * origin, exactly as before channels existed.
61
- *
62
- * Why this exists: the canary CLI replaces the stable one on a device under
63
- * the same package name, so an engineer's existing production config would
64
- * otherwise keep a canary build talking to production with production
65
- * credentials (R17).
66
- */
67
- export function foreignChannelOrigin(key, value) {
68
- if (RUNTIME_BRAND.channel !== "canary" || !value) return null;
69
- const { brandDefault, environmentSuffix } = CHANNEL_ORIGIN_KEYS[key];
70
- const normalized = normalizeGatewayUrl(value);
71
- const override = process.env[brandedEnvironmentName(environmentSuffix)];
72
- if (normalized === brandDefault || (override && normalized === normalizeGatewayUrl(override))) return null;
73
- return brandDefault;
74
- }
75
-
76
- /**
77
- * Returns the parsed config object, or null if it doesn't exist yet. Throws
78
- * on malformed JSON, and — under the canary brand — on a stored origin outside
79
- * the canary row unless `rejectForeignOrigins` is false (`impel auth` reads
80
- * that way so it can move a device off a production config).
81
- */
82
- export function loadConfig({ rejectForeignOrigins = true } = {}) {
50
+ /** Returns the parsed config object, or null if it doesn't exist yet. Throws on malformed JSON. */
51
+ export function loadConfig() {
83
52
  let raw;
84
53
  try {
85
54
  raw = fs.readFileSync(CONFIG_PATH, "utf8");
@@ -87,27 +56,15 @@ export function loadConfig({ rejectForeignOrigins = true } = {}) {
87
56
  if (err.code === "ENOENT") return null;
88
57
  throw err;
89
58
  }
90
- let config;
91
59
  try {
92
- config = JSON.parse(raw);
60
+ const config = JSON.parse(raw);
61
+ if (config?.gatewayUrl) config.gatewayUrl = normalizeGatewayUrl(config.gatewayUrl);
62
+ return config;
93
63
  } catch {
94
64
  throw new Error(
95
65
  `${CONFIG_PATH} exists but isn't valid JSON. Fix or delete it, then run \`${RUNTIME_BRAND.cli.command} auth\` again.`
96
66
  );
97
67
  }
98
- if (config?.gatewayUrl) config.gatewayUrl = normalizeGatewayUrl(config.gatewayUrl);
99
- if (rejectForeignOrigins) {
100
- for (const key of Object.keys(CHANNEL_ORIGIN_KEYS)) {
101
- const expected = foreignChannelOrigin(key, config?.[key]);
102
- if (expected) {
103
- throw new Error(
104
- `${CONFIG_PATH} stores ${key} ${normalizeGatewayUrl(config[key])}, but this ${RUNTIME_BRAND.channel} build talks only to ${expected}; `
105
- + `run \`${RUNTIME_BRAND.cli.command} auth\` to re-enrol this device against the ${RUNTIME_BRAND.channel} origins.`
106
- );
107
- }
108
- }
109
- }
110
- return config;
111
68
  }
112
69
 
113
70
  /** Writes the config atomically-ish and locks it down to 0600 (owner read/write only). */
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
 
3
3
  import { main as upstreamMain } from "../cli.js";
4
- import { brandedText, RUNTIME_BRAND, validateRuntimeBrand, versionLine } from "../runtimeBrand.js";
4
+ import { brandedText, RUNTIME_BRAND, validateRuntimeBrand } from "../runtimeBrand.js";
5
5
 
6
6
  const ALIASES = Object.freeze({
7
7
  apps: "app",
@@ -95,7 +95,7 @@ export function createImpelCliExtension({ brand, entrypoint, version }) {
95
95
  return;
96
96
  }
97
97
  if (["--version", "-v"].includes(rawCommand)) {
98
- process.stdout.write(`${versionLine(version)}\n`);
98
+ process.stdout.write(`${version}\n`);
99
99
  return;
100
100
  }
101
101
  const command = ALIASES[rawCommand] || rawCommand;
@@ -328,6 +328,7 @@ export function createGatewayCli(options) {
328
328
  `name = ${JSON.stringify(`${brand.product.displayName} Gateway`)}`,
329
329
  `base_url = ${JSON.stringify(gatewayRoutes.codex(gatewayUrl))}`,
330
330
  'wire_api = "responses"',
331
+ "supports_websockets = false",
331
332
  "",
332
333
  `[model_providers.${brand.cli.providerId}.auth]`,
333
334
  `command = ${JSON.stringify(auth.command)}`,
@@ -17,4 +17,9 @@
17
17
  // v47 persists `features.apps = false` in the managed ChatGPT desktop profile
18
18
  // so the hosted codex_apps connector stops handshaking against the gateway
19
19
  // without a credential (the same gate 0.20.61 passed to CLI sessions).
20
- export const CURRENT_CONFIG_VERSION = 47;
20
+ // v48 moves the isolated CLI Codex provider from the raw
21
+ // `/chatgpt_passthrough/backend-api/codex` route (Codex-native models only) to
22
+ // the typed `/openai/v1` route the managed ChatGPT desktop profile uses, and
23
+ // pins `supports_websockets = false` so Codex stops retrying a wss://
24
+ // handshake the gateway refuses before every HTTPS turn.
25
+ export const CURRENT_CONFIG_VERSION = 48;
package/src/posthog.js CHANGED
@@ -101,7 +101,7 @@ export const TELEMETRY_CONTRACT = Object.freeze({
101
101
  name: "cli_bug_report",
102
102
  origin: "server",
103
103
  properties: [
104
- { key: "channel", type: "string", required: true, values: ["latest", "next", "canary"] },
104
+ { key: "channel", type: "string", required: true, values: ["latest", "next"] },
105
105
  { key: "cliVersion", type: "string", required: true, maxLength: 32 },
106
106
  { key: "hasMessage", type: "boolean", required: true },
107
107
  {
@@ -123,7 +123,7 @@ export const TELEMETRY_CONTRACT = Object.freeze({
123
123
  name: "cli_command_run",
124
124
  origin: "cli",
125
125
  properties: [
126
- { key: "channel", type: "string", required: true, values: ["latest", "next", "canary"] },
126
+ { key: "channel", type: "string", required: true, values: ["latest", "next"] },
127
127
  { key: "cliVersion", type: "string", required: true, maxLength: 32 },
128
128
  { key: "command", type: "string", required: true, maxLength: 64 },
129
129
  {
@@ -14,48 +14,22 @@ const SUPPORTED_COMMANDS = new Set([
14
14
  "models", "agents", "update", "use", "experimental",
15
15
  ]);
16
16
 
17
- /**
18
- * The closed per-channel origin row (canary release cycle, KTD9).
19
- *
20
- * A brand names its channel and every default origin must be that channel's
21
- * row: a canary build pointed at production, or a stable build pointed at
22
- * dev, is rejected at validation. `channel` is absent from the shipped
23
- * source's third-party brands and defaults to `stable`, which constrains
24
- * nothing beyond "not the canary row" so embedders' own origins stay valid.
25
- * `scripts/bake-canary-brand.mjs` rewrites DEFAULT below to the canary row at
26
- * publish time; the literals there are what it substitutes.
27
- */
28
- export const CHANNEL_ORIGINS = Object.freeze({
29
- stable: Object.freeze({
30
- gateway: "https://gateway.useimpel.com",
31
- sessions: "https://sessions.useimpel.com",
32
- controlPlane: "https://www.useimpel.com",
33
- }),
34
- canary: Object.freeze({
35
- gateway: "https://gateway.dev.useimpel.com",
36
- sessions: "https://sessions.dev.useimpel.com",
37
- controlPlane: "https://next.dev.useimpel.com",
38
- }),
39
- });
40
- const CANARY_ORIGIN_SET = new Set(Object.values(CHANNEL_ORIGINS.canary));
41
-
42
17
  const DEFAULT = Object.freeze({
43
18
  schemaVersion: 1,
44
- channel: "canary",
45
19
  product: Object.freeze({ id: "impel", displayName: "Impel" }),
46
20
  cli: Object.freeze({
47
21
  command: "impel",
48
22
  packageName: "impel-cli",
49
- configNamespace: "impel-canary",
23
+ configNamespace: "impel",
50
24
  providerId: "impel",
51
25
  managedMarker: "impel-cli",
52
26
  environmentPrefix: "IMPEL",
53
27
  }),
54
28
  auth: Object.freeze({ patPrefix: "impel_pat_", tenantPrefix: "impel_tenant_" }),
55
29
  tenant: Object.freeze({ defaultId: null, displayName: null }),
56
- gateway: Object.freeze({ defaultOrigin: "https://gateway.dev.useimpel.com" }),
57
- sessions: Object.freeze({ defaultOrigin: "https://sessions.dev.useimpel.com" }),
58
- controlPlane: Object.freeze({ defaultOrigin: "https://next.dev.useimpel.com" }),
30
+ gateway: Object.freeze({ defaultOrigin: "https://gateway.useimpel.com" }),
31
+ sessions: Object.freeze({ defaultOrigin: "https://sessions.useimpel.com" }),
32
+ controlPlane: Object.freeze({ defaultOrigin: "https://www.useimpel.com" }),
59
33
  updates: Object.freeze({ registry: null }),
60
34
  apps: Object.freeze({
61
35
  displayPrefix: "Impel",
@@ -100,32 +74,10 @@ function pathSegment(name, value) {
100
74
  return result;
101
75
  }
102
76
 
103
- function channel(value) {
104
- if (value == null) return "stable";
105
- if (value === "stable" || value === "canary") return value;
106
- throw new Error("impel-cli runtime brand channel must be stable or canary");
107
- }
108
-
109
- /** Validate one default origin against the brand's channel row (R10). */
110
- function channelOrigin(name, value, brandChannel) {
111
- const normalized = origin(name, value);
112
- const column = name.split(".")[0];
113
- if (brandChannel === "canary") {
114
- const expected = CHANNEL_ORIGINS.canary[column];
115
- if (normalized !== expected) {
116
- throw new Error(`impel-cli runtime brand ${name} must be ${expected} on the canary channel`);
117
- }
118
- } else if (CANARY_ORIGIN_SET.has(normalized)) {
119
- throw new Error(`impel-cli runtime brand ${name} is a canary origin; the stable channel must not carry it`);
120
- }
121
- return normalized;
122
- }
123
-
124
77
  export function validateRuntimeBrand(input) {
125
78
  if (!input || Array.isArray(input) || typeof input !== "object" || input.schemaVersion !== 1) {
126
79
  throw new Error("impel-cli runtime brand schemaVersion must be 1");
127
80
  }
128
- const brandChannel = channel(input.channel);
129
81
  const command = text("cli.command", input.cli?.command, SAFE_ID);
130
82
  const packageName = text("cli.packageName", input.cli?.packageName || command, SAFE_PACKAGE);
131
83
  if (path.isAbsolute(packageName)) throw new Error("impel-cli runtime brand cli.packageName is invalid");
@@ -140,7 +92,6 @@ export function validateRuntimeBrand(input) {
140
92
  }
141
93
  return Object.freeze({
142
94
  schemaVersion: 1,
143
- channel: brandChannel,
144
95
  product: Object.freeze({
145
96
  id: text("product.id", input.product?.id, SAFE_ID),
146
97
  displayName: text("product.displayName", input.product?.displayName),
@@ -161,15 +112,14 @@ export function validateRuntimeBrand(input) {
161
112
  defaultId: defaultTenant,
162
113
  displayName: input.tenant?.displayName == null ? defaultTenant : text("tenant.displayName", input.tenant.displayName),
163
114
  }),
164
- gateway: Object.freeze({ defaultOrigin: channelOrigin("gateway.defaultOrigin", input.gateway?.defaultOrigin, brandChannel) }),
115
+ gateway: Object.freeze({ defaultOrigin: origin("gateway.defaultOrigin", input.gateway?.defaultOrigin) }),
165
116
  sessions: Object.freeze({
166
- defaultOrigin: channelOrigin(
117
+ defaultOrigin: origin(
167
118
  "sessions.defaultOrigin",
168
119
  input.sessions?.defaultOrigin || input.gateway?.defaultOrigin,
169
- brandChannel,
170
120
  ),
171
121
  }),
172
- controlPlane: Object.freeze({ defaultOrigin: channelOrigin("controlPlane.defaultOrigin", input.controlPlane?.defaultOrigin, brandChannel) }),
122
+ controlPlane: Object.freeze({ defaultOrigin: origin("controlPlane.defaultOrigin", input.controlPlane?.defaultOrigin) }),
173
123
  updates: Object.freeze({
174
124
  registry: input.updates?.registry == null
175
125
  ? null
@@ -208,15 +158,6 @@ export function brandedEnvironmentName(suffix) {
208
158
  return `${RUNTIME_BRAND.cli.environmentPrefix}_${suffix}`;
209
159
  }
210
160
 
211
- /**
212
- * The line `--version` prints. Off the stable channel it names the channel so
213
- * a tester can tell a canary process from a stable one at a glance (R8); the
214
- * stable output is the bare version, byte-identical to before channels existed.
215
- */
216
- export function versionLine(version) {
217
- return RUNTIME_BRAND.channel === "stable" ? String(version) : `${version} (${RUNTIME_BRAND.channel})`;
218
- }
219
-
220
161
  export function brandedText(value) {
221
162
  return String(value)
222
163
  .replaceAll("impel-cli", `${RUNTIME_BRAND.cli.command}-cli`)
package/src/updates.js CHANGED
@@ -36,22 +36,13 @@ export function updateRegistry() {
36
36
  ).replace(/\/+$/u, "");
37
37
  }
38
38
 
39
- /** The closed set of npm dist-tags the CLI can follow. */
40
- export function normalizeUpdateTag(tag) {
41
- if (tag === "latest" || tag === "next" || tag === "canary") return tag;
39
+ function normalizeUpdateTag(tag) {
40
+ if (tag === "latest" || tag === "next") return tag;
42
41
  throw new Error(`unsupported npm update tag "${tag}"`);
43
42
  }
44
43
 
45
- /**
46
- * Keep prerelease installs on npm's prerelease channel.
47
- *
48
- * A canary build is keyed on the brand channel rather than its version
49
- * string: `<patch+1>-canary.<N>` is a prerelease, but it must follow the
50
- * `canary` dist-tag, not `next` — and under the stable brand that same
51
- * version is only ever a prerelease that follows `next`.
52
- */
44
+ /** Keep prerelease installs on npm's prerelease channel. */
53
45
  export function updateTagForVersion(version) {
54
- if (RUNTIME_BRAND.channel === "canary") return "canary";
55
46
  const parsed = validVersion(version) ? version.match(VERSION_RE) : null;
56
47
  return parsed?.[4] ? "next" : "latest";
57
48
  }
@@ -1,92 +0,0 @@
1
- #!/usr/bin/env node
2
- // Bake the canary channel into the runtime brand (canary release cycle, KTD7).
3
- //
4
- // `publish-canary.yml` runs this in the working tree immediately before
5
- // `pnpm pack`, after the test suite has run on the unmodified tree. It
6
- // rewrites the brand DEFAULT in `src/runtimeBrand.js` in place: the channel
7
- // literal becomes `canary`, the three production `defaultOrigin` literals
8
- // become the canary row of CHANNEL_ORIGINS (read from the very module being
9
- // baked so the row has one source of truth), and the CLI config namespace
10
- // becomes `impel-canary` so stable and canary state never share files.
11
- //
12
- // It is a one-shot publish step, not a formatter: every literal it replaces
13
- // must occur exactly once in its shipped form, a file that is already baked
14
- // is refused, and the result is re-imported and validated before the script
15
- // exits 0 — a half-baked or doubly-baked tree can never be packed.
16
- //
17
- // Usage: node scripts/bake-canary-brand.mjs [path/to/runtimeBrand.js]
18
-
19
- import fs from "node:fs";
20
- import path from "node:path";
21
- import { fileURLToPath, pathToFileURL } from "node:url";
22
-
23
- const DEFAULT_TARGET = fileURLToPath(new URL("../src/runtimeBrand.js", import.meta.url));
24
- const CANARY_MARKER = 'channel: "canary",';
25
-
26
- function fail(message) {
27
- process.stderr.write(`bake-canary-brand: ${message}\n`);
28
- process.exit(1);
29
- }
30
-
31
- /** Import `target` fresh (never from this process's module cache) with no brand in the environment. */
32
- async function importBrandModule(target, cacheBuster) {
33
- // The module reads IMPEL_CLI_RUNTIME_BRAND at import time; the bake is about
34
- // the DEFAULT, so an inherited brand must not stand in for it.
35
- delete process.env.IMPEL_CLI_RUNTIME_BRAND;
36
- return import(`${pathToFileURL(target).href}?bake=${cacheBuster}`);
37
- }
38
-
39
- function replaceExactlyOnce(source, from, to) {
40
- const occurrences = source.split(from).length - 1;
41
- if (occurrences !== 1) {
42
- fail(`expected exactly one occurrence of ${JSON.stringify(from)} in the brand source, found ${occurrences}`);
43
- }
44
- return source.replace(from, to);
45
- }
46
-
47
- async function main() {
48
- const target = path.resolve(process.argv[2] || DEFAULT_TARGET);
49
- const original = fs.readFileSync(target, "utf8");
50
- if (original.includes(CANARY_MARKER)) fail(`${target} is already baked for the canary channel`);
51
-
52
- const { CHANNEL_ORIGINS } = await importBrandModule(target, "before");
53
- const rows = [
54
- ['channel: "stable",', CANARY_MARKER],
55
- ['configNamespace: "impel",', 'configNamespace: "impel-canary",'],
56
- ...["gateway", "sessions", "controlPlane"].map((column) => [
57
- `defaultOrigin: ${JSON.stringify(CHANNEL_ORIGINS.stable[column])}`,
58
- `defaultOrigin: ${JSON.stringify(CHANNEL_ORIGINS.canary[column])}`,
59
- ]),
60
- ];
61
- let baked = original;
62
- for (const [from, to] of rows) baked = replaceExactlyOnce(baked, from, to);
63
-
64
- // Atomic replace, then prove the result by importing it: the DEFAULT must
65
- // now be the canary row and must pass the validator's own channel check.
66
- const temporary = `${target}.bake-${process.pid}`;
67
- fs.writeFileSync(temporary, baked, { mode: fs.statSync(target).mode & 0o777 });
68
- fs.renameSync(temporary, target);
69
- try {
70
- const { RUNTIME_BRAND, validateRuntimeBrand } = await importBrandModule(target, "after");
71
- validateRuntimeBrand(RUNTIME_BRAND);
72
- const mismatch = ["gateway", "sessions", "controlPlane"].find((column) => (
73
- RUNTIME_BRAND[column].defaultOrigin !== CHANNEL_ORIGINS.canary[column]
74
- ));
75
- if (RUNTIME_BRAND.channel !== "canary" || mismatch) {
76
- throw new Error(`baked DEFAULT is not the canary row (${mismatch || "channel"})`);
77
- }
78
- if (RUNTIME_BRAND.cli.configNamespace !== "impel-canary") {
79
- throw new Error(`baked CLI config namespace is not isolated (${RUNTIME_BRAND.cli.configNamespace})`);
80
- }
81
- process.stdout.write(
82
- `bake-canary-brand: ${target} now carries channel canary `
83
- + `(gateway ${RUNTIME_BRAND.gateway.defaultOrigin}, sessions ${RUNTIME_BRAND.sessions.defaultOrigin}, `
84
- + `controlPlane ${RUNTIME_BRAND.controlPlane.defaultOrigin}, config ${RUNTIME_BRAND.cli.configNamespace})\n`,
85
- );
86
- } catch (error) {
87
- fs.writeFileSync(target, original);
88
- fail(`baked brand failed verification and was restored: ${error?.message || error}`);
89
- }
90
- }
91
-
92
- await main();