@wrongstack/plugins 0.281.3 → 0.282.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.
Files changed (84) hide show
  1. package/README.md +30 -4
  2. package/dist/accessibility-auditor.d.ts +40 -0
  3. package/dist/accessibility-auditor.js +411 -0
  4. package/dist/agent-handoff.d.ts +37 -0
  5. package/dist/agent-handoff.js +298 -0
  6. package/dist/api-compatibility-gate.d.ts +38 -0
  7. package/dist/api-compatibility-gate.js +357 -0
  8. package/dist/auto-escalate.d.ts +1 -1
  9. package/dist/auto-i18n-extractor.d.ts +36 -0
  10. package/dist/auto-i18n-extractor.js +335 -0
  11. package/dist/branch-guard.d.ts +6 -5
  12. package/dist/branch-guard.js +54 -4
  13. package/dist/checkpoint.js +18 -0
  14. package/dist/code-metrics.d.ts +31 -0
  15. package/dist/code-metrics.js +338 -0
  16. package/dist/commit-validator.js +67 -12
  17. package/dist/context-pins.js +4 -4
  18. package/dist/cost-tracker.d.ts +1 -1
  19. package/dist/cost-tracker.js +58 -19
  20. package/dist/cron.js +18 -8
  21. package/dist/dead-code-detector.d.ts +34 -0
  22. package/dist/dead-code-detector.js +354 -0
  23. package/dist/dep-guard.js +47 -6
  24. package/dist/dependency-vulnerability-gate.d.ts +35 -0
  25. package/dist/dependency-vulnerability-gate.js +308 -0
  26. package/dist/diff-summary.js +97 -10
  27. package/dist/doc-sync-guard.d.ts +33 -0
  28. package/dist/doc-sync-guard.js +223 -0
  29. package/dist/duplicate-code-detector.d.ts +33 -0
  30. package/dist/duplicate-code-detector.js +384 -0
  31. package/dist/feature-flag-tracker.d.ts +38 -0
  32. package/dist/feature-flag-tracker.js +316 -0
  33. package/dist/file-watcher.js +85 -36
  34. package/dist/format-on-save.js +99 -18
  35. package/dist/import-organizer.js +73 -14
  36. package/dist/index.d.ts +27 -0
  37. package/dist/index.js +11205 -2106
  38. package/dist/interface-contract-guard.d.ts +37 -0
  39. package/dist/interface-contract-guard.js +302 -0
  40. package/dist/knowledge-graph.d.ts +45 -0
  41. package/dist/knowledge-graph.js +325 -0
  42. package/dist/license-audit-gate.d.ts +34 -0
  43. package/dist/license-audit-gate.js +260 -0
  44. package/dist/llm-cache.js +5 -0
  45. package/dist/loop-breaker.d.ts +0 -38
  46. package/dist/loop-breaker.js +209 -8
  47. package/dist/migration-planner.d.ts +30 -0
  48. package/dist/migration-planner.js +349 -0
  49. package/dist/model-router.js +5 -0
  50. package/dist/performance-regression-gate.d.ts +33 -0
  51. package/dist/performance-regression-gate.js +315 -0
  52. package/dist/plugin-stack-observer.d.ts +35 -0
  53. package/dist/plugin-stack-observer.js +138 -0
  54. package/dist/pr-drafter.d.ts +35 -0
  55. package/dist/pr-drafter.js +334 -0
  56. package/dist/prompt-firewall.js +5 -0
  57. package/dist/refactor-suggester.d.ts +38 -0
  58. package/dist/refactor-suggester.js +382 -0
  59. package/dist/release-notes-generator.d.ts +27 -0
  60. package/dist/release-notes-generator.js +209 -0
  61. package/dist/schema-evolution-guard.d.ts +42 -0
  62. package/dist/schema-evolution-guard.js +319 -0
  63. package/dist/security-hotspot-scanner.d.ts +30 -0
  64. package/dist/security-hotspot-scanner.js +402 -0
  65. package/dist/semantic-search-indexer.d.ts +38 -0
  66. package/dist/semantic-search-indexer.js +436 -0
  67. package/dist/shell-check.js +38 -3
  68. package/dist/smart-rename.d.ts +26 -0
  69. package/dist/smart-rename.js +170 -0
  70. package/dist/spec-linker.js +273 -133
  71. package/dist/test-coverage-gate.d.ts +37 -0
  72. package/dist/test-coverage-gate.js +263 -0
  73. package/dist/test-flake-detector.d.ts +27 -0
  74. package/dist/test-flake-detector.js +274 -0
  75. package/dist/test-generator.d.ts +32 -0
  76. package/dist/test-generator.js +243 -0
  77. package/dist/test-runner-gate.js +154 -22
  78. package/dist/todo-listener.d.ts +2 -2
  79. package/dist/todo-listener.js +5 -5
  80. package/dist/token-throttle.js +5 -0
  81. package/dist/type-gate.d.ts +37 -0
  82. package/dist/type-gate.js +311 -0
  83. package/package.json +112 -4
  84. package/LICENSE +0 -21
@@ -0,0 +1,316 @@
1
+ import { readFileSync, existsSync, statSync, readdirSync } from 'fs';
2
+ import { resolve, isAbsolute, relative, extname } from 'path';
3
+
4
+ // src/feature-flag-tracker/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ scanCount: 0,
8
+ flagCount: 0,
9
+ hookInvocationCount: 0,
10
+ warningCount: 0,
11
+ errorCount: 0,
12
+ hookUnregister: null
13
+ };
14
+ var DEFAULT_PATTERNS = [
15
+ String.raw`isFeatureEnabled\(['"]([^'"]+)['"]\)`,
16
+ String.raw`featureFlags?\.([A-Za-z_$][A-Za-z0-9_$]*)`,
17
+ String.raw`useFeatureFlag\(['"]([^'"]+)['"]\)`,
18
+ String.raw`flags\.([A-Za-z_$][A-Za-z0-9_$]*)`
19
+ ];
20
+ var DEFAULTS = {
21
+ enabled: false,
22
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
23
+ patterns: [],
24
+ maxFindings: 50
25
+ };
26
+ function readConfig(raw) {
27
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
28
+ const r = raw;
29
+ return {
30
+ enabled: r["enabled"] !== false,
31
+ extensions: Array.isArray(r["extensions"]) ? r["extensions"].filter((x) => typeof x === "string") : DEFAULTS.extensions,
32
+ patterns: Array.isArray(r["patterns"]) ? r["patterns"].filter((x) => typeof x === "string") : DEFAULTS.patterns,
33
+ maxFindings: typeof r["maxFindings"] === "number" && r["maxFindings"] >= 1 && r["maxFindings"] <= 500 ? r["maxFindings"] : DEFAULTS.maxFindings
34
+ };
35
+ }
36
+ function withinProject(p) {
37
+ if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
38
+ const root = process.cwd();
39
+ const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
40
+ const rel = relative(root, resolved);
41
+ if (rel === "" || rel === ".") return true;
42
+ if (rel.startsWith("..")) return false;
43
+ if (isAbsolute(rel)) return false;
44
+ return true;
45
+ }
46
+ function normalizeExtensions(exts) {
47
+ return exts.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`);
48
+ }
49
+ function matchesExtension(p, exts) {
50
+ return exts.includes(extname(p).toLowerCase());
51
+ }
52
+ function collectSourceFiles(root, exts) {
53
+ const files = [];
54
+ if (!existsSync(root)) return files;
55
+ const s = statSync(root);
56
+ if (s.isFile()) {
57
+ if (matchesExtension(root, exts)) files.push(root);
58
+ return files;
59
+ }
60
+ if (!s.isDirectory()) return files;
61
+ function walk(dir) {
62
+ let entries;
63
+ try {
64
+ entries = readdirSync(dir);
65
+ } catch {
66
+ return;
67
+ }
68
+ for (const entry of entries) {
69
+ if (entry === "node_modules" || entry === "dist" || entry === ".git" || entry === "coverage") continue;
70
+ const full = resolve(dir, entry);
71
+ let st;
72
+ try {
73
+ st = statSync(full);
74
+ } catch {
75
+ continue;
76
+ }
77
+ if (st.isDirectory()) {
78
+ walk(full);
79
+ } else if (st.isFile() && matchesExtension(full, exts)) {
80
+ files.push(full);
81
+ }
82
+ }
83
+ }
84
+ walk(root);
85
+ return files;
86
+ }
87
+ function toPosix(p) {
88
+ return p.replace(/\\/g, "/");
89
+ }
90
+ function relativePath(p) {
91
+ return toPosix(relative(process.cwd(), p));
92
+ }
93
+ function compilePatterns(patterns) {
94
+ const sources = [...DEFAULT_PATTERNS, ...patterns];
95
+ return sources.map((src) => new RegExp(src, "g"));
96
+ }
97
+ function scanFile(filePath, content, patterns, maxFindings) {
98
+ const usages = [];
99
+ const lines = content.split(/\r?\n/);
100
+ for (const re of patterns) {
101
+ re.lastIndex = 0;
102
+ let m;
103
+ while ((m = re.exec(content)) !== null) {
104
+ const flag = m[1] ?? m[0];
105
+ const lineNo = content.slice(0, m.index).split(/\r?\n/).length;
106
+ const context = (lines[lineNo - 1] ?? "").trim();
107
+ usages.push({
108
+ flag,
109
+ file: relativePath(filePath),
110
+ line: lineNo,
111
+ context,
112
+ pattern: re.source
113
+ });
114
+ if (usages.length >= maxFindings) break;
115
+ }
116
+ if (usages.length >= maxFindings) break;
117
+ }
118
+ return usages.slice(0, maxFindings);
119
+ }
120
+ function scanPath(rawPath, cfg) {
121
+ const root = process.cwd();
122
+ const resolved = isAbsolute(rawPath) ? resolve(rawPath) : resolve(root, rawPath);
123
+ const exts = normalizeExtensions(cfg.extensions);
124
+ const files = collectSourceFiles(resolved, exts);
125
+ const patterns = compilePatterns(cfg.patterns);
126
+ const usages = [];
127
+ for (const p of files) {
128
+ try {
129
+ const content = readFileSync(p, "utf-8");
130
+ usages.push(...scanFile(p, content, patterns, cfg.maxFindings));
131
+ if (usages.length >= cfg.maxFindings) break;
132
+ } catch {
133
+ }
134
+ }
135
+ return { usages: usages.slice(0, cfg.maxFindings), scannedFiles: files.length };
136
+ }
137
+ var plugin = {
138
+ name: "feature-flag-tracker",
139
+ version: "0.1.0",
140
+ description: "Scans source files for feature-flag-like expressions and reports usages",
141
+ apiVersion: API_VERSION,
142
+ capabilities: { tools: true, hooks: true },
143
+ defaultConfig: { ...DEFAULTS },
144
+ configSchema: {
145
+ type: "object",
146
+ properties: {
147
+ enabled: { type: "boolean", default: true, description: "Master switch." },
148
+ extensions: {
149
+ type: "array",
150
+ items: { type: "string" },
151
+ default: [".ts", ".tsx", ".js", ".jsx"],
152
+ description: "File extensions to scan."
153
+ },
154
+ patterns: {
155
+ type: "array",
156
+ items: { type: "string" },
157
+ default: [],
158
+ description: "Extra regex patterns (merged with built-in defaults)."
159
+ },
160
+ maxFindings: {
161
+ type: "number",
162
+ minimum: 1,
163
+ maximum: 500,
164
+ default: 50,
165
+ description: "Maximum flag usages reported per scan."
166
+ }
167
+ }
168
+ },
169
+ setup(api) {
170
+ state.scanCount = 0;
171
+ state.flagCount = 0;
172
+ state.hookInvocationCount = 0;
173
+ state.warningCount = 0;
174
+ state.errorCount = 0;
175
+ if (state.hookUnregister) {
176
+ try {
177
+ state.hookUnregister();
178
+ } catch {
179
+ }
180
+ state.hookUnregister = null;
181
+ }
182
+ const cfg = readConfig(api.config.extensions?.["feature-flag-tracker"]);
183
+ const hook = (input) => {
184
+ if (!cfg.enabled) return;
185
+ if (input.toolResult?.isError) return;
186
+ const inp = input.toolInput ?? {};
187
+ const sourcePath = inp["path"];
188
+ if (!sourcePath || typeof sourcePath !== "string") return;
189
+ if (!withinProject(sourcePath)) return;
190
+ const exts = normalizeExtensions(cfg.extensions);
191
+ if (!matchesExtension(sourcePath, exts)) return;
192
+ state.hookInvocationCount += 1;
193
+ const resolved = resolve(process.cwd(), sourcePath);
194
+ let content;
195
+ try {
196
+ content = readFileSync(resolved, "utf-8");
197
+ } catch {
198
+ state.errorCount += 1;
199
+ return;
200
+ }
201
+ const patterns = compilePatterns(cfg.patterns);
202
+ const usages = scanFile(resolved, content, patterns, cfg.maxFindings);
203
+ if (usages.length === 0) return;
204
+ state.warningCount += usages.length;
205
+ const flags = [...new Set(usages.map((u) => u.flag))].join(", ");
206
+ return {
207
+ additionalContext: `
208
+ \u{1F6A9} feature-flag-tracker: ${sourcePath} references feature flag(s): ${flags}.
209
+ Make sure flag behavior is intentional and consider updating flag inventory/docs.`
210
+ };
211
+ };
212
+ state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
213
+ api.tools.register({
214
+ name: "scan_feature_flags",
215
+ description: "Scan source files for feature-flag-like expressions (isFeatureEnabled, featureFlags.*, useFeatureFlag, flags.*, plus custom patterns).",
216
+ inputSchema: {
217
+ type: "object",
218
+ properties: {
219
+ path: { type: "string", default: ".", description: "File or directory path to scan." }
220
+ }
221
+ },
222
+ permission: "auto",
223
+ category: "Diagnostics",
224
+ mutating: false,
225
+ async execute(input) {
226
+ if (!cfg.enabled) return { ok: false, error: "feature-flag-tracker is disabled" };
227
+ const rawPath = typeof input.path === "string" ? input.path : ".";
228
+ if (!withinProject(rawPath)) {
229
+ return { ok: false, error: "path is outside the project root" };
230
+ }
231
+ state.scanCount += 1;
232
+ let result;
233
+ try {
234
+ result = scanPath(rawPath, cfg);
235
+ } catch (err) {
236
+ state.errorCount += 1;
237
+ return { ok: false, error: String(err) };
238
+ }
239
+ state.flagCount += result.usages.length;
240
+ return {
241
+ ok: true,
242
+ path: relativePath(resolve(process.cwd(), rawPath)),
243
+ scannedFiles: result.scannedFiles,
244
+ usages: result.usages
245
+ };
246
+ }
247
+ });
248
+ api.tools.register({
249
+ name: "feature_flag_status",
250
+ description: "Reports feature-flag-tracker state: config + counters.",
251
+ inputSchema: { type: "object", properties: {} },
252
+ permission: "auto",
253
+ category: "Diagnostics",
254
+ mutating: false,
255
+ async execute() {
256
+ return {
257
+ ok: true,
258
+ enabled: cfg.enabled,
259
+ extensions: cfg.extensions,
260
+ patterns: cfg.patterns,
261
+ maxFindings: cfg.maxFindings,
262
+ counters: {
263
+ scans: state.scanCount,
264
+ flags: state.flagCount,
265
+ hookInvocations: state.hookInvocationCount,
266
+ warnings: state.warningCount,
267
+ errors: state.errorCount
268
+ }
269
+ };
270
+ }
271
+ });
272
+ api.log.info("feature-flag-tracker plugin loaded", {
273
+ version: "0.1.0",
274
+ extensions: cfg.extensions,
275
+ customPatterns: cfg.patterns.length
276
+ });
277
+ },
278
+ teardown(api) {
279
+ if (state.hookUnregister) {
280
+ try {
281
+ state.hookUnregister();
282
+ } catch {
283
+ }
284
+ state.hookUnregister = null;
285
+ }
286
+ const final = {
287
+ scans: state.scanCount,
288
+ flags: state.flagCount,
289
+ hookInvocations: state.hookInvocationCount,
290
+ warnings: state.warningCount,
291
+ errors: state.errorCount
292
+ };
293
+ state.scanCount = 0;
294
+ state.flagCount = 0;
295
+ state.hookInvocationCount = 0;
296
+ state.warningCount = 0;
297
+ state.errorCount = 0;
298
+ api.log.info("feature-flag-tracker: teardown complete", { final });
299
+ },
300
+ async health() {
301
+ return {
302
+ ok: state.errorCount === 0,
303
+ message: state.errorCount ? `feature-flag-tracker: ${state.errorCount} error(s)` : `feature-flag-tracker: ${state.scanCount} scan(s), ${state.flagCount} flag usage(s)`,
304
+ counters: {
305
+ scans: state.scanCount,
306
+ flags: state.flagCount,
307
+ hookInvocations: state.hookInvocationCount,
308
+ warnings: state.warningCount,
309
+ errors: state.errorCount
310
+ }
311
+ };
312
+ }
313
+ };
314
+ var feature_flag_tracker_default = plugin;
315
+
316
+ export { feature_flag_tracker_default as default };
@@ -1,8 +1,18 @@
1
1
  import { watch } from 'fs';
2
- import * as path from 'path';
2
+ import { isAbsolute, resolve, relative } from 'path';
3
3
 
4
4
  // src/file-watcher/index.ts
5
5
  var API_VERSION = "^0.1.10";
6
+ function withinProject(p) {
7
+ if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
8
+ const root = process.cwd();
9
+ const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
10
+ const rel = relative(root, resolved);
11
+ if (rel === "" || rel === ".") return true;
12
+ if (rel.startsWith("..")) return false;
13
+ if (isAbsolute(rel)) return false;
14
+ return true;
15
+ }
6
16
  var watch_idCounter = 0;
7
17
  function nextId() {
8
18
  return `watch_${++watch_idCounter}_${Date.now().toString(36)}`;
@@ -71,16 +81,30 @@ var plugin = {
71
81
  function debounceEvent(key, fn, ms) {
72
82
  const existing = debounceTimers.get(key);
73
83
  if (existing) clearTimeout(existing);
74
- debounceTimers.set(key, setTimeout(() => {
75
- debounceTimers.delete(key);
76
- fn();
77
- }, ms));
84
+ debounceTimers.set(
85
+ key,
86
+ setTimeout(() => {
87
+ debounceTimers.delete(key);
88
+ fn();
89
+ }, ms)
90
+ );
78
91
  }
79
92
  const autoIndex = api.config.extensions?.["file-watcher"]?.["autoIndex"] ?? false;
80
93
  const indexProjectRoot = api.config.extensions?.["file-watcher"]?.["indexProjectRoot"] ?? "";
94
+ const safeIndexRoot = indexProjectRoot !== "" && withinProject(indexProjectRoot) ? indexProjectRoot : "";
95
+ if (indexProjectRoot !== "" && safeIndexRoot === "") {
96
+ api.log.warn(
97
+ "file-watcher: indexProjectRoot is outside the project root \u2014 using watched dirPath instead",
98
+ {
99
+ indexProjectRoot
100
+ }
101
+ );
102
+ }
81
103
  const INDEXABLE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
82
104
  function isIndexableFile(filePath) {
83
- return INDEXABLE_EXTENSIONS.has(path.extname(filePath));
105
+ const dot = filePath.lastIndexOf(".");
106
+ const ext = dot >= 0 ? filePath.slice(dot).toLowerCase() : "";
107
+ return INDEXABLE_EXTENSIONS.has(ext);
84
108
  }
85
109
  function safeWatchDir(dirPath, recursive, handle) {
86
110
  try {
@@ -88,34 +112,42 @@ var plugin = {
88
112
  if (!filename) return;
89
113
  const fullPath = `${dirPath}/${filename}`;
90
114
  const key = `${handle.id}:${fullPath}:${eventType}`;
91
- debounceEvent(key, () => {
92
- api.emitCustom("file-watcher:changed", {
93
- watch_id: handle.id,
94
- path: fullPath,
95
- event: eventType,
96
- filename,
97
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
98
- });
99
- api.metrics.counter("file_change", 1, { event: eventType ?? "unknown" });
100
- api.log.debug(`file-watcher: ${eventType} ${fullPath} (watch=${handle.id})`);
101
- if (autoIndex && isIndexableFile(fullPath)) {
102
- debounceEvent(`index:${fullPath}`, async () => {
103
- try {
104
- const { enqueueReindex } = await import('@wrongstack/tools/codebase-index');
105
- const root = indexProjectRoot || dirPath;
106
- enqueueReindex({
107
- projectRoot: root,
108
- files: [fullPath],
109
- onError: (err) => api.log.warn(`file-watcher: auto-index failed for ${fullPath}: ${err}`)
110
- });
111
- api.metrics.counter("index_file", 1);
112
- api.log.debug(`file-watcher: auto-index scheduled for ${fullPath}`);
113
- } catch (err) {
114
- api.log.warn(`file-watcher: auto-index failed for ${fullPath}: ${err}`);
115
- }
116
- }, debounceMs);
117
- }
118
- }, debounceMs);
115
+ debounceEvent(
116
+ key,
117
+ () => {
118
+ api.emitCustom("file-watcher:changed", {
119
+ watch_id: handle.id,
120
+ path: fullPath,
121
+ event: eventType,
122
+ filename,
123
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
124
+ });
125
+ api.metrics.counter("file_change", 1, { event: eventType ?? "unknown" });
126
+ api.log.debug(`file-watcher: ${eventType} ${fullPath} (watch=${handle.id})`);
127
+ if (autoIndex && isIndexableFile(fullPath)) {
128
+ debounceEvent(
129
+ `index:${fullPath}`,
130
+ async () => {
131
+ try {
132
+ const { enqueueReindex } = await import('@wrongstack/tools/codebase-index');
133
+ const root = safeIndexRoot || dirPath;
134
+ enqueueReindex({
135
+ projectRoot: root,
136
+ files: [fullPath],
137
+ onError: (err) => api.log.warn(`file-watcher: auto-index failed for ${fullPath}: ${err}`)
138
+ });
139
+ api.metrics.counter("index_file", 1);
140
+ api.log.debug(`file-watcher: auto-index scheduled for ${fullPath}`);
141
+ } catch (err) {
142
+ api.log.warn(`file-watcher: auto-index failed for ${fullPath}: ${err}`);
143
+ }
144
+ },
145
+ debounceMs
146
+ );
147
+ }
148
+ },
149
+ debounceMs
150
+ );
119
151
  });
120
152
  watcher.on("error", (err) => {
121
153
  api.log.warn(`file-watcher: error on ${dirPath}: ${err}`);
@@ -156,14 +188,31 @@ var plugin = {
156
188
  async execute(input) {
157
189
  const rawPaths = input["paths"];
158
190
  if (!rawPaths || typeof rawPaths !== "object" || !Array.isArray(rawPaths)) {
159
- return { ok: false, error: "paths must be an array of file/directory paths", watch_id: null };
191
+ return {
192
+ ok: false,
193
+ error: "paths must be an array of file/directory paths",
194
+ watch_id: null
195
+ };
160
196
  }
161
197
  const paths = rawPaths;
162
198
  if (paths.length === 0) {
163
- return { ok: false, error: "paths array is empty \u2014 provide at least one path", watch_id: null };
199
+ return {
200
+ ok: false,
201
+ error: "paths array is empty \u2014 provide at least one path",
202
+ watch_id: null
203
+ };
164
204
  }
165
205
  const events = input["events"] ?? ["change", "add", "delete"];
166
206
  const recursive = input["recursive"] ?? true;
207
+ const bad = paths.find((p) => !withinProject(p));
208
+ if (bad !== void 0) {
209
+ return {
210
+ ok: false,
211
+ error: `path is outside the project root: ${bad}`,
212
+ watch_id: null,
213
+ rejectedOutsideProject: true
214
+ };
215
+ }
167
216
  const id = nextId();
168
217
  const handle = {
169
218
  id,