@adhd/apigen-python-env 0.2.7 → 0.2.8

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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 0.2.8 (2026-10-21)
2
+
3
+ ### 🩹 Fixes
4
+
5
+ - **apigen-python-env:** do not report a timeout for a child that exited cleanly and is still draining stdout ([8c0e6216](https://github.com/PseudoSky/adhd/commit/8c0e6216))
6
+ - **apigen-python-env:** settle extractor timeout on exit or close and destroy pipes after kill ([06e642a7](https://github.com/PseudoSky/adhd/commit/06e642a7))
7
+ - **apigen-python-env:** settle extractor on close and bound timeoutMs to setTimeout range ([5d46329d](https://github.com/PseudoSky/adhd/commit/5d46329d))
8
+ - **apigen-python-env:** bound the extractor run with a timeout that kills the child ([d8d8c1c5](https://github.com/PseudoSky/adhd/commit/d8d8c1c5))
9
+ - **workspace:** restore the publish gate's heavy lane and canonicalise the gate ([45532731](https://github.com/PseudoSky/adhd/commit/45532731))
10
+
11
+ ### ❤️ Thank You
12
+
13
+ - pseudosky
14
+
1
15
  ## 0.2.6 (2026-09-26)
2
16
 
3
17
  ### 🚀 Features
package/README.md CHANGED
@@ -33,6 +33,11 @@ const { python, venvDir, pythonPkgDir } = ensurePythonEnv({ extras: ['grpc'] })
33
33
  concurrent `serve` children don't double-provision.
34
34
  - **`resolvePythonPkgDir()`** — locates the `apigen_python` sources (shared by
35
35
  both py plugins; also exported for tools like the conformance gate).
36
+ - **`runExtractorEmitJson(python, pythonPkgDir, modulePath, namespace, label, { timeoutMs })`**
37
+ — runs `apigen_python.extractor --emit-json` to completion and parses its
38
+ operations. `timeoutMs` defaults to `120000`.
39
+ On expiry the child is killed with SIGKILL and the promise rejects with an error
40
+ naming the command; the rejection is raised only after the child has exited.
36
41
 
37
42
  Environment knobs:
38
43
 
package/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { ensurePythonEnv, resolvePythonPkgDir } from './lib/python-env';
2
2
  export type { IPythonEnv, IEnsurePythonEnvOptions } from './lib/python-env';
3
- export { runExtractorEmitJson } from './lib/run-extractor';
3
+ export { runExtractorEmitJson, DEFAULT_EXTRACTOR_TIMEOUT_MS } from './lib/run-extractor';
4
+ export type { IRunExtractorOptions } from './lib/run-extractor';
package/index.js CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));let c=require("node:fs");c=s(c);let l=require("node:os");l=s(l);let u=require("node:path");u=s(u);let d=require("node:crypto"),f=require("node:child_process");function p(e){let t=e??__dirname,n=u.resolve(t,`..`,`..`,`..`,`python`);if(c.existsSync(u.join(n,`apigen_python`)))return n;let r=u.join(t,`python`);if(c.existsSync(u.join(r,`apigen_python`)))return r;let i=t;for(let e=0;e<20;e++){let e=u.join(i,`packages`,`apigen`,`python`);if(c.existsSync(u.join(e,`apigen_python`)))return e;let t=u.dirname(i);if(t===i)break;i=t}return n}var m=[`python3.13`,`python3.12`,`python3.11`,`python3`],h=[3,11];function g(e){let t=(0,f.spawnSync)(e,[`-c`,`import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")`],{encoding:`utf8`,timeout:15e3});if(t.status!==0||!t.stdout)return;let[n,r]=t.stdout.trim().split(`.`).map(Number);if(Number.isFinite(n)&&Number.isFinite(r))return[n,r]}function _(){for(let e of m){let t=g(e);if(t&&(t[0]>h[0]||t[0]===h[0]&&t[1]>=h[1]))return e}throw Error(`apigen-python-env: no Python >= ${h.join(`.`)} found on PATH (tried: ${m.join(`, `)}). Install Python ${h.join(`.`)}+ or set APIGEN_PYTHON to a provisioned interpreter.`)}function v(e){return process.platform===`win32`?u.join(e,`Scripts`,`python.exe`):u.join(e,`bin`,`python3`)}function y(e){let t=c.readFileSync(u.join(e,`pyproject.toml`));return(0,d.createHash)(`sha256`).update(t).digest(`hex`)}function b(e){try{let t=JSON.parse(c.readFileSync(e,`utf8`));if(typeof t.pyprojectHash==`string`&&Array.isArray(t.extras))return t}catch{}}function x(e,t,n){let r=Date.now()+t;for(;;)try{c.mkdirSync(e,{recursive:!1});break}catch{if(Date.now()>r){try{c.rmdirSync(e)}catch{}continue}(0,f.spawnSync)(process.execPath,[`-e`,`setTimeout(()=>{}, 250)`],{timeout:5e3})}try{return n()}finally{try{c.rmdirSync(e)}catch{}}}function S(e={}){let t=p(),n=process.env.APIGEN_PYTHON;if(n){if(!c.existsSync(n))throw Error(`apigen-python-env: APIGEN_PYTHON points to a missing file: ${n}`);return{python:n,venvDir:``,pythonPkgDir:t}}let r=[...new Set(e.extras??[])].sort(),i=e.envHome??process.env.APIGEN_PYENV_HOME??u.join(l.homedir(),`.adhd`,`apigen`),a=u.join(i,`pyvenv`),o=u.join(a,`.apigen-stamp`),s=y(t),d=v(a),m=()=>{if(!c.existsSync(d))return!1;let e=b(o);return e?.pyprojectHash===s&&r.every(t=>e.extras.includes(t))};return m()?{python:d,venvDir:a,pythonPkgDir:t}:(c.mkdirSync(i,{recursive:!0}),x(u.join(i,`pyvenv.lock`),e.lockTimeoutMs??3e5,()=>{if(m())return{python:d,venvDir:a,pythonPkgDir:t};let e=b(o),n=[...new Set([...e?.extras??[],...r])].sort();c.rmSync(a,{recursive:!0,force:!0});let i=_(),p=(0,f.spawnSync)(i,[`-m`,`venv`,a],{encoding:`utf8`,timeout:12e4});if(p.status!==0)throw Error(`apigen-python-env: "${i} -m venv" failed (exit ${p.status}):\n${p.stderr}`);let h=u.join(t,`build`);c.existsSync(h)&&c.rmSync(h,{recursive:!0,force:!0});for(let e of c.readdirSync(t))e.endsWith(`.egg-info`)&&c.rmSync(u.join(t,e),{recursive:!0,force:!0});let g=c.mkdtempSync(u.join(l.tmpdir(),`apigen-python-wheel-`));try{let e=(0,f.spawnSync)(d,[`-m`,`pip`,`wheel`,`--no-deps`,`--wheel-dir`,g,t],{encoding:`utf8`,timeout:6e5});if(e.status!==0)throw Error(`apigen-python-env: pip wheel failed (exit ${e.status}):\n${e.stderr}\nstdout:\n${e.stdout}`);let r=c.readdirSync(g).filter(e=>e.endsWith(`.whl`));if(r.length===0)throw Error(`apigen-python-env: pip wheel produced no .whl file`);let i=u.join(g,r[0]),a=n.length?`${i}[${n.join(`,`)}]`:i,o=(0,f.spawnSync)(d,[`-m`,`pip`,`install`,`--quiet`,`--disable-pip-version-check`,a],{encoding:`utf8`,timeout:6e5});if(o.status!==0)throw Error(`apigen-python-env: pip install "${a}" failed (exit ${o.status}):\n${o.stderr}\nstdout:\n${o.stdout}`)}finally{c.rmSync(g,{recursive:!0,force:!0})}let v={pyprojectHash:s,extras:n};return c.writeFileSync(o,JSON.stringify(v)),{python:d,venvDir:a,pythonPkgDir:t}}))}function C(e,t,n,r,i){return new Promise((a,o)=>{let s=(0,f.spawn)(e,[`-m`,`apigen_python.extractor`,n,`--namespace`,r,`--emit-json`],{cwd:t,env:{...process.env,PYTHONPATH:t},stdio:[`ignore`,`pipe`,`pipe`]}),c=``,l=``;s.stdout?.on(`data`,e=>{c+=e.toString(`utf8`)}),s.stderr?.on(`data`,e=>{l+=e.toString(`utf8`)}),s.on(`error`,o),s.on(`exit`,e=>{if(e!==0){o(Error(`${i}: extractor --emit-json exited with code ${e}:\n${l}`));return}try{a(JSON.parse(c))}catch(e){o(Error(`${i}: extractor --emit-json produced invalid JSON (${e.message}):\n${c}`))}})})}exports.ensurePythonEnv=S,exports.resolvePythonPkgDir=p,exports.runExtractorEmitJson=C;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));let c=require("node:fs");c=s(c);let l=require("node:os");l=s(l);let u=require("node:path");u=s(u);let d=require("node:crypto"),f=require("node:child_process");function p(e){let t=e??__dirname,n=u.resolve(t,`..`,`..`,`..`,`python`);if(c.existsSync(u.join(n,`apigen_python`)))return n;let r=u.join(t,`python`);if(c.existsSync(u.join(r,`apigen_python`)))return r;let i=t;for(let e=0;e<20;e++){let e=u.join(i,`packages`,`apigen`,`python`);if(c.existsSync(u.join(e,`apigen_python`)))return e;let t=u.dirname(i);if(t===i)break;i=t}return n}var m=[`python3.13`,`python3.12`,`python3.11`,`python3`],h=[3,11];function g(e){let t=(0,f.spawnSync)(e,[`-c`,`import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")`],{encoding:`utf8`,timeout:15e3});if(t.status!==0||!t.stdout)return;let[n,r]=t.stdout.trim().split(`.`).map(Number);if(Number.isFinite(n)&&Number.isFinite(r))return[n,r]}function _(){for(let e of m){let t=g(e);if(t&&(t[0]>h[0]||t[0]===h[0]&&t[1]>=h[1]))return e}throw Error(`apigen-python-env: no Python >= ${h.join(`.`)} found on PATH (tried: ${m.join(`, `)}). Install Python ${h.join(`.`)}+ or set APIGEN_PYTHON to a provisioned interpreter.`)}function v(e){return process.platform===`win32`?u.join(e,`Scripts`,`python.exe`):u.join(e,`bin`,`python3`)}function y(e){let t=c.readFileSync(u.join(e,`pyproject.toml`));return(0,d.createHash)(`sha256`).update(t).digest(`hex`)}function b(e){try{let t=JSON.parse(c.readFileSync(e,`utf8`));if(typeof t.pyprojectHash==`string`&&Array.isArray(t.extras))return t}catch{}}function x(e,t,n){let r=Date.now()+t;for(;;)try{c.mkdirSync(e,{recursive:!1});break}catch{if(Date.now()>r){try{c.rmdirSync(e)}catch{}continue}(0,f.spawnSync)(process.execPath,[`-e`,`setTimeout(()=>{}, 250)`],{timeout:5e3})}try{return n()}finally{try{c.rmdirSync(e)}catch{}}}function S(e={}){let t=p(),n=process.env.APIGEN_PYTHON;if(n){if(!c.existsSync(n))throw Error(`apigen-python-env: APIGEN_PYTHON points to a missing file: ${n}`);return{python:n,venvDir:``,pythonPkgDir:t}}let r=[...new Set(e.extras??[])].sort(),i=e.envHome??process.env.APIGEN_PYENV_HOME??u.join(l.homedir(),`.adhd`,`apigen`),a=u.join(i,`pyvenv`),o=u.join(a,`.apigen-stamp`),s=y(t),d=v(a),m=()=>{if(!c.existsSync(d))return!1;let e=b(o);return e?.pyprojectHash===s&&r.every(t=>e.extras.includes(t))};return m()?{python:d,venvDir:a,pythonPkgDir:t}:(c.mkdirSync(i,{recursive:!0}),x(u.join(i,`pyvenv.lock`),e.lockTimeoutMs??3e5,()=>{if(m())return{python:d,venvDir:a,pythonPkgDir:t};let e=b(o),n=[...new Set([...e?.extras??[],...r])].sort();c.rmSync(a,{recursive:!0,force:!0});let i=_(),p=(0,f.spawnSync)(i,[`-m`,`venv`,a],{encoding:`utf8`,timeout:12e4});if(p.status!==0)throw Error(`apigen-python-env: "${i} -m venv" failed (exit ${p.status}):\n${p.stderr}`);let h=u.join(t,`build`);c.existsSync(h)&&c.rmSync(h,{recursive:!0,force:!0});for(let e of c.readdirSync(t))e.endsWith(`.egg-info`)&&c.rmSync(u.join(t,e),{recursive:!0,force:!0});let g=c.mkdtempSync(u.join(l.tmpdir(),`apigen-python-wheel-`));try{let e=(0,f.spawnSync)(d,[`-m`,`pip`,`wheel`,`--no-deps`,`--wheel-dir`,g,t],{encoding:`utf8`,timeout:6e5});if(e.status!==0)throw Error(`apigen-python-env: pip wheel failed (exit ${e.status}):\n${e.stderr}\nstdout:\n${e.stdout}`);let r=c.readdirSync(g).filter(e=>e.endsWith(`.whl`));if(r.length===0)throw Error(`apigen-python-env: pip wheel produced no .whl file`);let i=u.join(g,r[0]),a=n.length?`${i}[${n.join(`,`)}]`:i,o=(0,f.spawnSync)(d,[`-m`,`pip`,`install`,`--quiet`,`--disable-pip-version-check`,a],{encoding:`utf8`,timeout:6e5});if(o.status!==0)throw Error(`apigen-python-env: pip install "${a}" failed (exit ${o.status}):\n${o.stderr}\nstdout:\n${o.stdout}`)}finally{c.rmSync(g,{recursive:!0,force:!0})}let v={pyprojectHash:s,extras:n};return c.writeFileSync(o,JSON.stringify(v)),{python:d,venvDir:a,pythonPkgDir:t}}))}var C=12e4,w=2147483647;function T(e,t,n,r,i,a={}){let o=a.timeoutMs??12e4;if(!Number.isFinite(o)||o<=0)return Promise.reject(RangeError(`${i}: extractor timeoutMs must be a positive finite number, got ${o}`));if(o>w)return Promise.reject(RangeError(`${i}: extractor timeoutMs must not exceed ${w} ms (the setTimeout maximum), got ${o}`));let s=[`-m`,`apigen_python.extractor`,n,`--namespace`,r,`--emit-json`],c=[e,...s].join(` `);return new Promise((n,r)=>{let a=(0,f.spawn)(e,s,{cwd:t,env:{...process.env,PYTHONPATH:t},stdio:[`ignore`,`pipe`,`pipe`]}),l=``,u=``,d=!1,p=!1,m=!1,h=e=>{m||(m=!0,clearTimeout(y),e())},g=()=>Error(`${i}: extractor --emit-json timed out after ${o} ms and was killed (command: ${c})${u?`:\n${u}`:``}`),_=!1,v=()=>{if(p&&_){_=!1,y=setTimeout(v,o);return}d=!0,a.kill(`SIGKILL`),a.stdout?.destroy(),a.stderr?.destroy(),p&&h(()=>r(g()))},y=setTimeout(v,o);a.stdout?.on(`data`,e=>{p&&(_=!0),l+=e.toString(`utf8`)}),a.stderr?.on(`data`,e=>{u+=e.toString(`utf8`)}),a.on(`error`,e=>{h(()=>r(e))}),a.on(`exit`,()=>{p=!0,d&&h(()=>r(g()))}),a.on(`close`,e=>{h(()=>{if(d){r(g());return}if(e!==0){r(Error(`${i}: extractor --emit-json exited with code ${e}:\n${u}`));return}try{n(JSON.parse(l))}catch(e){r(Error(`${i}: extractor --emit-json produced invalid JSON (${e.message}):\n${l}`))}})})})}exports.DEFAULT_EXTRACTOR_TIMEOUT_MS=C,exports.ensurePythonEnv=S,exports.resolvePythonPkgDir=p,exports.runExtractorEmitJson=T;
package/index.mjs CHANGED
@@ -172,16 +172,21 @@ function h(r = {}) {
172
172
  }
173
173
  //#endregion
174
174
  //#region src/lib/run-extractor.ts
175
- function g(e, t, n, r, a) {
176
- return new Promise((o, s) => {
177
- let c = i(e, [
178
- "-m",
179
- "apigen_python.extractor",
180
- n,
181
- "--namespace",
182
- r,
183
- "--emit-json"
184
- ], {
175
+ var g = 12e4, _ = 2147483647;
176
+ function v(e, t, n, r, a, o = {}) {
177
+ let s = o.timeoutMs ?? 12e4;
178
+ if (!Number.isFinite(s) || s <= 0) return Promise.reject(/* @__PURE__ */ RangeError(`${a}: extractor timeoutMs must be a positive finite number, got ${s}`));
179
+ if (s > _) return Promise.reject(/* @__PURE__ */ RangeError(`${a}: extractor timeoutMs must not exceed ${_} ms (the setTimeout maximum), got ${s}`));
180
+ let c = [
181
+ "-m",
182
+ "apigen_python.extractor",
183
+ n,
184
+ "--namespace",
185
+ r,
186
+ "--emit-json"
187
+ ], l = [e, ...c].join(" ");
188
+ return new Promise((n, r) => {
189
+ let o = i(e, c, {
185
190
  cwd: t,
186
191
  env: {
187
192
  ...process.env,
@@ -192,23 +197,41 @@ function g(e, t, n, r, a) {
192
197
  "pipe",
193
198
  "pipe"
194
199
  ]
195
- }), l = "", u = "";
196
- c.stdout?.on("data", (e) => {
197
- l += e.toString("utf8");
198
- }), c.stderr?.on("data", (e) => {
199
- u += e.toString("utf8");
200
- }), c.on("error", s), c.on("exit", (e) => {
201
- if (e !== 0) {
202
- s(/* @__PURE__ */ Error(`${a}: extractor --emit-json exited with code ${e}:\n${u}`));
200
+ }), u = "", d = "", f = !1, p = !1, m = !1, h = (e) => {
201
+ m || (m = !0, clearTimeout(y), e());
202
+ }, g = () => /* @__PURE__ */ Error(`${a}: extractor --emit-json timed out after ${s} ms and was killed (command: ${l})${d ? `:\n${d}` : ""}`), _ = !1, v = () => {
203
+ if (p && _) {
204
+ _ = !1, y = setTimeout(v, s);
203
205
  return;
204
206
  }
205
- try {
206
- o(JSON.parse(l));
207
- } catch (e) {
208
- s(/* @__PURE__ */ Error(`${a}: extractor --emit-json produced invalid JSON (${e.message}):\n${l}`));
209
- }
207
+ f = !0, o.kill("SIGKILL"), o.stdout?.destroy(), o.stderr?.destroy(), p && h(() => r(g()));
208
+ }, y = setTimeout(v, s);
209
+ o.stdout?.on("data", (e) => {
210
+ p && (_ = !0), u += e.toString("utf8");
211
+ }), o.stderr?.on("data", (e) => {
212
+ d += e.toString("utf8");
213
+ }), o.on("error", (e) => {
214
+ h(() => r(e));
215
+ }), o.on("exit", () => {
216
+ p = !0, f && h(() => r(g()));
217
+ }), o.on("close", (e) => {
218
+ h(() => {
219
+ if (f) {
220
+ r(g());
221
+ return;
222
+ }
223
+ if (e !== 0) {
224
+ r(/* @__PURE__ */ Error(`${a}: extractor --emit-json exited with code ${e}:\n${d}`));
225
+ return;
226
+ }
227
+ try {
228
+ n(JSON.parse(u));
229
+ } catch (e) {
230
+ r(/* @__PURE__ */ Error(`${a}: extractor --emit-json produced invalid JSON (${e.message}):\n${u}`));
231
+ }
232
+ });
210
233
  });
211
234
  });
212
235
  }
213
236
  //#endregion
214
- export { h as ensurePythonEnv, o as resolvePythonPkgDir, g as runExtractorEmitJson };
237
+ export { g as DEFAULT_EXTRACTOR_TIMEOUT_MS, h as ensurePythonEnv, o as resolvePythonPkgDir, v as runExtractorEmitJson };
@@ -1,5 +1,21 @@
1
1
  import { Operation } from '@adhd/apigen-core-client';
2
2
 
3
+ /**
4
+ * Default wall-clock budget for one `--emit-json` extraction: 120 000 ms. Extraction
5
+ * of a typical module finishes in well under a second; the budget exists so a hung
6
+ * extractor fails the caller instead of stalling it indefinitely.
7
+ */
8
+ export declare const DEFAULT_EXTRACTOR_TIMEOUT_MS = 120000;
9
+ /** Options for {@link runExtractorEmitJson}. */
10
+ export interface IRunExtractorOptions {
11
+ /**
12
+ * Wall-clock limit in milliseconds for the extractor process. On expiry the child is
13
+ * killed (SIGKILL) and the promise rejects with a timeout error naming the command.
14
+ * Must be a positive finite number no greater than 2147483647 (the `setTimeout` maximum).
15
+ * Defaults to {@link DEFAULT_EXTRACTOR_TIMEOUT_MS}.
16
+ */
17
+ timeoutMs?: number;
18
+ }
3
19
  /**
4
20
  * Runs `python -m apigen_python.extractor --emit-json` to completion and
5
21
  * parses its stdout into `Operation[]` — phase 1 of the py-flask / py-grpc
@@ -16,5 +32,6 @@ import { Operation } from '@adhd/apigen-core-client';
16
32
  * @param label - Short caller identifier (e.g. `'py-flask'`, `'py-grpc'`)
17
33
  * prefixed onto error messages so failures are traceable
18
34
  * to their caller.
35
+ * @param options - {@link IRunExtractorOptions}; `timeoutMs` bounds the run.
19
36
  */
20
- export declare function runExtractorEmitJson(python: string, pythonPkgDir: string, modulePath: string, namespace: string, label: string): Promise<Operation[]>;
37
+ export declare function runExtractorEmitJson(python: string, pythonPkgDir: string, modulePath: string, namespace: string, label: string, options?: IRunExtractorOptions): Promise<Operation[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhd/apigen-python-env",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "main": "./index.js",
5
5
  "module": "./index.mjs",
6
6
  "types": "./index.d.ts",
@@ -12,7 +12,7 @@
12
12
  "provisioning"
13
13
  ],
14
14
  "dependencies": {
15
- "@adhd/apigen-core-client": "^0.3.3"
15
+ "@adhd/apigen-core-client": "^0.3.6"
16
16
  },
17
17
  "publishConfig": {
18
18
  "access": "public"