@ekanos/cli 0.5.0 → 0.5.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/pack.js +7 -4
- package/dist/pack.js.map +1 -1
- package/dist/project-checks.d.ts +6 -0
- package/dist/project-checks.js +11 -0
- package/dist/project-checks.js.map +1 -1
- package/package.json +1 -1
- package/templates/package.json.tmpl +1 -1
package/dist/pack.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as zlib from 'node:zlib';
|
|
4
4
|
import { preconditionError, validationError } from './errors.js';
|
|
5
|
-
import { VITEST_CONFIG_NAMES } from './project-checks.js';
|
|
5
|
+
import { ESLINT_CONFIG_NAMES, VITEST_CONFIG_NAMES } from './project-checks.js';
|
|
6
6
|
/**
|
|
7
7
|
* Deterministic project packing for `ekanos publish`.
|
|
8
8
|
*
|
|
@@ -17,6 +17,9 @@ import { VITEST_CONFIG_NAMES } from './project-checks.js';
|
|
|
17
17
|
* - `tsconfig*.json` at the project root (when present)
|
|
18
18
|
* - a vitest/vite config at the project root (when present — see
|
|
19
19
|
* `VITEST_CONFIG_NAMES`)
|
|
20
|
+
* - an ESLint flat config at the project root (when present — see
|
|
21
|
+
* `ESLINT_CONFIG_NAMES`): the gate's `checks/capability-lint` runs the
|
|
22
|
+
* capability preset from it, and refuses an archive without one
|
|
20
23
|
* - everything under `src/`, recursively
|
|
21
24
|
*
|
|
22
25
|
* The tsconfig/vitest entries exist because the hosted gate's
|
|
@@ -99,8 +102,8 @@ export function selectProjectFiles(projectDir) {
|
|
|
99
102
|
* search. */
|
|
100
103
|
const TSCONFIG_PATTERN = /^tsconfig(\.[^./]+)?\.json$/;
|
|
101
104
|
/**
|
|
102
|
-
* The project-root config files the hosted gate's `checks/typecheck
|
|
103
|
-
* `checks/tests` stages need to run the project's OWN `tsc`/`vitest`
|
|
105
|
+
* The project-root config files the hosted gate's `checks/typecheck`,
|
|
106
|
+
* `checks/tests` and `checks/capability-lint` stages need to run the project's OWN `tsc`/`vitest`
|
|
104
107
|
* meaningfully — see the module doc comment above. Each is included only
|
|
105
108
|
* when present as a regular file (never a symlink, matching every other
|
|
106
109
|
* entry point in this walk), so a project without one of these (unusual, but
|
|
@@ -115,7 +118,7 @@ function selectRootConfigFiles(projectDir) {
|
|
|
115
118
|
continue;
|
|
116
119
|
selected.push(entry.name);
|
|
117
120
|
}
|
|
118
|
-
for (const name of VITEST_CONFIG_NAMES) {
|
|
121
|
+
for (const name of [...VITEST_CONFIG_NAMES, ...ESLINT_CONFIG_NAMES]) {
|
|
119
122
|
if (isRegularFile(path.join(projectDir, name)))
|
|
120
123
|
selected.push(name);
|
|
121
124
|
}
|
package/dist/pack.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pack.js","sourceRoot":"","sources":["../src/pack.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as zlib from 'node:zlib';\n\nimport { preconditionError, validationError } from './errors';\nimport { VITEST_CONFIG_NAMES } from './project-checks';\n\n/**\n * Deterministic project packing for `ekanos publish`.\n *\n * A minimal ustar writer over Node's built-ins — no `tar` dependency. The\n * selection is deliberately a WHITELIST, not \"everything minus a blocklist\":\n * the submission is reviewed by an operator and installed into Ekanos, so the\n * archive must contain exactly the files the toolchain knows about and\n * nothing a working directory happens to hold.\n *\n * - `ekanos.json`, `package.json` (required)\n * - `README.md` (when present)\n * - `tsconfig*.json` at the project root (when present)\n * - a vitest/vite config at the project root (when present — see\n * `VITEST_CONFIG_NAMES`)\n * - everything under `src/`, recursively\n *\n * The tsconfig/vitest entries exist because the hosted gate's\n * `checks/typecheck` and `checks/tests` stages run the project's OWN `tsc`\n * and `vitest` — over the ARCHIVE, not the partner's working directory — so\n * without them there is no `tsconfig.json` to compile against (`tsc --noEmit`\n * falls back to printing its help text and exiting 1, not a type error) and\n * no test config to load the SDK's server-inline block. `ekanos init`\n * scaffolds both at the project root, so a project published straight from\n * `init` always has them; this walk just stops silently dropping them.\n *\n * Excluded everywhere: dotfiles and dot-directories (which covers `.ekanos`,\n * `.git`, `.env`), `node_modules`, and anything that is not a regular file\n * (symlinks are skipped, not followed — a link pointing outside the project\n * must never pull foreign bytes into a submission).\n *\n * Determinism: entries are sorted by path and every header carries a fixed\n * mtime, so packing the same tree twice yields byte-identical archives and a\n * stable sha256.\n */\n\nconst BLOCK_SIZE = 512;\n\n/** Fixed header mtime — determinism over provenance (the VCS owns history). */\nconst FIXED_MTIME_SECONDS = 0;\n\n/** The server refuses archives over 5 MiB; fail fast on the client too. */\n// 4 MiB — mirrors the server route's cap, which itself sits under Vercel's\n// ~4.5 MB request-body ceiling so oversize is caught here with a clear error\n// instead of by an opaque platform 413.\nexport const MAX_ARCHIVE_BYTES = 4 * 1024 * 1024;\n\nexport interface PackedProject {\n /** The gzipped tarball. */\n archive: Buffer;\n /** Project-relative POSIX paths, in the order they were packed. */\n files: string[];\n}\n\nexport function packProject(projectDir: string): PackedProject {\n const files = selectProjectFiles(projectDir);\n\n const blocks: Buffer[] = [];\n\n for (const relative of files) {\n const absolute = path.join(projectDir, relative);\n const content = fs.readFileSync(absolute);\n\n blocks.push(tarHeader(relative, content.length));\n blocks.push(content);\n\n const remainder = content.length % BLOCK_SIZE;\n if (remainder !== 0) {\n blocks.push(Buffer.alloc(BLOCK_SIZE - remainder));\n }\n }\n\n // End-of-archive: two zero blocks.\n blocks.push(Buffer.alloc(BLOCK_SIZE * 2));\n\n const archive = zlib.gzipSync(Buffer.concat(blocks), { level: 9 });\n\n if (archive.length > MAX_ARCHIVE_BYTES) {\n throw validationError(\n `The packed archive is ${archive.length} bytes, over the ` +\n `${MAX_ARCHIVE_BYTES}-byte submission limit.`,\n 'Reduce the project to source only — large binary assets do not ' +\n 'belong in an integration submission.',\n );\n }\n\n return { archive, files };\n}\n\n/**\n * The whitelist walk. Exported for tests: the selection rules are the part\n * with security consequences, so they are asserted directly.\n */\nexport function selectProjectFiles(projectDir: string): string[] {\n for (const required of ['ekanos.json', 'package.json']) {\n if (!isRegularFile(path.join(projectDir, required))) {\n throw preconditionError(\n `Cannot pack ${projectDir}: ${required} is missing.`,\n `Run \"ekanos publish\" from an integration project (or pass --dir), ` +\n `and run \"ekanos init\" first if this is a new project.`,\n );\n }\n }\n\n const selected = ['ekanos.json', 'package.json'];\n\n if (isRegularFile(path.join(projectDir, 'README.md'))) {\n selected.push('README.md');\n }\n\n selected.push(...selectRootConfigFiles(projectDir));\n\n const srcDir = path.join(projectDir, 'src');\n // lstat, not stat: `src` itself as a symlink must not pull a foreign tree\n // into the archive — the same rule walk() applies to every entry inside.\n if (fs.existsSync(srcDir) && fs.lstatSync(srcDir).isDirectory()) {\n selected.push(...walk(projectDir, 'src'));\n }\n\n return selected.sort();\n}\n\n/** `tsconfig.json`, plus any sibling a root `tsconfig.json` might `extends`\n * (e.g. `tsconfig.build.json`) — matched by name, not content, since packing\n * runs before anything parses JSON with comments. Root-level only: this is\n * about the build/test config the gate's own stages need, not a recursive\n * search. */\nconst TSCONFIG_PATTERN = /^tsconfig(\\.[^./]+)?\\.json$/;\n\n/**\n * The project-root config files the hosted gate's `checks/typecheck` and\n * `checks/tests` stages need to run the project's OWN `tsc`/`vitest`\n * meaningfully — see the module doc comment above. Each is included only\n * when present as a regular file (never a symlink, matching every other\n * entry point in this walk), so a project without one of these (unusual, but\n * not invalid) packs exactly as before.\n */\nfunction selectRootConfigFiles(projectDir: string): string[] {\n const selected: string[] = [];\n\n for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {\n if (!TSCONFIG_PATTERN.test(entry.name)) continue;\n if (!isRegularFile(path.join(projectDir, entry.name))) continue;\n selected.push(entry.name);\n }\n\n for (const name of VITEST_CONFIG_NAMES) {\n if (isRegularFile(path.join(projectDir, name))) selected.push(name);\n }\n\n return selected;\n}\n\nfunction walk(projectDir: string, relativeDir: string): string[] {\n const absoluteDir = path.join(projectDir, relativeDir);\n const collected: string[] = [];\n\n for (const entry of fs.readdirSync(absoluteDir, { withFileTypes: true })) {\n if (entry.name.startsWith('.')) continue;\n if (entry.name === 'node_modules') continue;\n\n const relative = `${relativeDir}/${entry.name}`;\n\n if (entry.isDirectory()) {\n collected.push(...walk(projectDir, relative));\n continue;\n }\n\n // Regular files only. `isFile()` is false for symlinks under\n // `withFileTypes`, so a link — wherever it points — is skipped here.\n if (entry.isFile()) {\n collected.push(relative);\n }\n }\n\n return collected;\n}\n\nfunction isRegularFile(absolute: string): boolean {\n try {\n return fs.lstatSync(absolute).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * One 512-byte ustar header. Long paths use the ustar `prefix` field\n * (155 + '/' + 100); a path that fits neither is refused with a clear error\n * rather than silently truncated.\n */\nfunction tarHeader(relativePath: string, size: number): Buffer {\n const { name, prefix } = splitUstarPath(relativePath);\n const header = Buffer.alloc(BLOCK_SIZE);\n\n header.write(name, 0, 100, 'utf8');\n writeOctal(header, 100, 8, 0o644); // mode\n writeOctal(header, 108, 8, 0); // uid\n writeOctal(header, 116, 8, 0); // gid\n writeOctal(header, 124, 12, size);\n writeOctal(header, 136, 12, FIXED_MTIME_SECONDS);\n header.fill(' ', 148, 156); // checksum placeholder: eight spaces\n header.write('0', 156, 1, 'utf8'); // typeflag: regular file\n header.write('ustar', 257, 5, 'utf8');\n header.write('00', 263, 2, 'utf8'); // version\n header.write(prefix, 345, 155, 'utf8');\n\n let checksum = 0;\n for (const byte of header) checksum += byte;\n\n // Six octal digits, NUL, space — the historical checksum encoding.\n header.write(`${checksum.toString(8).padStart(6, '0')}\\0 `, 148, 8, 'latin1');\n\n return header;\n}\n\nfunction splitUstarPath(relativePath: string): {\n name: string;\n prefix: string;\n} {\n if (Buffer.byteLength(relativePath, 'utf8') <= 100) {\n return { name: relativePath, prefix: '' };\n }\n\n // Split at a '/' so that name ≤ 100 bytes and prefix ≤ 155 bytes.\n for (let i = relativePath.length - 1; i > 0; i--) {\n if (relativePath[i] !== '/') continue;\n\n const prefix = relativePath.slice(0, i);\n const name = relativePath.slice(i + 1);\n\n if (\n Buffer.byteLength(name, 'utf8') <= 100 &&\n Buffer.byteLength(prefix, 'utf8') <= 155\n ) {\n return { name, prefix };\n }\n }\n\n throw validationError(\n `Cannot pack \"${relativePath}\": the path is too long for a tar entry.`,\n 'Shorten the file path (tar limits one segment to 100 bytes and the ' +\n 'directory prefix to 155).',\n );\n}\n\nfunction writeOctal(\n header: Buffer,\n offset: number,\n length: number,\n value: number,\n): void {\n header.write(\n `${value.toString(8).padStart(length - 1, '0')}\\0`,\n offset,\n length,\n 'latin1',\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pack.js","sourceRoot":"","sources":["../src/pack.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as zlib from 'node:zlib';\n\nimport { preconditionError, validationError } from './errors';\nimport { ESLINT_CONFIG_NAMES, VITEST_CONFIG_NAMES } from './project-checks';\n\n/**\n * Deterministic project packing for `ekanos publish`.\n *\n * A minimal ustar writer over Node's built-ins — no `tar` dependency. The\n * selection is deliberately a WHITELIST, not \"everything minus a blocklist\":\n * the submission is reviewed by an operator and installed into Ekanos, so the\n * archive must contain exactly the files the toolchain knows about and\n * nothing a working directory happens to hold.\n *\n * - `ekanos.json`, `package.json` (required)\n * - `README.md` (when present)\n * - `tsconfig*.json` at the project root (when present)\n * - a vitest/vite config at the project root (when present — see\n * `VITEST_CONFIG_NAMES`)\n * - an ESLint flat config at the project root (when present — see\n * `ESLINT_CONFIG_NAMES`): the gate's `checks/capability-lint` runs the\n * capability preset from it, and refuses an archive without one\n * - everything under `src/`, recursively\n *\n * The tsconfig/vitest entries exist because the hosted gate's\n * `checks/typecheck` and `checks/tests` stages run the project's OWN `tsc`\n * and `vitest` — over the ARCHIVE, not the partner's working directory — so\n * without them there is no `tsconfig.json` to compile against (`tsc --noEmit`\n * falls back to printing its help text and exiting 1, not a type error) and\n * no test config to load the SDK's server-inline block. `ekanos init`\n * scaffolds both at the project root, so a project published straight from\n * `init` always has them; this walk just stops silently dropping them.\n *\n * Excluded everywhere: dotfiles and dot-directories (which covers `.ekanos`,\n * `.git`, `.env`), `node_modules`, and anything that is not a regular file\n * (symlinks are skipped, not followed — a link pointing outside the project\n * must never pull foreign bytes into a submission).\n *\n * Determinism: entries are sorted by path and every header carries a fixed\n * mtime, so packing the same tree twice yields byte-identical archives and a\n * stable sha256.\n */\n\nconst BLOCK_SIZE = 512;\n\n/** Fixed header mtime — determinism over provenance (the VCS owns history). */\nconst FIXED_MTIME_SECONDS = 0;\n\n/** The server refuses archives over 5 MiB; fail fast on the client too. */\n// 4 MiB — mirrors the server route's cap, which itself sits under Vercel's\n// ~4.5 MB request-body ceiling so oversize is caught here with a clear error\n// instead of by an opaque platform 413.\nexport const MAX_ARCHIVE_BYTES = 4 * 1024 * 1024;\n\nexport interface PackedProject {\n /** The gzipped tarball. */\n archive: Buffer;\n /** Project-relative POSIX paths, in the order they were packed. */\n files: string[];\n}\n\nexport function packProject(projectDir: string): PackedProject {\n const files = selectProjectFiles(projectDir);\n\n const blocks: Buffer[] = [];\n\n for (const relative of files) {\n const absolute = path.join(projectDir, relative);\n const content = fs.readFileSync(absolute);\n\n blocks.push(tarHeader(relative, content.length));\n blocks.push(content);\n\n const remainder = content.length % BLOCK_SIZE;\n if (remainder !== 0) {\n blocks.push(Buffer.alloc(BLOCK_SIZE - remainder));\n }\n }\n\n // End-of-archive: two zero blocks.\n blocks.push(Buffer.alloc(BLOCK_SIZE * 2));\n\n const archive = zlib.gzipSync(Buffer.concat(blocks), { level: 9 });\n\n if (archive.length > MAX_ARCHIVE_BYTES) {\n throw validationError(\n `The packed archive is ${archive.length} bytes, over the ` +\n `${MAX_ARCHIVE_BYTES}-byte submission limit.`,\n 'Reduce the project to source only — large binary assets do not ' +\n 'belong in an integration submission.',\n );\n }\n\n return { archive, files };\n}\n\n/**\n * The whitelist walk. Exported for tests: the selection rules are the part\n * with security consequences, so they are asserted directly.\n */\nexport function selectProjectFiles(projectDir: string): string[] {\n for (const required of ['ekanos.json', 'package.json']) {\n if (!isRegularFile(path.join(projectDir, required))) {\n throw preconditionError(\n `Cannot pack ${projectDir}: ${required} is missing.`,\n `Run \"ekanos publish\" from an integration project (or pass --dir), ` +\n `and run \"ekanos init\" first if this is a new project.`,\n );\n }\n }\n\n const selected = ['ekanos.json', 'package.json'];\n\n if (isRegularFile(path.join(projectDir, 'README.md'))) {\n selected.push('README.md');\n }\n\n selected.push(...selectRootConfigFiles(projectDir));\n\n const srcDir = path.join(projectDir, 'src');\n // lstat, not stat: `src` itself as a symlink must not pull a foreign tree\n // into the archive — the same rule walk() applies to every entry inside.\n if (fs.existsSync(srcDir) && fs.lstatSync(srcDir).isDirectory()) {\n selected.push(...walk(projectDir, 'src'));\n }\n\n return selected.sort();\n}\n\n/** `tsconfig.json`, plus any sibling a root `tsconfig.json` might `extends`\n * (e.g. `tsconfig.build.json`) — matched by name, not content, since packing\n * runs before anything parses JSON with comments. Root-level only: this is\n * about the build/test config the gate's own stages need, not a recursive\n * search. */\nconst TSCONFIG_PATTERN = /^tsconfig(\\.[^./]+)?\\.json$/;\n\n/**\n * The project-root config files the hosted gate's `checks/typecheck`,\n * `checks/tests` and `checks/capability-lint` stages need to run the project's OWN `tsc`/`vitest`\n * meaningfully — see the module doc comment above. Each is included only\n * when present as a regular file (never a symlink, matching every other\n * entry point in this walk), so a project without one of these (unusual, but\n * not invalid) packs exactly as before.\n */\nfunction selectRootConfigFiles(projectDir: string): string[] {\n const selected: string[] = [];\n\n for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {\n if (!TSCONFIG_PATTERN.test(entry.name)) continue;\n if (!isRegularFile(path.join(projectDir, entry.name))) continue;\n selected.push(entry.name);\n }\n\n for (const name of [...VITEST_CONFIG_NAMES, ...ESLINT_CONFIG_NAMES]) {\n if (isRegularFile(path.join(projectDir, name))) selected.push(name);\n }\n\n return selected;\n}\n\nfunction walk(projectDir: string, relativeDir: string): string[] {\n const absoluteDir = path.join(projectDir, relativeDir);\n const collected: string[] = [];\n\n for (const entry of fs.readdirSync(absoluteDir, { withFileTypes: true })) {\n if (entry.name.startsWith('.')) continue;\n if (entry.name === 'node_modules') continue;\n\n const relative = `${relativeDir}/${entry.name}`;\n\n if (entry.isDirectory()) {\n collected.push(...walk(projectDir, relative));\n continue;\n }\n\n // Regular files only. `isFile()` is false for symlinks under\n // `withFileTypes`, so a link — wherever it points — is skipped here.\n if (entry.isFile()) {\n collected.push(relative);\n }\n }\n\n return collected;\n}\n\nfunction isRegularFile(absolute: string): boolean {\n try {\n return fs.lstatSync(absolute).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * One 512-byte ustar header. Long paths use the ustar `prefix` field\n * (155 + '/' + 100); a path that fits neither is refused with a clear error\n * rather than silently truncated.\n */\nfunction tarHeader(relativePath: string, size: number): Buffer {\n const { name, prefix } = splitUstarPath(relativePath);\n const header = Buffer.alloc(BLOCK_SIZE);\n\n header.write(name, 0, 100, 'utf8');\n writeOctal(header, 100, 8, 0o644); // mode\n writeOctal(header, 108, 8, 0); // uid\n writeOctal(header, 116, 8, 0); // gid\n writeOctal(header, 124, 12, size);\n writeOctal(header, 136, 12, FIXED_MTIME_SECONDS);\n header.fill(' ', 148, 156); // checksum placeholder: eight spaces\n header.write('0', 156, 1, 'utf8'); // typeflag: regular file\n header.write('ustar', 257, 5, 'utf8');\n header.write('00', 263, 2, 'utf8'); // version\n header.write(prefix, 345, 155, 'utf8');\n\n let checksum = 0;\n for (const byte of header) checksum += byte;\n\n // Six octal digits, NUL, space — the historical checksum encoding.\n header.write(`${checksum.toString(8).padStart(6, '0')}\\0 `, 148, 8, 'latin1');\n\n return header;\n}\n\nfunction splitUstarPath(relativePath: string): {\n name: string;\n prefix: string;\n} {\n if (Buffer.byteLength(relativePath, 'utf8') <= 100) {\n return { name: relativePath, prefix: '' };\n }\n\n // Split at a '/' so that name ≤ 100 bytes and prefix ≤ 155 bytes.\n for (let i = relativePath.length - 1; i > 0; i--) {\n if (relativePath[i] !== '/') continue;\n\n const prefix = relativePath.slice(0, i);\n const name = relativePath.slice(i + 1);\n\n if (\n Buffer.byteLength(name, 'utf8') <= 100 &&\n Buffer.byteLength(prefix, 'utf8') <= 155\n ) {\n return { name, prefix };\n }\n }\n\n throw validationError(\n `Cannot pack \"${relativePath}\": the path is too long for a tar entry.`,\n 'Shorten the file path (tar limits one segment to 100 bytes and the ' +\n 'directory prefix to 155).',\n );\n}\n\nfunction writeOctal(\n header: Buffer,\n offset: number,\n length: number,\n value: number,\n): void {\n header.write(\n `${value.toString(8).padStart(length - 1, '0')}\\0`,\n offset,\n length,\n 'latin1',\n );\n}\n"]}
|
package/dist/project-checks.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ import { type EkanosProject } from './project.js';
|
|
|
7
7
|
* property can never disagree on what "the test config" even is.
|
|
8
8
|
*/
|
|
9
9
|
export declare const VITEST_CONFIG_NAMES: string[];
|
|
10
|
+
/**
|
|
11
|
+
* The ESLint flat-config basenames the hosted gate's `checks/capability-lint`
|
|
12
|
+
* looks for in the ARCHIVE — the same four, so a project that lints locally
|
|
13
|
+
* is never refused for a config `ekanos publish` left behind.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ESLINT_CONFIG_NAMES: string[];
|
|
10
16
|
/**
|
|
11
17
|
* The host's own compiler-strictness overlay a project's tsconfig should
|
|
12
18
|
* already satisfy — mirrors the exact flags (`HOST_TSC_FLAGS`) the gate's
|
package/dist/project-checks.js
CHANGED
|
@@ -19,6 +19,17 @@ export const VITEST_CONFIG_NAMES = [
|
|
|
19
19
|
'vite.config.js',
|
|
20
20
|
'vite.config.mjs',
|
|
21
21
|
];
|
|
22
|
+
/**
|
|
23
|
+
* The ESLint flat-config basenames the hosted gate's `checks/capability-lint`
|
|
24
|
+
* looks for in the ARCHIVE — the same four, so a project that lints locally
|
|
25
|
+
* is never refused for a config `ekanos publish` left behind.
|
|
26
|
+
*/
|
|
27
|
+
export const ESLINT_CONFIG_NAMES = [
|
|
28
|
+
'eslint.config.mjs',
|
|
29
|
+
'eslint.config.js',
|
|
30
|
+
'eslint.config.cjs',
|
|
31
|
+
'eslint.config.ts',
|
|
32
|
+
];
|
|
22
33
|
/** The exact block the hint quotes — the one config that makes the SDK load. */
|
|
23
34
|
const REQUIRED_INLINE_BLOCK = [
|
|
24
35
|
'test: {',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-checks.js","sourceRoot":"","sources":["../src/project-checks.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { type EkanosProject, isDocUrl } from './project';\n\n/**\n * Every vitest/vite config basename `ekanos init` might have scaffolded (or a\n * partner might have renamed to). Exported so `pack.ts` can pack exactly the\n * same set it checks here — one list, so packing and validating this project\n * property can never disagree on what \"the test config\" even is.\n */\nexport const VITEST_CONFIG_NAMES = [\n 'vitest.config.ts',\n 'vitest.config.mts',\n 'vitest.config.cts',\n 'vitest.config.js',\n 'vitest.config.mjs',\n 'vitest.config.cjs',\n 'vite.config.ts',\n 'vite.config.mts',\n 'vite.config.js',\n 'vite.config.mjs',\n];\n\n/** The exact block the hint quotes — the one config that makes the SDK load. */\nconst REQUIRED_INLINE_BLOCK = [\n 'test: {',\n ' server: {',\n ' deps: {',\n \" inline: ['@ekanos/sdk'],\",\n ' },',\n ' },',\n '}',\n].join('\\n');\n\n/**\n * The host's own compiler-strictness overlay a project's tsconfig should\n * already satisfy — mirrors the exact flags (`HOST_TSC_FLAGS`) the gate's\n * own SECOND `tsc --noEmit` pass checks, read from the host's real tsconfig\n * (source of truth for the values themselves). Hardcoded here for the same\n * reason it is hardcoded there: this package ships to a partner's local\n * machine, so it cannot read a file inside the host's own repo at runtime.\n * Rules-as-data via `/api/partner/toolchain` supersedes this hardcoded copy\n * later — until then, keep both copies in sync by hand.\n */\nexport const HOST_COMPILER_RULES: Record<string, boolean> = {\n strict: true,\n noUncheckedIndexedAccess: true,\n};\n\n/** Matches `tsconfig.json`, `tsconfig.build.json`, etc. — mirrors the pattern\n * `pack.ts` and the gate's `typecheck.ts` use to find a project's tsconfig. */\nconst TSCONFIG_PATTERN = /^tsconfig(\\.[^./]+)?\\.json$/;\n\n/**\n * Project-level integrity checks that read files in the project root (next to\n * ekanos.json), NOT the integration definition. These are deliberately kept\n * out of the schema package's definition collectors: the definition is valid or\n * not on its own; whether the partner's TEST runner is configured to load the\n * published SDK is a property of THIS project.\n *\n * The highest-value check: a partner's vitest must inline `@ekanos/sdk`. The\n * published SDK dist imports extensionless `next/link`, so without\n * `test.server.deps.inline: ['@ekanos/sdk']` vitest fails to load the SDK, the\n * test file silently imports nothing, and vitest reports green over zero tests.\n *\n * The check is conditional: if the project declares NO vitest/vite config at\n * all, there is nothing to assert (the `test` verb would surface a missing\n * runner on its own). If a config IS present, it MUST inline the SDK.\n */\nexport function collectProjectFindings(projectDir: string): Finding[] {\n const configFile = VITEST_CONFIG_NAMES.map((name) =>\n path.join(projectDir, name),\n ).find((candidate) => fs.existsSync(candidate));\n\n if (!configFile) return [];\n\n const source = fs.readFileSync(configFile, 'utf8');\n if (vitestInlinesSdk(source)) return [];\n\n return [\n {\n check: 'project.vitest-inline-sdk',\n severity: 'error',\n file: configFile,\n message:\n `${path.basename(configFile)} does not inline @ekanos/sdk. The ` +\n `published SDK dist imports extensionless \"next/link\", so without ` +\n `test.server.deps.inline including \"@ekanos/sdk\", vitest cannot load ` +\n `the SDK — the test file imports nothing and vitest reports green ` +\n `over zero tests.`,\n hint:\n `Add this to your vitest config's defineConfig so the SDK is ` +\n `transformed for the test runner:\\n\\n${REQUIRED_INLINE_BLOCK}`,\n },\n ];\n}\n\n/**\n * Advisory check on the `docs` array: a relative entry that resolves to\n * nothing on disk gets a WARNING — never an error, because a missing doc file\n * must not block a publish (the gate refuses only error-severity findings).\n * URLs are never checked: `validate` does no network, ever, and an agent\n * dereferences them at authoring time instead.\n */\nexport function collectDocsFindings(\n project: EkanosProject,\n projectDir: string,\n): Finding[] {\n const findings: Finding[] = [];\n\n for (const entry of project.docs ?? []) {\n if (isDocUrl(entry)) continue;\n\n const resolved = path.resolve(projectDir, entry);\n if (fs.existsSync(resolved)) continue;\n\n findings.push({\n check: 'docs/missing-file',\n severity: 'warn',\n file: resolved,\n message:\n `ekanos.json lists \"${entry}\" under \"docs\", but no file exists ` +\n `there (resolved to ${resolved}).`,\n hint:\n `Create ${entry}, or fix the path in ekanos.json's \"docs\" array so ` +\n 'it points at real API documentation.',\n });\n }\n\n return findings;\n}\n\n/**\n * WARN when the project's tsconfig does not already set the host's\n * compiler-strictness overlay (`HOST_COMPILER_RULES`). This exists because a\n * project can pass its own `checks/typecheck` cleanly, publish, and only THEN\n * discover `checks/typecheck-host-rules` (the gate's second, stricter pass)\n * fails on the exact same source — e.g. `noUncheckedIndexedAccess` catching a\n * possibly-`undefined` array/record access `strict` alone allows. WARN, not\n * ERROR: this check reads a partner's local tsconfig, not the gate's own\n * verdict, so it must never block a publish that is actually fine — a\n * project whose tsconfig extends something odd, or omits `compilerOptions`\n * intentionally, still gets to publish and find out from the real gate.\n * `publish` escalates this specific check to ERROR locally (see its\n * severity-escalation list) so a submission that WOULD fail the gate never\n * gets uploaded in the first place.\n *\n * Absent tsconfig is not this check's concern — `checks/typecheck` (and its\n * host-rules sibling) already report that loudly on their own.\n */\nexport function collectHostCompilerRulesFindings(\n projectDir: string,\n): Finding[] {\n const tsconfigPath = findTsconfigPath(projectDir);\n if (!tsconfigPath) return [];\n\n const raw = fs.readFileSync(tsconfigPath, 'utf8');\n const tsconfigName = path.basename(tsconfigPath);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(stripJsonComments(raw));\n } catch {\n // An unparseable tsconfig is a real defect, but not one THIS check can\n // reason about — `tsc` itself will refuse it far more usefully. info\n // severity: surfaced, but never blocks anything.\n return [\n {\n check: 'project.host-compiler-rules',\n severity: 'info',\n file: tsconfigPath,\n message:\n `${tsconfigName} could not be parsed as JSON (even tolerating ` +\n `comments and trailing commas), so this check could not verify it ` +\n `sets the host's compiler rules ` +\n `(${Object.keys(HOST_COMPILER_RULES).join(', ')}).`,\n hint: `Fix ${tsconfigName}'s syntax, then re-run validate.`,\n },\n ];\n }\n\n const compilerOptions =\n typeof parsed === 'object' &&\n parsed !== null &&\n typeof (parsed as { compilerOptions?: unknown }).compilerOptions ===\n 'object' &&\n (parsed as { compilerOptions?: unknown }).compilerOptions !== null\n ? ((parsed as { compilerOptions: Record<string, unknown> })\n .compilerOptions ?? {})\n : {};\n\n const missing = Object.entries(HOST_COMPILER_RULES)\n .filter(([name, required]) => compilerOptions[name] !== required)\n .map(([name]) => name);\n\n if (missing.length === 0) return [];\n\n return [\n {\n check: 'project.host-compiler-rules',\n severity: 'warn',\n file: tsconfigPath,\n message:\n `${tsconfigName} does not set ${missing.join(' and ')} in ` +\n `compilerOptions. The gate re-checks your submission under the ` +\n `host's own stricter compiler rules — a project that typechecks ` +\n `cleanly under its own laxer tsconfig can still fail that pass.`,\n hint:\n `Add ${missing.map((name) => `\"${name}\": true`).join(', ')} to ` +\n `compilerOptions in ${tsconfigName}.`,\n },\n ];\n}\n\n/** `tsconfig.json` if present (matching what a bare `tsc --noEmit` would\n * auto-discover), else the first `tsconfig.*.json` found. */\nfunction findTsconfigPath(projectDir: string): string | undefined {\n let names: string[];\n try {\n names = fs\n .readdirSync(projectDir, { withFileTypes: true })\n .filter((entry) => entry.isFile() && TSCONFIG_PATTERN.test(entry.name))\n .map((entry) => entry.name);\n } catch {\n return undefined;\n }\n if (names.length === 0) return undefined;\n const name = names.includes('tsconfig.json')\n ? 'tsconfig.json'\n : [...names].sort()[0];\n return name ? path.join(projectDir, name) : undefined;\n}\n\n/**\n * A minimal, string-literal-aware strip of `//` and `/* *\\/` comments —\n * tsconfig.json is conventionally JSONC, but nothing in this package's\n * dependency graph parses it as such. Not a full JSONC parser: it does not\n * touch trailing commas beyond the one cleanup pass below, and it makes no\n * attempt to validate the result — `JSON.parse` does that, and a project\n * with genuinely malformed JSON falls through to this check's `info`-severity\n * catch, never a crash.\n */\nfunction stripJsonComments(source: string): string {\n let out = '';\n let inString = false;\n let quote = '';\n\n for (let i = 0; i < source.length; i++) {\n const char = source[i] ?? '';\n\n if (inString) {\n out += char;\n if (char === '\\\\') {\n out += source[++i] ?? '';\n continue;\n }\n if (char === quote) inString = false;\n continue;\n }\n\n if (char === '\"' || char === \"'\") {\n inString = true;\n quote = char;\n out += char;\n continue;\n }\n\n if (char === '/' && source[i + 1] === '/') {\n while (i < source.length && source[i] !== '\\n') i++;\n out += '\\n';\n continue;\n }\n\n if (char === '/' && source[i + 1] === '*') {\n i += 2;\n while (\n i < source.length &&\n !(source[i] === '*' && source[i + 1] === '/')\n ) {\n i++;\n }\n i++; // consume the closing '/'\n continue;\n }\n\n out += char;\n }\n\n // Trailing commas before a closing brace/bracket.\n return out.replace(/,(\\s*[}\\]])/g, '$1');\n}\n\n/**\n * True when the config source contains an `inline: [ … ]` array that lists\n * `@ekanos/sdk`. Textual on purpose — evaluating a partner's config module\n * would run arbitrary code; a lint-style scan does not.\n */\nfunction vitestInlinesSdk(source: string): boolean {\n const inlineArrayPattern = /inline\\s*:\\s*\\[([\\s\\S]*?)\\]/g;\n for (\n let match = inlineArrayPattern.exec(source);\n match !== null;\n match = inlineArrayPattern.exec(source)\n ) {\n if (/['\"]@ekanos\\/sdk['\"]/.test(match[1] ?? '')) return true;\n }\n return false;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"project-checks.js","sourceRoot":"","sources":["../src/project-checks.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { type EkanosProject, isDocUrl } from './project';\n\n/**\n * Every vitest/vite config basename `ekanos init` might have scaffolded (or a\n * partner might have renamed to). Exported so `pack.ts` can pack exactly the\n * same set it checks here — one list, so packing and validating this project\n * property can never disagree on what \"the test config\" even is.\n */\nexport const VITEST_CONFIG_NAMES = [\n 'vitest.config.ts',\n 'vitest.config.mts',\n 'vitest.config.cts',\n 'vitest.config.js',\n 'vitest.config.mjs',\n 'vitest.config.cjs',\n 'vite.config.ts',\n 'vite.config.mts',\n 'vite.config.js',\n 'vite.config.mjs',\n];\n\n/**\n * The ESLint flat-config basenames the hosted gate's `checks/capability-lint`\n * looks for in the ARCHIVE — the same four, so a project that lints locally\n * is never refused for a config `ekanos publish` left behind.\n */\nexport const ESLINT_CONFIG_NAMES = [\n 'eslint.config.mjs',\n 'eslint.config.js',\n 'eslint.config.cjs',\n 'eslint.config.ts',\n];\n\n/** The exact block the hint quotes — the one config that makes the SDK load. */\nconst REQUIRED_INLINE_BLOCK = [\n 'test: {',\n ' server: {',\n ' deps: {',\n \" inline: ['@ekanos/sdk'],\",\n ' },',\n ' },',\n '}',\n].join('\\n');\n\n/**\n * The host's own compiler-strictness overlay a project's tsconfig should\n * already satisfy — mirrors the exact flags (`HOST_TSC_FLAGS`) the gate's\n * own SECOND `tsc --noEmit` pass checks, read from the host's real tsconfig\n * (source of truth for the values themselves). Hardcoded here for the same\n * reason it is hardcoded there: this package ships to a partner's local\n * machine, so it cannot read a file inside the host's own repo at runtime.\n * Rules-as-data via `/api/partner/toolchain` supersedes this hardcoded copy\n * later — until then, keep both copies in sync by hand.\n */\nexport const HOST_COMPILER_RULES: Record<string, boolean> = {\n strict: true,\n noUncheckedIndexedAccess: true,\n};\n\n/** Matches `tsconfig.json`, `tsconfig.build.json`, etc. — mirrors the pattern\n * `pack.ts` and the gate's `typecheck.ts` use to find a project's tsconfig. */\nconst TSCONFIG_PATTERN = /^tsconfig(\\.[^./]+)?\\.json$/;\n\n/**\n * Project-level integrity checks that read files in the project root (next to\n * ekanos.json), NOT the integration definition. These are deliberately kept\n * out of the schema package's definition collectors: the definition is valid or\n * not on its own; whether the partner's TEST runner is configured to load the\n * published SDK is a property of THIS project.\n *\n * The highest-value check: a partner's vitest must inline `@ekanos/sdk`. The\n * published SDK dist imports extensionless `next/link`, so without\n * `test.server.deps.inline: ['@ekanos/sdk']` vitest fails to load the SDK, the\n * test file silently imports nothing, and vitest reports green over zero tests.\n *\n * The check is conditional: if the project declares NO vitest/vite config at\n * all, there is nothing to assert (the `test` verb would surface a missing\n * runner on its own). If a config IS present, it MUST inline the SDK.\n */\nexport function collectProjectFindings(projectDir: string): Finding[] {\n const configFile = VITEST_CONFIG_NAMES.map((name) =>\n path.join(projectDir, name),\n ).find((candidate) => fs.existsSync(candidate));\n\n if (!configFile) return [];\n\n const source = fs.readFileSync(configFile, 'utf8');\n if (vitestInlinesSdk(source)) return [];\n\n return [\n {\n check: 'project.vitest-inline-sdk',\n severity: 'error',\n file: configFile,\n message:\n `${path.basename(configFile)} does not inline @ekanos/sdk. The ` +\n `published SDK dist imports extensionless \"next/link\", so without ` +\n `test.server.deps.inline including \"@ekanos/sdk\", vitest cannot load ` +\n `the SDK — the test file imports nothing and vitest reports green ` +\n `over zero tests.`,\n hint:\n `Add this to your vitest config's defineConfig so the SDK is ` +\n `transformed for the test runner:\\n\\n${REQUIRED_INLINE_BLOCK}`,\n },\n ];\n}\n\n/**\n * Advisory check on the `docs` array: a relative entry that resolves to\n * nothing on disk gets a WARNING — never an error, because a missing doc file\n * must not block a publish (the gate refuses only error-severity findings).\n * URLs are never checked: `validate` does no network, ever, and an agent\n * dereferences them at authoring time instead.\n */\nexport function collectDocsFindings(\n project: EkanosProject,\n projectDir: string,\n): Finding[] {\n const findings: Finding[] = [];\n\n for (const entry of project.docs ?? []) {\n if (isDocUrl(entry)) continue;\n\n const resolved = path.resolve(projectDir, entry);\n if (fs.existsSync(resolved)) continue;\n\n findings.push({\n check: 'docs/missing-file',\n severity: 'warn',\n file: resolved,\n message:\n `ekanos.json lists \"${entry}\" under \"docs\", but no file exists ` +\n `there (resolved to ${resolved}).`,\n hint:\n `Create ${entry}, or fix the path in ekanos.json's \"docs\" array so ` +\n 'it points at real API documentation.',\n });\n }\n\n return findings;\n}\n\n/**\n * WARN when the project's tsconfig does not already set the host's\n * compiler-strictness overlay (`HOST_COMPILER_RULES`). This exists because a\n * project can pass its own `checks/typecheck` cleanly, publish, and only THEN\n * discover `checks/typecheck-host-rules` (the gate's second, stricter pass)\n * fails on the exact same source — e.g. `noUncheckedIndexedAccess` catching a\n * possibly-`undefined` array/record access `strict` alone allows. WARN, not\n * ERROR: this check reads a partner's local tsconfig, not the gate's own\n * verdict, so it must never block a publish that is actually fine — a\n * project whose tsconfig extends something odd, or omits `compilerOptions`\n * intentionally, still gets to publish and find out from the real gate.\n * `publish` escalates this specific check to ERROR locally (see its\n * severity-escalation list) so a submission that WOULD fail the gate never\n * gets uploaded in the first place.\n *\n * Absent tsconfig is not this check's concern — `checks/typecheck` (and its\n * host-rules sibling) already report that loudly on their own.\n */\nexport function collectHostCompilerRulesFindings(\n projectDir: string,\n): Finding[] {\n const tsconfigPath = findTsconfigPath(projectDir);\n if (!tsconfigPath) return [];\n\n const raw = fs.readFileSync(tsconfigPath, 'utf8');\n const tsconfigName = path.basename(tsconfigPath);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(stripJsonComments(raw));\n } catch {\n // An unparseable tsconfig is a real defect, but not one THIS check can\n // reason about — `tsc` itself will refuse it far more usefully. info\n // severity: surfaced, but never blocks anything.\n return [\n {\n check: 'project.host-compiler-rules',\n severity: 'info',\n file: tsconfigPath,\n message:\n `${tsconfigName} could not be parsed as JSON (even tolerating ` +\n `comments and trailing commas), so this check could not verify it ` +\n `sets the host's compiler rules ` +\n `(${Object.keys(HOST_COMPILER_RULES).join(', ')}).`,\n hint: `Fix ${tsconfigName}'s syntax, then re-run validate.`,\n },\n ];\n }\n\n const compilerOptions =\n typeof parsed === 'object' &&\n parsed !== null &&\n typeof (parsed as { compilerOptions?: unknown }).compilerOptions ===\n 'object' &&\n (parsed as { compilerOptions?: unknown }).compilerOptions !== null\n ? ((parsed as { compilerOptions: Record<string, unknown> })\n .compilerOptions ?? {})\n : {};\n\n const missing = Object.entries(HOST_COMPILER_RULES)\n .filter(([name, required]) => compilerOptions[name] !== required)\n .map(([name]) => name);\n\n if (missing.length === 0) return [];\n\n return [\n {\n check: 'project.host-compiler-rules',\n severity: 'warn',\n file: tsconfigPath,\n message:\n `${tsconfigName} does not set ${missing.join(' and ')} in ` +\n `compilerOptions. The gate re-checks your submission under the ` +\n `host's own stricter compiler rules — a project that typechecks ` +\n `cleanly under its own laxer tsconfig can still fail that pass.`,\n hint:\n `Add ${missing.map((name) => `\"${name}\": true`).join(', ')} to ` +\n `compilerOptions in ${tsconfigName}.`,\n },\n ];\n}\n\n/** `tsconfig.json` if present (matching what a bare `tsc --noEmit` would\n * auto-discover), else the first `tsconfig.*.json` found. */\nfunction findTsconfigPath(projectDir: string): string | undefined {\n let names: string[];\n try {\n names = fs\n .readdirSync(projectDir, { withFileTypes: true })\n .filter((entry) => entry.isFile() && TSCONFIG_PATTERN.test(entry.name))\n .map((entry) => entry.name);\n } catch {\n return undefined;\n }\n if (names.length === 0) return undefined;\n const name = names.includes('tsconfig.json')\n ? 'tsconfig.json'\n : [...names].sort()[0];\n return name ? path.join(projectDir, name) : undefined;\n}\n\n/**\n * A minimal, string-literal-aware strip of `//` and `/* *\\/` comments —\n * tsconfig.json is conventionally JSONC, but nothing in this package's\n * dependency graph parses it as such. Not a full JSONC parser: it does not\n * touch trailing commas beyond the one cleanup pass below, and it makes no\n * attempt to validate the result — `JSON.parse` does that, and a project\n * with genuinely malformed JSON falls through to this check's `info`-severity\n * catch, never a crash.\n */\nfunction stripJsonComments(source: string): string {\n let out = '';\n let inString = false;\n let quote = '';\n\n for (let i = 0; i < source.length; i++) {\n const char = source[i] ?? '';\n\n if (inString) {\n out += char;\n if (char === '\\\\') {\n out += source[++i] ?? '';\n continue;\n }\n if (char === quote) inString = false;\n continue;\n }\n\n if (char === '\"' || char === \"'\") {\n inString = true;\n quote = char;\n out += char;\n continue;\n }\n\n if (char === '/' && source[i + 1] === '/') {\n while (i < source.length && source[i] !== '\\n') i++;\n out += '\\n';\n continue;\n }\n\n if (char === '/' && source[i + 1] === '*') {\n i += 2;\n while (\n i < source.length &&\n !(source[i] === '*' && source[i + 1] === '/')\n ) {\n i++;\n }\n i++; // consume the closing '/'\n continue;\n }\n\n out += char;\n }\n\n // Trailing commas before a closing brace/bracket.\n return out.replace(/,(\\s*[}\\]])/g, '$1');\n}\n\n/**\n * True when the config source contains an `inline: [ … ]` array that lists\n * `@ekanos/sdk`. Textual on purpose — evaluating a partner's config module\n * would run arbitrary code; a lint-style scan does not.\n */\nfunction vitestInlinesSdk(source: string): boolean {\n const inlineArrayPattern = /inline\\s*:\\s*\\[([\\s\\S]*?)\\]/g;\n for (\n let match = inlineArrayPattern.exec(source);\n match !== null;\n match = inlineArrayPattern.exec(source)\n ) {\n if (/['\"]@ekanos\\/sdk['\"]/.test(match[1] ?? '')) return true;\n }\n return false;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekanos/cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The Ekanos partner toolchain CLI: scaffold, validate, and test an Ekanos integration against the published @ekanos packages. Agent-native — every verb speaks JSON with a stable exit-code taxonomy.",
|
|
6
6
|
"keywords": [
|