@getrefino/cli 0.1.0-rc.1 → 0.1.0-rc.3
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 +86 -3
- package/dist/cli.js +72 -10
- package/dist/format.d.ts +7 -0
- package/dist/format.js +50 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -32,7 +32,38 @@ components.
|
|
|
32
32
|
|
|
33
33
|
## Install into a site
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
The order matters, and it starts on the Refino dashboard rather than in the
|
|
36
|
+
repository.
|
|
37
|
+
|
|
38
|
+
1. **Connect the site in the Refino dashboard (<https://app.refino.dev>).** You
|
|
39
|
+
give it the repository, the branch, where the copy file will live, and the
|
|
40
|
+
URL the site is served from. Refino gives you a **site id** (`site_…`).
|
|
41
|
+
- The branch must be **the branch that will contain the Refino files and be
|
|
42
|
+
deployed**. Refino reads and commits the copy file on that branch and
|
|
43
|
+
nowhere else.
|
|
44
|
+
- The copy file **does not have to exist yet.** `init` writes it in step 2.
|
|
45
|
+
Until the site is installed and deployed, the dashboard shows it as
|
|
46
|
+
*awaiting install*; that is the expected state, not a fault.
|
|
47
|
+
2. **Install into the repository**, passing that site id:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx @getrefino/cli init --agent --yes --refino-site site_…
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
3. **Hand `.refino/AGENT_INSTRUCTIONS.md` and `.refino/plan.json` to your
|
|
54
|
+
coding agent** and let it do exactly what they say.
|
|
55
|
+
4. **Verify**: `npx @getrefino/cli verify`, fixing only what it reports.
|
|
56
|
+
5. **Commit and merge** that work onto the branch the site is configured to
|
|
57
|
+
use, and deploy it.
|
|
58
|
+
6. **Open `/edit`** on the deployed site.
|
|
59
|
+
|
|
60
|
+
If the site was connected to Refino again later — disconnected and reconnected,
|
|
61
|
+
or moved to another account — it has a new site id, and step 2 is how the
|
|
62
|
+
repository learns it. Re-run `init --agent --yes --refino-site <new id>`,
|
|
63
|
+
commit and deploy.
|
|
64
|
+
|
|
65
|
+
Without `--refino-site` the same command sets up a **self-hosted** site, which
|
|
66
|
+
holds its own editor password and repository token instead:
|
|
36
67
|
|
|
37
68
|
```bash
|
|
38
69
|
npx @getrefino/cli init --agent
|
|
@@ -53,9 +84,50 @@ Then check the result:
|
|
|
53
84
|
npx @getrefino/cli verify
|
|
54
85
|
```
|
|
55
86
|
|
|
87
|
+
## Upgrading a site to a newer Refino
|
|
88
|
+
|
|
89
|
+
Run the command you installed with, from the newer version:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx @getrefino/cli@latest init --agent --yes --refino-site site_…
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Updating the `@getrefino/*` packages is only half of an upgrade. Refino also
|
|
96
|
+
generates runtime code into your repository (`refino/`, the `/edit` route), and
|
|
97
|
+
installing a package never touches a file already committed there — so a site
|
|
98
|
+
can be on the newest packages and still running an older release's integration.
|
|
99
|
+
`init` is what moves that half.
|
|
100
|
+
|
|
101
|
+
You never have to delete or move a generated file. `init` refreshes every one
|
|
102
|
+
it can prove you have not edited and reports it:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Updated Refino-generated integration:
|
|
106
|
+
refino/refino-site.ts 0.1.0-rc.1 → 0.1.0-rc.3
|
|
107
|
+
refino/copy-editing.tsx 0.1.0-rc.1 → 0.1.0-rc.3
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
A file **you** changed is never overwritten. It is listed as needing review,
|
|
111
|
+
Refino's current version is written beside it as `<file>.refino-new`, and
|
|
112
|
+
`init` exits non-zero rather than let a half-upgraded site look finished. Move
|
|
113
|
+
your changes onto that version, replace the original with it, delete the
|
|
114
|
+
`.refino-new` file, and run `init` again.
|
|
115
|
+
|
|
116
|
+
Refino knows which is which from `.refino/generated.json`, the record it writes
|
|
117
|
+
of the exact bytes it generated — commit it with the rest. Sites installed
|
|
118
|
+
before that file existed are recognised from the bytes of the older release, so
|
|
119
|
+
they upgrade the same way.
|
|
120
|
+
|
|
121
|
+
`refino verify` reports the same three states — current, refreshable, needs
|
|
122
|
+
review — without changing anything.
|
|
123
|
+
|
|
56
124
|
## If you are a coding agent
|
|
57
125
|
|
|
58
|
-
1. Run `npx @getrefino/cli init --agent
|
|
126
|
+
1. Run `npx @getrefino/cli init --agent --refino-site <site id>` in the
|
|
127
|
+
repository (the owner gets the site id from the Refino dashboard; without
|
|
128
|
+
one, `init --agent` sets up a self-hosted site instead). Add
|
|
129
|
+
`--via claude|codex|gemini|grok|other` so Refino knows which installation
|
|
130
|
+
routes work.
|
|
59
131
|
2. Read `.refino/AGENT_INSTRUCTIONS.md` and `.refino/plan.json` and do exactly
|
|
60
132
|
what they say. **They are authoritative.** They were generated from an
|
|
61
133
|
inspection of this specific repository — do not invent integration steps,
|
|
@@ -63,6 +135,13 @@ npx @getrefino/cli verify
|
|
|
63
135
|
3. Run `npx @getrefino/cli verify` and fix only what it reports. Repeat until
|
|
64
136
|
it passes.
|
|
65
137
|
|
|
138
|
+
If you are **upgrading** an existing installation rather than creating one, run
|
|
139
|
+
the same `init` command from the newer version and read its output. A non-zero
|
|
140
|
+
exit means a generated file has local changes Refino would have had to discard:
|
|
141
|
+
resolve each one against the `.refino-new` file it left beside the original,
|
|
142
|
+
then run `init` again. Do not delete or move generated files to get past it,
|
|
143
|
+
and do not report the upgrade as done while `init` or `verify` still fails.
|
|
144
|
+
|
|
66
145
|
## Commands
|
|
67
146
|
|
|
68
147
|
| Command | What it does |
|
|
@@ -75,7 +154,11 @@ npx @getrefino/cli verify
|
|
|
75
154
|
|
|
76
155
|
Every command takes `--json` for machine-readable output and `--cwd <dir>` to
|
|
77
156
|
operate on another directory. `refino init --dry-run` shows what would be
|
|
78
|
-
written without writing anything
|
|
157
|
+
written without writing anything. No command overwrites an existing file, with
|
|
158
|
+
one exception: `init --refino-site <id>` updates the generated `refino/`
|
|
159
|
+
constants module and the `refino` block of `refino.config.json` when they still
|
|
160
|
+
name a different site, which is how a site reconnected in the Refino dashboard
|
|
161
|
+
is re-recorded. Nothing you wrote by hand is touched.
|
|
79
162
|
|
|
80
163
|
## Packages
|
|
81
164
|
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { writeFileSync } from "node:fs";
|
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline/promises";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
|
-
import { INSTRUCTIONS_FILE, PLAN_FILE, TOOL_VERSION, appDirectory, applyPlan, inspectRepository, planMigration, renderAgentInstructions, scanCopy, verifyIntegration, } from "@getrefino/onboarding";
|
|
9
|
+
import { INSTRUCTIONS_FILE, PLAN_FILE, TOOL_VERSION, appDirectory, applyPlan, inspectRepository, planMigration, renderAgentInstructions, resolveInstallMethod, scanCopy, verifyIntegration, } from "@getrefino/onboarding";
|
|
10
10
|
import { bold, cyan, dim, formatApply, formatCandidates, formatInspection, formatPlanSummary, formatVerification, red, yellow } from "./format.js";
|
|
11
11
|
const TAGLINE = "Refino lets site owners edit visible website copy in the browser while the repository stays the source of truth.";
|
|
12
12
|
const HELP = `refino ${TOOL_VERSION} (@getrefino/cli)
|
|
@@ -16,13 +16,42 @@ ${TAGLINE}
|
|
|
16
16
|
It is not a CMS: the copy lives in one JSON file in the repository, an edit is a
|
|
17
17
|
commit, and removing Refino leaves the content exactly where it was.
|
|
18
18
|
|
|
19
|
-
Install into a site
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
Install into a site, in this order
|
|
20
|
+
1. Connect the site in the Refino dashboard (https://app.refino.dev) and copy
|
|
21
|
+
its site id. You give it the repository, the branch that will carry the
|
|
22
|
+
Refino files and be deployed, and where the copy file will live. The copy
|
|
23
|
+
file does not have to exist yet.
|
|
24
|
+
2. npx @getrefino/cli init --agent --yes --refino-site <site_id>
|
|
25
|
+
inspects the repo and writes the setup files, .refino/plan.json and
|
|
26
|
+
.refino/AGENT_INSTRUCTIONS.md.
|
|
27
|
+
3. Hand those two files to your coding agent and let it do exactly what they say.
|
|
28
|
+
4. npx @getrefino/cli verify fix only what it reports; repeat until it passes
|
|
29
|
+
5. Commit and merge that work onto the branch the site is configured to use, then
|
|
30
|
+
deploy it. Refino reads and commits the copy file on that branch.
|
|
31
|
+
6. Open /edit on the deployed site.
|
|
32
|
+
|
|
33
|
+
Without --refino-site the same steps set up a self-hosted site, which holds its
|
|
34
|
+
own password and repository token instead.
|
|
35
|
+
|
|
36
|
+
Upgrade an existing installation
|
|
37
|
+
Re-run exactly the command you installed with, from the new version:
|
|
38
|
+
|
|
39
|
+
npx @getrefino/cli@latest init --agent --yes --refino-site <site_id>
|
|
40
|
+
|
|
41
|
+
Upgrading the @getrefino/* packages is not enough on its own. Refino also
|
|
42
|
+
copies runtime code into the repository (refino/), and installing a package
|
|
43
|
+
does not touch a file already committed there, so both have to move. init
|
|
44
|
+
refreshes every generated file it can prove you have not edited, says which
|
|
45
|
+
ones it changed and from which version, and never touches one you have:
|
|
46
|
+
those are listed as needing review, Refino's current version is written
|
|
47
|
+
beside each as <file>.refino-new, and init exits non-zero so a half-upgraded
|
|
48
|
+
site is never reported as a finished one. You never have to delete or move a
|
|
49
|
+
generated file yourself.
|
|
50
|
+
|
|
51
|
+
refino verify reports the same three states without changing anything.
|
|
23
52
|
|
|
24
53
|
Usage
|
|
25
|
-
refino init [--refino-site <id> [--refino-url <origin>]] [--route <path>]... [--all] [--agent | --dry-run | --plan] [--yes]
|
|
54
|
+
refino init [--refino-site <id> [--refino-url <origin>]] [--via <installer>] [--route <path>]... [--all] [--agent | --dry-run | --plan] [--yes]
|
|
26
55
|
refino inspect [--json]
|
|
27
56
|
refino scan [--route <path>]... [--all] [--no-shared] [--json] [--include-excluded]
|
|
28
57
|
refino plan [--route <path>]... [--all] [--format text|json|agent] [--out <file>]
|
|
@@ -38,6 +67,11 @@ Options
|
|
|
38
67
|
--refino-site <id> Connect to Refino (hosted mode): the site id from the Refino dashboard.
|
|
39
68
|
The site then needs no editor password, secret, repository token or server code.
|
|
40
69
|
--refino-url <origin> Refino app origin (default: https://app.refino.dev)
|
|
70
|
+
--via <installer> Who is doing the installation: claude, codex, gemini, grok, cli,
|
|
71
|
+
manual or other. Recorded as a public constant in the site and
|
|
72
|
+
told to Refino so it knows which installation routes work. It is
|
|
73
|
+
never guessed: without this flag (or REFINO_INSTALL_METHOD) it
|
|
74
|
+
stays "unknown".
|
|
41
75
|
--agent Write safe setup files + .refino/AGENT_INSTRUCTIONS.md
|
|
42
76
|
--dry-run Show what --agent would write without writing
|
|
43
77
|
--plan Print the plan only
|
|
@@ -46,7 +80,8 @@ Options
|
|
|
46
80
|
--run verify: also run the repository's typecheck/lint/test/build
|
|
47
81
|
|
|
48
82
|
If you are a coding agent
|
|
49
|
-
1. npx @getrefino/cli init --agent
|
|
83
|
+
1. npx @getrefino/cli init --agent --via <your name: claude, codex, gemini, grok or other>
|
|
84
|
+
(inspect, plan, write setup files + instructions)
|
|
50
85
|
2. Read .refino/AGENT_INSTRUCTIONS.md and .refino/plan.json and do exactly what they say.
|
|
51
86
|
They are authoritative: do not invent integration steps of your own.
|
|
52
87
|
3. npx @getrefino/cli verify (fix only what it reports, then run it again)
|
|
@@ -65,6 +100,7 @@ function parse(argv) {
|
|
|
65
100
|
"content-file": { type: "string" },
|
|
66
101
|
"refino-site": { type: "string" },
|
|
67
102
|
"refino-url": { type: "string" },
|
|
103
|
+
via: { type: "string" },
|
|
68
104
|
agent: { type: "boolean", default: false },
|
|
69
105
|
"dry-run": { type: "boolean", default: false },
|
|
70
106
|
plan: { type: "boolean", default: false },
|
|
@@ -88,6 +124,7 @@ function parse(argv) {
|
|
|
88
124
|
contentFile: values["content-file"],
|
|
89
125
|
refinoSite: values["refino-site"],
|
|
90
126
|
refinoUrl: values["refino-url"],
|
|
127
|
+
via: values.via,
|
|
91
128
|
agent: values.agent,
|
|
92
129
|
dryRun: values["dry-run"],
|
|
93
130
|
plan: values.plan,
|
|
@@ -110,10 +147,17 @@ function planOptions(args) {
|
|
|
110
147
|
options.includeShared = args.shared;
|
|
111
148
|
if (args.contentFile)
|
|
112
149
|
options.contentFile = args.contentFile;
|
|
113
|
-
if (args.refinoSite)
|
|
114
|
-
|
|
150
|
+
if (args.refinoSite) {
|
|
151
|
+
// Whatever the installer said, or nothing. Never inferred from the environment.
|
|
152
|
+
const installMethod = resolveInstallMethod(args.via, process.env);
|
|
153
|
+
options.refino = args.refinoUrl
|
|
154
|
+
? { siteId: args.refinoSite, appUrl: args.refinoUrl, installMethod }
|
|
155
|
+
: { siteId: args.refinoSite, installMethod };
|
|
156
|
+
}
|
|
115
157
|
else if (args.refinoUrl)
|
|
116
158
|
throw new Error("--refino-url needs --refino-site.");
|
|
159
|
+
else if (args.via)
|
|
160
|
+
throw new Error("--via needs --refino-site: it is recorded on the site connected to Refino.");
|
|
117
161
|
return options;
|
|
118
162
|
}
|
|
119
163
|
function inspect(args) {
|
|
@@ -256,7 +300,25 @@ export async function run(argv) {
|
|
|
256
300
|
console.log("");
|
|
257
301
|
if (mode === "dry-run") {
|
|
258
302
|
console.log(dim("Nothing was written. Run `refino init --agent` to write these files."));
|
|
259
|
-
|
|
303
|
+
if (!result.needsReview)
|
|
304
|
+
return 0;
|
|
305
|
+
// A dry run is also the read-only way to ask "would this upgrade
|
|
306
|
+
// cleanly?", so it answers with the same exit code the real run would.
|
|
307
|
+
console.log(red(`${result.generated.filter((state) => state.status === "review").length} generated file(s) have local changes and would not be updated; --agent would leave them alone and report them.`));
|
|
308
|
+
return 1;
|
|
309
|
+
}
|
|
310
|
+
// A generated file Refino left behind because it had been edited means
|
|
311
|
+
// this site is not running the integration this version generates. That
|
|
312
|
+
// is the whole failure this exit code exists for: init used to say
|
|
313
|
+
// "Next steps" over a half-upgraded site, and nothing downstream could
|
|
314
|
+
// tell. Say what is true, and fail.
|
|
315
|
+
if (result.needsReview) {
|
|
316
|
+
const count = result.generated.filter((state) => state.status === "review").length;
|
|
317
|
+
console.log(red(bold(`Refino did not finish: ${count} generated file(s) listed above were left as they are, and are not what Refino ${TOOL_VERSION} generates.`)));
|
|
318
|
+
console.log(red("This installation is NOT current. Resolve each one, then run this command again."));
|
|
319
|
+
console.log("");
|
|
320
|
+
console.log(dim(`Everything else was written; ${PLAN_FILE} and ${INSTRUCTIONS_FILE} are up to date.`));
|
|
321
|
+
return 1;
|
|
260
322
|
}
|
|
261
323
|
console.log(bold("Next steps"));
|
|
262
324
|
if (result.installCommand)
|
package/dist/format.d.ts
CHANGED
|
@@ -11,4 +11,11 @@ export declare function formatCandidates(candidates: readonly CopyCandidate[], o
|
|
|
11
11
|
}): string;
|
|
12
12
|
export declare function formatPlanSummary(plan: MigrationPlan): string;
|
|
13
13
|
export declare function formatApply(result: ApplyResult): string;
|
|
14
|
+
/**
|
|
15
|
+
* The upgrade half of an init run: what Refino refreshed, and what it refused
|
|
16
|
+
* to touch. The second list is the important one -- it is the difference
|
|
17
|
+
* between "your installation is current" and "your installation is not", and
|
|
18
|
+
* it has to be impossible to skim past.
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatGeneratedUpgrade(result: ApplyResult): string;
|
|
14
21
|
export declare function formatVerification(result: VerificationResult): string;
|
package/dist/format.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { COMPARISON_SUFFIX } from "@getrefino/onboarding";
|
|
1
2
|
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
2
3
|
const paint = (code) => (text) => (useColor ? `[${code}m${text}[0m` : text);
|
|
3
4
|
export const bold = paint("1");
|
|
@@ -76,7 +77,12 @@ export function formatPlanSummary(plan) {
|
|
|
76
77
|
rows.push(line("Provider", `${plan.integration.provider.file ?? "?"} [${plan.integration.provider.strategy}]`));
|
|
77
78
|
rows.push(line("Endpoint", `${plan.integration.endpoint.path} [${plan.integration.endpoint.kind}]`));
|
|
78
79
|
rows.push(line("Auth", `${plan.integration.auth.strategy}${plan.integration.auth.existing.length > 0 ? ` (existing: ${plan.integration.auth.existing.join(", ")})` : ""}`));
|
|
79
|
-
|
|
80
|
+
// A connected site has no adapter and no variables of its own: Refino holds
|
|
81
|
+
// the credential and commits. Naming environment variables it will never set
|
|
82
|
+
// (the self-hosted list is empty here) read as a truncated sentence.
|
|
83
|
+
rows.push(line("Persistence", plan.refino
|
|
84
|
+
? "Refino commits to the repository (no adapter, no variables in this site)"
|
|
85
|
+
: `local file in development; GitHub via ${plan.integration.persistence.github.envVars.join(", ")}`));
|
|
80
86
|
rows.push("");
|
|
81
87
|
rows.push(bold("Copy"));
|
|
82
88
|
rows.push(line("Selected", green(String(plan.summary.selected))));
|
|
@@ -119,6 +125,49 @@ export function formatApply(result) {
|
|
|
119
125
|
for (const item of result.skipped)
|
|
120
126
|
rows.push(` ${dim("skip".padEnd(7))} ${item.path} ${dim(item.reason)}`);
|
|
121
127
|
}
|
|
128
|
+
const upgrade = formatGeneratedUpgrade(result);
|
|
129
|
+
if (upgrade) {
|
|
130
|
+
rows.push("");
|
|
131
|
+
rows.push(upgrade);
|
|
132
|
+
}
|
|
133
|
+
return rows.join("\n");
|
|
134
|
+
}
|
|
135
|
+
function versionArrow(state) {
|
|
136
|
+
return state.from && state.from !== state.to ? `${state.from} → ${state.to}` : state.to;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The upgrade half of an init run: what Refino refreshed, and what it refused
|
|
140
|
+
* to touch. The second list is the important one -- it is the difference
|
|
141
|
+
* between "your installation is current" and "your installation is not", and
|
|
142
|
+
* it has to be impossible to skim past.
|
|
143
|
+
*/
|
|
144
|
+
export function formatGeneratedUpgrade(result) {
|
|
145
|
+
const updated = result.generated.filter((state) => state.status === "update");
|
|
146
|
+
const review = result.generated.filter((state) => state.status === "review");
|
|
147
|
+
if (updated.length === 0 && review.length === 0)
|
|
148
|
+
return "";
|
|
149
|
+
const rows = [];
|
|
150
|
+
const width = updated.length > 0 ? Math.max(...updated.map((state) => state.path.length)) : 0;
|
|
151
|
+
if (updated.length > 0) {
|
|
152
|
+
rows.push(bold(result.dryRun ? "Refino-generated integration that would be updated:" : "Updated Refino-generated integration:"));
|
|
153
|
+
for (const state of updated)
|
|
154
|
+
rows.push(` ${state.path.padEnd(width)} ${cyan(versionArrow(state))}`);
|
|
155
|
+
}
|
|
156
|
+
if (review.length > 0) {
|
|
157
|
+
if (updated.length > 0)
|
|
158
|
+
rows.push("");
|
|
159
|
+
rows.push(red(bold("Needs manual review — NOT updated:")));
|
|
160
|
+
for (const state of review) {
|
|
161
|
+
// No version arrow here: the whole point is that this file's provenance
|
|
162
|
+
// could not be established, so printing one would be a claim.
|
|
163
|
+
rows.push(` ${yellow(state.path)}`);
|
|
164
|
+
rows.push(` ${dim("why:")} ${state.reason}`);
|
|
165
|
+
if (!result.dryRun && state.comparisonPath) {
|
|
166
|
+
rows.push(` ${dim("next:")} Compare it with ${state.comparisonPath} (Refino's ${state.to} version, written beside it just now).`);
|
|
167
|
+
rows.push(` Move your changes onto that version, replace ${state.path} with it, delete the ${COMPARISON_SUFFIX} file, then run init again.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
122
171
|
return rows.join("\n");
|
|
123
172
|
}
|
|
124
173
|
export function formatVerification(result) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getrefino/cli",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.3",
|
|
4
4
|
"description": "Refino installer. Run `npx @getrefino/cli init` in a website's repository to let its owner edit visible copy in the browser while Git remains the source of truth. Inspects the repo, writes an authoritative migration plan and instructions for a coding agent, then verifies the result.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"refino",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"./package.json": "./package.json"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@getrefino/onboarding": "0.1.0-rc.
|
|
48
|
+
"@getrefino/onboarding": "0.1.0-rc.3"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "22.20.2",
|