@ekanos/cli 0.1.9 → 0.2.0
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/auth/fusion-api.js +2 -2
- package/dist/auth/fusion-api.js.map +1 -1
- 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/generated/fa-free-names.js +2 -2
- package/dist/generated/fa-free-names.js.map +1 -1
- package/dist/icon-findings.js +5 -5
- package/dist/icon-findings.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +23 -5
- package/dist/index.js.map +1 -1
- package/dist/pack.js +43 -0
- package/dist/pack.js.map +1 -1
- package/dist/project-checks.d.ts +37 -0
- package/dist/project-checks.js +169 -1
- package/dist/project-checks.js.map +1 -1
- package/dist/publish-api.d.ts +24 -11
- package/dist/publish-api.js +7 -8
- 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/sources-api.d.ts +3 -3
- package/dist/sources-api.js.map +1 -1
- 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/toolchain-api.d.ts +5 -5
- package/dist/toolchain-api.js.map +1 -1
- 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 -2
- 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/ui.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
/**
|
|
3
|
+
* The one color-on/off decision for the whole CLI. True only when the
|
|
4
|
+
* stream is a real terminal AND `NO_COLOR` is unset AND `TERM` is not
|
|
5
|
+
* `"dumb"` — EXCEPT `FORCE_COLOR`, which overrides everything else on when
|
|
6
|
+
* set to anything other than `"0"` (matching the de facto convention: Node,
|
|
7
|
+
* chalk, and picocolors itself all treat a defined-and-nonzero `FORCE_COLOR`
|
|
8
|
+
* as "yes, even if you don't think you can").
|
|
9
|
+
*/
|
|
10
|
+
export function colorsEnabled(probe) {
|
|
11
|
+
const { env } = probe;
|
|
12
|
+
if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== '0')
|
|
13
|
+
return true;
|
|
14
|
+
if (env.NO_COLOR !== undefined)
|
|
15
|
+
return false;
|
|
16
|
+
if (env.TERM === 'dumb')
|
|
17
|
+
return false;
|
|
18
|
+
return probe.isTTY;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether the destination can render the box-drawing/Unicode glyphs this
|
|
22
|
+
* module prefers. Deliberately simple: a "dumb" terminal never can, and a
|
|
23
|
+
* locale that names a non-UTF-8 charset can't be trusted to either. Anything
|
|
24
|
+
* else — including an unset locale, which is the common case for CI/agent
|
|
25
|
+
* environments — is assumed capable rather than guessed at further.
|
|
26
|
+
*/
|
|
27
|
+
function unicodeSupported(env) {
|
|
28
|
+
var _a, _b, _c;
|
|
29
|
+
if (env.TERM === 'dumb')
|
|
30
|
+
return false;
|
|
31
|
+
const locale = (_c = (_b = (_a = env.LANG) !== null && _a !== void 0 ? _a : env.LC_ALL) !== null && _b !== void 0 ? _b : env.LC_CTYPE) !== null && _c !== void 0 ? _c : '';
|
|
32
|
+
if (locale && !/utf-?8/i.test(locale))
|
|
33
|
+
return false;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const UNICODE_GLYPHS = { ok: '✓', err: '✖', warn: '▲', info: '●' };
|
|
37
|
+
const ASCII_GLYPHS = { ok: 'ok', err: 'x', warn: '!', info: '*' };
|
|
38
|
+
/** Right-pad (left-align) a cell to `width`, ignoring color codes in length math is the CALLER's job — pad before coloring, never after. */
|
|
39
|
+
function padCell(text, width, align) {
|
|
40
|
+
return align === 'right' ? text.padStart(width) : text.padEnd(width);
|
|
41
|
+
}
|
|
42
|
+
export function createUi(probe) {
|
|
43
|
+
const colors = colorsEnabled(probe);
|
|
44
|
+
const c = pc.createColors(colors);
|
|
45
|
+
const glyphs = unicodeSupported(probe.env) ? UNICODE_GLYPHS : ASCII_GLYPHS;
|
|
46
|
+
const err = (text) => c.red(text);
|
|
47
|
+
const warn = (text) => c.yellow(text);
|
|
48
|
+
const ok = (text) => c.green(text);
|
|
49
|
+
const accent = (text) => c.cyan(text);
|
|
50
|
+
const dim = (text) => c.dim(text);
|
|
51
|
+
const bold = (text) => c.bold(text);
|
|
52
|
+
return {
|
|
53
|
+
colors,
|
|
54
|
+
glyphs,
|
|
55
|
+
err,
|
|
56
|
+
warn,
|
|
57
|
+
ok,
|
|
58
|
+
accent,
|
|
59
|
+
dim,
|
|
60
|
+
bold,
|
|
61
|
+
section(title) {
|
|
62
|
+
return dim(title.toUpperCase());
|
|
63
|
+
},
|
|
64
|
+
keyValue(rows) {
|
|
65
|
+
const keyWidth = Math.max(0, ...rows.map(([key]) => key.length));
|
|
66
|
+
return rows
|
|
67
|
+
.map(([key, value]) => `${dim(key.padEnd(keyWidth))} ${value}`)
|
|
68
|
+
.join('\n');
|
|
69
|
+
},
|
|
70
|
+
table(headers, rows) {
|
|
71
|
+
const columnCount = headers.length;
|
|
72
|
+
const isNumeric = (value) => typeof value === 'number' ||
|
|
73
|
+
(value.trim() !== '' && !Number.isNaN(Number(value)));
|
|
74
|
+
const aligns = Array.from({ length: columnCount }, (_, col) => rows.length > 0 && rows.every((row) => { var _a; return isNumeric((_a = row[col]) !== null && _a !== void 0 ? _a : ''); })
|
|
75
|
+
? 'right'
|
|
76
|
+
: 'left');
|
|
77
|
+
const widths = Array.from({ length: columnCount }, (_, col) => {
|
|
78
|
+
var _a, _b;
|
|
79
|
+
return Math.max((_b = (_a = headers[col]) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0, ...rows.map((row) => { var _a; return String((_a = row[col]) !== null && _a !== void 0 ? _a : '').length; }));
|
|
80
|
+
});
|
|
81
|
+
const headerLine = headers
|
|
82
|
+
.map((header, col) => {
|
|
83
|
+
var _a, _b;
|
|
84
|
+
return dim(padCell(header.toUpperCase(), (_a = widths[col]) !== null && _a !== void 0 ? _a : 0, (_b = aligns[col]) !== null && _b !== void 0 ? _b : 'left'));
|
|
85
|
+
})
|
|
86
|
+
.join(' ');
|
|
87
|
+
const bodyLines = rows.map((row) => row
|
|
88
|
+
.map((cell, col) => { var _a, _b; return padCell(String(cell), (_a = widths[col]) !== null && _a !== void 0 ? _a : 0, (_b = aligns[col]) !== null && _b !== void 0 ? _b : 'left'); })
|
|
89
|
+
.join(' '));
|
|
90
|
+
return [headerLine, ...bodyLines].join('\n');
|
|
91
|
+
},
|
|
92
|
+
hint(text) {
|
|
93
|
+
return accent(`→ ${text}`);
|
|
94
|
+
},
|
|
95
|
+
nextAction(cmd, why) {
|
|
96
|
+
const prefix = why ? `→ ${why}: ` : '→ ';
|
|
97
|
+
return `\n${dim(prefix)}${bold(cmd)}`;
|
|
98
|
+
},
|
|
99
|
+
codeframe(opts) {
|
|
100
|
+
var _a, _b;
|
|
101
|
+
const contextLines = (_a = opts.contextLines) !== null && _a !== void 0 ? _a : 2;
|
|
102
|
+
const lines = opts.source.split('\n');
|
|
103
|
+
const start = Math.max(1, opts.line - contextLines);
|
|
104
|
+
const end = Math.min(lines.length, opts.line + contextLines);
|
|
105
|
+
const gutterWidth = String(end).length;
|
|
106
|
+
const rows = [];
|
|
107
|
+
for (let n = start; n <= end; n += 1) {
|
|
108
|
+
const isTarget = n === opts.line;
|
|
109
|
+
const text = (_b = lines[n - 1]) !== null && _b !== void 0 ? _b : '';
|
|
110
|
+
const marker = isTarget ? '>' : ' ';
|
|
111
|
+
const lineNo = String(n).padStart(gutterWidth);
|
|
112
|
+
const rendered = isTarget ? bold(text) : text;
|
|
113
|
+
rows.push(`${isTarget ? accent(marker) : marker} ${dim(lineNo)} ${dim('│')} ${rendered}`);
|
|
114
|
+
}
|
|
115
|
+
return rows.join('\n');
|
|
116
|
+
},
|
|
117
|
+
badge(text, kind = 'dim') {
|
|
118
|
+
const wrapped = `[${text}]`;
|
|
119
|
+
switch (kind) {
|
|
120
|
+
case 'err':
|
|
121
|
+
return err(wrapped);
|
|
122
|
+
case 'warn':
|
|
123
|
+
return warn(wrapped);
|
|
124
|
+
case 'accent':
|
|
125
|
+
return accent(wrapped);
|
|
126
|
+
default:
|
|
127
|
+
return dim(wrapped);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=ui.js.map
|
package/dist/ui.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"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","sourcesContent":["import pc from 'picocolors';\n\n/**\n * The CLI's sole style layer. Every color, glyph, and human-output component\n * lives here — nothing else in this package may `import 'picocolors'` or\n * write a raw ANSI escape (the gradient banner in `banner.ts` is the one\n * documented exception: it paints a true-color gradient picocolors cannot\n * express, but even it asks THIS module's `colorsEnabled` for the on/off\n * decision, so there is exactly one gate).\n *\n * Everything here renders plain strings. Printing is still `CliContext`'s\n * job (`ctx.log`) — this module never touches a stream directly, so the\n * stdout-purity invariant in `context.ts` cannot be bypassed by a stray\n * color call.\n */\n\n/** What `colorsEnabled` needs to know to decide — never the real stream. */\nexport interface ColorProbe {\n /** Whether the destination (stderr, always — see `context.ts`) is a TTY. */\n isTTY: boolean;\n env: Readonly<Record<string, string | undefined>>;\n}\n\n/**\n * The one color-on/off decision for the whole CLI. True only when the\n * stream is a real terminal AND `NO_COLOR` is unset AND `TERM` is not\n * `\"dumb\"` — EXCEPT `FORCE_COLOR`, which overrides everything else on when\n * set to anything other than `\"0\"` (matching the de facto convention: Node,\n * chalk, and picocolors itself all treat a defined-and-nonzero `FORCE_COLOR`\n * as \"yes, even if you don't think you can\").\n */\nexport function colorsEnabled(probe: ColorProbe): boolean {\n const { env } = probe;\n\n if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== '0') return true;\n if (env.NO_COLOR !== undefined) return false;\n if (env.TERM === 'dumb') return false;\n\n return probe.isTTY;\n}\n\n/**\n * Whether the destination can render the box-drawing/Unicode glyphs this\n * module prefers. Deliberately simple: a \"dumb\" terminal never can, and a\n * locale that names a non-UTF-8 charset can't be trusted to either. Anything\n * else — including an unset locale, which is the common case for CI/agent\n * environments — is assumed capable rather than guessed at further.\n */\nfunction unicodeSupported(\n env: Readonly<Record<string, string | undefined>>,\n): boolean {\n if (env.TERM === 'dumb') return false;\n\n const locale = env.LANG ?? env.LC_ALL ?? env.LC_CTYPE ?? '';\n if (locale && !/utf-?8/i.test(locale)) return false;\n\n return true;\n}\n\nexport interface Glyphs {\n ok: string;\n err: string;\n warn: string;\n info: string;\n}\n\nconst UNICODE_GLYPHS: Glyphs = { ok: '✓', err: '✖', warn: '▲', info: '●' };\nconst ASCII_GLYPHS: Glyphs = { ok: 'ok', err: 'x', warn: '!', info: '*' };\n\n/** One row of `ui.keyValue`. */\nexport type KeyValueRow = readonly [key: string, value: string];\n\nexport interface CodeframeOptions {\n /** Not rendered directly — callers already print `file:line` above/around this. Kept for future use and self-documentation at call sites. */\n file?: string;\n /** 1-indexed line to highlight. */\n line: number;\n /** The full source text of `file`. */\n source: string;\n /** Lines of context above/below the target line. Defaults to 2. */\n contextLines?: number;\n}\n\nexport type BadgeKind = 'dim' | 'accent' | 'warn' | 'err';\n\nexport interface Ui {\n readonly colors: boolean;\n readonly glyphs: Glyphs;\n\n err(text: string): string;\n warn(text: string): string;\n ok(text: string): string;\n accent(text: string): string;\n dim(text: string): string;\n bold(text: string): string;\n\n /** A dim, uppercased section header. */\n section(title: string): string;\n /** Aligned `key value` rows, 2-space gutter, keys dimmed. */\n keyValue(rows: readonly KeyValueRow[]): string;\n /** A header-having, left/right-aligned table with no box-drawing. */\n table(\n headers: readonly string[],\n rows: readonly (readonly (string | number)[])[],\n ): string;\n /** A `→ ` prefixed, accent-colored hint line. */\n hint(text: string): string;\n /**\n * THE closing line of any failure path: a blank line, then a dim\n * `→ <why>: ` (or bare `→ `) followed by the bold remediation command.\n */\n nextAction(cmd: string, why?: string): string;\n /** A Prisma/Vite-style source snippet around one line. */\n codeframe(opts: CodeframeOptions): string;\n /** A bracketed, dim-by-default tag: `[gate · 2h ago]`. */\n badge(text: string, kind?: BadgeKind): string;\n}\n\n/** Right-pad (left-align) a cell to `width`, ignoring color codes in length math is the CALLER's job — pad before coloring, never after. */\nfunction padCell(text: string, width: number, align: 'left' | 'right'): string {\n return align === 'right' ? text.padStart(width) : text.padEnd(width);\n}\n\nexport function createUi(probe: ColorProbe): Ui {\n const colors = colorsEnabled(probe);\n const c = pc.createColors(colors);\n const glyphs = unicodeSupported(probe.env) ? UNICODE_GLYPHS : ASCII_GLYPHS;\n\n const err = (text: string) => c.red(text);\n const warn = (text: string) => c.yellow(text);\n const ok = (text: string) => c.green(text);\n const accent = (text: string) => c.cyan(text);\n const dim = (text: string) => c.dim(text);\n const bold = (text: string) => c.bold(text);\n\n return {\n colors,\n glyphs,\n err,\n warn,\n ok,\n accent,\n dim,\n bold,\n\n section(title: string): string {\n return dim(title.toUpperCase());\n },\n\n keyValue(rows: readonly KeyValueRow[]): string {\n const keyWidth = Math.max(0, ...rows.map(([key]) => key.length));\n\n return rows\n .map(([key, value]) => `${dim(key.padEnd(keyWidth))} ${value}`)\n .join('\\n');\n },\n\n table(\n headers: readonly string[],\n rows: readonly (readonly (string | number)[])[],\n ): string {\n const columnCount = headers.length;\n const isNumeric = (value: string | number) =>\n typeof value === 'number' ||\n (value.trim() !== '' && !Number.isNaN(Number(value)));\n\n const aligns: Array<'left' | 'right'> = Array.from(\n { length: columnCount },\n (_, col) =>\n rows.length > 0 && rows.every((row) => isNumeric(row[col] ?? ''))\n ? 'right'\n : 'left',\n );\n\n const widths = Array.from({ length: columnCount }, (_, col) =>\n Math.max(\n headers[col]?.length ?? 0,\n ...rows.map((row) => String(row[col] ?? '').length),\n ),\n );\n\n const headerLine = headers\n .map((header, col) =>\n dim(\n padCell(\n header.toUpperCase(),\n widths[col] ?? 0,\n aligns[col] ?? 'left',\n ),\n ),\n )\n .join(' ');\n\n const bodyLines = rows.map((row) =>\n row\n .map((cell, col) =>\n padCell(String(cell), widths[col] ?? 0, aligns[col] ?? 'left'),\n )\n .join(' '),\n );\n\n return [headerLine, ...bodyLines].join('\\n');\n },\n\n hint(text: string): string {\n return accent(`→ ${text}`);\n },\n\n nextAction(cmd: string, why?: string): string {\n const prefix = why ? `→ ${why}: ` : '→ ';\n return `\\n${dim(prefix)}${bold(cmd)}`;\n },\n\n codeframe(opts: CodeframeOptions): string {\n const contextLines = opts.contextLines ?? 2;\n const lines = opts.source.split('\\n');\n const start = Math.max(1, opts.line - contextLines);\n const end = Math.min(lines.length, opts.line + contextLines);\n const gutterWidth = String(end).length;\n\n const rows: string[] = [];\n for (let n = start; n <= end; n += 1) {\n const isTarget = n === opts.line;\n const text = lines[n - 1] ?? '';\n const marker = isTarget ? '>' : ' ';\n const lineNo = String(n).padStart(gutterWidth);\n const rendered = isTarget ? bold(text) : text;\n\n rows.push(\n `${isTarget ? accent(marker) : marker} ${dim(lineNo)} ${dim('│')} ${rendered}`,\n );\n }\n\n return rows.join('\\n');\n },\n\n badge(text: string, kind: BadgeKind = 'dim'): string {\n const wrapped = `[${text}]`;\n\n switch (kind) {\n case 'err':\n return err(wrapped);\n case 'warn':\n return warn(wrapped);\n case 'accent':\n return accent(wrapped);\n default:\n return dim(wrapped);\n }\n },\n };\n}\n"]}
|
|
@@ -20,3 +20,25 @@ import type { LoadedProject } from './project.js';
|
|
|
20
20
|
* it, not tests that fake their own reachability.
|
|
21
21
|
*/
|
|
22
22
|
export declare function collectAllFindings(loaded: LoadedProject): Promise<Finding[]>;
|
|
23
|
+
/**
|
|
24
|
+
* `package.json`'s `version` field and the `version` `defineIntegration()`
|
|
25
|
+
* declares are two more independent sources of truth for the same fact —
|
|
26
|
+
* this project shipped weather@0.1.1 with a package.json bump, but the
|
|
27
|
+
* declaration itself still said 0.1.0. Generated seeds embed the
|
|
28
|
+
* DECLARATION's version in their header, so a stale declaration produces a
|
|
29
|
+
* seed that looks stale even though the package itself moved on.
|
|
30
|
+
*
|
|
31
|
+
* WARN, not error, unlike slug agreement: a mismatched version is a quality
|
|
32
|
+
* signal a partner should fix, not a broken cross-reference that breaks
|
|
33
|
+
* something else at runtime, so it must not block `publish`. And a project
|
|
34
|
+
* with no package.json, or a package.json with no `version` field, has
|
|
35
|
+
* nothing to compare the declaration against — skip rather than warn on
|
|
36
|
+
* absence.
|
|
37
|
+
*/
|
|
38
|
+
export declare function collectVersionAgreementFindings(integration: {
|
|
39
|
+
slug: string;
|
|
40
|
+
entry: string;
|
|
41
|
+
entryPath: string;
|
|
42
|
+
}, definition: {
|
|
43
|
+
version?: unknown;
|
|
44
|
+
}, projectDir: string): Finding[];
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { collectCollisionFindings, collectDefinitionFindings, } from '@ekanos/integration-schema';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
2
4
|
import { preconditionError } from './errors.js';
|
|
3
5
|
import { collectIconFindings } from './icon-findings.js';
|
|
4
6
|
import { loadDefinition } from './load-definition.js';
|
|
5
|
-
import { collectDocsFindings, collectProjectFindings } from './project-checks.js';
|
|
7
|
+
import { collectDocsFindings, collectHostCompilerRulesFindings, collectProjectFindings, } from './project-checks.js';
|
|
6
8
|
/**
|
|
7
9
|
* The full findings pass shared by `validate` (which reports them) and
|
|
8
10
|
* `publish` (which refuses to submit on any error-severity finding). One
|
|
@@ -32,6 +34,7 @@ export async function collectAllFindings(loaded) {
|
|
|
32
34
|
file: integration.entryPath,
|
|
33
35
|
}));
|
|
34
36
|
findings.push(...collectSlugAgreementFindings(integration, result.definition));
|
|
37
|
+
findings.push(...collectVersionAgreementFindings(integration, result.definition, loaded.projectDir));
|
|
35
38
|
definitions.push(result.definition);
|
|
36
39
|
continue;
|
|
37
40
|
}
|
|
@@ -58,6 +61,7 @@ export async function collectAllFindings(loaded) {
|
|
|
58
61
|
findings.push(...collectProjectFindings(loaded.projectDir));
|
|
59
62
|
findings.push(...collectDocsFindings(loaded.project, loaded.projectDir));
|
|
60
63
|
findings.push(...collectIconFindings(loaded.projectDir));
|
|
64
|
+
findings.push(...collectHostCompilerRulesFindings(loaded.projectDir));
|
|
61
65
|
return findings;
|
|
62
66
|
}
|
|
63
67
|
/**
|
|
@@ -90,4 +94,56 @@ function collectSlugAgreementFindings(integration, definition) {
|
|
|
90
94
|
},
|
|
91
95
|
];
|
|
92
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* `package.json`'s `version` field and the `version` `defineIntegration()`
|
|
99
|
+
* declares are two more independent sources of truth for the same fact —
|
|
100
|
+
* this project shipped weather@0.1.1 with a package.json bump, but the
|
|
101
|
+
* declaration itself still said 0.1.0. Generated seeds embed the
|
|
102
|
+
* DECLARATION's version in their header, so a stale declaration produces a
|
|
103
|
+
* seed that looks stale even though the package itself moved on.
|
|
104
|
+
*
|
|
105
|
+
* WARN, not error, unlike slug agreement: a mismatched version is a quality
|
|
106
|
+
* signal a partner should fix, not a broken cross-reference that breaks
|
|
107
|
+
* something else at runtime, so it must not block `publish`. And a project
|
|
108
|
+
* with no package.json, or a package.json with no `version` field, has
|
|
109
|
+
* nothing to compare the declaration against — skip rather than warn on
|
|
110
|
+
* absence.
|
|
111
|
+
*/
|
|
112
|
+
export function collectVersionAgreementFindings(integration, definition, projectDir) {
|
|
113
|
+
const declared = definition.version;
|
|
114
|
+
if (typeof declared !== 'string')
|
|
115
|
+
return [];
|
|
116
|
+
const manifestPath = path.join(projectDir, 'package.json');
|
|
117
|
+
if (!fs.existsSync(manifestPath))
|
|
118
|
+
return [];
|
|
119
|
+
let manifest;
|
|
120
|
+
try {
|
|
121
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
122
|
+
}
|
|
123
|
+
catch (_a) {
|
|
124
|
+
// Malformed package.json is npm's problem to report, not a validate
|
|
125
|
+
// finding — nothing here can compare against an unparseable manifest.
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const packageVersion = typeof manifest === 'object' && manifest !== null
|
|
129
|
+
? manifest.version
|
|
130
|
+
: undefined;
|
|
131
|
+
if (typeof packageVersion !== 'string' || packageVersion === declared) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
return [
|
|
135
|
+
{
|
|
136
|
+
check: 'project.version-agreement',
|
|
137
|
+
severity: 'warn',
|
|
138
|
+
file: integration.entryPath,
|
|
139
|
+
message: `package.json declares version "${packageVersion}", but the ` +
|
|
140
|
+
`defineIntegration() declaration for "${integration.slug}" still ` +
|
|
141
|
+
`says "${declared}". Generated seed headers embed the ` +
|
|
142
|
+
"declaration's version, so a stale declaration ships stale-looking " +
|
|
143
|
+
'metadata even though the package itself moved on.',
|
|
144
|
+
hint: `Update the "version" passed to defineIntegration() in ` +
|
|
145
|
+
`${integration.entryPath} to match package.json's "${packageVersion}".`,
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
}
|
|
93
149
|
//# sourceMappingURL=validate-findings.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-findings.js","sourceRoot":"","sources":["../src/validate-findings.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n DefinitionCollisionInput,\n Finding,\n} from '@ekanos/integration-schema';\nimport {\n collectCollisionFindings,\n collectDefinitionFindings,\n} from '@ekanos/integration-schema';\n\nimport { preconditionError } from './errors';\nimport { collectIconFindings } from './icon-findings';\nimport { loadDefinition } from './load-definition';\nimport type { LoadedProject } from './project';\nimport { collectDocsFindings, collectProjectFindings } from './project-checks';\n\n/**\n * The full findings pass shared by `validate` (which reports them) and\n * `publish` (which refuses to submit on any error-severity finding). One\n * implementation so the publish gate can never drift from what `validate`\n * checks — a project that validates clean is, by construction, a project that\n * publish will accept locally.\n *\n * NOTE on schema skew: an earlier version of this function rewrote the\n * unrecognized-key hint when the project's `@ekanos/integration-schema` was\n * newer than the CLI's (see `schema-skew.ts`). That rewrite was dead code —\n * `assertSchemaNotSkewed` throws PRECONDITION_FAILED before either `validate`\n * or `publish` ever calls this function in that state, so a project-newer\n * finding can never reach here to be rewritten, and the precondition error\n * already names both versions and points at the fix. It was removed rather\n * than kept \"just in case\": if a future caller ever makes validate proceed\n * under skew instead of failing fast, THAT is when a rewrite here becomes\n * necessary again, and it should be reintroduced with a real caller driving\n * it, not tests that fake their own reachability.\n */\nexport async function collectAllFindings(\n loaded: LoadedProject,\n): Promise<Finding[]> {\n const findings: Finding[] = [];\n const definitions: DefinitionCollisionInput[] = [];\n\n for (const integration of loaded.integrations) {\n const result = await loadDefinition(\n integration.entryPath,\n loaded.projectDir,\n );\n\n if (result.ok) {\n findings.push(\n ...collectDefinitionFindings(result.definition, {\n file: integration.entryPath,\n }),\n );\n findings.push(\n ...collectSlugAgreementFindings(\n integration,\n result.definition as { slug?: unknown },\n ),\n );\n definitions.push(result.definition as DefinitionCollisionInput);\n continue;\n }\n\n if (result.kind === 'rejected') {\n // The module loaded but the SDK/schema rejected the definition at import\n // time — surface it as a validation finding rather than a crash.\n findings.push({\n check: 'definition.load',\n severity: 'error',\n file: integration.entryPath,\n message: result.message,\n hint:\n 'Fix the integration definition so defineIntegration() accepts it, ' +\n 'then re-run validate.',\n });\n continue;\n }\n\n // A genuine module-load failure is a precondition, not a finding.\n throw preconditionError(\n `Could not load the integration definition for \"${integration.slug}\": ` +\n result.message,\n 'Ensure the entry module and its installed dependencies load under ' +\n 'Node, then re-run validate.',\n );\n }\n\n // Cross-checks only mean something with more than one definition in hand.\n findings.push(...collectCollisionFindings(definitions));\n findings.push(...collectProjectFindings(loaded.projectDir));\n findings.push(...collectDocsFindings(loaded.project, loaded.projectDir));\n findings.push(...collectIconFindings(loaded.projectDir));\n\n return findings;\n}\n\n/**\n * `ekanos.json` and the definition each carry a slug, and nothing compared\n * them: a project could declare `something-else` while the definition said\n * `repo-activity` and validate would report `{ ok: true, findings: [] }`.\n *\n * That is the identifier every surface addresses the integration by — the\n * harness route, the product slug, the widget id prefix, the MCP server — so\n * two sources of truth disagreeing is not a style question. It is an error,\n * not a warning, for the same reason a silent default is worse than a loud\n * one: the failure it causes shows up somewhere else entirely.\n */\nfunction collectSlugAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { slug?: unknown },\n): Finding[] {\n const declared = definition.slug;\n if (typeof declared !== 'string' || declared === integration.slug) return [];\n\n return [\n {\n check: 'project.slug-agreement',\n severity: 'error',\n file: integration.entryPath,\n message:\n `ekanos.json declares slug \"${integration.slug}\" for this entry, but ` +\n `the definition says \"${declared}\". The slug addresses the ` +\n 'integration everywhere — its harness route, its product record, its ' +\n 'widget ids — so the two must agree.',\n hint:\n `Change one to match the other: either set \"slug\": \"${declared}\" in ` +\n `ekanos.json, or pass slug: '${integration.slug}' to ` +\n 'defineIntegration().',\n },\n ];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"validate-findings.js","sourceRoot":"","sources":["../src/validate-findings.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n DefinitionCollisionInput,\n Finding,\n} from '@ekanos/integration-schema';\nimport {\n collectCollisionFindings,\n collectDefinitionFindings,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { preconditionError } from './errors';\nimport { collectIconFindings } from './icon-findings';\nimport { loadDefinition } from './load-definition';\nimport type { LoadedProject } from './project';\nimport {\n collectDocsFindings,\n collectHostCompilerRulesFindings,\n collectProjectFindings,\n} from './project-checks';\n\n/**\n * The full findings pass shared by `validate` (which reports them) and\n * `publish` (which refuses to submit on any error-severity finding). One\n * implementation so the publish gate can never drift from what `validate`\n * checks — a project that validates clean is, by construction, a project that\n * publish will accept locally.\n *\n * NOTE on schema skew: an earlier version of this function rewrote the\n * unrecognized-key hint when the project's `@ekanos/integration-schema` was\n * newer than the CLI's (see `schema-skew.ts`). That rewrite was dead code —\n * `assertSchemaNotSkewed` throws PRECONDITION_FAILED before either `validate`\n * or `publish` ever calls this function in that state, so a project-newer\n * finding can never reach here to be rewritten, and the precondition error\n * already names both versions and points at the fix. It was removed rather\n * than kept \"just in case\": if a future caller ever makes validate proceed\n * under skew instead of failing fast, THAT is when a rewrite here becomes\n * necessary again, and it should be reintroduced with a real caller driving\n * it, not tests that fake their own reachability.\n */\nexport async function collectAllFindings(\n loaded: LoadedProject,\n): Promise<Finding[]> {\n const findings: Finding[] = [];\n const definitions: DefinitionCollisionInput[] = [];\n\n for (const integration of loaded.integrations) {\n const result = await loadDefinition(\n integration.entryPath,\n loaded.projectDir,\n );\n\n if (result.ok) {\n findings.push(\n ...collectDefinitionFindings(result.definition, {\n file: integration.entryPath,\n }),\n );\n findings.push(\n ...collectSlugAgreementFindings(\n integration,\n result.definition as { slug?: unknown },\n ),\n );\n findings.push(\n ...collectVersionAgreementFindings(\n integration,\n result.definition as { version?: unknown },\n loaded.projectDir,\n ),\n );\n definitions.push(result.definition as DefinitionCollisionInput);\n continue;\n }\n\n if (result.kind === 'rejected') {\n // The module loaded but the SDK/schema rejected the definition at import\n // time — surface it as a validation finding rather than a crash.\n findings.push({\n check: 'definition.load',\n severity: 'error',\n file: integration.entryPath,\n message: result.message,\n hint:\n 'Fix the integration definition so defineIntegration() accepts it, ' +\n 'then re-run validate.',\n });\n continue;\n }\n\n // A genuine module-load failure is a precondition, not a finding.\n throw preconditionError(\n `Could not load the integration definition for \"${integration.slug}\": ` +\n result.message,\n 'Ensure the entry module and its installed dependencies load under ' +\n 'Node, then re-run validate.',\n );\n }\n\n // Cross-checks only mean something with more than one definition in hand.\n findings.push(...collectCollisionFindings(definitions));\n findings.push(...collectProjectFindings(loaded.projectDir));\n findings.push(...collectDocsFindings(loaded.project, loaded.projectDir));\n findings.push(...collectIconFindings(loaded.projectDir));\n findings.push(...collectHostCompilerRulesFindings(loaded.projectDir));\n\n return findings;\n}\n\n/**\n * `ekanos.json` and the definition each carry a slug, and nothing compared\n * them: a project could declare `something-else` while the definition said\n * `repo-activity` and validate would report `{ ok: true, findings: [] }`.\n *\n * That is the identifier every surface addresses the integration by — the\n * harness route, the product slug, the widget id prefix, the MCP server — so\n * two sources of truth disagreeing is not a style question. It is an error,\n * not a warning, for the same reason a silent default is worse than a loud\n * one: the failure it causes shows up somewhere else entirely.\n */\nfunction collectSlugAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { slug?: unknown },\n): Finding[] {\n const declared = definition.slug;\n if (typeof declared !== 'string' || declared === integration.slug) return [];\n\n return [\n {\n check: 'project.slug-agreement',\n severity: 'error',\n file: integration.entryPath,\n message:\n `ekanos.json declares slug \"${integration.slug}\" for this entry, but ` +\n `the definition says \"${declared}\". The slug addresses the ` +\n 'integration everywhere — its harness route, its product record, its ' +\n 'widget ids — so the two must agree.',\n hint:\n `Change one to match the other: either set \"slug\": \"${declared}\" in ` +\n `ekanos.json, or pass slug: '${integration.slug}' to ` +\n 'defineIntegration().',\n },\n ];\n}\n\n/**\n * `package.json`'s `version` field and the `version` `defineIntegration()`\n * declares are two more independent sources of truth for the same fact —\n * this project shipped weather@0.1.1 with a package.json bump, but the\n * declaration itself still said 0.1.0. Generated seeds embed the\n * DECLARATION's version in their header, so a stale declaration produces a\n * seed that looks stale even though the package itself moved on.\n *\n * WARN, not error, unlike slug agreement: a mismatched version is a quality\n * signal a partner should fix, not a broken cross-reference that breaks\n * something else at runtime, so it must not block `publish`. And a project\n * with no package.json, or a package.json with no `version` field, has\n * nothing to compare the declaration against — skip rather than warn on\n * absence.\n */\nexport function collectVersionAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { version?: unknown },\n projectDir: string,\n): Finding[] {\n const declared = definition.version;\n if (typeof declared !== 'string') return [];\n\n const manifestPath = path.join(projectDir, 'package.json');\n if (!fs.existsSync(manifestPath)) return [];\n\n let manifest: unknown;\n try {\n manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch {\n // Malformed package.json is npm's problem to report, not a validate\n // finding — nothing here can compare against an unparseable manifest.\n return [];\n }\n\n const packageVersion =\n typeof manifest === 'object' && manifest !== null\n ? (manifest as { version?: unknown }).version\n : undefined;\n if (typeof packageVersion !== 'string' || packageVersion === declared) {\n return [];\n }\n\n return [\n {\n check: 'project.version-agreement',\n severity: 'warn',\n file: integration.entryPath,\n message:\n `package.json declares version \"${packageVersion}\", but the ` +\n `defineIntegration() declaration for \"${integration.slug}\" still ` +\n `says \"${declared}\". Generated seed headers embed the ` +\n \"declaration's version, so a stale declaration ships stale-looking \" +\n 'metadata even though the package itself moved on.',\n hint:\n `Update the \"version\" passed to defineIntegration() in ` +\n `${integration.entryPath} to match package.json's \"${packageVersion}\".`,\n },\n ];\n}\n"]}
|
package/dist/wizard.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { CliContext } from './context.js';
|
|
2
|
+
/**
|
|
3
|
+
* The clack-powered wizard seam.
|
|
4
|
+
*
|
|
5
|
+
* This is the ONE file in the package that imports `@clack/prompts` — `ui.ts`
|
|
6
|
+
* stays prompt-free (its color/glyph tokens gate a PLAIN string renderer with
|
|
7
|
+
* no interactivity of its own), and this module is the seam any future
|
|
8
|
+
* prompt-driven command shares rather than each wiring clack directly. Thin,
|
|
9
|
+
* named wrappers rather than a re-export of the clack namespace so a test can
|
|
10
|
+
* `vi.mock('@clack/prompts')` and assert call order without its target
|
|
11
|
+
* (`init`, `login`, …) needing to know clack exists underneath.
|
|
12
|
+
*
|
|
13
|
+
* A command reaches for this seam instead of `ui.ts`'s plain renderer only
|
|
14
|
+
* when `canWizard()` below says yes — human mode AND a real stdout TTY AND a
|
|
15
|
+
* real stdin TTY, all three, checked fresh for that command's own streams.
|
|
16
|
+
* Anything less (JSON mode, a piped stream, a non-interactive stdin) falls
|
|
17
|
+
* back to the plain renderer; this seam never partially renders.
|
|
18
|
+
*/
|
|
19
|
+
export interface WizardSpinner {
|
|
20
|
+
start(message?: string): void;
|
|
21
|
+
stop(message?: string): void;
|
|
22
|
+
}
|
|
23
|
+
export interface Wizard {
|
|
24
|
+
intro(title: string): void;
|
|
25
|
+
outro(message: string): void;
|
|
26
|
+
/** A block of pre-formatted, copy-pasteable text — clack draws a box around it. */
|
|
27
|
+
note(message: string, title?: string): void;
|
|
28
|
+
cancel(message: string): void;
|
|
29
|
+
/** Passthrough of clack's cancel-symbol check, for a future prompt-based command. */
|
|
30
|
+
isCancel(value: unknown): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* A spinner pre-wired to exit cleanly on ctrl-c — see `exitCancelled` for
|
|
33
|
+
* why this cannot simply throw and let `run()`'s try/catch take it from
|
|
34
|
+
* there the way every other refusal in this CLI does.
|
|
35
|
+
*/
|
|
36
|
+
spinner(): WizardSpinner;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The ONE non-throwing exit path in this package. Every other refusal is a
|
|
40
|
+
* `CliError` that `run()`'s top-level try/catch turns into `ctx.fail(...)` —
|
|
41
|
+
* clean by construction, because it always has a live call stack to unwind
|
|
42
|
+
* back to. A ctrl-c while a `wizard.spinner()` is running cannot take that
|
|
43
|
+
* path: `@clack/prompts`' spinner registers its OWN `process.on('SIGINT', …)`
|
|
44
|
+
* listener for the lifetime of `start()`..`stop()`, and Node only runs its
|
|
45
|
+
* default exit-on-SIGINT behaviour when NO listener is registered — so
|
|
46
|
+
* without an explicit exit here, ctrl-c would silently stop the spinner's
|
|
47
|
+
* animation and otherwise do nothing, leaving `login`'s device-code poll
|
|
48
|
+
* (which can legitimately run for minutes) spinning in the background with
|
|
49
|
+
* no way back to a prompt.
|
|
50
|
+
*
|
|
51
|
+
* So this mirrors `ctx.fail`'s OUTPUT contract by hand — stderr only, an
|
|
52
|
+
* `Error: …` / `Hint: …` pair, never a stack trace — and exits with the same
|
|
53
|
+
* code a plain usage error would (`EXIT_CODES.USAGE`), matching every other
|
|
54
|
+
* clean-refusal path in the CLI. Exported so tests can assert on it without
|
|
55
|
+
* a real `process.exit` ending the test run.
|
|
56
|
+
*/
|
|
57
|
+
export declare function exitCancelled(): void;
|
|
58
|
+
export declare const wizard: Wizard;
|
|
59
|
+
/**
|
|
60
|
+
* Whether a clack-powered wizard flow may run for this invocation.
|
|
61
|
+
*
|
|
62
|
+
* Mirrors `publish.ts`'s `canPrompt`, with one addition: `ctx.jsonMode` alone
|
|
63
|
+
* is not enough (`--no-json` forces human mode even against a piped stdout,
|
|
64
|
+
* and says nothing about stdin), so this needs BOTH streams confirmed
|
|
65
|
+
* interactive, not just one:
|
|
66
|
+
*
|
|
67
|
+
* - stdout a real TTY: clack renders to stdout (unlike the rest of this
|
|
68
|
+
* CLI's human output, which is stderr-only — see `ui.ts`'s docs), and
|
|
69
|
+
* `ctx.jsonMode` false does not by itself guarantee stdout is a terminal.
|
|
70
|
+
* - stdin a real TTY: clack reads keypresses from it interactively. A
|
|
71
|
+
* closed or non-interactive stdin (a cron job, a wrapper script) can
|
|
72
|
+
* never resolve an interactive prompt, the same reason `publish`'s y/N
|
|
73
|
+
* confirm gates on it.
|
|
74
|
+
*
|
|
75
|
+
* Anything else falls back to the plain, existing human-mode rendering —
|
|
76
|
+
* never a hang.
|
|
77
|
+
*/
|
|
78
|
+
export declare function canWizard(ctx: CliContext, probe?: {
|
|
79
|
+
stdinIsTTY?: boolean;
|
|
80
|
+
}): boolean;
|
package/dist/wizard.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as clack from '@clack/prompts';
|
|
2
|
+
import { EXIT_CODES } from './exit-codes.js';
|
|
3
|
+
/**
|
|
4
|
+
* The ONE non-throwing exit path in this package. Every other refusal is a
|
|
5
|
+
* `CliError` that `run()`'s top-level try/catch turns into `ctx.fail(...)` —
|
|
6
|
+
* clean by construction, because it always has a live call stack to unwind
|
|
7
|
+
* back to. A ctrl-c while a `wizard.spinner()` is running cannot take that
|
|
8
|
+
* path: `@clack/prompts`' spinner registers its OWN `process.on('SIGINT', …)`
|
|
9
|
+
* listener for the lifetime of `start()`..`stop()`, and Node only runs its
|
|
10
|
+
* default exit-on-SIGINT behaviour when NO listener is registered — so
|
|
11
|
+
* without an explicit exit here, ctrl-c would silently stop the spinner's
|
|
12
|
+
* animation and otherwise do nothing, leaving `login`'s device-code poll
|
|
13
|
+
* (which can legitimately run for minutes) spinning in the background with
|
|
14
|
+
* no way back to a prompt.
|
|
15
|
+
*
|
|
16
|
+
* So this mirrors `ctx.fail`'s OUTPUT contract by hand — stderr only, an
|
|
17
|
+
* `Error: …` / `Hint: …` pair, never a stack trace — and exits with the same
|
|
18
|
+
* code a plain usage error would (`EXIT_CODES.USAGE`), matching every other
|
|
19
|
+
* clean-refusal path in the CLI. Exported so tests can assert on it without
|
|
20
|
+
* a real `process.exit` ending the test run.
|
|
21
|
+
*/
|
|
22
|
+
export function exitCancelled() {
|
|
23
|
+
process.stderr.write('Error: Cancelled.\nHint: Re-run the command to try again.\n');
|
|
24
|
+
process.exit(EXIT_CODES.USAGE);
|
|
25
|
+
}
|
|
26
|
+
export const wizard = {
|
|
27
|
+
intro(title) {
|
|
28
|
+
clack.intro(title);
|
|
29
|
+
},
|
|
30
|
+
outro(message) {
|
|
31
|
+
clack.outro(message);
|
|
32
|
+
},
|
|
33
|
+
note(message, title) {
|
|
34
|
+
clack.note(message, title);
|
|
35
|
+
},
|
|
36
|
+
cancel(message) {
|
|
37
|
+
clack.cancel(message);
|
|
38
|
+
},
|
|
39
|
+
isCancel(value) {
|
|
40
|
+
return clack.isCancel(value);
|
|
41
|
+
},
|
|
42
|
+
spinner() {
|
|
43
|
+
const s = clack.spinner({ onCancel: exitCancelled });
|
|
44
|
+
return {
|
|
45
|
+
start: (message) => s.start(message),
|
|
46
|
+
stop: (message) => s.stop(message),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Whether a clack-powered wizard flow may run for this invocation.
|
|
52
|
+
*
|
|
53
|
+
* Mirrors `publish.ts`'s `canPrompt`, with one addition: `ctx.jsonMode` alone
|
|
54
|
+
* is not enough (`--no-json` forces human mode even against a piped stdout,
|
|
55
|
+
* and says nothing about stdin), so this needs BOTH streams confirmed
|
|
56
|
+
* interactive, not just one:
|
|
57
|
+
*
|
|
58
|
+
* - stdout a real TTY: clack renders to stdout (unlike the rest of this
|
|
59
|
+
* CLI's human output, which is stderr-only — see `ui.ts`'s docs), and
|
|
60
|
+
* `ctx.jsonMode` false does not by itself guarantee stdout is a terminal.
|
|
61
|
+
* - stdin a real TTY: clack reads keypresses from it interactively. A
|
|
62
|
+
* closed or non-interactive stdin (a cron job, a wrapper script) can
|
|
63
|
+
* never resolve an interactive prompt, the same reason `publish`'s y/N
|
|
64
|
+
* confirm gates on it.
|
|
65
|
+
*
|
|
66
|
+
* Anything else falls back to the plain, existing human-mode rendering —
|
|
67
|
+
* never a hang.
|
|
68
|
+
*/
|
|
69
|
+
export function canWizard(ctx, probe = {}) {
|
|
70
|
+
var _a;
|
|
71
|
+
const stdinIsTTY = (_a = probe.stdinIsTTY) !== null && _a !== void 0 ? _a : process.stdin.isTTY === true;
|
|
72
|
+
return !ctx.jsonMode && ctx.stdoutIsTTY && stdinIsTTY;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=wizard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wizard.js","sourceRoot":"","sources":["../src/wizard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AAGxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAyC1C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,6DAA6D,CAC9D,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAW;IAC5B,KAAK,CAAC,KAAK;QACT,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,KAAK,CAAC,OAAO;QACX,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IACD,IAAI,CAAC,OAAO,EAAE,KAAK;QACjB,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,CAAC,OAAO;QACZ,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;IACD,QAAQ,CAAC,KAAK;QACZ,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO;QACL,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC;QACrD,OAAO;YACL,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;YACpC,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;SACnC,CAAC;IACJ,CAAC;CACF,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,SAAS,CACvB,GAAe,EACf,QAAkC,EAAE;;IAEpC,MAAM,UAAU,GAAG,MAAA,KAAK,CAAC,UAAU,mCAAI,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC;IACpE,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,WAAW,IAAI,UAAU,CAAC;AACxD,CAAC","sourcesContent":["import * as clack from '@clack/prompts';\n\nimport type { CliContext } from './context';\nimport { EXIT_CODES } from './exit-codes';\n\n/**\n * The clack-powered wizard seam.\n *\n * This is the ONE file in the package that imports `@clack/prompts` — `ui.ts`\n * stays prompt-free (its color/glyph tokens gate a PLAIN string renderer with\n * no interactivity of its own), and this module is the seam any future\n * prompt-driven command shares rather than each wiring clack directly. Thin,\n * named wrappers rather than a re-export of the clack namespace so a test can\n * `vi.mock('@clack/prompts')` and assert call order without its target\n * (`init`, `login`, …) needing to know clack exists underneath.\n *\n * A command reaches for this seam instead of `ui.ts`'s plain renderer only\n * when `canWizard()` below says yes — human mode AND a real stdout TTY AND a\n * real stdin TTY, all three, checked fresh for that command's own streams.\n * Anything less (JSON mode, a piped stream, a non-interactive stdin) falls\n * back to the plain renderer; this seam never partially renders.\n */\n\nexport interface WizardSpinner {\n start(message?: string): void;\n stop(message?: string): void;\n}\n\nexport interface Wizard {\n intro(title: string): void;\n outro(message: string): void;\n /** A block of pre-formatted, copy-pasteable text — clack draws a box around it. */\n note(message: string, title?: string): void;\n cancel(message: string): void;\n /** Passthrough of clack's cancel-symbol check, for a future prompt-based command. */\n isCancel(value: unknown): boolean;\n /**\n * A spinner pre-wired to exit cleanly on ctrl-c — see `exitCancelled` for\n * why this cannot simply throw and let `run()`'s try/catch take it from\n * there the way every other refusal in this CLI does.\n */\n spinner(): WizardSpinner;\n}\n\n/**\n * The ONE non-throwing exit path in this package. Every other refusal is a\n * `CliError` that `run()`'s top-level try/catch turns into `ctx.fail(...)` —\n * clean by construction, because it always has a live call stack to unwind\n * back to. A ctrl-c while a `wizard.spinner()` is running cannot take that\n * path: `@clack/prompts`' spinner registers its OWN `process.on('SIGINT', …)`\n * listener for the lifetime of `start()`..`stop()`, and Node only runs its\n * default exit-on-SIGINT behaviour when NO listener is registered — so\n * without an explicit exit here, ctrl-c would silently stop the spinner's\n * animation and otherwise do nothing, leaving `login`'s device-code poll\n * (which can legitimately run for minutes) spinning in the background with\n * no way back to a prompt.\n *\n * So this mirrors `ctx.fail`'s OUTPUT contract by hand — stderr only, an\n * `Error: …` / `Hint: …` pair, never a stack trace — and exits with the same\n * code a plain usage error would (`EXIT_CODES.USAGE`), matching every other\n * clean-refusal path in the CLI. Exported so tests can assert on it without\n * a real `process.exit` ending the test run.\n */\nexport function exitCancelled(): void {\n process.stderr.write(\n 'Error: Cancelled.\\nHint: Re-run the command to try again.\\n',\n );\n process.exit(EXIT_CODES.USAGE);\n}\n\nexport const wizard: Wizard = {\n intro(title) {\n clack.intro(title);\n },\n outro(message) {\n clack.outro(message);\n },\n note(message, title) {\n clack.note(message, title);\n },\n cancel(message) {\n clack.cancel(message);\n },\n isCancel(value) {\n return clack.isCancel(value);\n },\n spinner() {\n const s = clack.spinner({ onCancel: exitCancelled });\n return {\n start: (message) => s.start(message),\n stop: (message) => s.stop(message),\n };\n },\n};\n\n/**\n * Whether a clack-powered wizard flow may run for this invocation.\n *\n * Mirrors `publish.ts`'s `canPrompt`, with one addition: `ctx.jsonMode` alone\n * is not enough (`--no-json` forces human mode even against a piped stdout,\n * and says nothing about stdin), so this needs BOTH streams confirmed\n * interactive, not just one:\n *\n * - stdout a real TTY: clack renders to stdout (unlike the rest of this\n * CLI's human output, which is stderr-only — see `ui.ts`'s docs), and\n * `ctx.jsonMode` false does not by itself guarantee stdout is a terminal.\n * - stdin a real TTY: clack reads keypresses from it interactively. A\n * closed or non-interactive stdin (a cron job, a wrapper script) can\n * never resolve an interactive prompt, the same reason `publish`'s y/N\n * confirm gates on it.\n *\n * Anything else falls back to the plain, existing human-mode rendering —\n * never a hang.\n */\nexport function canWizard(\n ctx: CliContext,\n probe: { stdinIsTTY?: boolean } = {},\n): boolean {\n const stdinIsTTY = probe.stdinIsTTY ?? process.stdin.isTTY === true;\n return !ctx.jsonMode && ctx.stdoutIsTTY && stdinIsTTY;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekanos/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
4
5
|
"type": "module",
|
|
5
6
|
"description": "The Ekanos partner toolchain CLI: scaffold, validate, and test a Fusion integration against the published @ekanos packages. Agent-native — every verb speaks JSON with a stable exit-code taxonomy.",
|
|
6
7
|
"license": "MIT",
|
|
@@ -32,8 +33,10 @@
|
|
|
32
33
|
"access": "public"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
36
|
+
"@clack/prompts": "1.7.0",
|
|
35
37
|
"@ekanos/integration-schema": "0.1.5",
|
|
36
|
-
"esbuild": "0.28.1"
|
|
38
|
+
"esbuild": "0.28.1",
|
|
39
|
+
"picocolors": "1.1.1"
|
|
37
40
|
},
|
|
38
41
|
"peerDependencies": {
|
|
39
42
|
"zod": "^3.25.76"
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
"description": "A Fusion integration built with @ekanos/sdk.",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"typecheck": "tsc --noEmit",
|
|
9
|
-
"test": "vitest run"
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"validate": "npx ekanos validate",
|
|
11
|
+
"prepare": "node scripts/setup-hooks.mjs"
|
|
10
12
|
},
|
|
11
13
|
"dependencies": {
|
|
12
14
|
"@ekanos/sdk": "^0.1.0",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Dependency-free git hook bootstrap — no husky, no extra dependency. Wired
|
|
3
|
+
// via package.json's "prepare" script, so it runs once on `npm install` /
|
|
4
|
+
// `pnpm install` and writes a single pre-commit hook that re-runs the exact
|
|
5
|
+
// findings pass `ekanos publish` gates on. A commit that would fail the
|
|
6
|
+
// gate fails here first, locally, before a partner burns a submit cycle on
|
|
7
|
+
// it.
|
|
8
|
+
//
|
|
9
|
+
// Tolerates a non-git directory silently: a fresh scaffold copied somewhere
|
|
10
|
+
// before `git init`, or a checkout that vendors this project without version
|
|
11
|
+
// control, must still install cleanly. Nothing here is required for the
|
|
12
|
+
// integration itself to work — only for this optional local safety net.
|
|
13
|
+
|
|
14
|
+
import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
|
|
17
|
+
const gitDir = path.resolve('.git');
|
|
18
|
+
if (!existsSync(gitDir)) {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
23
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
26
|
+
const hookBody = `#!/bin/sh
|
|
27
|
+
# Written by scripts/setup-hooks.mjs (via the "prepare" script). Runs the
|
|
28
|
+
# same findings pass "ekanos publish" gates on, so a commit that would fail
|
|
29
|
+
# the gate fails here first. Skip once with: SKIP_EKANOS_HOOK=1 git commit
|
|
30
|
+
if [ -n "$SKIP_EKANOS_HOOK" ]; then
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if ! npx ekanos validate --json >/dev/null 2>&1; then
|
|
35
|
+
echo "ekanos validate found error-severity findings — run 'npx ekanos validate' to see them." >&2
|
|
36
|
+
echo "Skip this check once with: SKIP_EKANOS_HOOK=1 git commit ..." >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
writeFileSync(hookPath, hookBody, { mode: 0o755 });
|
|
42
|
+
// Belt and suspenders: writeFileSync's `mode` option only applies when the
|
|
43
|
+
// file did not already exist, so a re-run over a hook a previous version of
|
|
44
|
+
// this script left behind (with different permissions) still ends executable.
|
|
45
|
+
chmodSync(hookPath, 0o755);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"//": "strict and noUncheckedIndexedAccess mirror the host's own compiler rules. The gate re-checks your submission under both a second time (checks/typecheck-host-rules) regardless of what this file says, so keeping them on here means local-green implies gate-typecheck-green — see `ekanos validate`'s project.host-compiler-rules check if either ever drifts off.",
|
|
2
3
|
"compilerOptions": {
|
|
3
4
|
"target": "ES2022",
|
|
4
5
|
"lib": ["dom", "dom.iterable", "ES2022"],
|
|
@@ -6,6 +7,7 @@
|
|
|
6
7
|
"moduleResolution": "bundler",
|
|
7
8
|
"jsx": "react-jsx",
|
|
8
9
|
"strict": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
9
11
|
"noEmit": true,
|
|
10
12
|
"skipLibCheck": true,
|
|
11
13
|
"esModuleInterop": true,
|