nexus-ai-pro 1.24.0 → 1.25.0
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/API_STABILITY.md +15 -0
- package/CHANGELOG.md +46 -1
- package/MIGRATING.md +169 -0
- package/README.md +6 -2
- package/dist/capabilities/negotiate.d.ts +1 -0
- package/dist/cli/args.js +2 -0
- package/dist/cli/migrate.d.ts +52 -0
- package/dist/cli/migrate.js +224 -0
- package/dist/cli/root-moves.d.ts +5 -0
- package/dist/cli/root-moves.js +501 -0
- package/dist/cli.js +8 -1
- package/dist/graph/checkpoint-migration.d.ts +24 -0
- package/dist/graph/checkpoint-migration.js +12 -0
- package/dist/graph/index.d.ts +1 -0
- package/dist/graph/index.js +1 -0
- package/dist/hallucination/index.d.ts +10 -0
- package/dist/hallucination/index.js +5 -0
- package/dist/index.d.ts +1086 -88
- package/dist/index.js +30 -30
- package/dist/models/registry.d.ts +11 -1
- package/dist/models/registry.js +8 -0
- package/dist/next/route-handler.d.ts +3 -0
- package/dist/ops/index.d.ts +12 -0
- package/dist/ops/index.js +6 -0
- package/dist/optimizer/index.d.ts +1 -0
- package/dist/optimizer/index.js +1 -0
- package/dist/pipeline/index.d.ts +7 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/router/auto-router.js +10 -1
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.js +1 -0
- package/dist/server/tenancy.d.ts +1 -1
- package/dist/server/tenancy.js +3 -2
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.js +3 -0
- package/dist/types/config.d.ts +10 -1
- package/dist/types/graph.d.ts +6 -1
- package/dist/types/providers.d.ts +17 -2
- package/dist/utils/safe-fetch.d.ts +1 -0
- package/dist-cjs/capabilities/negotiate.d.ts +1 -0
- package/dist-cjs/cli/args.js +2 -0
- package/dist-cjs/cli/migrate.d.ts +52 -0
- package/dist-cjs/cli/migrate.js +232 -0
- package/dist-cjs/cli/root-moves.d.ts +5 -0
- package/dist-cjs/cli/root-moves.js +504 -0
- package/dist-cjs/graph/checkpoint-migration.d.ts +24 -0
- package/dist-cjs/graph/checkpoint-migration.js +15 -0
- package/dist-cjs/graph/index.d.ts +1 -0
- package/dist-cjs/graph/index.js +3 -1
- package/dist-cjs/hallucination/index.d.ts +10 -0
- package/dist-cjs/hallucination/index.js +22 -0
- package/dist-cjs/index.d.ts +1086 -88
- package/dist-cjs/models/registry.d.ts +11 -1
- package/dist-cjs/models/registry.js +15 -0
- package/dist-cjs/next/route-handler.d.ts +3 -0
- package/dist-cjs/ops/index.d.ts +12 -0
- package/dist-cjs/ops/index.js +18 -0
- package/dist-cjs/optimizer/index.d.ts +1 -0
- package/dist-cjs/optimizer/index.js +3 -1
- package/dist-cjs/pipeline/index.d.ts +7 -0
- package/dist-cjs/pipeline/index.js +6 -0
- package/dist-cjs/router/auto-router.js +9 -0
- package/dist-cjs/security/index.d.ts +1 -0
- package/dist-cjs/security/index.js +3 -1
- package/dist-cjs/server/tenancy.d.ts +1 -1
- package/dist-cjs/server/tenancy.js +3 -2
- package/dist-cjs/testing/index.d.ts +8 -0
- package/dist-cjs/testing/index.js +12 -0
- package/dist-cjs/types/config.d.ts +10 -1
- package/dist-cjs/types/graph.d.ts +6 -1
- package/dist-cjs/types/providers.d.ts +17 -2
- package/dist-cjs/utils/safe-fetch.d.ts +1 -0
- package/package.json +120 -2
package/API_STABILITY.md
CHANGED
|
@@ -457,6 +457,21 @@ with nexus-ai-pro and declares the core versions it works with as a peer depende
|
|
|
457
457
|
- The access rules are a guarantee: loopback binding by default, a token on every request, the token
|
|
458
458
|
in a header for every change, and loopback hosts only unless others are allowed.
|
|
459
459
|
|
|
460
|
+
## 2.0 preparation stage (1.25.0)
|
|
461
|
+
|
|
462
|
+
The last 1.x minor deprecates what 2.0 removes. Deprecated APIs keep working, unchanged, for the rest
|
|
463
|
+
of the 1.x line.
|
|
464
|
+
|
|
465
|
+
- The 2.0 root import keeps the core client, its config builders, the types of its configuration,
|
|
466
|
+
requests, and responses, and the errors it throws. Every other root export is deprecated in favour of
|
|
467
|
+
the subpath that already provides it, under the same name or, where noted, another.
|
|
468
|
+
- Every deprecated option warns once per process through Node's `DeprecationWarning` channel, so
|
|
469
|
+
`--throw-deprecation` finds them in a test run. Deprecated exports and fields are marked in the type
|
|
470
|
+
declarations, where editors show them.
|
|
471
|
+
- `nexus migrate` and its map follow the root's deprecations; both are generated from the export map,
|
|
472
|
+
so they cannot disagree.
|
|
473
|
+
- A 1.x checkpoint stays readable throughout 2.x, by both checkpointers and by `migrateCheckpoint()`.
|
|
474
|
+
|
|
460
475
|
## Deployment stage (1.24.0)
|
|
461
476
|
|
|
462
477
|
Deployments (`nexus-ai-pro/server/deployments`), tenant limits (`nexus-ai-pro/server/tenancy`), and the
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,50 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.25.0] - 2026-10-01
|
|
8
|
+
|
|
9
|
+
The bridge to 2.0. Everything 2.0 removes is deprecated now, every capability has a subpath of its
|
|
10
|
+
own, and the tools to migrate ship with it, so a codebase can move while it is still on 1.x.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **A subpath for every capability.** What could only be imported from the root has an entry point
|
|
15
|
+
of its own, each with no third-party dependency: `nexus-ai-pro/grounding` (grounded prompts,
|
|
16
|
+
verification, self-consistency, knowledge graphs), `/connectors` (web tools), `/ops` (metrics,
|
|
17
|
+
health, audit, OpenTelemetry export, rate limiting), `/pipeline`, `/router`, `/testing`
|
|
18
|
+
(conformance suites), `/next` (the route handler), `/rag/files` (scanned file ingestion), and
|
|
19
|
+
`/optimizer/cost` (cost estimates and budgets). Grounding alone is 15 KB, where it used to mean the
|
|
20
|
+
584 KB root and its validators. `hardenPrompt()` joins `/security`, `Tokenizer` joins `/optimizer`, the
|
|
21
|
+
registry's data joins `/models`, and the capability types join `/capabilities`.
|
|
22
|
+
- **`nexus migrate`**, a codemod that moves imports to the subpaths the 2.0 root keeps them on. It
|
|
23
|
+
rewrites `import`, `import type`, `export … from`, and destructured `require()` in TypeScript,
|
|
24
|
+
JavaScript, and Markdown code blocks, keeps local names when a name moved under another, and lists
|
|
25
|
+
what it cannot rewrite safely. `--check` fails a CI job while anything is left.
|
|
26
|
+
- **`MIGRATING.md`**, shipped in the package: every change 2.0 makes, with before-and-after code.
|
|
27
|
+
- **Modalities with a direction**: `inputModalities` and `outputModalities` on a model's capabilities,
|
|
28
|
+
`modalitiesOf()` on `nexus-ai-pro/models` to read either shape, and both as routing requirements.
|
|
29
|
+
- **The 2.0 checkpoint schema, readable now**: `migrateCheckpoint()` and `GraphCheckpointV2` on
|
|
30
|
+
`nexus-ai-pro/graph`.
|
|
31
|
+
|
|
32
|
+
### Deprecated
|
|
33
|
+
|
|
34
|
+
- 499 exports of the root import, each in favour of the subpath that already provides it. 2.0 keeps
|
|
35
|
+
only the core client, its config builders, its types, and the errors it throws on the root. Editors
|
|
36
|
+
strike a deprecated import through and name the subpath; `nexus migrate` moves them.
|
|
37
|
+
- `ModelCapabilities.modalities` and `routing.requiredCapabilities.modalities`, for the directional
|
|
38
|
+
fields. The routing requirement warns once per process.
|
|
39
|
+
- `GraphCheckpoint.interrupt`, for `interrupts`.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- The guides' examples import from subpaths, as 2.0 requires.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- The agents guide pointed at `nexus-ai-pro/connectors`, which did not exist. It does now.
|
|
48
|
+
- `tenantLimits({ now })` drives the default usage and rate stores too. They kept the system clock, so
|
|
49
|
+
a budget judged on a replaced clock reset when the real day ended.
|
|
50
|
+
|
|
7
51
|
## [1.24.0] - 2026-09-30
|
|
8
52
|
|
|
9
53
|
Deployment at scale, self-managed. Running assistants as a service without a hosted platform:
|
|
@@ -1211,7 +1255,8 @@ shared policy vocabulary is in place for a future release that revisits this.
|
|
|
1211
1255
|
- URL fetching rejects unsafe private-network targets and limits response reads.
|
|
1212
1256
|
- CLI and audit findings no longer disclose detected secret values.
|
|
1213
1257
|
|
|
1214
|
-
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.
|
|
1258
|
+
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.25.0...HEAD
|
|
1259
|
+
[1.25.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.24.0...v1.25.0
|
|
1215
1260
|
[1.24.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.23.0...v1.24.0
|
|
1216
1261
|
[1.23.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.22.0...v1.23.0
|
|
1217
1262
|
[1.22.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.21.0...v1.22.0
|
package/MIGRATING.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Migrating to 2.0
|
|
2
|
+
|
|
3
|
+
2.0.0 removes what the 1.x line deprecated. Everything you need to get ready ships in 1.25, so you can
|
|
4
|
+
migrate while you are still on 1.x. Nothing below breaks before 2.0.
|
|
5
|
+
|
|
6
|
+
Start with the codemod. It rewrites what it can safely, and lists the rest:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx nexus migrate src # reports what would change
|
|
10
|
+
npx nexus migrate src --write # rewrites the files
|
|
11
|
+
npx nexus migrate src --check # exits 1 while anything is left, for CI
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then run your tests with `node --throw-deprecation`. Every deprecated option warns once per process,
|
|
15
|
+
so a clean run means nothing you call is going away.
|
|
16
|
+
|
|
17
|
+
| Change | The codemod | By hand |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| [The root import keeps only the core](#the-root-import-keeps-only-the-core) | Rewrites the imports | Namespace and dynamic imports of the root |
|
|
20
|
+
| [`estimatedCost` is removed](#a-responses-estimatedcost-is-removed) | Lists each read | Read `cost.amount` |
|
|
21
|
+
| [`ImageManagerConfig` is removed](#imagemanagerconfig-is-removed) | Renames it | — |
|
|
22
|
+
| [Options that were never read are removed](#options-that-were-never-read-are-removed) | — | Delete them; each warns today |
|
|
23
|
+
| [Modalities say which way they flow](#modalities-say-which-way-they-flow) | — | Set the two new fields |
|
|
24
|
+
| [Checkpoints gain ids and lose `interrupt`](#checkpoints-gain-ids-and-lose-interrupt) | — | Read `interrupts` |
|
|
25
|
+
| [Validators and Node types become optional](#validators-and-node-types-become-optional) | — | Install what you use |
|
|
26
|
+
| [Planned, not yet final](#planned-not-yet-final) | — | Nothing yet |
|
|
27
|
+
|
|
28
|
+
## The root import keeps only the core
|
|
29
|
+
|
|
30
|
+
In 2.0, `nexus-ai-pro` exports the core client and nothing else. That is:
|
|
31
|
+
|
|
32
|
+
- `NexusAI`, `createNexus`, and the config builders;
|
|
33
|
+
- `tool()`, the streaming helpers, and the usage and pricing helpers for custom providers;
|
|
34
|
+
- the types of the client's configuration, requests, and responses;
|
|
35
|
+
- the errors the client throws: `NexusProviderError`, `NexusSecurityError`, `NexusRateLimitError`,
|
|
36
|
+
`NexusCapabilityError`, `TokenBudgetError`, `CostBudgetError`, and `ResponseFormatError`.
|
|
37
|
+
|
|
38
|
+
Every other export is on its own subpath already. In 1.25 each one is deprecated on the root, and your
|
|
39
|
+
editor strikes it through with the subpath to use.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// Before
|
|
43
|
+
import { NexusAI, MemoryVectorStore, withRagContext, OperationRunner } from 'nexus-ai-pro';
|
|
44
|
+
|
|
45
|
+
// After
|
|
46
|
+
import { NexusAI } from 'nexus-ai-pro';
|
|
47
|
+
import { OperationRunner } from 'nexus-ai-pro/operations';
|
|
48
|
+
import { withRagContext } from 'nexus-ai-pro/grounding';
|
|
49
|
+
import { MemoryVectorStore } from 'nexus-ai-pro/rag';
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The codemod does this for `import`, `import type`, `export … from`, and destructured `require()`. A
|
|
53
|
+
name that moved under another name keeps its local name through an alias:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// Before
|
|
57
|
+
import type { AgentModelClient } from 'nexus-ai-pro';
|
|
58
|
+
// After
|
|
59
|
+
import type { AgentLoopModelClient as AgentModelClient } from 'nexus-ai-pro/agent';
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
It cannot split a namespace import (`import * as nexus from 'nexus-ai-pro'`) or a dynamic `import()`,
|
|
63
|
+
so it lists them. Import each name you use from its subpath instead. Every subpath and what it costs to
|
|
64
|
+
install is in the [packaging guide](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/packaging.md).
|
|
65
|
+
|
|
66
|
+
The smaller root is the point: an application that uses one family no longer loads the client, its
|
|
67
|
+
validators, or the model registry.
|
|
68
|
+
|
|
69
|
+
## A response's `estimatedCost` is removed
|
|
70
|
+
|
|
71
|
+
The formatted string on a response's metadata goes. Read the numeric cost and format it yourself:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// Before
|
|
75
|
+
console.log(response.meta.estimatedCost); // '$0.0012'
|
|
76
|
+
|
|
77
|
+
// After
|
|
78
|
+
const cost = response.meta.cost;
|
|
79
|
+
if (cost) console.log(new Intl.NumberFormat('en-US', { style: 'currency', currency: cost.currency }).format(cost.amount));
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
A plan's `estimatedCost`, from `ai.plan()`, is an object and stays.
|
|
83
|
+
|
|
84
|
+
## `ImageManagerConfig` is removed
|
|
85
|
+
|
|
86
|
+
It was an alias of `ImageConfig`. The codemod renames the import and keeps your local name.
|
|
87
|
+
|
|
88
|
+
## Options that were never read are removed
|
|
89
|
+
|
|
90
|
+
These were accepted and ignored. Each one already warns once per process when it is set.
|
|
91
|
+
|
|
92
|
+
| Option | Deprecated in |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `GoogleProviderConfig.projectId` | 1.16.0 |
|
|
95
|
+
| `OllamaProviderConfig.timeout` | 1.16.0 |
|
|
96
|
+
| `MetricsConfig.prometheus` | 1.16.0 |
|
|
97
|
+
| `DensificationConfig.preserveMarkdown` | 1.16.0 |
|
|
98
|
+
| `HealthConfig.latencyHalfLife` | 1.16.0 |
|
|
99
|
+
| `InjectionDetectionConfig.sensitivity` | 1.19.0 |
|
|
100
|
+
| `ToolPolicyConfig.requiresApproval` | 1.19.0 |
|
|
101
|
+
|
|
102
|
+
Delete them. Nothing changes, because nothing read them.
|
|
103
|
+
|
|
104
|
+
## Modalities say which way they flow
|
|
105
|
+
|
|
106
|
+
A model's `modalities` list mixed inputs and outputs: `vision` meant an image in, and `image` meant an
|
|
107
|
+
image out. 2.0 replaces it with `inputModalities` and `outputModalities`, which 1.25 already reads.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// A model you define, before
|
|
111
|
+
models: { registry: { 'my-model': { modalities: ['text', 'vision'], /* … */ } } }
|
|
112
|
+
|
|
113
|
+
// After: keep modalities until 2.0, and add the two new fields
|
|
114
|
+
models: {
|
|
115
|
+
registry: {
|
|
116
|
+
'my-model': { modalities: ['text', 'vision'], inputModalities: ['text', 'image'], outputModalities: ['text'], /* … */ },
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Read either shape through `modalitiesOf()` from `nexus-ai-pro/models`. In routing requirements, replace
|
|
122
|
+
`modalities` with `inputModalities` or `outputModalities`; the old field warns once.
|
|
123
|
+
|
|
124
|
+
## Checkpoints gain ids and lose `interrupt`
|
|
125
|
+
|
|
126
|
+
The 2.0 checkpoint schema gives every checkpoint an id and its pending tasks, and keeps questions only
|
|
127
|
+
in `interrupts`. Both checkpointers keep reading 1.x checkpoints throughout 2.x, so stored threads
|
|
128
|
+
resume.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// Before
|
|
132
|
+
const question = checkpoint.interrupt;
|
|
133
|
+
|
|
134
|
+
// After
|
|
135
|
+
const [question] = checkpoint.interrupts ?? [];
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
To write code against the 2.0 shape today, read checkpoints through `migrateCheckpoint()` from
|
|
139
|
+
`nexus-ai-pro/graph`, which returns it from either schema.
|
|
140
|
+
|
|
141
|
+
## Validators and Node types become optional
|
|
142
|
+
|
|
143
|
+
`zod`, `ajv`, and `ajv-formats` become optional peer dependencies, and so does `@types/node`. The target is
|
|
144
|
+
that a graph-only application installs at most 3.5 MB, instead of about 10 MB today.
|
|
145
|
+
|
|
146
|
+
If you validate with schemas, or compile TypeScript against Node's types, add what you use:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm install zod ajv ajv-formats
|
|
150
|
+
npm install --save-dev @types/node
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The 2.0 release notes list exactly which calls need each one.
|
|
154
|
+
|
|
155
|
+
## Planned, not yet final
|
|
156
|
+
|
|
157
|
+
These are designed during the 2.0 cycle. This guide gains their before-and-after code when they land.
|
|
158
|
+
|
|
159
|
+
- **One lifecycle for every operation.** Completions, streams, embeddings, voice, images, jobs, graphs,
|
|
160
|
+
and agents share authorization, budgets, hooks, audit, metrics, and tracing. Custom providers receive
|
|
161
|
+
a `ProviderCallContext` with the abort signal, deadline, request id, trace context, and idempotency key.
|
|
162
|
+
- **Typed pipeline contexts.** `PipelineContext` becomes generic over its request and response, and
|
|
163
|
+
`PipelineStepName` lists only the built-in steps.
|
|
164
|
+
- **Mixed outputs.** A tool result can carry asset references instead of base64 JSON.
|
|
165
|
+
|
|
166
|
+
## Runtime
|
|
167
|
+
|
|
168
|
+
2.0 runs on Node 22 and newer. If it ships after Node 22's end of life in April 2027, the floor moves to
|
|
169
|
+
Node 24.
|
package/README.md
CHANGED
|
@@ -224,9 +224,13 @@ Shipped so far:
|
|
|
224
224
|
- a shared studio with accounts, roles, an audit log, and comments; a context hub; insights with
|
|
225
225
|
evaluated fix proposals; and evaluation caching;
|
|
226
226
|
- self-managed deployment at scale: revisions and canaries with automatic rollback, a worker queue
|
|
227
|
-
that autoscales on Kubernetes, and per-tenant limits
|
|
227
|
+
that autoscales on Kubernetes, and per-tenant limits;
|
|
228
|
+
- the bridge to 2.0: every capability on a subpath of its own, everything 2.0 removes deprecated, and
|
|
229
|
+
a codemod that moves your imports.
|
|
228
230
|
|
|
229
|
-
Next: 2.0.0 consolidates.
|
|
231
|
+
Next: 2.0.0 consolidates. It slims the root import to the core client; `npx nexus migrate src --write`
|
|
232
|
+
moves your imports today, and [MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md)
|
|
233
|
+
covers the rest. The image family leaves experimental once recorded live
|
|
230
234
|
conformance passes on all three backends.
|
|
231
235
|
|
|
232
236
|
## Before Production
|
|
@@ -45,3 +45,4 @@ export interface NegotiateOptions {
|
|
|
45
45
|
* declaration means the registry does not know, not that the provider refuses.
|
|
46
46
|
*/
|
|
47
47
|
export declare function negotiateCompletionRequest(request: CompletionRequest, capabilities: ModelCapabilities | undefined, options?: NegotiateOptions): NegotiationResult<CompletionRequest>;
|
|
48
|
+
export type { CapabilityConfig, CapabilityPolicy, CapabilityWarning, CapabilityWarningAction, } from '../types/capabilities.js';
|
package/dist/cli/args.js
CHANGED
|
@@ -3,6 +3,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
3
3
|
export const BOOLEAN_FLAGS = new Set([
|
|
4
4
|
'all',
|
|
5
5
|
'allow-dataset-mismatch',
|
|
6
|
+
'check',
|
|
6
7
|
'densify',
|
|
7
8
|
'fail',
|
|
8
9
|
'fail-on-regression',
|
|
@@ -10,6 +11,7 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
10
11
|
'json',
|
|
11
12
|
'print',
|
|
12
13
|
'reveal-values',
|
|
14
|
+
'write',
|
|
13
15
|
]);
|
|
14
16
|
export function parseArgs(args) {
|
|
15
17
|
const positionals = [];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ParsedArgs } from './args.js';
|
|
2
|
+
/** One thing `migrateSource()` changed, or found and left for a person. */
|
|
3
|
+
export interface MigrationNote {
|
|
4
|
+
/** The line, starting at 1. */
|
|
5
|
+
line: number;
|
|
6
|
+
/** What was done, or what to do. */
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
/** What `migrateSource()` produced for one file's text. */
|
|
10
|
+
export interface MigrationOutcome {
|
|
11
|
+
/** The text after migration; the input itself when nothing changed. */
|
|
12
|
+
text: string;
|
|
13
|
+
/** Imports rewritten and names renamed. */
|
|
14
|
+
changes: MigrationNote[];
|
|
15
|
+
/** What it found and could not rewrite safely, for a person to finish. */
|
|
16
|
+
notes: MigrationNote[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Migrates one file's text to the 2.0 imports: names the root import drops are imported from their
|
|
20
|
+
* subpaths, names that moved under another name keep their local name through an alias, and deprecated
|
|
21
|
+
* aliases are replaced. What cannot be rewritten safely — a namespace import of the root, a dynamic
|
|
22
|
+
* import, a read of `estimatedCost` — is reported instead of guessed at.
|
|
23
|
+
*
|
|
24
|
+
* Markdown is migrated inside its TypeScript and JavaScript code blocks only. Running it twice changes
|
|
25
|
+
* nothing the second time.
|
|
26
|
+
*/
|
|
27
|
+
export declare function migrateSource(text: string, options?: {
|
|
28
|
+
file?: string;
|
|
29
|
+
}): MigrationOutcome;
|
|
30
|
+
/** Options for `migrateFiles()`. */
|
|
31
|
+
export interface MigrateFilesOptions {
|
|
32
|
+
/** Writes the migrated files. Without it, nothing is written and the result says what would change. */
|
|
33
|
+
write?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** What `migrateFiles()` did with one file. */
|
|
36
|
+
export interface FileMigration extends Omit<MigrationOutcome, 'text'> {
|
|
37
|
+
/** The file, as given or found. */
|
|
38
|
+
file: string;
|
|
39
|
+
/** Whether its text changed. */
|
|
40
|
+
changed: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Migrates every source and Markdown file under the given paths, skipping `node_modules`, build
|
|
44
|
+
* output, and version control. Files are written only with `write`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function migrateFiles(paths: readonly string[], options?: MigrateFilesOptions): Promise<FileMigration[]>;
|
|
47
|
+
/**
|
|
48
|
+
* `nexus migrate [paths…] [--write] [--check] [--json]` moves imports of the root to the subpaths the
|
|
49
|
+
* 2.0 root import keeps them on. Without `--write` it only reports; `--check` exits 1 when a file still
|
|
50
|
+
* needs migrating, for a CI job that keeps a codebase ready.
|
|
51
|
+
*/
|
|
52
|
+
export declare function runMigrateCommand({ positionals, flags }: ParsedArgs): Promise<void>;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CliUsageError, flagBool, writeJson } from './args.js';
|
|
4
|
+
import { ROOT_MOVES } from './root-moves.js';
|
|
5
|
+
const RENAMES = { ImageManagerConfig: 'ImageConfig' };
|
|
6
|
+
const PACKAGE = 'nexus-ai-pro';
|
|
7
|
+
const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
|
|
8
|
+
const MARKDOWN_EXTENSIONS = new Set(['.md', '.mdx']);
|
|
9
|
+
const FENCE_LANGUAGES = new Set(['ts', 'tsx', 'typescript', 'js', 'jsx', 'javascript', 'mjs', 'cjs', 'mts', 'cts']);
|
|
10
|
+
const SKIPPED_DIRECTORIES = new Set(['node_modules', '.git', 'dist', 'dist-cjs', 'build', 'coverage', '.next', 'out']);
|
|
11
|
+
export function migrateSource(text, options = {}) {
|
|
12
|
+
const extension = path.extname(options.file ?? '').toLowerCase();
|
|
13
|
+
if (MARKDOWN_EXTENSIONS.has(extension))
|
|
14
|
+
return migrateMarkdown(text);
|
|
15
|
+
return migrateCode(text, 0);
|
|
16
|
+
}
|
|
17
|
+
export async function migrateFiles(paths, options = {}) {
|
|
18
|
+
const results = [];
|
|
19
|
+
for (const file of await collect(paths)) {
|
|
20
|
+
const original = await readFile(file, 'utf8');
|
|
21
|
+
const outcome = migrateSource(original, { file });
|
|
22
|
+
const changed = outcome.text !== original;
|
|
23
|
+
if (changed && options.write)
|
|
24
|
+
await writeFile(file, outcome.text);
|
|
25
|
+
if (changed || outcome.notes.length > 0) {
|
|
26
|
+
results.push({ file, changed, changes: outcome.changes, notes: outcome.notes });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
export async function runMigrateCommand({ positionals, flags }) {
|
|
32
|
+
const write = flagBool(flags, 'write');
|
|
33
|
+
const checking = flagBool(flags, 'check');
|
|
34
|
+
if (write && checking)
|
|
35
|
+
throw new CliUsageError('--write and --check cannot be combined.');
|
|
36
|
+
const results = await migrateFiles(positionals.length > 0 ? positionals : ['.'], { write });
|
|
37
|
+
const pending = results.filter((result) => result.changed);
|
|
38
|
+
if (flagBool(flags, 'json')) {
|
|
39
|
+
writeJson({ written: write, files: results });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
for (const result of results) {
|
|
43
|
+
const verb = result.changed ? (write ? 'migrated' : 'would migrate') : 'needs a look';
|
|
44
|
+
console.log(`${result.file}: ${verb}`);
|
|
45
|
+
for (const change of result.changes)
|
|
46
|
+
console.log(` ${change.line}: ${change.message}`);
|
|
47
|
+
for (const note of result.notes)
|
|
48
|
+
console.log(` ${note.line}: TODO ${note.message}`);
|
|
49
|
+
}
|
|
50
|
+
const notes = results.reduce((total, result) => total + result.notes.length, 0);
|
|
51
|
+
const summary = pending.length === 0
|
|
52
|
+
? 'Nothing to migrate.'
|
|
53
|
+
: write
|
|
54
|
+
? `Migrated ${pending.length} file${pending.length === 1 ? '' : 's'}.`
|
|
55
|
+
: `${pending.length} file${pending.length === 1 ? '' : 's'} to migrate. Run again with --write.`;
|
|
56
|
+
console.log(notes > 0 ? `${summary} ${notes} place${notes === 1 ? '' : 's'} to finish by hand.` : summary);
|
|
57
|
+
}
|
|
58
|
+
if (checking && pending.length > 0)
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
}
|
|
61
|
+
function migrateMarkdown(text) {
|
|
62
|
+
const changes = [];
|
|
63
|
+
const notes = [];
|
|
64
|
+
const fence = /^([ \t]*)(```+|~~~+)[ \t]*([\w-]*)[^\n]*\n([\s\S]*?)^\1\2[ \t]*$/gm;
|
|
65
|
+
let output = '';
|
|
66
|
+
let last = 0;
|
|
67
|
+
for (const match of text.matchAll(fence)) {
|
|
68
|
+
const language = (match[3] ?? '').toLowerCase();
|
|
69
|
+
if (!FENCE_LANGUAGES.has(language))
|
|
70
|
+
continue;
|
|
71
|
+
const body = match[4];
|
|
72
|
+
const bodyStart = (match.index ?? 0) + match[0].indexOf('\n') + 1;
|
|
73
|
+
const offset = lineOf(text, bodyStart) - 1;
|
|
74
|
+
const outcome = migrateCode(body, offset);
|
|
75
|
+
changes.push(...outcome.changes);
|
|
76
|
+
notes.push(...outcome.notes);
|
|
77
|
+
if (outcome.text === body)
|
|
78
|
+
continue;
|
|
79
|
+
output += text.slice(last, bodyStart) + outcome.text;
|
|
80
|
+
last = bodyStart + body.length;
|
|
81
|
+
}
|
|
82
|
+
return { text: last === 0 ? text : output + text.slice(last), changes, notes };
|
|
83
|
+
}
|
|
84
|
+
const NAMED = /^([ \t]*)(import|export)(\s+type)?\s*\{([^}]*)\}\s*from\s*(['"])(nexus-ai-pro(?:\/[\w./-]+)?)\5([ \t]*;?)/gm;
|
|
85
|
+
const REQUIRE = /^([ \t]*)(const|let|var)\s*\{([^}]*)\}\s*=\s*require\(\s*(['"])(nexus-ai-pro(?:\/[\w./-]+)?)\4\s*\)([ \t]*;?)/gm;
|
|
86
|
+
function migrateCode(text, lineOffset) {
|
|
87
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
88
|
+
const changes = [];
|
|
89
|
+
const notes = [];
|
|
90
|
+
const at = (index) => lineOf(text, index) + lineOffset;
|
|
91
|
+
let output = text.replace(NAMED, (whole, indent, keyword, typeOnly, body, quote, from, end, index) => {
|
|
92
|
+
const specifiers = parseSpecifiers(body, false);
|
|
93
|
+
if (!specifiers)
|
|
94
|
+
return whole;
|
|
95
|
+
const rewritten = rewrite(specifiers, from, (message) => changes.push({ line: at(index), message }));
|
|
96
|
+
if (!rewritten)
|
|
97
|
+
return whole;
|
|
98
|
+
const multiline = body.includes('\n');
|
|
99
|
+
return rewritten
|
|
100
|
+
.map(([module, list]) => `${indent}${keyword}${typeOnly ? ' type' : ''} ${braces(list, indent, multiline, eol, false)} from ${quote}${module}${quote}${end}`)
|
|
101
|
+
.join(eol);
|
|
102
|
+
});
|
|
103
|
+
output = output.replace(REQUIRE, (whole, indent, declaration, body, quote, from, end, index) => {
|
|
104
|
+
const specifiers = parseSpecifiers(body, true);
|
|
105
|
+
if (!specifiers)
|
|
106
|
+
return whole;
|
|
107
|
+
const rewritten = rewrite(specifiers, from, (message) => changes.push({ line: at(index), message }));
|
|
108
|
+
if (!rewritten)
|
|
109
|
+
return whole;
|
|
110
|
+
const multiline = body.includes('\n');
|
|
111
|
+
return rewritten
|
|
112
|
+
.map(([module, list]) => `${indent}${declaration} ${braces(list, indent, multiline, eol, true)} = require(${quote}${module}${quote})${end}`)
|
|
113
|
+
.join(eol);
|
|
114
|
+
});
|
|
115
|
+
for (const match of text.matchAll(/import\s+\*\s+as\s+\w+\s+from\s*['"]nexus-ai-pro['"]/g)) {
|
|
116
|
+
notes.push({
|
|
117
|
+
line: at(match.index ?? 0),
|
|
118
|
+
message: 'a namespace import of the root: import each name from its subpath',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
for (const match of text.matchAll(/export\s+\*\s+from\s*['"]nexus-ai-pro['"]/g)) {
|
|
122
|
+
notes.push({ line: at(match.index ?? 0), message: 're-exports the whole root: re-export the subpaths you need' });
|
|
123
|
+
}
|
|
124
|
+
for (const match of text.matchAll(/\bimport\(\s*['"]nexus-ai-pro['"]\s*\)/g)) {
|
|
125
|
+
notes.push({
|
|
126
|
+
line: at(match.index ?? 0),
|
|
127
|
+
message: 'a dynamic import of the root: import the subpath that has what you use',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
for (const match of text.matchAll(/(?:const|let|var)\s+\w+\s*=\s*require\(\s*['"]nexus-ai-pro['"]\s*\)/g)) {
|
|
131
|
+
notes.push({
|
|
132
|
+
line: at(match.index ?? 0),
|
|
133
|
+
message: 'requires the whole root: require the subpaths that have what you use',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
for (const match of text.matchAll(/\.estimatedCost\b(?!\s*[.?[])/g)) {
|
|
137
|
+
notes.push({
|
|
138
|
+
line: at(match.index ?? 0),
|
|
139
|
+
message: "a response's `estimatedCost` string is removed in 2.0: read `cost.amount`, a number, and format it",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return { text: output, changes, notes };
|
|
143
|
+
}
|
|
144
|
+
function parseSpecifiers(body, destructuring) {
|
|
145
|
+
const cleaned = body.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
|
|
146
|
+
const specifiers = [];
|
|
147
|
+
for (const part of cleaned.split(',')) {
|
|
148
|
+
const item = part.trim();
|
|
149
|
+
if (!item)
|
|
150
|
+
continue;
|
|
151
|
+
const match = destructuring
|
|
152
|
+
? /^([A-Za-z_$][\w$]*)(?:\s*:\s*([A-Za-z_$][\w$]*))?$/.exec(item)
|
|
153
|
+
: /^(type\s+)?([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(item);
|
|
154
|
+
if (!match)
|
|
155
|
+
return undefined;
|
|
156
|
+
specifiers.push(destructuring
|
|
157
|
+
? { type: false, name: match[1], ...(match[2] ? { alias: match[2] } : {}) }
|
|
158
|
+
: { type: Boolean(match[1]), name: match[2], ...(match[3] ? { alias: match[3] } : {}) });
|
|
159
|
+
}
|
|
160
|
+
return specifiers;
|
|
161
|
+
}
|
|
162
|
+
function rewrite(specifiers, from, record) {
|
|
163
|
+
const groups = new Map();
|
|
164
|
+
let touched = false;
|
|
165
|
+
for (const specifier of specifiers) {
|
|
166
|
+
let module = from;
|
|
167
|
+
let name = specifier.name;
|
|
168
|
+
const move = from === PACKAGE ? ROOT_MOVES[specifier.name] : undefined;
|
|
169
|
+
if (move) {
|
|
170
|
+
[module] = move;
|
|
171
|
+
name = move[1] ?? specifier.name;
|
|
172
|
+
record(`${specifier.name}: from '${module}'${name === specifier.name ? '' : ` as ${name}`}`);
|
|
173
|
+
touched = true;
|
|
174
|
+
}
|
|
175
|
+
const replacement = RENAMES[name];
|
|
176
|
+
if (replacement) {
|
|
177
|
+
record(`${name}: renamed ${replacement}`);
|
|
178
|
+
name = replacement;
|
|
179
|
+
touched = true;
|
|
180
|
+
}
|
|
181
|
+
const local = specifier.alias ?? specifier.name;
|
|
182
|
+
const entry = { type: specifier.type, name, ...(local === name ? {} : { alias: local }) };
|
|
183
|
+
groups.set(module, [...(groups.get(module) ?? []), entry]);
|
|
184
|
+
}
|
|
185
|
+
if (!touched)
|
|
186
|
+
return undefined;
|
|
187
|
+
return [...groups].sort(([a], [b]) => (a === from ? -1 : b === from ? 1 : a.localeCompare(b)));
|
|
188
|
+
}
|
|
189
|
+
function braces(list, indent, multiline, eol, destructuring) {
|
|
190
|
+
const items = list.map((item) => destructuring
|
|
191
|
+
? item.alias
|
|
192
|
+
? `${item.name}: ${item.alias}`
|
|
193
|
+
: item.name
|
|
194
|
+
: `${item.type ? 'type ' : ''}${item.name}${item.alias ? ` as ${item.alias}` : ''}`);
|
|
195
|
+
if (!multiline)
|
|
196
|
+
return `{ ${items.join(', ')} }`;
|
|
197
|
+
return `{${eol}${items.map((item) => `${indent} ${item},`).join(eol)}${eol}${indent}}`;
|
|
198
|
+
}
|
|
199
|
+
function lineOf(text, index) {
|
|
200
|
+
let line = 1;
|
|
201
|
+
for (let position = text.indexOf('\n'); position !== -1 && position < index; position = text.indexOf('\n', position + 1)) {
|
|
202
|
+
line += 1;
|
|
203
|
+
}
|
|
204
|
+
return line;
|
|
205
|
+
}
|
|
206
|
+
async function collect(paths) {
|
|
207
|
+
const files = [];
|
|
208
|
+
const visit = async (target) => {
|
|
209
|
+
const info = await stat(target);
|
|
210
|
+
if (info.isDirectory()) {
|
|
211
|
+
if (SKIPPED_DIRECTORIES.has(path.basename(target)))
|
|
212
|
+
return;
|
|
213
|
+
for (const entry of (await readdir(target)).sort())
|
|
214
|
+
await visit(path.join(target, entry));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const extension = path.extname(target).toLowerCase();
|
|
218
|
+
if (CODE_EXTENSIONS.has(extension) || MARKDOWN_EXTENSIONS.has(extension))
|
|
219
|
+
files.push(target);
|
|
220
|
+
};
|
|
221
|
+
for (const target of paths)
|
|
222
|
+
await visit(target);
|
|
223
|
+
return files;
|
|
224
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each export the 2.0 root import drops lives instead: the subpath, and the name it has there
|
|
3
|
+
* when that differs. `nexus migrate` rewrites imports from it.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ROOT_MOVES: Readonly<Record<string, readonly [subpath: string, name?: string]>>;
|