@hanv89/azure-arch-skill 0.1.0-rc.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hanv89
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @hanv89/azure-arch-skill
2
+
3
+ > **Status**: pre-publish. The package is not yet on npmjs.com — the `npx` command below works once the first release ships.
4
+
5
+ CLI installer for the Azure architecture diagram skill. Drops the skill bundle (`SKILL.md` + worked PlantUML examples) into your AI coding agent's skill folder so that prompts like *"draw an Azure context diagram"* produce diagrams that use official Microsoft Azure architecture icons.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js ≥ 20
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npx @hanv89/azure-arch-skill install --agent=claude-code
15
+ ```
16
+
17
+ Supported agents:
18
+
19
+ - Claude Code — `~/.claude/skills/azure-architecture-diagram/`
20
+
21
+ Planned (later releases): Codex CLI, Cursor.
22
+
23
+ ## Subcommands
24
+
25
+ - `install --agent=<name>` — copy the skill bundle into the agent's skill folder.
26
+ - `uninstall --agent=<name>` — remove the skill from the agent's skill folder.
27
+ - `update --agent=<name>` — refresh an installed skill to the latest version.
28
+ - `list` — list installed skills and their versions.
29
+
30
+ The icons themselves stay in this repository and are referenced from PlantUML via public `raw.githubusercontent.com` URLs — no additional download or hosting required.
31
+
32
+ ## Environment overrides
33
+
34
+ Two environment variables exist for validation / CI use. Production users should not set them.
35
+
36
+ - **`AZURE_ARCH_SKILL_BASE_URL`** — override the bundle source. Restricted to `https://raw.githubusercontent.com/hanv89/azure-icons-for-architecture-diagrams/...` paths; any other host or scheme is rejected. The CLI emits `warn: AZURE_ARCH_SKILL_BASE_URL override active: <value>` to stderr whenever the override fires.
37
+ - **`AZURE_ARCH_SKILL_TARGET_ROOT`** — widen the `--target` allow-list beyond the default `~/.claude/`. Setting this lets `install` / `uninstall` / `list` operate on directories outside the user's Claude config tree, so it MUST point at a directory you control (e.g. `mktemp -d` output in a test script). The CLI emits `warn: AZURE_ARCH_SKILL_TARGET_ROOT override active: <value>` once per command when honored. Never set this in a production shell — a stray `--target=$HOME` invocation against a widened allow-list could remove unrelated files.
38
+
39
+ ## Repository
40
+
41
+ See the [project README](https://github.com/hanv89/azure-icons-for-architecture-diagrams#readme) for icon source, licensing, and the full skill specification.
@@ -0,0 +1,316 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.claudeCodeAdapter = exports.parseFrontmatter = void 0;
30
+ const fs = __importStar(require("node:fs/promises"));
31
+ const path = __importStar(require("node:path"));
32
+ const os = __importStar(require("node:os"));
33
+ const package_json_1 = __importDefault(require("../../package.json"));
34
+ const DEFAULT_BASE_RAW_URL = "https://raw.githubusercontent.com/hanv89/azure-icons-for-architecture-diagrams/main";
35
+ // SKILL_NAME must stay in lockstep with dist/skill/SKILL.md frontmatter `name`.
36
+ // Renaming the skill is a breaking change requiring a coordinated CLI release;
37
+ // existing installs become un-uninstallable until users upgrade the CLI
38
+ // (uninstall's allow-list refuses folders whose SKILL.md `name` differs).
39
+ const SKILL_NAME = "azure-architecture-diagram";
40
+ const BUNDLE_FILES = [
41
+ { src: "dist/skill/SKILL.md", dest: "SKILL.md" },
42
+ { src: "dist/skill/examples/01-context.puml", dest: "examples/01-context.puml" },
43
+ ];
44
+ const CANARY_ICON_PATH = "dist/Azure/Compute/AzureVirtualMachine.png";
45
+ const FETCH_TIMEOUT_MS = 30_000;
46
+ const USER_AGENT = `azure-arch-skill/${package_json_1.default.version}`;
47
+ const ALLOWED_BASE_URL_HOSTS = new Set(["raw.githubusercontent.com"]);
48
+ const ALLOWED_BASE_URL_PATH_PREFIX = "/hanv89/azure-icons-for-architecture-diagrams/";
49
+ function baseUrl() {
50
+ const override = process.env.AZURE_ARCH_SKILL_BASE_URL;
51
+ if (!override)
52
+ return DEFAULT_BASE_RAW_URL;
53
+ let u;
54
+ try {
55
+ u = new URL(override);
56
+ }
57
+ catch {
58
+ throw new Error(`AZURE_ARCH_SKILL_BASE_URL is not a valid URL: ${override}`);
59
+ }
60
+ if (u.protocol !== "https:") {
61
+ throw new Error(`AZURE_ARCH_SKILL_BASE_URL must use https; got ${u.protocol}`);
62
+ }
63
+ if (!ALLOWED_BASE_URL_HOSTS.has(u.hostname)) {
64
+ throw new Error(`AZURE_ARCH_SKILL_BASE_URL host '${u.hostname}' not in allow-list (${[...ALLOWED_BASE_URL_HOSTS].join(", ")})`);
65
+ }
66
+ if (!u.pathname.startsWith(ALLOWED_BASE_URL_PATH_PREFIX)) {
67
+ throw new Error(`AZURE_ARCH_SKILL_BASE_URL path must start with ${ALLOWED_BASE_URL_PATH_PREFIX}`);
68
+ }
69
+ process.stderr.write(`warn: AZURE_ARCH_SKILL_BASE_URL override active: ${override}\n`);
70
+ return override.replace(/\/$/, "");
71
+ }
72
+ function defaultTarget() {
73
+ return path.join(os.homedir(), ".claude", "skills", SKILL_NAME);
74
+ }
75
+ function defaultSkillsRoot() {
76
+ return path.join(os.homedir(), ".claude", "skills");
77
+ }
78
+ let envTargetRootWarned = false;
79
+ /**
80
+ * Resolve `target` and assert it lives inside an allowed root. Resolution
81
+ * follows symlinks (via fs.realpath on the deepest existing ancestor) so
82
+ * a symlink inside an allowed root that points outside cannot bypass the
83
+ * check.
84
+ *
85
+ * The default allow-list is `~/.claude/` only. Setting the
86
+ * `AZURE_ARCH_SKILL_TARGET_ROOT` env var widens it to include that root
87
+ * (intended for validation/CI use against a `mktemp -d` directory).
88
+ * Production users should never set the env var.
89
+ */
90
+ async function safeResolveTarget(target) {
91
+ const lexicallyResolved = path.resolve(target);
92
+ // Walk up to the deepest existing ancestor and realpath it — install creates
93
+ // a not-yet-existing target so we can't realpath the leaf directly.
94
+ let probe = lexicallyResolved;
95
+ let realProbe = null;
96
+ while (true) {
97
+ try {
98
+ realProbe = await fs.realpath(probe);
99
+ break;
100
+ }
101
+ catch (err) {
102
+ const code = err.code;
103
+ if (code !== "ENOENT" && code !== "ENOTDIR")
104
+ throw err;
105
+ const parent = path.dirname(probe);
106
+ if (parent === probe) {
107
+ throw new Error(`unable to resolve target ${target}`);
108
+ }
109
+ probe = parent;
110
+ }
111
+ }
112
+ const tail = lexicallyResolved.slice(probe.length);
113
+ const realResolved = path.resolve(realProbe + tail);
114
+ const home = os.homedir();
115
+ const explicit = process.env.AZURE_ARCH_SKILL_TARGET_ROOT;
116
+ if (explicit && !envTargetRootWarned) {
117
+ process.stderr.write(`warn: AZURE_ARCH_SKILL_TARGET_ROOT override active: ${explicit}\n`);
118
+ envTargetRootWarned = true;
119
+ }
120
+ const allowedRoots = [
121
+ path.resolve(path.join(home, ".claude")),
122
+ explicit ? path.resolve(explicit) : null,
123
+ ].filter((r) => r !== null);
124
+ const inside = allowedRoots.some(root => realResolved === root || realResolved.startsWith(root + path.sep));
125
+ if (!inside) {
126
+ const allowList = `~/.claude${explicit ? `, $AZURE_ARCH_SKILL_TARGET_ROOT=${explicit}` : ""}`;
127
+ throw new Error(`refusing to operate on ${realResolved} (resolved from ${target}) - outside allowed roots (${allowList})`);
128
+ }
129
+ return realResolved;
130
+ }
131
+ async function fetchWithTimeout(url, init = {}) {
132
+ const ctrl = new AbortController();
133
+ const t = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
134
+ try {
135
+ return await fetch(url, {
136
+ ...init,
137
+ signal: ctrl.signal,
138
+ headers: { ...(init.headers || {}), "User-Agent": USER_AGENT },
139
+ });
140
+ }
141
+ finally {
142
+ clearTimeout(t);
143
+ }
144
+ }
145
+ async function fetchText(url) {
146
+ const res = await fetchWithTimeout(url);
147
+ if (!res.ok)
148
+ throw new Error(`fetch ${url} returned HTTP ${res.status}`);
149
+ return res.text();
150
+ }
151
+ async function headOk(url) {
152
+ const res = await fetchWithTimeout(url, { method: "HEAD" });
153
+ return res.ok;
154
+ }
155
+ /**
156
+ * Minimal YAML frontmatter parser — supports only single-line scalar `key: value`
157
+ * pairs with optional `"` or `'` quoting. Multi-line scalars (`|`, `>`), nested
158
+ * mappings, lists, comments after `#`, and YAML null/booleans are NOT handled
159
+ * and may silently mis-parse.
160
+ *
161
+ * Intentional: SKILL.md frontmatter currently has 4 single-line scalar keys
162
+ * (name, description, version, requires_icons). Swap in `js-yaml` when the
163
+ * format grows beyond that.
164
+ */
165
+ function parseFrontmatter(md) {
166
+ // Strip optional UTF-8 BOM (some editors emit it on save).
167
+ const text = md.replace(/^/, "");
168
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
169
+ if (!match)
170
+ return {};
171
+ const out = {};
172
+ for (const line of match[1].split(/\r?\n/)) {
173
+ const kv = line.match(/^(\w+):\s*(.*?)\s*$/);
174
+ if (!kv)
175
+ continue;
176
+ const [, key] = kv;
177
+ let rawValue = kv[2];
178
+ // Strip trailing ` # comment` from unquoted scalars.
179
+ if (!rawValue.startsWith('"') && !rawValue.startsWith("'")) {
180
+ rawValue = rawValue.replace(/\s+#.*$/, "");
181
+ }
182
+ const value = rawValue.replace(/^["']|["']$/g, "");
183
+ if (key === "name" || key === "version" || key === "requires_icons") {
184
+ out[key] = value;
185
+ }
186
+ }
187
+ return out;
188
+ }
189
+ exports.parseFrontmatter = parseFrontmatter;
190
+ /**
191
+ * Returns true iff `dir` contains a SKILL.md whose frontmatter `name` field
192
+ * matches our skill. Used by uninstall to refuse deleting paths that aren't
193
+ * our skill folder.
194
+ */
195
+ async function isOurSkillDir(dir) {
196
+ try {
197
+ const skillMd = await fs.readFile(path.join(dir, "SKILL.md"), "utf8");
198
+ const fm = parseFrontmatter(skillMd);
199
+ return fm.name === SKILL_NAME;
200
+ }
201
+ catch {
202
+ return false;
203
+ }
204
+ }
205
+ async function withFatalReturn(fn) {
206
+ try {
207
+ return await fn();
208
+ }
209
+ catch (err) {
210
+ process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}\n`);
211
+ return 1;
212
+ }
213
+ }
214
+ async function install(opts) {
215
+ return withFatalReturn(async () => {
216
+ const target = await safeResolveTarget(opts.target ?? defaultTarget());
217
+ const base = baseUrl();
218
+ // Strict install path: target must end with the canonical skill folder name
219
+ // unless the caller has opted into a wider AZURE_ARCH_SKILL_TARGET_ROOT.
220
+ if (!process.env.AZURE_ARCH_SKILL_TARGET_ROOT && path.basename(target) !== SKILL_NAME) {
221
+ throw new Error(`refusing to install at ${target} - target basename must be '${SKILL_NAME}' (default ~/.claude/skills/${SKILL_NAME}/). Set AZURE_ARCH_SKILL_TARGET_ROOT to install into a custom test root.`);
222
+ }
223
+ // Detect partial vs complete prior installs across BUNDLE_FILES.
224
+ const presence = await Promise.all(BUNDLE_FILES.map(async ({ dest }) => ({
225
+ dest,
226
+ exists: await fs.stat(path.join(target, dest)).then(() => true).catch(() => false),
227
+ })));
228
+ const someExist = presence.some(p => p.exists);
229
+ const allExist = presence.every(p => p.exists);
230
+ if (someExist && !opts.overwrite) {
231
+ throw new Error(allExist
232
+ ? `${target} already contains an install. Run 'azure-arch-skill update --agent=claude-code' to refresh.`
233
+ : `${target} contains a partial install (${presence.filter(p => !p.exists).map(p => p.dest).join(", ")} missing). Run 'azure-arch-skill update --agent=claude-code' to repair.`);
234
+ }
235
+ const skillUrl = `${base}/${BUNDLE_FILES[0].src}`;
236
+ const skillMd = await fetchText(skillUrl);
237
+ const fm = parseFrontmatter(skillMd);
238
+ if (!fm.requires_icons) {
239
+ throw new Error("SKILL.md missing requires_icons frontmatter");
240
+ }
241
+ const canaryUrl = `${base}/${CANARY_ICON_PATH}`;
242
+ const reachable = await headOk(canaryUrl);
243
+ if (!reachable) {
244
+ throw new Error(`icon-set unreachable - HEAD ${canaryUrl} failed (skill declares requires_icons=${fm.requires_icons}; this release verifies reachability only, strict semver match planned)`);
245
+ }
246
+ // Mkdir the parent of every bundle dest so future deeper-nested entries work.
247
+ for (const { dest } of BUNDLE_FILES) {
248
+ await fs.mkdir(path.dirname(path.join(target, dest)), { recursive: true });
249
+ }
250
+ await fs.writeFile(path.join(target, BUNDLE_FILES[0].dest), skillMd, "utf8");
251
+ for (const { src, dest } of BUNDLE_FILES.slice(1)) {
252
+ const body = await fetchText(`${base}/${src}`);
253
+ await fs.writeFile(path.join(target, dest), body, "utf8");
254
+ }
255
+ process.stdout.write(`installed ${SKILL_NAME} to ${target}\n`);
256
+ return 0;
257
+ });
258
+ }
259
+ async function uninstall(opts) {
260
+ return withFatalReturn(async () => {
261
+ const target = await safeResolveTarget(opts.target ?? defaultTarget());
262
+ const exists = await fs.stat(target).then(() => true).catch(() => false);
263
+ if (!exists) {
264
+ process.stdout.write(`(nothing to uninstall at ${target})\n`);
265
+ return 0;
266
+ }
267
+ const ours = await isOurSkillDir(target);
268
+ if (!ours) {
269
+ throw new Error(`refusing to remove ${target} - not an azure-architecture-diagram skill folder (no matching SKILL.md). Move/rename the directory or remove it manually if intentional.`);
270
+ }
271
+ try {
272
+ await fs.rm(target, { recursive: true, force: false });
273
+ }
274
+ catch (err) {
275
+ const stillExists = await fs.stat(target).then(() => true).catch(() => false);
276
+ if (stillExists) {
277
+ const msg = err instanceof Error ? err.message : String(err);
278
+ throw new Error(`uninstall partially failed at ${target}: ${msg}; manual cleanup may be required`);
279
+ }
280
+ throw err;
281
+ }
282
+ process.stdout.write(`uninstalled ${SKILL_NAME} from ${target}\n`);
283
+ return 0;
284
+ });
285
+ }
286
+ async function update(opts) {
287
+ return install({ ...opts, overwrite: true });
288
+ }
289
+ async function list(opts) {
290
+ return withFatalReturn(async () => {
291
+ const root = await safeResolveTarget(opts.target ?? defaultSkillsRoot());
292
+ const exists = await fs.stat(root).then(() => true).catch(() => false);
293
+ if (!exists) {
294
+ process.stdout.write("(no skills installed)\n");
295
+ return 0;
296
+ }
297
+ const entries = await fs.readdir(root, { withFileTypes: true });
298
+ const rows = [];
299
+ for (const e of entries) {
300
+ if (!e.isDirectory())
301
+ continue;
302
+ const skillMdPath = path.join(root, e.name, "SKILL.md");
303
+ try {
304
+ const md = await fs.readFile(skillMdPath, "utf8");
305
+ const fm = parseFrontmatter(md);
306
+ rows.push(`${fm.name ?? e.name}\t${fm.version ?? "?"}`);
307
+ }
308
+ catch {
309
+ // not a skill folder; skip silently
310
+ }
311
+ }
312
+ process.stdout.write(rows.length ? rows.join("\n") + "\n" : "(no skills installed)\n");
313
+ return 0;
314
+ });
315
+ }
316
+ exports.claudeCodeAdapter = { install, uninstall, update, list };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/index.js ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const package_json_1 = __importDefault(require("../package.json"));
9
+ const claude_code_1 = require("./adapters/claude-code");
10
+ const ADAPTERS = {
11
+ "claude-code": claude_code_1.claudeCodeAdapter,
12
+ };
13
+ const SUPPORTED_AGENTS = Object.keys(ADAPTERS);
14
+ function pickAdapter(agent) {
15
+ if (!(agent in ADAPTERS)) {
16
+ throw new Error(`unknown agent: ${agent} (supported: ${SUPPORTED_AGENTS.join(", ")})`);
17
+ }
18
+ return ADAPTERS[agent];
19
+ }
20
+ const program = new commander_1.Command()
21
+ .name("azure-arch-skill")
22
+ .description("Install the Azure architecture diagram skill into your AI coding agent.")
23
+ .version(package_json_1.default.version, "-V, --version");
24
+ function defineSubcommand(name, description) {
25
+ program
26
+ .command(name)
27
+ .description(description)
28
+ .requiredOption("--agent <name>", `target AI agent (${SUPPORTED_AGENTS.join("|")})`)
29
+ .option("--target <dir>", "override target directory (validation use)")
30
+ .action(async (opts) => {
31
+ // Set process.exitCode so any pending async cleanup (file handles, the
32
+ // override-warning stderr write) drains before the event loop empties.
33
+ // Both this top-level path and adapter-internal failures emit a single
34
+ // '^fatal: ' prefix line on stderr — log-parsers can rely on the prefix.
35
+ process.exitCode = await pickAdapter(opts.agent)[name]({ target: opts.target });
36
+ });
37
+ }
38
+ defineSubcommand("install", "Install the skill into an AI agent's skill folder.");
39
+ defineSubcommand("uninstall", "Remove a previously installed skill.");
40
+ defineSubcommand("update", "Update an installed skill to the latest version.");
41
+ defineSubcommand("list", "List installed skills and their versions.");
42
+ program.parseAsync(process.argv).catch(err => {
43
+ process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}\n`);
44
+ process.exit(1);
45
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@hanv89/azure-arch-skill",
3
+ "version": "0.1.0-rc.1",
4
+ "description": "Install the Azure architecture diagram skill into your AI coding agent (Claude Code, Codex CLI, Cursor).",
5
+ "bin": {
6
+ "azure-arch-skill": "dist/index.js"
7
+ },
8
+ "main": "dist/index.js",
9
+ "files": [
10
+ "dist",
11
+ "!dist/**/*.test.*",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "start": "node dist/index.js",
18
+ "test": "tsc && node --test dist/adapters/*.test.js",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/hanv89/azure-icons-for-architecture-diagrams.git",
28
+ "directory": "packages/cli"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/hanv89/azure-icons-for-architecture-diagrams/issues"
32
+ },
33
+ "homepage": "https://github.com/hanv89/azure-icons-for-architecture-diagrams#readme",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "commander": "14.0.3"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "20.11.30",
42
+ "typescript": "5.4.5"
43
+ }
44
+ }