@bitkyc08/opencodex 2.41.0 → 2.43.0-preview.20260906
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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-VGeQEZ_v.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +54 -24
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/identity.ts +8 -2
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +259 -29
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +47 -13
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +49 -9
- package/src/cli/account-extended.ts +7 -1
- package/src/cli/capabilities.ts +15 -4
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/observe.ts +3 -1
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +272 -32
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +44 -5
- package/src/codex/catalog/metadata.ts +149 -14
- package/src/codex/catalog/native-models.ts +116 -4
- package/src/codex/catalog/parsing.ts +122 -8
- package/src/codex/catalog/provider-fetch.ts +154 -23
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +169 -0
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +99 -34
- package/src/codex/injected-marker.ts +30 -4
- package/src/codex/journal.ts +14 -0
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +474 -178
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/account-quota-rank.ts +40 -1
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/chatgpt-device.ts +187 -0
- package/src/oauth/chatgpt.ts +31 -4
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +140 -29
- package/src/oauth/log.ts +3 -0
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/muse-subscription-usage.ts +95 -0
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +625 -98
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +122 -28
- package/src/server/live.ts +18 -4
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +45 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +510 -57
- package/src/server/responses/empty-completion-guard.ts +4 -0
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/request.ts +8 -0
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-B2YjLA-i.css +0 -1
- package/gui/dist/assets/index-aPup8CKb.js +0 -112
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
|
+
import { resolvePendingInitialModelSelection } from "../providers/initial-model-selection-runtime";
|
|
2
3
|
import { captureCatalogAdmissionSnapshot } from "./catalog-admission";
|
|
3
4
|
import { convergeCodexCatalog } from "./convergence";
|
|
4
5
|
import type {
|
|
@@ -152,6 +153,8 @@ export function createManagementConvergeCodex(
|
|
|
152
153
|
catalogRefresh: unexpectedCatalogFailure(false),
|
|
153
154
|
});
|
|
154
155
|
}
|
|
156
|
+
// Registration choices are committed independently, before sealing catalog authority.
|
|
157
|
+
await resolvePendingInitialModelSelection(retainedConfig as OcxConfig);
|
|
155
158
|
const snapshot = captureCatalogAdmissionSnapshot(retainedConfig);
|
|
156
159
|
const result = await convergeCodexCatalog(snapshot, request, {
|
|
157
160
|
onCommitBegin: () => { commitBegan = true; },
|
|
@@ -149,6 +149,13 @@ export function compareClientVersionsForTests(left: string, right: string): numb
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/** Numeric-segment comparison. Only used to pick the highest floor in a known-good set. */
|
|
152
|
+
// Prerelease and build suffixes are deliberately NOT ordered. Splitting on [.+-] turns the
|
|
153
|
+
// suffix into a non-finite segment that reads as 0, so `0.144.0-rc.1` sorts at or above
|
|
154
|
+
// `0.144.0` rather than below it, the inverse of semver. That is tolerable because every
|
|
155
|
+
// version this ranks against is a release version: the gated floor, the measured minimum, and
|
|
156
|
+
// the snapshot's `minimal_client_version` rows. A prerelease runtime is therefore passed
|
|
157
|
+
// through exactly as it was before the floor bound tier 2, so this is not a new hazard. Order
|
|
158
|
+
// the suffix properly before reusing this anywhere a prerelease has to sort below its release.
|
|
152
159
|
function compareClientVersions(left: string, right: string): number {
|
|
153
160
|
const l = left.split(/[.+-]/).map(Number);
|
|
154
161
|
const r = right.split(/[.+-]/).map(Number);
|
|
@@ -165,9 +172,10 @@ function compareClientVersions(left: string, right: string): number {
|
|
|
165
172
|
*
|
|
166
173
|
* 1. the inbound request's own `client_version` — the only value certainly describing the
|
|
167
174
|
* client being answered;
|
|
168
|
-
|
|
169
|
-
* Retained sync refreshes runtime evidence before
|
|
170
|
-
* usable here; the persisted file itself carries no
|
|
175
|
+
* 2. the selected Codex runtime version, for callers with no request of their own — but never
|
|
176
|
+
* below `GATED_MODEL_CLIENT_VERSION_FLOOR`. Retained sync refreshes runtime evidence before
|
|
177
|
+
* discovery, which is what makes this usable here; the persisted file itself carries no
|
|
178
|
+
* freshness guarantee.
|
|
171
179
|
* 3. the floor this build's own bundled roster records for the models being gated
|
|
172
180
|
* (`GATED_MODEL_CLIENT_VERSION_FLOOR`).
|
|
173
181
|
*
|
|
@@ -178,6 +186,25 @@ function compareClientVersions(left: string, right: string): number {
|
|
|
178
186
|
* snapshot states the gated models require, so asking under it is the narrowest question
|
|
179
187
|
* that can still return them.
|
|
180
188
|
*
|
|
189
|
+
* The floor binds tier 2 as well, and that is the whole of #3436. #3022 gave tier 3 the
|
|
190
|
+
* measured minimum but left tier 2 to speak for itself, so a host whose persisted runtime was
|
|
191
|
+
* REAL but OLD — 0.141.0 against a measured 0.144.0 — asked upstream under a version upstream
|
|
192
|
+
* filters on, got an honest roster with no gpt-5.6, and lost sol/terra/luna everywhere. That
|
|
193
|
+
* made an outdated CLI strictly worse than no CLI at all, since the runtime-less host already
|
|
194
|
+
* asked at the floor and kept its models. The clamp only ever raises: a runtime at or above
|
|
195
|
+
* the floor is preserved exactly, because a newer client can drive models the floor cannot
|
|
196
|
+
* name.
|
|
197
|
+
*
|
|
198
|
+
* Which tier answers is a question about WHICH QUESTION IS BEING ASKED, not about background
|
|
199
|
+
* versus request path — `isDirectCallerEntitledToCodexModel` and both authorization paths in
|
|
200
|
+
* `auth-context.ts` are inbound requests that reach tier 2 because they carry no version:
|
|
201
|
+
*
|
|
202
|
+
* - No inbound version: the caller is asking whether the ACCOUNT owns the model. Upstream
|
|
203
|
+
* only incidentally filters that answer by version, so ask at no less than the floor.
|
|
204
|
+
* - An inbound version: the caller is asking what THAT CLIENT may use. Answer verbatim, even
|
|
205
|
+
* when it is older than the floor. Clamping there would advertise rows the client told us
|
|
206
|
+
* it cannot drive (#2548) and would turn an honest `unknown` into a cached `denied`.
|
|
207
|
+
*
|
|
181
208
|
* There is deliberately no `0.0.0`-style fallback. A placeholder describes a client that
|
|
182
209
|
* predates every gated model, which is what made upstream answer with an empty roster and
|
|
183
210
|
* turned absent evidence into a manufactured confirmed negative (#2886).
|
|
@@ -195,7 +222,23 @@ export function resolveCodexEntitlementClientVersion(
|
|
|
195
222
|
const selected = bypass
|
|
196
223
|
? readRuntimeVersion(loadRuntime)
|
|
197
224
|
: memoizedPersistedRuntimeVersion(loadRuntime, options.now ?? Date.now());
|
|
198
|
-
return selected
|
|
225
|
+
return raisedToGatedFloor(selected);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The gated floor as a lower bound rather than a fallback.
|
|
230
|
+
*
|
|
231
|
+
* Applied only on the way out of the resolver. `readRuntimeVersion` and
|
|
232
|
+
* `memoizedPersistedRuntimeVersion` keep reporting what is actually on disk, because
|
|
233
|
+
* `selectedVersion` is probe evidence that runtime identity, catalog cache keys,
|
|
234
|
+
* `X-Codex-Version` and install provenance all read for their own reasons. Clamping the
|
|
235
|
+
* persisted value itself would corrupt every one of them to fix one question.
|
|
236
|
+
*/
|
|
237
|
+
function raisedToGatedFloor(selected: string | null): string {
|
|
238
|
+
if (selected === null) return GATED_MODEL_CLIENT_VERSION_FLOOR;
|
|
239
|
+
return compareClientVersions(selected, GATED_MODEL_CLIENT_VERSION_FLOOR) >= 0
|
|
240
|
+
? selected
|
|
241
|
+
: GATED_MODEL_CLIENT_VERSION_FLOOR;
|
|
199
242
|
}
|
|
200
243
|
const MODEL_ROSTER_TTL_MS = 5 * 60_000;
|
|
201
244
|
|
|
@@ -575,6 +618,13 @@ async function fetchAccountModels(
|
|
|
575
618
|
return unconfirmedAccountModels(credential, clientVersion, now, { kind: "parsed-empty" });
|
|
576
619
|
}
|
|
577
620
|
const hasUnknownGatedAbsence = [...ACCOUNT_GATED_NATIVE_MODEL_MINIMUM_CLIENT_VERSIONS]
|
|
621
|
+
// Reachable only through tier 1, an inbound client_version below the floor. Every other
|
|
622
|
+
// resolution is now structurally >= every recorded minimum, because the floor is the max
|
|
623
|
+
// of the derived value and the same measured constant these minimums hold. So this is the
|
|
624
|
+
// escape hatch for a self-declared old client, not live protection on the background path:
|
|
625
|
+
// there, an absence really was asked for at an adequate version and is a denial. If
|
|
626
|
+
// upstream ever raises its true requirement above the measured constant, that constant is
|
|
627
|
+
// the only thing standing between an entitled account and a five-minute cached denial.
|
|
578
628
|
.some(([modelId, minimum]) => (
|
|
579
629
|
!models.has(modelId) && compareClientVersions(clientVersion, minimum) < 0
|
|
580
630
|
));
|
|
@@ -32,6 +32,8 @@ interface TomlDocument {
|
|
|
32
32
|
sections: Map<string, Record<string, string>>;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
type TomlMultilineDelimiter = '"""' | "'''";
|
|
36
|
+
|
|
35
37
|
let diagnosticsCache: { at: number; warnings: ProjectCodexConfigWarning[] } | null = null;
|
|
36
38
|
|
|
37
39
|
function hasInjectedOpenaiBaseUrl(content: string): boolean {
|
|
@@ -55,14 +57,102 @@ function parseTomlString(raw: string): string {
|
|
|
55
57
|
return raw.slice(1, -1);
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
function multilineCloseIndex(
|
|
61
|
+
line: string,
|
|
62
|
+
delimiter: TomlMultilineDelimiter,
|
|
63
|
+
from: number,
|
|
64
|
+
): number {
|
|
65
|
+
let index = line.indexOf(delimiter, from);
|
|
66
|
+
while (index >= 0 && delimiter === '"""') {
|
|
67
|
+
let backslashes = 0;
|
|
68
|
+
for (let cursor = index - 1; cursor >= 0 && line[cursor] === "\\"; cursor -= 1) {
|
|
69
|
+
backslashes += 1;
|
|
70
|
+
}
|
|
71
|
+
if (backslashes % 2 === 0) break;
|
|
72
|
+
index = line.indexOf(delimiter, index + delimiter.length);
|
|
73
|
+
}
|
|
74
|
+
return index;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Find a multiline TOML string that starts outside a comment or single-line string.
|
|
79
|
+
*
|
|
80
|
+
* This parser intentionally understands only the root/table subset needed by Codex
|
|
81
|
+
* diagnostics. It still has to skip multiline string bodies lexically: prose in
|
|
82
|
+
* `developer_instructions` can contain key-shaped examples or `[table]` snippets, and
|
|
83
|
+
* treating those examples as configuration changes the diagnostic's meaning.
|
|
84
|
+
*/
|
|
85
|
+
function multilineStateAfterLine(
|
|
86
|
+
line: string,
|
|
87
|
+
active: TomlMultilineDelimiter | null,
|
|
88
|
+
): { active: TomlMultilineDelimiter | null; consumed: boolean } {
|
|
89
|
+
if (active) {
|
|
90
|
+
const close = multilineCloseIndex(line, active, 0);
|
|
91
|
+
if (close < 0) return { active, consumed: true };
|
|
92
|
+
const tail = multilineStateAfterLine(line.slice(close + active.length), null);
|
|
93
|
+
return {
|
|
94
|
+
active: tail.active,
|
|
95
|
+
consumed: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let quote: '"' | "'" | null = null;
|
|
100
|
+
let escaped = false;
|
|
101
|
+
let consumed = false;
|
|
102
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
103
|
+
const character = line[index]!;
|
|
104
|
+
if (quote === '"') {
|
|
105
|
+
if (escaped) escaped = false;
|
|
106
|
+
else if (character === "\\") escaped = true;
|
|
107
|
+
else if (character === quote) quote = null;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (quote === "'") {
|
|
111
|
+
if (character === quote) quote = null;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (character === "#") return { active: null, consumed };
|
|
115
|
+
|
|
116
|
+
const delimiter: TomlMultilineDelimiter | null = line.startsWith('"""', index)
|
|
117
|
+
? '"""'
|
|
118
|
+
: line.startsWith("'''", index)
|
|
119
|
+
? "'''"
|
|
120
|
+
: null;
|
|
121
|
+
if (delimiter) {
|
|
122
|
+
consumed = true;
|
|
123
|
+
const close = multilineCloseIndex(line, delimiter, index + delimiter.length);
|
|
124
|
+
if (close < 0) return { active: delimiter, consumed: true };
|
|
125
|
+
index = close + delimiter.length - 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (character === '"' || character === "'") quote = character;
|
|
129
|
+
}
|
|
130
|
+
return { active: null, consumed };
|
|
131
|
+
}
|
|
132
|
+
|
|
58
133
|
/** Lightweight TOML parse for root keys and [section] tables (Codex config shape). */
|
|
59
134
|
export function parseTomlDocument(content: string): TomlDocument {
|
|
60
135
|
const root: Record<string, string> = {};
|
|
61
136
|
const sections = new Map<string, Record<string, string>>();
|
|
62
137
|
let current = root;
|
|
138
|
+
let multiline: TomlMultilineDelimiter | null = null;
|
|
63
139
|
|
|
64
140
|
for (const line of content.split("\n")) {
|
|
65
|
-
const
|
|
141
|
+
const wasMultiline = multiline !== null;
|
|
142
|
+
const multilineState = multilineStateAfterLine(line, multiline);
|
|
143
|
+
multiline = multilineState.active;
|
|
144
|
+
if (multilineState.consumed) {
|
|
145
|
+
// The declaration itself is still configuration even though its multiline VALUE must
|
|
146
|
+
// not be scanned as keys/tables. A legacy root key using a multiline value therefore
|
|
147
|
+
// remains visible to strict-config diagnostics; only the body is opaque.
|
|
148
|
+
if (!wasMultiline) {
|
|
149
|
+
const declaration = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*(?:"""|''')/);
|
|
150
|
+
if (declaration) current[declaration[1]!] = "";
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const table = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
|
|
66
156
|
if (table) {
|
|
67
157
|
const name = table[1]!.trim();
|
|
68
158
|
const section = sections.get(name) ?? {};
|
|
@@ -422,4 +512,4 @@ export function printProjectCodexConfigWarnings(
|
|
|
422
512
|
}
|
|
423
513
|
}
|
|
424
514
|
return warnings;
|
|
425
|
-
}
|
|
515
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Character policy — see the header. Defined over Unicode SCALAR VALUES, not
|
|
3
|
+
// UTF-16 code units, because a lone surrogate is not a scalar value and UTF-8
|
|
4
|
+
// encoding would silently substitute U+FFFD.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export interface CharacterFinding {
|
|
8
|
+
/** code-point index, consistent across module, route and editor */
|
|
9
|
+
position: number;
|
|
10
|
+
reason: "control" | "unpaired-surrogate";
|
|
11
|
+
codePoint: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Tab to four spaces, CRLF and lone CR to LF. Applied BEFORE validation. */
|
|
15
|
+
export function normalizeBody(body: string): string {
|
|
16
|
+
return body.replace(/\r\n?/g, "\n").replace(/\t/g, " ");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** First offending scalar, or null. Run AFTER normalizeBody. */
|
|
20
|
+
export function findInvalidCharacter(body: string): CharacterFinding | null {
|
|
21
|
+
let position = 0;
|
|
22
|
+
for (let i = 0; i < body.length; ) {
|
|
23
|
+
const code = body.codePointAt(i)!;
|
|
24
|
+
const unit = body.charCodeAt(i);
|
|
25
|
+
const isHighSurrogate = unit >= 0xd800 && unit <= 0xdbff;
|
|
26
|
+
const isLowSurrogate = unit >= 0xdc00 && unit <= 0xdfff;
|
|
27
|
+
// codePointAt only combines a well-formed pair, so a surviving surrogate
|
|
28
|
+
// code point here is unpaired by construction.
|
|
29
|
+
if ((isHighSurrogate || isLowSurrogate) && code === unit) {
|
|
30
|
+
return { position, reason: "unpaired-surrogate", codePoint: code };
|
|
31
|
+
}
|
|
32
|
+
const isNewline = code === 0x0a;
|
|
33
|
+
const isC0 = code < 0x20 && !isNewline;
|
|
34
|
+
const isDel = code === 0x7f;
|
|
35
|
+
const isC1 = code >= 0x80 && code <= 0x9f;
|
|
36
|
+
if (isC0 || isDel || isC1) {
|
|
37
|
+
return { position, reason: "control", codePoint: code };
|
|
38
|
+
}
|
|
39
|
+
i += code > 0xffff ? 2 : 1;
|
|
40
|
+
position += 1;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* TOML basic-string encoding, total over the accepted set: three rules, none of
|
|
47
|
+
* them in the range where `Bun.TOML.parse` misbehaves. `\r` cannot appear
|
|
48
|
+
* because normalizeBody removed it; control characters cannot appear because
|
|
49
|
+
* findInvalidCharacter rejected them.
|
|
50
|
+
*/
|
|
51
|
+
export function encodeBasicString(body: string): string {
|
|
52
|
+
return `"${body.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Inverse of `encodeBasicString`, deliberately narrow: it accepts ONLY the three
|
|
57
|
+
* escapes we emit. `\t`, `\f`, `\b`, `\r` and `\uXXXX` are refused rather than
|
|
58
|
+
* guessed — decoding them correctly is exactly the ambiguity the restricted set
|
|
59
|
+
* exists to avoid.
|
|
60
|
+
*/
|
|
61
|
+
export function decodeBasicString(literal: string): string | null {
|
|
62
|
+
if (literal.length < 2 || !literal.startsWith('"') || !literal.endsWith('"')) return null;
|
|
63
|
+
const inner = literal.slice(1, -1);
|
|
64
|
+
let out = "";
|
|
65
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
66
|
+
const ch = inner[i]!;
|
|
67
|
+
if (ch !== "\\") {
|
|
68
|
+
if (ch === '"') return null; // unescaped quote: not a single literal
|
|
69
|
+
out += ch;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const next = inner[i + 1];
|
|
73
|
+
if (next === "\\") out += "\\";
|
|
74
|
+
else if (next === '"') out += '"';
|
|
75
|
+
else if (next === "n") out += "\n";
|
|
76
|
+
else return null; // any other escape is outside what we will decode
|
|
77
|
+
i += 1;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { expandUserPath } from "../../config";
|
|
4
|
+
import { CODEX_CONFIG_PATH } from "../paths";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Paths
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface Paths {
|
|
11
|
+
configPath?: string;
|
|
12
|
+
storePath?: string;
|
|
13
|
+
baseVariantDir?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function activeCodexHome(): string {
|
|
17
|
+
const raw = process.env.CODEX_HOME?.trim();
|
|
18
|
+
if (!raw) return CODEX_CONFIG_PATH.slice(0, -"/config.toml".length);
|
|
19
|
+
const path = resolve(expandUserPath(raw));
|
|
20
|
+
try {
|
|
21
|
+
return realpathSync.native(path);
|
|
22
|
+
} catch {
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function activeConfigPath(opts?: Paths): string {
|
|
28
|
+
return opts?.configPath ?? join(activeCodexHome(), "config.toml");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function activeStorePath(opts?: Paths): string {
|
|
32
|
+
return opts?.storePath ?? join(activeCodexHome(), "opencodex-prompt.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Where authored base-prompt variants live, one markdown file per variant.
|
|
37
|
+
*
|
|
38
|
+
* A directory of real files rather than another JSON store, because
|
|
39
|
+
* `model_instructions_file` points Codex at a path it reads directly. Embedding the
|
|
40
|
+
* bodies in `opencodex-prompt.json` would mean materialising a temp file at selection
|
|
41
|
+
* time, which is a second write path for no gain.
|
|
42
|
+
*/
|
|
43
|
+
export function activeBaseVariantDir(opts?: Paths): string {
|
|
44
|
+
return opts?.baseVariantDir ?? join(activeCodexHome(), "opencodex-prompt-base");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export function journalPathFor(storePath: string): string {
|
|
49
|
+
return `${storePath.replace(/\.json$/, "")}.journal`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function lockPathFor(storePath: string): string {
|
|
53
|
+
return `${storePath.replace(/\.json$/, "")}.lock`;
|
|
54
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { createHash, type Hash } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Feed one named field into a fingerprint, framed so that no two distinct states
|
|
6
|
+
* can produce the same digest.
|
|
7
|
+
*
|
|
8
|
+
* Framing is the whole point. Concatenating `name + ":" + contents` is ambiguous:
|
|
9
|
+
* an adversarial review of the first version of this function showed that
|
|
10
|
+
* `{override: "left", agents: "right\nAGENTS.md:tail"}` and
|
|
11
|
+
* `{override: "left\nAGENTS.md:right", agents: "tail"}` hashed identically, because
|
|
12
|
+
* a file's own bytes can imitate the separator that follows it. That is exactly a
|
|
13
|
+
* missed invalidation: the fingerprint is the probe's admission key, so two
|
|
14
|
+
* different prompt states sharing a digest means one caller is served the other's
|
|
15
|
+
* stale text.
|
|
16
|
+
*
|
|
17
|
+
* A byte length cannot be forged by content, so each field carries one. Absence is
|
|
18
|
+
* a length of -1 rather than a sentinel string, because a sentinel is just more
|
|
19
|
+
* content: the same review found that `null` collided with a file whose bytes were
|
|
20
|
+
* literally NUL + "absent".
|
|
21
|
+
*/
|
|
22
|
+
export function updateFingerprintField(hash: Hash, name: string, contents: string | null): void {
|
|
23
|
+
const bytes = contents === null ? -1 : Buffer.byteLength(contents, "utf8");
|
|
24
|
+
hash.update(`\n${name}:${bytes}:`);
|
|
25
|
+
if (contents !== null) hash.update(contents);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Byte-level hashing. The revision covers COMPLETE file bytes plus existence,
|
|
31
|
+
// so removing the marker while leaving the value intact still changes it.
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export function readFileOrNull(path: string): string | null {
|
|
35
|
+
try {
|
|
36
|
+
if (!existsSync(path)) return null;
|
|
37
|
+
return readFileSync(path, "utf8");
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function computeRevision(configBytes: string | null, storeBytes: string | null): string {
|
|
44
|
+
const hash = createHash("sha256");
|
|
45
|
+
// Length-framed for the reason given on updateFingerprintField: with a bare
|
|
46
|
+
// separator, config bytes ending in "\nstore:" shift the boundary and two
|
|
47
|
+
// different pairs hash alike. That matters twice over — this value is both the
|
|
48
|
+
// probe's admission input and the optimistic-concurrency token compared in
|
|
49
|
+
// commit(), where a collision would let a write built on stale bytes through.
|
|
50
|
+
updateFingerprintField(hash, "cfg", configBytes);
|
|
51
|
+
updateFingerprintField(hash, "store", storeBytes);
|
|
52
|
+
return `sha256:${hash.digest("hex")}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { readFileOrNull as readFileBytes };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { OCX_SECTION_MARKER } from "../injected-marker";
|
|
2
|
+
import { encodeBasicString } from "./encoding";
|
|
3
|
+
import { TABLE_HEADER, ANY_DEV_INSTRUCTIONS, DEV_INSTRUCTIONS_KEY } from "./toml-read";
|
|
4
|
+
|
|
5
|
+
/** Line editing, not re-serialization: the user's comments and layout survive. */
|
|
6
|
+
function dominantEol(content: string): "\r\n" | "\n" {
|
|
7
|
+
const crlf = (content.match(/\r\n/g) ?? []).length;
|
|
8
|
+
if (crlf === 0) return "\n";
|
|
9
|
+
const bareLf = (content.match(/\n/g) ?? []).length - crlf;
|
|
10
|
+
return crlf >= bareLf ? "\r\n" : "\n";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function splitLines(content: string): string[] {
|
|
14
|
+
return content.replace(/\r\n/g, "\n").split("\n");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A leading UTF-8 BOM, split off so line editing never steps over it.
|
|
19
|
+
*
|
|
20
|
+
* Codex reads config.toml with Rust `toml_edit`, which accepts a BOM at byte 0 and
|
|
21
|
+
* nowhere else. Inserting the generated block at line index 0 pushed the BOM down
|
|
22
|
+
* to byte 58, the write reported success because our own byte comparison matched
|
|
23
|
+
* what we intended to write, and the next parse failed with
|
|
24
|
+
* "Expected a key but found (0xEF)" — a config file the user could no longer load,
|
|
25
|
+
* produced by a write that told them it worked.
|
|
26
|
+
*
|
|
27
|
+
* Editors on Windows write this byte routinely, so the file is not exotic.
|
|
28
|
+
*/
|
|
29
|
+
function splitBom(content: string): { bom: string; body: string } {
|
|
30
|
+
return content.startsWith("\ufeff")
|
|
31
|
+
? { bom: "\ufeff", body: content.slice(1) }
|
|
32
|
+
: { bom: "", body: content };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function joinLines(lines: string[], eol: "\r\n" | "\n"): string {
|
|
36
|
+
const text = lines.join("\n");
|
|
37
|
+
return eol === "\n" ? text : text.replace(/\n/g, "\r\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function firstTableIndex(lines: string[]): number {
|
|
41
|
+
const idx = lines.findIndex(l => TABLE_HEADER.test(l));
|
|
42
|
+
return idx === -1 ? lines.length : idx;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Set a root-scope boolean, inserting above the first table when absent. */
|
|
46
|
+
export function setRootBool(content: string, key: string, value: boolean): string {
|
|
47
|
+
const eol = dominantEol(content);
|
|
48
|
+
const { bom, body } = splitBom(content);
|
|
49
|
+
const lines = splitLines(body);
|
|
50
|
+
const limit = firstTableIndex(lines);
|
|
51
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52
|
+
const pattern = new RegExp(`^(\\s*${escaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`);
|
|
53
|
+
for (let i = 0; i < limit; i += 1) {
|
|
54
|
+
const m = pattern.exec(lines[i]!);
|
|
55
|
+
if (m) {
|
|
56
|
+
lines[i] = `${m[1]}${value}${m[2]}`;
|
|
57
|
+
return bom + joinLines(lines, eol);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
lines.splice(limit, 0, `${key} = ${value}`);
|
|
61
|
+
return bom + joinLines(lines, eol);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Set or REMOVE a root-scope basic string. `null` removes the key.
|
|
66
|
+
*
|
|
67
|
+
* Removal is what selecting the default variant does, and it has to be a real deletion
|
|
68
|
+
* rather than an empty string: `model_instructions_file = ""` is a path Codex would try
|
|
69
|
+
* to read, not an absent setting.
|
|
70
|
+
*/
|
|
71
|
+
export function setRootString(content: string, key: string, value: string | null): string {
|
|
72
|
+
const eol = dominantEol(content);
|
|
73
|
+
const { bom, body } = splitBom(content);
|
|
74
|
+
const lines = splitLines(body);
|
|
75
|
+
const limit = firstTableIndex(lines);
|
|
76
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
77
|
+
const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*"[^"]*"\\s*(?:#.*)?$`);
|
|
78
|
+
for (let i = 0; i < limit; i += 1) {
|
|
79
|
+
if (!pattern.test(lines[i]!)) continue;
|
|
80
|
+
if (value === null) lines.splice(i, 1);
|
|
81
|
+
else lines[i] = `${key} = ${encodeBasicString(value)}`;
|
|
82
|
+
return bom + joinLines(lines, eol);
|
|
83
|
+
}
|
|
84
|
+
if (value === null) return bom + joinLines(lines, eol);
|
|
85
|
+
lines.splice(limit, 0, `${key} = ${encodeBasicString(value)}`);
|
|
86
|
+
return bom + joinLines(lines, eol);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Set a boolean inside `[table]`, appending the table when absent. */
|
|
90
|
+
export function setTableBool(content: string, table: string, key: string, value: boolean): string {
|
|
91
|
+
const eol = dominantEol(content);
|
|
92
|
+
const { bom, body } = splitBom(content);
|
|
93
|
+
const lines = splitLines(body);
|
|
94
|
+
const escaped = table.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
95
|
+
const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l));
|
|
96
|
+
if (start === -1) {
|
|
97
|
+
const tail = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
|
98
|
+
lines.splice(tail, 0, `[${table}]`, `${key} = ${value}`);
|
|
99
|
+
return bom + joinLines(lines, eol);
|
|
100
|
+
}
|
|
101
|
+
const keyEscaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
102
|
+
const pattern = new RegExp(`^(\\s*${keyEscaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`);
|
|
103
|
+
let end = start + 1;
|
|
104
|
+
while (end < lines.length && !TABLE_HEADER.test(lines[end]!)) end += 1;
|
|
105
|
+
for (let i = start + 1; i < end; i += 1) {
|
|
106
|
+
const m = pattern.exec(lines[i]!);
|
|
107
|
+
if (m) {
|
|
108
|
+
lines[i] = `${m[1]}${value}${m[2]}`;
|
|
109
|
+
return bom + joinLines(lines, eol);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
lines.splice(end, 0, `${key} = ${value}`);
|
|
113
|
+
return bom + joinLines(lines, eol);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Replace, insert, or remove the generated two-line block. Canonical form is
|
|
118
|
+
* marker + assignment at the top of the document; replacement is "find the
|
|
119
|
+
* marker, replace the next line" rather than a span search.
|
|
120
|
+
*/
|
|
121
|
+
export function setProjection(content: string | null, projection: string | null): string {
|
|
122
|
+
const base = content ?? "";
|
|
123
|
+
const eol = dominantEol(base);
|
|
124
|
+
// The BOM is held aside for the whole edit. This is the function that produced
|
|
125
|
+
// the corruption: the insert below is at index 0, which put the marker line
|
|
126
|
+
// ahead of a byte that is only legal at byte 0.
|
|
127
|
+
const { bom, body } = splitBom(base);
|
|
128
|
+
const lines = splitLines(body);
|
|
129
|
+
const limit = firstTableIndex(lines);
|
|
130
|
+
|
|
131
|
+
let markerAt = -1;
|
|
132
|
+
for (let i = 0; i < limit; i += 1) {
|
|
133
|
+
if (i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER) && ANY_DEV_INSTRUCTIONS.test(lines[i]!)) {
|
|
134
|
+
markerAt = i - 1;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (markerAt !== -1) {
|
|
140
|
+
if (projection === null) lines.splice(markerAt, 2);
|
|
141
|
+
else lines[markerAt + 1] = `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`;
|
|
142
|
+
return bom + joinLines(lines, eol);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (projection === null) return bom + joinLines(lines, eol);
|
|
146
|
+
lines.splice(0, 0, OCX_SECTION_MARKER, `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`);
|
|
147
|
+
return bom + joinLines(lines, eol);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
/** Remove an unowned or reshaped `developer_instructions` from the root scope. */
|
|
152
|
+
export function removeUnownedProjection(content: string): string {
|
|
153
|
+
const eol = dominantEol(content);
|
|
154
|
+
const lines = splitLines(content);
|
|
155
|
+
const limit = firstTableIndex(lines);
|
|
156
|
+
for (let i = 0; i < limit; i += 1) {
|
|
157
|
+
if (!ANY_DEV_INSTRUCTIONS.test(lines[i]!)) continue;
|
|
158
|
+
const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER);
|
|
159
|
+
lines.splice(marked ? i - 1 : i, marked ? 2 : 1);
|
|
160
|
+
return joinLines(lines, eol);
|
|
161
|
+
}
|
|
162
|
+
return joinLines(lines, eol);
|
|
163
|
+
}
|