@ekanos/cli 0.1.10 → 0.2.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/dist/banner.d.ts +5 -1
- package/dist/banner.js +7 -2
- package/dist/banner.js.map +1 -1
- package/dist/commands/init.d.ts +13 -1
- package/dist/commands/init.js +70 -13
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +13 -0
- package/dist/commands/login.js +41 -10
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/publish.js +55 -3
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/status.d.ts +68 -2
- package/dist/commands/status.js +312 -43
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/test.d.ts +27 -5
- package/dist/commands/test.js +85 -9
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/validate.js +115 -3
- package/dist/commands/validate.js.map +1 -1
- package/dist/context.d.ts +37 -1
- package/dist/context.js +27 -1
- package/dist/context.js.map +1 -1
- package/dist/did-you-mean.d.ts +36 -0
- package/dist/did-you-mean.js +192 -0
- package/dist/did-you-mean.js.map +1 -0
- package/dist/findings-renderer.d.ts +59 -0
- package/dist/findings-renderer.js +271 -0
- package/dist/findings-renderer.js.map +1 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +23 -5
- package/dist/index.js.map +1 -1
- package/dist/project-checks.d.ts +30 -0
- package/dist/project-checks.js +162 -0
- package/dist/project-checks.js.map +1 -1
- package/dist/publish-api.d.ts +13 -0
- package/dist/publish-api.js.map +1 -1
- package/dist/source-locator.d.ts +67 -0
- package/dist/source-locator.js +284 -0
- package/dist/source-locator.js.map +1 -0
- package/dist/test-report.d.ts +32 -0
- package/dist/test-report.js +255 -0
- package/dist/test-report.js.map +1 -0
- package/dist/ui.d.ts +75 -0
- package/dist/ui.js +132 -0
- package/dist/ui.js.map +1 -0
- package/dist/validate-findings.d.ts +22 -0
- package/dist/validate-findings.js +57 -1
- package/dist/validate-findings.js.map +1 -1
- package/dist/wizard.d.ts +80 -0
- package/dist/wizard.js +74 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +5 -3
- package/templates/package.json.tmpl +3 -1
- package/templates/scripts/setup-hooks.mjs.tmpl +45 -0
- package/templates/tsconfig.json.tmpl +2 -0
package/dist/publish-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-api.js","sourceRoot":"","sources":["../src/publish-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for the Fusion submission intake route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side is the host's `/api/partner/submissions` route. As with\n * the auth routes (see `auth/fusion-api.ts`), the two halves are coupled\n * only by HTTP; tests stub `fetch`, never the route.\n *\n * POST /api/partner/submissions [Authorization: Bearer <access_token>]\n * ← multipart/form-data:\n * source — Fusion source slug\n * slug — integration slug (kebab-case)\n * version — semver\n * manifest — JSON string (the parsed ekanos.json + declaration summary)\n * archive — gzipped tarball, ≤ 4 MiB\n * → 201 { ok: true, data: { submissionId, slug, version, state,\n * archiveSha256 } }\n * → 400 { error, code: \"INVALID_REQUEST\" | \"INVALID_ARCHIVE\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"FORBIDDEN\" } no developer seat on the source\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 409 { error, code: \"VERSION_EXISTS\" } this (slug, version) is\n * already submitted\n * → 413 { error, code: \"ARCHIVE_TOO_LARGE\" }\n * → 3xx treated as 401 (an\n * auth-gated Fusion page\n * answers 307)\n *\n * GET /api/partner/submissions?source=<slug|id>[&detail=findings]\n * [Authorization: Bearer]\n * → 200 { ok: true, data: { source, integrations: [{ id, slug, name,\n * createdAt, versions: [{ id, version, state, archiveSha256,\n * submittedAt, gatedAt,\n * // present once a gate has run (gate_report is non-null):\n * errorCount?, warnCount?,\n * // present ONLY with ?detail=findings, alongside the above:\n * findings?, stages?\n * }] }] } }\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 3xx treated as 401\n *\n * `state` walks submitted -> gated|rejected -> dev -> prod -> revoked,\n * enforced by a database trigger on the host's side (the full transition\n * matrix lives there). `gated`/`dev`/`rejected` are the\n * terminal-for-waiting states `ekanos status --watch` polls for.\n * ---------------------------------------------------------------------------\n */\n\n/**\n * The server's manifest cap — 64 KiB of UTF-8, enforced server-side against\n * `Buffer.byteLength`. Mirrored here so `publish` can refuse an oversized\n * manifest with a clear message instead of the server's generic 400.\n */\nexport const MAX_MANIFEST_BYTES = 64 * 1024;\n\nexport interface SubmissionPayload {\n source: string;\n slug: string;\n version: string;\n /** JSON string — already serialized by the caller. */\n manifest: string;\n archive: Uint8Array;\n}\n\nexport interface SubmissionReceipt {\n submissionId: string;\n slug: string;\n version: string;\n state: string;\n archiveSha256: string;\n}\n\nexport type SubmitResult =\n | { status: 'ok'; receipt: SubmissionReceipt }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\nexport async function submitArchive(\n host: string,\n accessToken: string,\n payload: SubmissionPayload,\n): Promise<SubmitResult> {\n const form = new FormData();\n form.append('source', payload.source);\n form.append('slug', payload.slug);\n form.append('version', payload.version);\n form.append('manifest', payload.manifest);\n form.append(\n 'archive',\n // Copy into a fresh Uint8Array: Buffer is Uint8Array<ArrayBufferLike>,\n // which BlobPart refuses (a SharedArrayBuffer view is not a BlobPart).\n new Blob([new Uint8Array(payload.archive)], { type: 'application/gzip' }),\n 'archive.tgz',\n );\n\n const response = await request(`${host}/api/partner/submissions`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n body: form,\n });\n\n // 401, or the 307 an auth-gated Fusion route answers with: the token did\n // not authenticate. The caller owns the refresh-and-retry decision.\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ??\n `${host} refused the submission: this account does not hold the ` +\n `developer seat on source \"${payload.source}\".`,\n `Ask the source operator to grant your account the developer seat for ` +\n `\"${payload.source}\", then retry.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${payload.source}\".`,\n `Check the --source value (or the \"source\" field in ekanos.json) ` +\n `against the slug your Fusion operator gave you.`,\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n serverMessage ??\n `Version ${payload.version} of \"${payload.slug}\" is already submitted.`,\n `Submitted versions are immutable. Bump the \"version\" field in ` +\n `package.json and publish again.`,\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw validationError(\n serverMessage ?? `${host} rejected the submission as invalid.`,\n `Fix the reported problem and re-run \"ekanos publish\". If the message ` +\n `names the archive, re-check the project layout (\"ekanos validate\").`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const receipt = {\n submissionId: str(data.submissionId),\n slug: str(data.slug),\n version: str(data.version),\n state: str(data.state),\n archiveSha256: str(data.archiveSha256),\n };\n\n if (Object.values(receipt).some((value) => value === null)) {\n throw networkError(\n `${host} accepted the submission but returned an incomplete receipt.`,\n `Confirm the deployment is running a Fusion build that includes the ` +\n `partner submissions route, then check the submission's state with ` +\n `your operator before re-publishing.`,\n );\n }\n\n return { status: 'ok', receipt: receipt as SubmissionReceipt };\n}\n\n/**\n * One gate finding, mirroring `@ekanos/integration-schema`'s `Finding` shape\n * (the same one `validate`/`publish` carry in `data.findings`). Declared\n * locally rather than imported: the gate's own report type lives in a\n * server-side toolchain package private to the platform monorepo, which this\n * partner-facing CLI has no business depending on — the wire shape is the\n * only contract.\n */\nexport interface GateFinding {\n check: string;\n severity: 'error' | 'warn' | 'info';\n file?: string;\n line?: number;\n message: string;\n hint: string;\n}\n\nexport interface GateStageResult {\n id: string;\n status: 'pass' | 'fail' | 'skipped';\n durationMs: number;\n}\n\nexport interface SubmissionVersion {\n id: string;\n version: string;\n state: string;\n archiveSha256: string | null;\n submittedAt: string | null;\n gatedAt: string | null;\n /** Present once a gate has run. */\n errorCount: number | null;\n warnCount: number | null;\n /** Present only when the request passed `?detail=findings`. */\n findings?: GateFinding[];\n stages?: GateStageResult[];\n}\n\nexport interface SubmissionListing {\n id: string;\n slug: string;\n name: string | null;\n createdAt: string | null;\n versions: SubmissionVersion[];\n}\n\nexport type ListResult =\n | { status: 'ok'; integrations: SubmissionListing[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the caller's visible submissions for a source. Same authorization model\n * as `submitArchive`: the seat SELECT policies filter server-side, so a caller\n * with no seat sees an empty list rather than an error.\n *\n * `detail: 'findings'` requests the full per-version findings/stages arrays\n * (used by `status --findings` and `status --watch`); omitted, the server\n * answers with counts only — see the wire contract above.\n */\nexport async function listSubmissions(\n host: string,\n accessToken: string,\n source: string,\n options: { detail?: 'findings' } = {},\n): Promise<ListResult> {\n const query = new URLSearchParams({ source });\n if (options.detail) query.set('detail', options.detail);\n\n const response = await request(\n `${host}/api/partner/submissions?${query.toString()}`,\n {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n },\n );\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ?? `${host} refused to list submissions for \"${source}\".`,\n `Complete MFA in the browser if the message names it, then run ` +\n `\"ekanos login --host ${host}\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${source}\".`,\n `Check the \"source\" field in ekanos.json (or --source) against the ` +\n `slug your Fusion operator gave you.`,\n );\n }\n\n if (response.status === 400) {\n throw validationError(\n serverMessage ?? `${host} rejected the submissions query.`,\n `Fix the reported problem and re-run \"ekanos status\".`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.integrations) ? data.integrations : [];\n\n const integrations = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n createdAt: str(row.createdAt),\n versions: (Array.isArray(row.versions) ? row.versions : [])\n .filter(\n (version): version is Record<string, unknown> =>\n typeof version === 'object' && version !== null,\n )\n .map((version) => ({\n id: str(version.id) ?? '',\n version: str(version.version) ?? '',\n state: str(version.state) ?? '',\n archiveSha256: str(version.archiveSha256),\n submittedAt: str(version.submittedAt),\n gatedAt: str(version.gatedAt),\n errorCount: num(version.errorCount),\n warnCount: num(version.warnCount),\n ...(Array.isArray(version.findings)\n ? { findings: version.findings as GateFinding[] }\n : {}),\n ...(Array.isArray(version.stages)\n ? { stages: version.stages as GateStageResult[] }\n : {}),\n })),\n }));\n\n return { status: 'ok', integrations };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorized(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the submission.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n\nfunction num(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"publish-api.js","sourceRoot":"","sources":["../src/publish-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,eAAe,GAChB,MAAM,UAAU,CAAC;AAElB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,GAAG,IAAI,CAAC;AAwB5C,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAY,EACZ,WAAmB,EACnB,OAA0B;;IAE1B,MAAM,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM,CACT,SAAS;IACT,uEAAuE;IACvE,uEAAuE;IACvE,IAAI,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,EACzE,aAAa,CACd,CAAC;IAEF,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,0BAA0B,EAAE;QAChE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,aAAa,EAAE,UAAU,WAAW,EAAE;SACvC;QACD,IAAI,EAAE,IAAI;KACX,CAAC,CAAC;IAEH,yEAAyE;IACzE,oEAAoE;IACpE,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3D,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACpC,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC9C,MAAM,aAAa,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEtC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,cAAc,CAClB,aAAa,aAAb,aAAa,cAAb,aAAa,GACX,GAAG,IAAI,0DAA0D;YAC/D,6BAA6B,OAAO,CAAC,MAAM,IAAI,EACnD,uEAAuE;YACrE,IAAI,OAAO,CAAC,MAAM,gBAAgB,CACrC,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,aAAa,CACjB,aAAa,aAAb,aAAa,cAAb,aAAa,GAAI,GAAG,IAAI,6BAA6B,OAAO,CAAC,MAAM,IAAI,EACvE,kEAAkE;YAChE,iDAAiD,CACpD,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,iBAAiB,CACrB,aAAa,aAAb,aAAa,cAAb,aAAa,GACX,WAAW,OAAO,CAAC,OAAO,QAAQ,OAAO,CAAC,IAAI,yBAAyB,EACzE,gEAAgE;YAC9D,iCAAiC,CACpC,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACvD,MAAM,eAAe,CACnB,aAAa,aAAb,aAAa,cAAb,aAAa,GAAI,GAAG,IAAI,sCAAsC,EAC9D,uEAAuE;YACrE,qEAAqE,CACxE,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,YAAY,CAChB,GAAG,IAAI,sCAAsC,QAAQ,CAAC,MAAM,GAAG,EAC/D,QAAQ,CAAC,MAAM,IAAI,GAAG;YACpB,CAAC,CAAC,mEAAmE;gBACjE,8CAA8C;YAClD,CAAC,CAAC,+DAA+D;gBAC7D,yCAAyC,CAChD,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAA,IAAI,CAAC,IAAI,mCAAI,EAAE,CAA4B,CAAC;IAC1D,MAAM,OAAO,GAAG;QACd,YAAY,EAAE,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC;QACpC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;QACpB,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC;QAC1B,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC;QACtB,aAAa,EAAE,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC;KACvC,CAAC;IAEF,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,EAAE,CAAC;QAC3D,MAAM,YAAY,CAChB,GAAG,IAAI,8DAA8D,EACrE,qEAAqE;YACnE,oEAAoE;YACpE,qCAAqC,CACxC,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAA4B,EAAE,CAAC;AACjE,CAAC;AAkED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAY,EACZ,WAAmB,EACnB,MAAc,EACd,UAAmC,EAAE;;IAErC,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9C,IAAI,OAAO,CAAC,MAAM;QAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAExD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAC5B,GAAG,IAAI,4BAA4B,KAAK,CAAC,QAAQ,EAAE,EAAE,EACrD;QACE,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,aAAa,EAAE,UAAU,WAAW,EAAE;SACvC;KACF,CACF,CAAC;IAEF,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3D,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACpC,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC9C,MAAM,aAAa,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEtC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,cAAc,CAClB,aAAa,aAAb,aAAa,cAAb,aAAa,GAAI,GAAG,IAAI,qCAAqC,MAAM,IAAI,EACvE,gEAAgE;YAC9D,wBAAwB,IAAI,UAAU,CACzC,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,aAAa,CACjB,aAAa,aAAb,aAAa,cAAb,aAAa,GAAI,GAAG,IAAI,6BAA6B,MAAM,IAAI,EAC/D,oEAAoE;YAClE,qCAAqC,CACxC,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,eAAe,CACnB,aAAa,aAAb,aAAa,cAAb,aAAa,GAAI,GAAG,IAAI,kCAAkC,EAC1D,sDAAsD,CACvD,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,YAAY,CAChB,GAAG,IAAI,sCAAsC,QAAQ,CAAC,MAAM,GAAG,EAC/D,QAAQ,CAAC,MAAM,IAAI,GAAG;YACpB,CAAC,CAAC,mEAAmE;gBACjE,8CAA8C;YAClD,CAAC,CAAC,+DAA+D;gBAC7D,yCAAyC,CAChD,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAA,IAAI,CAAC,IAAI,mCAAI,EAAE,CAA4B,CAAC;IAC1D,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;IAEvE,MAAM,YAAY,GAAG,IAAI;SACtB,MAAM,CACL,CAAC,GAAG,EAAkC,EAAE,CACtC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAC1C;SACA,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;;QAAC,OAAA,CAAC;YACb,EAAE,EAAE,MAAA,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,mCAAI,EAAE;YACrB,IAAI,EAAE,MAAA,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,EAAE;YACzB,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;YACnB,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC;YAC7B,QAAQ,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;iBACxD,MAAM,CACL,CAAC,OAAO,EAAsC,EAAE,CAC9C,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,CAClD;iBACA,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;;gBAAC,OAAA,+BAChB,EAAE,EAAE,MAAA,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,mCAAI,EAAE,EACzB,OAAO,EAAE,MAAA,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,mCAAI,EAAE,EACnC,KAAK,EAAE,MAAA,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,mCAAI,EAAE,EAC/B,aAAa,EAAE,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,EACzC,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,EACrC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,EAC7B,UAAU,EAAE,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,EACnC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,IAC9B,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;oBACjC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAyB,EAAE;oBACjD,CAAC,CAAC,EAAE,CAAC,GACJ,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC;oBAC/B,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAA2B,EAAE;oBACjD,CAAC,CAAC,EAAE,CAAC,EACP,CAAA;aAAA,CAAC;SACN,CAAC,CAAA;KAAA,CAAC,CAAC;IAEN,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;AACxC,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,iBAAiB,CACrB,mBAAmB,IAAI,sCAAsC,EAC7D,4BAA4B,IAAI,cAAc,CAC/C,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,gBAAgB,CAC7B,QAAkB;IAElB,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAE9C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAClD,CAAC,CAAE,MAAkC;YACrC,CAAC,CAAC,EAAE,CAAC;IACT,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,MAAc;IAChC,OAAO,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,CAAC;AACvC,CAAC;AAED,SAAS,GAAG,CAAC,KAAc;IACzB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAED,SAAS,GAAG,CAAC,KAAc;IACzB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5E,CAAC","sourcesContent":["import { request } from './auth/fusion-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for the Fusion submission intake route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side is the host's `/api/partner/submissions` route. As with\n * the auth routes (see `auth/fusion-api.ts`), the two halves are coupled\n * only by HTTP; tests stub `fetch`, never the route.\n *\n * POST /api/partner/submissions [Authorization: Bearer <access_token>]\n * ← multipart/form-data:\n * source — Fusion source slug\n * slug — integration slug (kebab-case)\n * version — semver\n * manifest — JSON string (the parsed ekanos.json + declaration summary)\n * archive — gzipped tarball, ≤ 4 MiB\n * → 201 { ok: true, data: { submissionId, slug, version, state,\n * archiveSha256 } }\n * → 400 { error, code: \"INVALID_REQUEST\" | \"INVALID_ARCHIVE\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"FORBIDDEN\" } no developer seat on the source\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 409 { error, code: \"VERSION_EXISTS\" } this (slug, version) is\n * already submitted\n * → 413 { error, code: \"ARCHIVE_TOO_LARGE\" }\n * → 3xx treated as 401 (an\n * auth-gated Fusion page\n * answers 307)\n *\n * GET /api/partner/submissions?source=<slug|id>[&detail=findings]\n * [Authorization: Bearer]\n * → 200 { ok: true, data: { source, integrations: [{ id, slug, name,\n * createdAt, versions: [{ id, version, state, archiveSha256,\n * submittedAt, gatedAt,\n * // present once a gate has run (gate_report is non-null):\n * errorCount?, warnCount?,\n * // present ONLY with ?detail=findings, alongside the above:\n * findings?, stages?\n * }] }] } }\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 3xx treated as 401\n *\n * `state` walks submitted -> gated|rejected -> dev -> prod -> revoked,\n * enforced by a database trigger on the host's side (the full transition\n * matrix lives there). `gated`/`dev`/`rejected` are the\n * terminal-for-waiting states `ekanos status --watch` polls for.\n * ---------------------------------------------------------------------------\n */\n\n/**\n * The server's manifest cap — 64 KiB of UTF-8, enforced server-side against\n * `Buffer.byteLength`. Mirrored here so `publish` can refuse an oversized\n * manifest with a clear message instead of the server's generic 400.\n */\nexport const MAX_MANIFEST_BYTES = 64 * 1024;\n\nexport interface SubmissionPayload {\n source: string;\n slug: string;\n version: string;\n /** JSON string — already serialized by the caller. */\n manifest: string;\n archive: Uint8Array;\n}\n\nexport interface SubmissionReceipt {\n submissionId: string;\n slug: string;\n version: string;\n state: string;\n archiveSha256: string;\n}\n\nexport type SubmitResult =\n | { status: 'ok'; receipt: SubmissionReceipt }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\nexport async function submitArchive(\n host: string,\n accessToken: string,\n payload: SubmissionPayload,\n): Promise<SubmitResult> {\n const form = new FormData();\n form.append('source', payload.source);\n form.append('slug', payload.slug);\n form.append('version', payload.version);\n form.append('manifest', payload.manifest);\n form.append(\n 'archive',\n // Copy into a fresh Uint8Array: Buffer is Uint8Array<ArrayBufferLike>,\n // which BlobPart refuses (a SharedArrayBuffer view is not a BlobPart).\n new Blob([new Uint8Array(payload.archive)], { type: 'application/gzip' }),\n 'archive.tgz',\n );\n\n const response = await request(`${host}/api/partner/submissions`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n body: form,\n });\n\n // 401, or the 307 an auth-gated Fusion route answers with: the token did\n // not authenticate. The caller owns the refresh-and-retry decision.\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ??\n `${host} refused the submission: this account does not hold the ` +\n `developer seat on source \"${payload.source}\".`,\n `Ask the source operator to grant your account the developer seat for ` +\n `\"${payload.source}\", then retry.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${payload.source}\".`,\n `Check the --source value (or the \"source\" field in ekanos.json) ` +\n `against the slug your Fusion operator gave you.`,\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n serverMessage ??\n `Version ${payload.version} of \"${payload.slug}\" is already submitted.`,\n `Submitted versions are immutable. Bump the \"version\" field in ` +\n `package.json and publish again.`,\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw validationError(\n serverMessage ?? `${host} rejected the submission as invalid.`,\n `Fix the reported problem and re-run \"ekanos publish\". If the message ` +\n `names the archive, re-check the project layout (\"ekanos validate\").`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const receipt = {\n submissionId: str(data.submissionId),\n slug: str(data.slug),\n version: str(data.version),\n state: str(data.state),\n archiveSha256: str(data.archiveSha256),\n };\n\n if (Object.values(receipt).some((value) => value === null)) {\n throw networkError(\n `${host} accepted the submission but returned an incomplete receipt.`,\n `Confirm the deployment is running a Fusion build that includes the ` +\n `partner submissions route, then check the submission's state with ` +\n `your operator before re-publishing.`,\n );\n }\n\n return { status: 'ok', receipt: receipt as SubmissionReceipt };\n}\n\n/**\n * One gate finding, mirroring `@ekanos/integration-schema`'s `Finding` shape\n * (the same one `validate`/`publish` carry in `data.findings`). Declared\n * locally rather than imported: the gate's own report type lives in a\n * server-side toolchain package private to the platform monorepo, which this\n * partner-facing CLI has no business depending on — the wire shape is the\n * only contract.\n *\n * `origin`/`id`/`createdAt`/`resolvedAt` are the additive fields the host's\n * findings-merge endpoint puts on the wire once host-filed findings — CI\n * failures, host observations, a revocation reason — are merged in alongside\n * the gate's own verdict. A gate-only finding still omits them entirely\n * (this CLI's own `validate`/`publish` findings never set `origin`, which\n * the renderer reads as \"gate\"); they only appear on findings read back from\n * `GET /api/partner/submissions?detail=findings`.\n */\nexport interface GateFinding {\n check: string;\n severity: 'error' | 'warn' | 'info';\n file?: string;\n line?: number;\n message: string;\n hint: string;\n /** `'gate' | 'ci' | 'host' | 'revocation'` on the wire; kept as `string` — see the doc comment on `HostFindingRow` in `merge-findings.ts` for why the host stays defensive rather than a narrower literal union here. */\n origin?: string;\n id?: string;\n createdAt?: string;\n resolvedAt?: string | null;\n}\n\nexport interface GateStageResult {\n id: string;\n status: 'pass' | 'fail' | 'skipped';\n durationMs: number;\n}\n\nexport interface SubmissionVersion {\n id: string;\n version: string;\n state: string;\n archiveSha256: string | null;\n submittedAt: string | null;\n gatedAt: string | null;\n /** Present once a gate has run. */\n errorCount: number | null;\n warnCount: number | null;\n /** Present only when the request passed `?detail=findings`. */\n findings?: GateFinding[];\n stages?: GateStageResult[];\n}\n\nexport interface SubmissionListing {\n id: string;\n slug: string;\n name: string | null;\n createdAt: string | null;\n versions: SubmissionVersion[];\n}\n\nexport type ListResult =\n | { status: 'ok'; integrations: SubmissionListing[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the caller's visible submissions for a source. Same authorization model\n * as `submitArchive`: the seat SELECT policies filter server-side, so a caller\n * with no seat sees an empty list rather than an error.\n *\n * `detail: 'findings'` requests the full per-version findings/stages arrays\n * (used by `status --findings` and `status --watch`); omitted, the server\n * answers with counts only — see the wire contract above.\n */\nexport async function listSubmissions(\n host: string,\n accessToken: string,\n source: string,\n options: { detail?: 'findings' } = {},\n): Promise<ListResult> {\n const query = new URLSearchParams({ source });\n if (options.detail) query.set('detail', options.detail);\n\n const response = await request(\n `${host}/api/partner/submissions?${query.toString()}`,\n {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n },\n );\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ?? `${host} refused to list submissions for \"${source}\".`,\n `Complete MFA in the browser if the message names it, then run ` +\n `\"ekanos login --host ${host}\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${source}\".`,\n `Check the \"source\" field in ekanos.json (or --source) against the ` +\n `slug your Fusion operator gave you.`,\n );\n }\n\n if (response.status === 400) {\n throw validationError(\n serverMessage ?? `${host} rejected the submissions query.`,\n `Fix the reported problem and re-run \"ekanos status\".`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.integrations) ? data.integrations : [];\n\n const integrations = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n createdAt: str(row.createdAt),\n versions: (Array.isArray(row.versions) ? row.versions : [])\n .filter(\n (version): version is Record<string, unknown> =>\n typeof version === 'object' && version !== null,\n )\n .map((version) => ({\n id: str(version.id) ?? '',\n version: str(version.version) ?? '',\n state: str(version.state) ?? '',\n archiveSha256: str(version.archiveSha256),\n submittedAt: str(version.submittedAt),\n gatedAt: str(version.gatedAt),\n errorCount: num(version.errorCount),\n warnCount: num(version.warnCount),\n ...(Array.isArray(version.findings)\n ? { findings: version.findings as GateFinding[] }\n : {}),\n ...(Array.isArray(version.stages)\n ? { stages: version.stages as GateStageResult[] }\n : {}),\n })),\n }));\n\n return { status: 'ok', integrations };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorized(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the submission.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n\nfunction num(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort zod dot-path → source line resolution for `validate`'s
|
|
3
|
+
* schema findings.
|
|
4
|
+
*
|
|
5
|
+
* `collectDefinitionFindings` (in `@ekanos/integration-schema`) bakes a zod
|
|
6
|
+
* PATH into `Finding.message` (`"components.widgets.0.id: Required"`), never
|
|
7
|
+
* a source offset — zod reports a `path`, not a byte offset, and fabricating
|
|
8
|
+
* a line number would be a lie (see the `Finding` doc comment there). This
|
|
9
|
+
* module recovers a plausible line by scanning the partner's declaration
|
|
10
|
+
* source text: no AST, no new dependency — TypeScript's compiler API is a
|
|
11
|
+
* devDep in this workspace, not a runtime one, and the published CLI never
|
|
12
|
+
* ships it, so it is never imported here. Just a brace-tracking scan of the
|
|
13
|
+
* object/array literal passed to `defineIntegration(...)`.
|
|
14
|
+
*
|
|
15
|
+
* The rule that matters more than any mechanic below: a WRONG line number is
|
|
16
|
+
* worse than none. Anything this module can't resolve with confidence
|
|
17
|
+
* returns `{ confidence: 'none' }` — never a guess.
|
|
18
|
+
*/
|
|
19
|
+
export type LocateConfidence = 'exact' | 'enclosing' | 'none';
|
|
20
|
+
export type LocateResult = {
|
|
21
|
+
confidence: 'exact';
|
|
22
|
+
line: number;
|
|
23
|
+
} | {
|
|
24
|
+
confidence: 'enclosing';
|
|
25
|
+
line: number;
|
|
26
|
+
} | {
|
|
27
|
+
confidence: 'none';
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Appended, PLAIN TEXT (no ANSI — this module never touches color; see
|
|
31
|
+
* `ui.ts`'s doc comment on why nothing outside it may), to a finding's
|
|
32
|
+
* message when only the enclosing object/array was found, not the missing
|
|
33
|
+
* key itself. `findings-renderer.ts` recognizes this literal suffix and dims
|
|
34
|
+
* it at render time.
|
|
35
|
+
*/
|
|
36
|
+
export declare const ENCLOSING_NOTE = "(showing the enclosing object \u2014 add the missing key here)";
|
|
37
|
+
/** The literal path `collectDefinitionFindings` emits for a zod issue whose `path` array is empty. */
|
|
38
|
+
export declare function isRootPath(dotPath: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* `"components.widgets[0].id"` and `"components.widgets.0.id"` both split to
|
|
41
|
+
* `['components', 'widgets', '0', 'id']` — `collectDefinitionFindings` only
|
|
42
|
+
* ever emits the dot form (a plain `issue.path.join('.')`), but this accepts
|
|
43
|
+
* either, matching the pattern `findings-renderer.ts`'s dot-path detector
|
|
44
|
+
* already tolerates.
|
|
45
|
+
*/
|
|
46
|
+
export declare function splitDotPath(dotPath: string): string[];
|
|
47
|
+
/**
|
|
48
|
+
* A finding message is `"<dot-path>: <zod message>"` (or `(root): …` for an
|
|
49
|
+
* empty path) — see the `Finding` doc comment in `@ekanos/integration-schema`.
|
|
50
|
+
* Splits the two apart; `null` for a message with no such prefix (every
|
|
51
|
+
* finding NOT built from a zod issue, e.g. `definition.plain-data`).
|
|
52
|
+
*/
|
|
53
|
+
export declare function extractPathFromMessage(message: string): {
|
|
54
|
+
path: string;
|
|
55
|
+
rest: string;
|
|
56
|
+
} | null;
|
|
57
|
+
/**
|
|
58
|
+
* Locates `dotPath` (a zod path string, e.g. `"components.widgets.0.id"` or
|
|
59
|
+
* `"(root)"`) within `source`, the declaration file's full text. `source`
|
|
60
|
+
* MUST contain an inline `defineIntegration({ … })` call — a definition
|
|
61
|
+
* built from a variable, spread, or factory function can't be resolved
|
|
62
|
+
* statically and always comes back `none`. A one-line (minified/no-newline)
|
|
63
|
+
* source also always comes back `none`: a codeframe there can't isolate
|
|
64
|
+
* anything the reader couldn't already see, so it isn't worth the risk of a
|
|
65
|
+
* technically-correct-but-useless "line 1" frame.
|
|
66
|
+
*/
|
|
67
|
+
export declare function locateDotPath(source: string, dotPath: string): LocateResult;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort zod dot-path → source line resolution for `validate`'s
|
|
3
|
+
* schema findings.
|
|
4
|
+
*
|
|
5
|
+
* `collectDefinitionFindings` (in `@ekanos/integration-schema`) bakes a zod
|
|
6
|
+
* PATH into `Finding.message` (`"components.widgets.0.id: Required"`), never
|
|
7
|
+
* a source offset — zod reports a `path`, not a byte offset, and fabricating
|
|
8
|
+
* a line number would be a lie (see the `Finding` doc comment there). This
|
|
9
|
+
* module recovers a plausible line by scanning the partner's declaration
|
|
10
|
+
* source text: no AST, no new dependency — TypeScript's compiler API is a
|
|
11
|
+
* devDep in this workspace, not a runtime one, and the published CLI never
|
|
12
|
+
* ships it, so it is never imported here. Just a brace-tracking scan of the
|
|
13
|
+
* object/array literal passed to `defineIntegration(...)`.
|
|
14
|
+
*
|
|
15
|
+
* The rule that matters more than any mechanic below: a WRONG line number is
|
|
16
|
+
* worse than none. Anything this module can't resolve with confidence
|
|
17
|
+
* returns `{ confidence: 'none' }` — never a guess.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Appended, PLAIN TEXT (no ANSI — this module never touches color; see
|
|
21
|
+
* `ui.ts`'s doc comment on why nothing outside it may), to a finding's
|
|
22
|
+
* message when only the enclosing object/array was found, not the missing
|
|
23
|
+
* key itself. `findings-renderer.ts` recognizes this literal suffix and dims
|
|
24
|
+
* it at render time.
|
|
25
|
+
*/
|
|
26
|
+
export const ENCLOSING_NOTE = '(showing the enclosing object — add the missing key here)';
|
|
27
|
+
/** The literal path `collectDefinitionFindings` emits for a zod issue whose `path` array is empty. */
|
|
28
|
+
export function isRootPath(dotPath) {
|
|
29
|
+
return dotPath === '(root)';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `"components.widgets[0].id"` and `"components.widgets.0.id"` both split to
|
|
33
|
+
* `['components', 'widgets', '0', 'id']` — `collectDefinitionFindings` only
|
|
34
|
+
* ever emits the dot form (a plain `issue.path.join('.')`), but this accepts
|
|
35
|
+
* either, matching the pattern `findings-renderer.ts`'s dot-path detector
|
|
36
|
+
* already tolerates.
|
|
37
|
+
*/
|
|
38
|
+
export function splitDotPath(dotPath) {
|
|
39
|
+
var _a;
|
|
40
|
+
if (isRootPath(dotPath))
|
|
41
|
+
return [];
|
|
42
|
+
return (_a = dotPath.match(/[^.[\]]+/g)) !== null && _a !== void 0 ? _a : [];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A finding message is `"<dot-path>: <zod message>"` (or `(root): …` for an
|
|
46
|
+
* empty path) — see the `Finding` doc comment in `@ekanos/integration-schema`.
|
|
47
|
+
* Splits the two apart; `null` for a message with no such prefix (every
|
|
48
|
+
* finding NOT built from a zod issue, e.g. `definition.plain-data`).
|
|
49
|
+
*/
|
|
50
|
+
export function extractPathFromMessage(message) {
|
|
51
|
+
const match = /^((?:\(root\))|[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+|\[\d+\])*): ([^]+)$/.exec(message);
|
|
52
|
+
if (!match)
|
|
53
|
+
return null;
|
|
54
|
+
const [, path, rest] = match;
|
|
55
|
+
if (path === undefined || rest === undefined)
|
|
56
|
+
return null;
|
|
57
|
+
return { path, rest };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Replaces the CONTENTS of every string/template literal and comment with a
|
|
61
|
+
* neutral filler, preserving length, newlines, and every character outside
|
|
62
|
+
* those regions verbatim — so a later brace-depth scan never miscounts a
|
|
63
|
+
* `{`/`}`/`,` that only appears inside a string, while every index computed
|
|
64
|
+
* against the result still points at the same character in the original
|
|
65
|
+
* `source`. Delimiter characters (quotes, `/*`, `//`) are kept as-is; only
|
|
66
|
+
* their insides are masked.
|
|
67
|
+
*/
|
|
68
|
+
function mask(source) {
|
|
69
|
+
const out = source.split('');
|
|
70
|
+
const n = source.length;
|
|
71
|
+
let i = 0;
|
|
72
|
+
while (i < n) {
|
|
73
|
+
const ch = source[i];
|
|
74
|
+
const next = source[i + 1];
|
|
75
|
+
if (ch === '/' && next === '/') {
|
|
76
|
+
// Mask the `//` marker itself too, not just what follows it — a bare
|
|
77
|
+
// `/` isn't whitespace, so `skipMaskedWhitespace` needs the WHOLE
|
|
78
|
+
// comment (marker included) to read as blank in order to skip past a
|
|
79
|
+
// comment that precedes a key.
|
|
80
|
+
out[i] = ' ';
|
|
81
|
+
out[i + 1] = ' ';
|
|
82
|
+
i += 2;
|
|
83
|
+
while (i < n && source[i] !== '\n') {
|
|
84
|
+
out[i] = ' ';
|
|
85
|
+
i += 1;
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (ch === '/' && next === '*') {
|
|
90
|
+
out[i] = ' ';
|
|
91
|
+
out[i + 1] = ' ';
|
|
92
|
+
i += 2;
|
|
93
|
+
while (i < n && !(source[i] === '*' && source[i + 1] === '/')) {
|
|
94
|
+
if (source[i] !== '\n')
|
|
95
|
+
out[i] = ' ';
|
|
96
|
+
i += 1;
|
|
97
|
+
}
|
|
98
|
+
if (i < n)
|
|
99
|
+
out[i] = ' ';
|
|
100
|
+
if (i + 1 < n)
|
|
101
|
+
out[i + 1] = ' ';
|
|
102
|
+
i += 2;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
106
|
+
const quote = ch;
|
|
107
|
+
i += 1;
|
|
108
|
+
while (i < n && source[i] !== quote) {
|
|
109
|
+
if (source[i] === '\\') {
|
|
110
|
+
if (source[i + 1] !== '\n' && i + 1 < n)
|
|
111
|
+
out[i + 1] = 'x';
|
|
112
|
+
i += 2;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (source[i] !== '\n')
|
|
116
|
+
out[i] = 'x';
|
|
117
|
+
i += 1;
|
|
118
|
+
}
|
|
119
|
+
i += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
i += 1;
|
|
123
|
+
}
|
|
124
|
+
return out.join('');
|
|
125
|
+
}
|
|
126
|
+
/** Index of the delimiter matching `masked[openIndex]`, or `-1` for unbalanced/malformed input. Operates on masked text so a bracket inside a string never throws off the count. */
|
|
127
|
+
function findMatchingClose(masked, openIndex) {
|
|
128
|
+
const stack = [];
|
|
129
|
+
for (let i = openIndex; i < masked.length; i += 1) {
|
|
130
|
+
const ch = masked[i];
|
|
131
|
+
if (ch === '{')
|
|
132
|
+
stack.push('}');
|
|
133
|
+
else if (ch === '[')
|
|
134
|
+
stack.push(']');
|
|
135
|
+
else if (ch === '(')
|
|
136
|
+
stack.push(')');
|
|
137
|
+
else if (ch === '}' || ch === ']' || ch === ')') {
|
|
138
|
+
if (stack.pop() !== ch)
|
|
139
|
+
return -1;
|
|
140
|
+
if (stack.length === 0)
|
|
141
|
+
return i;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return -1;
|
|
145
|
+
}
|
|
146
|
+
/** Top-level (depth-0-relative-to-the-scope) member spans between a scope's delimiters, split at depth-0 commas — one span per object property or array element, in source order. */
|
|
147
|
+
function splitTopLevelMembers(masked, scope) {
|
|
148
|
+
const members = [];
|
|
149
|
+
let depth = 0;
|
|
150
|
+
let memberStart = scope.start + 1;
|
|
151
|
+
for (let i = scope.start + 1; i < scope.end; i += 1) {
|
|
152
|
+
const ch = masked[i];
|
|
153
|
+
if (ch === '{' || ch === '[' || ch === '(')
|
|
154
|
+
depth += 1;
|
|
155
|
+
else if (ch === '}' || ch === ']' || ch === ')')
|
|
156
|
+
depth -= 1;
|
|
157
|
+
else if (ch === ',' && depth === 0) {
|
|
158
|
+
members.push({ start: memberStart, end: i });
|
|
159
|
+
memberStart = i + 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
members.push({ start: memberStart, end: scope.end });
|
|
163
|
+
return members.filter((m) => masked.slice(m.start, m.end).trim() !== '');
|
|
164
|
+
}
|
|
165
|
+
function scopeAt(masked, index) {
|
|
166
|
+
const ch = masked[index];
|
|
167
|
+
if (ch !== '{' && ch !== '[')
|
|
168
|
+
return null;
|
|
169
|
+
const close = findMatchingClose(masked, index);
|
|
170
|
+
if (close === -1)
|
|
171
|
+
return null;
|
|
172
|
+
return { start: index, end: close, kind: ch };
|
|
173
|
+
}
|
|
174
|
+
function findRootScope(masked) {
|
|
175
|
+
var _a;
|
|
176
|
+
const match = /defineIntegration\s*\(/.exec(masked);
|
|
177
|
+
if (!match)
|
|
178
|
+
return null;
|
|
179
|
+
let i = match.index + match[0].length;
|
|
180
|
+
while (i < masked.length && /\s/.test((_a = masked[i]) !== null && _a !== void 0 ? _a : ''))
|
|
181
|
+
i += 1;
|
|
182
|
+
return scopeAt(masked, i);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* A quoted (`'id'`/`"id"`) or bare (`id`) object key immediately followed by
|
|
186
|
+
* `:`, anchored at the very start of the string it's matched against — the
|
|
187
|
+
* caller is responsible for having already skipped past any leading
|
|
188
|
+
* whitespace AND leading comment (both of which read as whitespace in the
|
|
189
|
+
* MASKED text, see `skipMaskedWhitespace`). Matched against the RAW source
|
|
190
|
+
* slice, never the masked one — masking a quoted key's own text would
|
|
191
|
+
* destroy the very name being compared.
|
|
192
|
+
*/
|
|
193
|
+
const KEY_PATTERN = /^(?:(['"])((?:\\.|(?!\1)[^\\])*)\1|([A-Za-z_$][\w$]*))\s*:/;
|
|
194
|
+
/** First index at or after `start` (and before `end`) whose MASKED character is not whitespace — a masked comment reads as whitespace here too, so this skips leading comments along with real whitespace/newlines. */
|
|
195
|
+
function skipMaskedWhitespace(masked, start, end) {
|
|
196
|
+
var _a;
|
|
197
|
+
let i = start;
|
|
198
|
+
while (i < end && /\s/.test((_a = masked[i]) !== null && _a !== void 0 ? _a : ''))
|
|
199
|
+
i += 1;
|
|
200
|
+
return i;
|
|
201
|
+
}
|
|
202
|
+
function descend(source, masked, scope, segment) {
|
|
203
|
+
var _a;
|
|
204
|
+
const members = splitTopLevelMembers(masked, scope);
|
|
205
|
+
if (scope.kind === '[') {
|
|
206
|
+
if (!/^\d+$/.test(segment))
|
|
207
|
+
return 'mismatch';
|
|
208
|
+
const member = members[Number(segment)];
|
|
209
|
+
if (!member)
|
|
210
|
+
return 'not-found';
|
|
211
|
+
const targetStart = skipMaskedWhitespace(masked, member.start, member.end);
|
|
212
|
+
return { targetStart, valueScope: scopeAt(masked, targetStart) };
|
|
213
|
+
}
|
|
214
|
+
for (const member of members) {
|
|
215
|
+
const contentStart = skipMaskedWhitespace(masked, member.start, member.end);
|
|
216
|
+
const raw = source.slice(contentStart, member.end);
|
|
217
|
+
const match = KEY_PATTERN.exec(raw);
|
|
218
|
+
if (!match)
|
|
219
|
+
continue;
|
|
220
|
+
const keyName = (_a = match[2]) !== null && _a !== void 0 ? _a : match[3];
|
|
221
|
+
if (keyName !== segment)
|
|
222
|
+
continue;
|
|
223
|
+
const valueStart = skipMaskedWhitespace(masked, contentStart + match[0].length, member.end);
|
|
224
|
+
return {
|
|
225
|
+
targetStart: contentStart,
|
|
226
|
+
valueScope: scopeAt(masked, valueStart),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return 'not-found';
|
|
230
|
+
}
|
|
231
|
+
function lineAt(source, index) {
|
|
232
|
+
let line = 1;
|
|
233
|
+
for (let i = 0; i < index && i < source.length; i += 1) {
|
|
234
|
+
if (source[i] === '\n')
|
|
235
|
+
line += 1;
|
|
236
|
+
}
|
|
237
|
+
return line;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Locates `dotPath` (a zod path string, e.g. `"components.widgets.0.id"` or
|
|
241
|
+
* `"(root)"`) within `source`, the declaration file's full text. `source`
|
|
242
|
+
* MUST contain an inline `defineIntegration({ … })` call — a definition
|
|
243
|
+
* built from a variable, spread, or factory function can't be resolved
|
|
244
|
+
* statically and always comes back `none`. A one-line (minified/no-newline)
|
|
245
|
+
* source also always comes back `none`: a codeframe there can't isolate
|
|
246
|
+
* anything the reader couldn't already see, so it isn't worth the risk of a
|
|
247
|
+
* technically-correct-but-useless "line 1" frame.
|
|
248
|
+
*/
|
|
249
|
+
export function locateDotPath(source, dotPath) {
|
|
250
|
+
// Strip trailing newline(s) before checking: a one-line declaration saved
|
|
251
|
+
// with a normal trailing EOF newline must still count as "one line," not
|
|
252
|
+
// "multi-line" by virtue of that single trailing `\n`.
|
|
253
|
+
if (!source.replace(/\n+$/, '').includes('\n'))
|
|
254
|
+
return { confidence: 'none' };
|
|
255
|
+
const masked = mask(source);
|
|
256
|
+
const root = findRootScope(masked);
|
|
257
|
+
if (!root)
|
|
258
|
+
return { confidence: 'none' };
|
|
259
|
+
const segments = splitDotPath(dotPath);
|
|
260
|
+
if (segments.length === 0) {
|
|
261
|
+
return { confidence: 'enclosing', line: lineAt(source, root.start) };
|
|
262
|
+
}
|
|
263
|
+
let scope = root;
|
|
264
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
265
|
+
const segment = segments[i];
|
|
266
|
+
if (segment === undefined)
|
|
267
|
+
break;
|
|
268
|
+
const result = descend(source, masked, scope, segment);
|
|
269
|
+
if (result === 'mismatch')
|
|
270
|
+
return { confidence: 'none' };
|
|
271
|
+
if (result === 'not-found') {
|
|
272
|
+
return { confidence: 'enclosing', line: lineAt(source, scope.start) };
|
|
273
|
+
}
|
|
274
|
+
const isLast = i === segments.length - 1;
|
|
275
|
+
if (isLast) {
|
|
276
|
+
return { confidence: 'exact', line: lineAt(source, result.targetStart) };
|
|
277
|
+
}
|
|
278
|
+
if (result.valueScope === null)
|
|
279
|
+
return { confidence: 'none' };
|
|
280
|
+
scope = result.valueScope;
|
|
281
|
+
}
|
|
282
|
+
return { confidence: 'none' };
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=source-locator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-locator.js","sourceRoot":"","sources":["../src/source-locator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Best-effort zod dot-path → source line resolution for `validate`'s\n * schema findings.\n *\n * `collectDefinitionFindings` (in `@ekanos/integration-schema`) bakes a zod\n * PATH into `Finding.message` (`\"components.widgets.0.id: Required\"`), never\n * a source offset — zod reports a `path`, not a byte offset, and fabricating\n * a line number would be a lie (see the `Finding` doc comment there). This\n * module recovers a plausible line by scanning the partner's declaration\n * source text: no AST, no new dependency — TypeScript's compiler API is a\n * devDep in this workspace, not a runtime one, and the published CLI never\n * ships it, so it is never imported here. Just a brace-tracking scan of the\n * object/array literal passed to `defineIntegration(...)`.\n *\n * The rule that matters more than any mechanic below: a WRONG line number is\n * worse than none. Anything this module can't resolve with confidence\n * returns `{ confidence: 'none' }` — never a guess.\n */\n\nexport type LocateConfidence = 'exact' | 'enclosing' | 'none';\n\nexport type LocateResult =\n | { confidence: 'exact'; line: number }\n | { confidence: 'enclosing'; line: number }\n | { confidence: 'none' };\n\n/**\n * Appended, PLAIN TEXT (no ANSI — this module never touches color; see\n * `ui.ts`'s doc comment on why nothing outside it may), to a finding's\n * message when only the enclosing object/array was found, not the missing\n * key itself. `findings-renderer.ts` recognizes this literal suffix and dims\n * it at render time.\n */\nexport const ENCLOSING_NOTE =\n '(showing the enclosing object — add the missing key here)';\n\n/** The literal path `collectDefinitionFindings` emits for a zod issue whose `path` array is empty. */\nexport function isRootPath(dotPath: string): boolean {\n return dotPath === '(root)';\n}\n\n/**\n * `\"components.widgets[0].id\"` and `\"components.widgets.0.id\"` both split to\n * `['components', 'widgets', '0', 'id']` — `collectDefinitionFindings` only\n * ever emits the dot form (a plain `issue.path.join('.')`), but this accepts\n * either, matching the pattern `findings-renderer.ts`'s dot-path detector\n * already tolerates.\n */\nexport function splitDotPath(dotPath: string): string[] {\n if (isRootPath(dotPath)) return [];\n return dotPath.match(/[^.[\\]]+/g) ?? [];\n}\n\n/**\n * A finding message is `\"<dot-path>: <zod message>\"` (or `(root): …` for an\n * empty path) — see the `Finding` doc comment in `@ekanos/integration-schema`.\n * Splits the two apart; `null` for a message with no such prefix (every\n * finding NOT built from a zod issue, e.g. `definition.plain-data`).\n */\nexport function extractPathFromMessage(\n message: string,\n): { path: string; rest: string } | null {\n const match =\n /^((?:\\(root\\))|[A-Za-z0-9_]+(?:\\.[A-Za-z0-9_]+|\\[\\d+\\])*): ([^]+)$/.exec(\n message,\n );\n if (!match) return null;\n\n const [, path, rest] = match;\n if (path === undefined || rest === undefined) return null;\n\n return { path, rest };\n}\n\ninterface Scope {\n /** Index of the opening delimiter (`{` or `[`) in both `source` and the masked text. */\n start: number;\n /** Index of the matching closing delimiter. */\n end: number;\n kind: '{' | '[';\n}\n\n/**\n * Replaces the CONTENTS of every string/template literal and comment with a\n * neutral filler, preserving length, newlines, and every character outside\n * those regions verbatim — so a later brace-depth scan never miscounts a\n * `{`/`}`/`,` that only appears inside a string, while every index computed\n * against the result still points at the same character in the original\n * `source`. Delimiter characters (quotes, `/*`, `//`) are kept as-is; only\n * their insides are masked.\n */\nfunction mask(source: string): string {\n const out = source.split('');\n const n = source.length;\n let i = 0;\n\n while (i < n) {\n const ch = source[i];\n const next = source[i + 1];\n\n if (ch === '/' && next === '/') {\n // Mask the `//` marker itself too, not just what follows it — a bare\n // `/` isn't whitespace, so `skipMaskedWhitespace` needs the WHOLE\n // comment (marker included) to read as blank in order to skip past a\n // comment that precedes a key.\n out[i] = ' ';\n out[i + 1] = ' ';\n i += 2;\n while (i < n && source[i] !== '\\n') {\n out[i] = ' ';\n i += 1;\n }\n continue;\n }\n\n if (ch === '/' && next === '*') {\n out[i] = ' ';\n out[i + 1] = ' ';\n i += 2;\n while (i < n && !(source[i] === '*' && source[i + 1] === '/')) {\n if (source[i] !== '\\n') out[i] = ' ';\n i += 1;\n }\n if (i < n) out[i] = ' ';\n if (i + 1 < n) out[i + 1] = ' ';\n i += 2;\n continue;\n }\n\n if (ch === \"'\" || ch === '\"' || ch === '`') {\n const quote = ch;\n i += 1;\n while (i < n && source[i] !== quote) {\n if (source[i] === '\\\\') {\n if (source[i + 1] !== '\\n' && i + 1 < n) out[i + 1] = 'x';\n i += 2;\n continue;\n }\n if (source[i] !== '\\n') out[i] = 'x';\n i += 1;\n }\n i += 1;\n continue;\n }\n\n i += 1;\n }\n\n return out.join('');\n}\n\n/** Index of the delimiter matching `masked[openIndex]`, or `-1` for unbalanced/malformed input. Operates on masked text so a bracket inside a string never throws off the count. */\nfunction findMatchingClose(masked: string, openIndex: number): number {\n const stack: string[] = [];\n\n for (let i = openIndex; i < masked.length; i += 1) {\n const ch = masked[i];\n if (ch === '{') stack.push('}');\n else if (ch === '[') stack.push(']');\n else if (ch === '(') stack.push(')');\n else if (ch === '}' || ch === ']' || ch === ')') {\n if (stack.pop() !== ch) return -1;\n if (stack.length === 0) return i;\n }\n }\n\n return -1;\n}\n\n/** Top-level (depth-0-relative-to-the-scope) member spans between a scope's delimiters, split at depth-0 commas — one span per object property or array element, in source order. */\nfunction splitTopLevelMembers(\n masked: string,\n scope: Scope,\n): Array<{ start: number; end: number }> {\n const members: Array<{ start: number; end: number }> = [];\n let depth = 0;\n let memberStart = scope.start + 1;\n\n for (let i = scope.start + 1; i < scope.end; i += 1) {\n const ch = masked[i];\n if (ch === '{' || ch === '[' || ch === '(') depth += 1;\n else if (ch === '}' || ch === ']' || ch === ')') depth -= 1;\n else if (ch === ',' && depth === 0) {\n members.push({ start: memberStart, end: i });\n memberStart = i + 1;\n }\n }\n members.push({ start: memberStart, end: scope.end });\n\n return members.filter((m) => masked.slice(m.start, m.end).trim() !== '');\n}\n\nfunction scopeAt(masked: string, index: number): Scope | null {\n const ch = masked[index];\n if (ch !== '{' && ch !== '[') return null;\n\n const close = findMatchingClose(masked, index);\n if (close === -1) return null;\n\n return { start: index, end: close, kind: ch };\n}\n\nfunction findRootScope(masked: string): Scope | null {\n const match = /defineIntegration\\s*\\(/.exec(masked);\n if (!match) return null;\n\n let i = match.index + match[0].length;\n while (i < masked.length && /\\s/.test(masked[i] ?? '')) i += 1;\n\n return scopeAt(masked, i);\n}\n\n/**\n * A quoted (`'id'`/`\"id\"`) or bare (`id`) object key immediately followed by\n * `:`, anchored at the very start of the string it's matched against — the\n * caller is responsible for having already skipped past any leading\n * whitespace AND leading comment (both of which read as whitespace in the\n * MASKED text, see `skipMaskedWhitespace`). Matched against the RAW source\n * slice, never the masked one — masking a quoted key's own text would\n * destroy the very name being compared.\n */\nconst KEY_PATTERN =\n /^(?:(['\"])((?:\\\\.|(?!\\1)[^\\\\])*)\\1|([A-Za-z_$][\\w$]*))\\s*:/;\n\n/** First index at or after `start` (and before `end`) whose MASKED character is not whitespace — a masked comment reads as whitespace here too, so this skips leading comments along with real whitespace/newlines. */\nfunction skipMaskedWhitespace(\n masked: string,\n start: number,\n end: number,\n): number {\n let i = start;\n while (i < end && /\\s/.test(masked[i] ?? '')) i += 1;\n return i;\n}\n\ntype DescendResult =\n 'not-found' | 'mismatch' | { targetStart: number; valueScope: Scope | null };\n\nfunction descend(\n source: string,\n masked: string,\n scope: Scope,\n segment: string,\n): DescendResult {\n const members = splitTopLevelMembers(masked, scope);\n\n if (scope.kind === '[') {\n if (!/^\\d+$/.test(segment)) return 'mismatch';\n\n const member = members[Number(segment)];\n if (!member) return 'not-found';\n\n const targetStart = skipMaskedWhitespace(masked, member.start, member.end);\n return { targetStart, valueScope: scopeAt(masked, targetStart) };\n }\n\n for (const member of members) {\n const contentStart = skipMaskedWhitespace(masked, member.start, member.end);\n const raw = source.slice(contentStart, member.end);\n const match = KEY_PATTERN.exec(raw);\n if (!match) continue;\n\n const keyName = match[2] ?? match[3];\n if (keyName !== segment) continue;\n\n const valueStart = skipMaskedWhitespace(\n masked,\n contentStart + match[0].length,\n member.end,\n );\n\n return {\n targetStart: contentStart,\n valueScope: scopeAt(masked, valueStart),\n };\n }\n\n return 'not-found';\n}\n\nfunction lineAt(source: string, index: number): number {\n let line = 1;\n for (let i = 0; i < index && i < source.length; i += 1) {\n if (source[i] === '\\n') line += 1;\n }\n return line;\n}\n\n/**\n * Locates `dotPath` (a zod path string, e.g. `\"components.widgets.0.id\"` or\n * `\"(root)\"`) within `source`, the declaration file's full text. `source`\n * MUST contain an inline `defineIntegration({ … })` call — a definition\n * built from a variable, spread, or factory function can't be resolved\n * statically and always comes back `none`. A one-line (minified/no-newline)\n * source also always comes back `none`: a codeframe there can't isolate\n * anything the reader couldn't already see, so it isn't worth the risk of a\n * technically-correct-but-useless \"line 1\" frame.\n */\nexport function locateDotPath(source: string, dotPath: string): LocateResult {\n // Strip trailing newline(s) before checking: a one-line declaration saved\n // with a normal trailing EOF newline must still count as \"one line,\" not\n // \"multi-line\" by virtue of that single trailing `\\n`.\n if (!source.replace(/\\n+$/, '').includes('\\n')) return { confidence: 'none' };\n\n const masked = mask(source);\n const root = findRootScope(masked);\n if (!root) return { confidence: 'none' };\n\n const segments = splitDotPath(dotPath);\n if (segments.length === 0) {\n return { confidence: 'enclosing', line: lineAt(source, root.start) };\n }\n\n let scope: Scope = root;\n\n for (let i = 0; i < segments.length; i += 1) {\n const segment = segments[i];\n if (segment === undefined) break;\n\n const result = descend(source, masked, scope, segment);\n\n if (result === 'mismatch') return { confidence: 'none' };\n if (result === 'not-found') {\n return { confidence: 'enclosing', line: lineAt(source, scope.start) };\n }\n\n const isLast = i === segments.length - 1;\n if (isLast) {\n return { confidence: 'exact', line: lineAt(source, result.targetStart) };\n }\n\n if (result.valueScope === null) return { confidence: 'none' };\n scope = result.valueScope;\n }\n\n return { confidence: 'none' };\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RenderableFinding } from './findings-renderer.js';
|
|
2
|
+
export interface TestSummary {
|
|
3
|
+
passed: number;
|
|
4
|
+
failed: number;
|
|
5
|
+
files: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ParsedTestReport {
|
|
8
|
+
/** One finding per failing test, grouped (via `check`) per test FILE. */
|
|
9
|
+
findings: RenderableFinding[];
|
|
10
|
+
/** Relative paths of files with at least one failure, first-seen order. */
|
|
11
|
+
failingFiles: string[];
|
|
12
|
+
summary: TestSummary | null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether the project's OWN `test` script names vitest — the one signal
|
|
16
|
+
* used to decide whether to inject the JSON reporter at all. Deliberately
|
|
17
|
+
* conservative: a jest project, a custom node runner, or a shell wrapper is
|
|
18
|
+
* left completely alone, with no reporter flags appended and no report
|
|
19
|
+
* parsed — `ekanos test` behaves exactly as it did before this feature.
|
|
20
|
+
*/
|
|
21
|
+
export declare function projectTestScriptUsesVitest(projectDir: string): boolean;
|
|
22
|
+
/** A fresh, unique path for the JSON report — one per invocation. The caller owns cleanup. */
|
|
23
|
+
export declare function newReportPath(): string;
|
|
24
|
+
/** The extra argv that makes vitest ALSO emit a JSON report alongside its normal terminal output. */
|
|
25
|
+
export declare function reporterArgs(outputFilePath: string): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Reads and parses `reportPath` end to end. `null` on ANY failure (missing
|
|
28
|
+
* file — vitest crashed before writing it; malformed JSON; a shape this
|
|
29
|
+
* function doesn't recognize) so the caller falls back to the raw runner
|
|
30
|
+
* output instead of the curated appendix, and never to a crash.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseTestReport(reportPath: string, projectDir: string): ParsedTestReport | null;
|