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 +1 -1
- package/RELEASE_NOTES.md +14 -0
- package/package.json +1 -1
- package/src/cli.js +1 -2
- package/src/cliProfiles.js +9 -6
- package/src/codexSetup.js +14 -5
- package/src/commands/auth.js +6 -22
- package/src/commands/update.js +2 -4
- package/src/config.js +5 -48
- package/src/extension/index.js +2 -2
- package/src/gateway/index.js +1 -0
- package/src/managedProfileVersion.js +6 -1
- package/src/posthog.js +2 -2
- package/src/runtimeBrand.js +7 -66
- package/src/updates.js +3 -12
- package/scripts/bake-canary-brand.mjs +0 -92
- package/scripts/canary-version.mjs +0 -173
- package/scripts/npm-dist-tags.mjs +0 -134
- package/scripts/pack-canary.mjs +0 -32
- package/scripts/pack-release.mjs +0 -27
- package/scripts/prepare-canary.mjs +0 -28
- package/scripts/release-gate.mjs +0 -133
- package/scripts/release-metadata.mjs +0 -18
- package/scripts/required-checks.mjs +0 -126
- package/scripts/verify-canary-install.mjs +0 -62
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`
|
|
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
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(
|
|
96
|
+
console.log(pkg.version);
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
// Accepts "claude" | "codex" | "all" | undefined; returns a normalized target
|
package/src/cliProfiles.js
CHANGED
|
@@ -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
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
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)}`,
|
package/src/commands/auth.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
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)
|
package/src/commands/update.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
convergenceSummaryDiagnostics,
|
|
16
16
|
readRecentConvergenceSummary,
|
|
17
17
|
} from "../convergenceSummary.js";
|
|
18
|
-
import { brandedEnvironmentName, brandedText, RUNTIME_BRAND
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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). */
|
package/src/extension/index.js
CHANGED
|
@@ -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
|
|
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(`${
|
|
98
|
+
process.stdout.write(`${version}\n`);
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
const command = ALIASES[rawCommand] || rawCommand;
|
package/src/gateway/index.js
CHANGED
|
@@ -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
|
-
|
|
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"
|
|
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"
|
|
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
|
{
|
package/src/runtimeBrand.js
CHANGED
|
@@ -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
|
|
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.
|
|
57
|
-
sessions: Object.freeze({ defaultOrigin: "https://sessions.
|
|
58
|
-
controlPlane: Object.freeze({ defaultOrigin: "https://
|
|
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:
|
|
115
|
+
gateway: Object.freeze({ defaultOrigin: origin("gateway.defaultOrigin", input.gateway?.defaultOrigin) }),
|
|
165
116
|
sessions: Object.freeze({
|
|
166
|
-
defaultOrigin:
|
|
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:
|
|
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
|
-
|
|
40
|
-
|
|
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();
|