@ekanos/cli 0.3.3 → 0.3.5
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/session.js +13 -4
- package/dist/auth/session.js.map +1 -1
- package/dist/banner.d.ts +1 -1
- package/dist/banner.js +4 -13
- package/dist/banner.js.map +1 -1
- package/dist/commands/dev.js +22 -6
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.js +68 -30
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.js +25 -14
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.js +30 -4
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/publish.js +25 -12
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/sources.js +26 -16
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +52 -39
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/test.js +24 -7
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/upgrade.js +85 -53
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/use.js +10 -2
- package/dist/commands/use.js.map +1 -1
- package/dist/commands/validate.js +21 -3
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/whoami.js +16 -6
- package/dist/commands/whoami.js.map +1 -1
- package/dist/context.js +10 -3
- package/dist/context.js.map +1 -1
- package/dist/pnpm-builds.d.ts +15 -0
- package/dist/pnpm-builds.js +48 -0
- package/dist/pnpm-builds.js.map +1 -0
- package/dist/ui.d.ts +36 -0
- package/dist/ui.js +77 -2
- package/dist/ui.js.map +1 -1
- package/package.json +2 -2
- package/templates/AGENTS.md.tmpl +63 -0
- package/templates/package.json.tmpl +4 -4
- package/templates/pnpm-workspace.yaml.tmpl +12 -5
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pnpm 10+ aborts with `ERR_PNPM_IGNORED_BUILDS` when a dependency has an
|
|
3
|
+
* install script nobody has approved or declined. The partner-facing fix is
|
|
4
|
+
* always the same edit, so name it instead of leaving them with an opaque
|
|
5
|
+
* "exited with code 1".
|
|
6
|
+
*/
|
|
7
|
+
/** Package names (versions stripped) from pnpm's ignored-build error, or `[]`. */
|
|
8
|
+
export declare function findIgnoredBuilds(output: string): string[];
|
|
9
|
+
/** One-sentence remedy naming the file, key and exact packages. */
|
|
10
|
+
export declare function ignoredBuildsHint(packages: readonly string[]): string;
|
|
11
|
+
/** Keeps only the tail of streamed output so matching stays cheap. */
|
|
12
|
+
export declare function createOutputTail(limit?: number): {
|
|
13
|
+
push: (chunk: Buffer | string) => void;
|
|
14
|
+
text: () => string;
|
|
15
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pnpm 10+ aborts with `ERR_PNPM_IGNORED_BUILDS` when a dependency has an
|
|
3
|
+
* install script nobody has approved or declined. The partner-facing fix is
|
|
4
|
+
* always the same edit, so name it instead of leaving them with an opaque
|
|
5
|
+
* "exited with code 1".
|
|
6
|
+
*/
|
|
7
|
+
const IGNORED_BUILDS_MARKER = 'ERR_PNPM_IGNORED_BUILDS';
|
|
8
|
+
const IGNORED_LIST = /Ignored build scripts:\s*([^\n]+)/;
|
|
9
|
+
/** Package names (versions stripped) from pnpm's ignored-build error, or `[]`. */
|
|
10
|
+
export function findIgnoredBuilds(output) {
|
|
11
|
+
var _a;
|
|
12
|
+
if (!output.includes(IGNORED_BUILDS_MARKER))
|
|
13
|
+
return [];
|
|
14
|
+
const list = (_a = IGNORED_LIST.exec(output)) === null || _a === void 0 ? void 0 : _a[1];
|
|
15
|
+
if (!list)
|
|
16
|
+
return [];
|
|
17
|
+
const names = list
|
|
18
|
+
.replace(/\.\s*$/, '')
|
|
19
|
+
.split(/,\s*/)
|
|
20
|
+
.map((entry) => entry.trim().replace(/(?<=.)@[^@/\s]+$/, ''))
|
|
21
|
+
.filter((name) => name.length > 0);
|
|
22
|
+
return [...new Set(names)];
|
|
23
|
+
}
|
|
24
|
+
/** One-sentence remedy naming the file, key and exact packages. */
|
|
25
|
+
export function ignoredBuildsHint(packages) {
|
|
26
|
+
const subject = packages.length > 0
|
|
27
|
+
? `pnpm ignored build scripts for ${packages.join(', ')}`
|
|
28
|
+
: 'pnpm ignored dependency build scripts';
|
|
29
|
+
const entries = packages.length > 0
|
|
30
|
+
? packages.map((name) => `'${name}': true`).join(', ')
|
|
31
|
+
: '<package>: true';
|
|
32
|
+
return (`${subject} (ERR_PNPM_IGNORED_BUILDS). Open pnpm-workspace.yaml, set ` +
|
|
33
|
+
`each of them under "allowBuilds" to true or false (e.g. ${entries}), ` +
|
|
34
|
+
'then run "pnpm install" again.');
|
|
35
|
+
}
|
|
36
|
+
/** Keeps only the tail of streamed output so matching stays cheap. */
|
|
37
|
+
export function createOutputTail(limit = 16384) {
|
|
38
|
+
let buffer = '';
|
|
39
|
+
return {
|
|
40
|
+
push: (chunk) => {
|
|
41
|
+
buffer += chunk.toString();
|
|
42
|
+
if (buffer.length > limit)
|
|
43
|
+
buffer = buffer.slice(-limit);
|
|
44
|
+
},
|
|
45
|
+
text: () => buffer,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=pnpm-builds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pnpm-builds.js","sourceRoot":"","sources":["../src/pnpm-builds.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,qBAAqB,GAAG,yBAAyB,CAAC;AACxD,MAAM,YAAY,GAAG,mCAAmC,CAAC;AAEzD,kFAAkF;AAClF,MAAM,UAAU,iBAAiB,CAAC,MAAc;;IAC9C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvD,MAAM,IAAI,GAAG,MAAA,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,0CAAG,CAAC,CAAC,CAAC;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IAErB,MAAM,KAAK,GAAG,IAAI;SACf,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,KAAK,CAAC,MAAM,CAAC;SACb,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;SAC5D,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAErC,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,iBAAiB,CAAC,QAA2B;IAC3D,MAAM,OAAO,GACX,QAAQ,CAAC,MAAM,GAAG,CAAC;QACjB,CAAC,CAAC,kCAAkC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACzD,CAAC,CAAC,uCAAuC,CAAC;IAC9C,MAAM,OAAO,GACX,QAAQ,CAAC,MAAM,GAAG,CAAC;QACjB,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QACtD,CAAC,CAAC,iBAAiB,CAAC;IAExB,OAAO,CACL,GAAG,OAAO,4DAA4D;QACtE,2DAA2D,OAAO,KAAK;QACvE,gCAAgC,CACjC,CAAC;AACJ,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,gBAAgB,CAAC,KAAK,GAAG,KAAM;IAI7C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,OAAO;QACL,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE;YACd,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC3B,IAAI,MAAM,CAAC,MAAM,GAAG,KAAK;gBAAE,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;QAC3D,CAAC;QACD,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM;KACnB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * pnpm 10+ aborts with `ERR_PNPM_IGNORED_BUILDS` when a dependency has an\n * install script nobody has approved or declined. The partner-facing fix is\n * always the same edit, so name it instead of leaving them with an opaque\n * \"exited with code 1\".\n */\n\nconst IGNORED_BUILDS_MARKER = 'ERR_PNPM_IGNORED_BUILDS';\nconst IGNORED_LIST = /Ignored build scripts:\\s*([^\\n]+)/;\n\n/** Package names (versions stripped) from pnpm's ignored-build error, or `[]`. */\nexport function findIgnoredBuilds(output: string): string[] {\n if (!output.includes(IGNORED_BUILDS_MARKER)) return [];\n\n const list = IGNORED_LIST.exec(output)?.[1];\n if (!list) return [];\n\n const names = list\n .replace(/\\.\\s*$/, '')\n .split(/,\\s*/)\n .map((entry) => entry.trim().replace(/(?<=.)@[^@/\\s]+$/, ''))\n .filter((name) => name.length > 0);\n\n return [...new Set(names)];\n}\n\n/** One-sentence remedy naming the file, key and exact packages. */\nexport function ignoredBuildsHint(packages: readonly string[]): string {\n const subject =\n packages.length > 0\n ? `pnpm ignored build scripts for ${packages.join(', ')}`\n : 'pnpm ignored dependency build scripts';\n const entries =\n packages.length > 0\n ? packages.map((name) => `'${name}': true`).join(', ')\n : '<package>: true';\n\n return (\n `${subject} (ERR_PNPM_IGNORED_BUILDS). Open pnpm-workspace.yaml, set ` +\n `each of them under \"allowBuilds\" to true or false (e.g. ${entries}), ` +\n 'then run \"pnpm install\" again.'\n );\n}\n\n/** Keeps only the tail of streamed output so matching stays cheap. */\nexport function createOutputTail(limit = 16_384): {\n push: (chunk: Buffer | string) => void;\n text: () => string;\n} {\n let buffer = '';\n return {\n push: (chunk) => {\n buffer += chunk.toString();\n if (buffer.length > limit) buffer = buffer.slice(-limit);\n },\n text: () => buffer,\n };\n}\n"]}
|
package/dist/ui.d.ts
CHANGED
|
@@ -31,9 +31,27 @@ export interface Glyphs {
|
|
|
31
31
|
err: string;
|
|
32
32
|
warn: string;
|
|
33
33
|
info: string;
|
|
34
|
+
/** The result-block title marker — the same `◇` the clack wizard draws. */
|
|
35
|
+
step: string;
|
|
34
36
|
}
|
|
35
37
|
/** One row of `ui.keyValue`. */
|
|
36
38
|
export type KeyValueRow = readonly [key: string, value: string];
|
|
39
|
+
/** One row of `ui.result`: a dim label and a value, or several values stacked under one label. */
|
|
40
|
+
export type ResultRow = readonly [
|
|
41
|
+
label: string,
|
|
42
|
+
value: string | readonly string[]
|
|
43
|
+
];
|
|
44
|
+
export type ResultTone = 'ok' | 'warn' | 'err';
|
|
45
|
+
/** What every command's successful human-mode ending is built from. */
|
|
46
|
+
export interface ResultBlock {
|
|
47
|
+
/** The one-line outcome: "Signed in", "Submitted", … Title case, no verb prefix, no period. */
|
|
48
|
+
title: string;
|
|
49
|
+
/** Defaults to `ok` (green marker). `warn` for a result that succeeded but needs attention. */
|
|
50
|
+
tone?: ResultTone;
|
|
51
|
+
rows?: readonly ResultRow[];
|
|
52
|
+
/** Secondary prose, dim, set apart from the rows by a blank line. Keep it short. */
|
|
53
|
+
notes?: readonly string[];
|
|
54
|
+
}
|
|
37
55
|
export interface CodeframeOptions {
|
|
38
56
|
/** Not rendered directly — callers already print `file:line` above/around this. Kept for future use and self-documentation at call sites. */
|
|
39
57
|
file?: string;
|
|
@@ -58,6 +76,13 @@ export interface Ui {
|
|
|
58
76
|
section(title: string): string;
|
|
59
77
|
/** Aligned `key value` rows, 2-space gutter, keys dimmed. */
|
|
60
78
|
keyValue(rows: readonly KeyValueRow[]): string;
|
|
79
|
+
/**
|
|
80
|
+
* THE success renderer: a marker + bold title line, then aligned
|
|
81
|
+
* `label value` rows indented under it, labels dim. A multi-value row
|
|
82
|
+
* stacks its values under one label. Plain strings throughout, so a
|
|
83
|
+
* monochrome terminal gets exactly the same layout minus the color.
|
|
84
|
+
*/
|
|
85
|
+
result(block: ResultBlock): string;
|
|
61
86
|
/** A header-having, left/right-aligned table with no box-drawing. */
|
|
62
87
|
table(headers: readonly string[], rows: readonly (readonly (string | number)[])[]): string;
|
|
63
88
|
/** A `→ ` prefixed, accent-colored hint line. */
|
|
@@ -73,3 +98,14 @@ export interface Ui {
|
|
|
73
98
|
badge(text: string, kind?: BadgeKind): string;
|
|
74
99
|
}
|
|
75
100
|
export declare function createUi(probe: ColorProbe): Ui;
|
|
101
|
+
/**
|
|
102
|
+
* A host URL as a person would say it: `https://x.site` -> `x.site`. The
|
|
103
|
+
* scheme only stays when it carries information (plain `http://`, as in a
|
|
104
|
+
* local sandbox) or when the string is not a URL at all.
|
|
105
|
+
*/
|
|
106
|
+
export declare function shortHost(host: string): string;
|
|
107
|
+
/** A path relative to `cwd` when it sits inside it, `~/…` under home, else as given. */
|
|
108
|
+
export declare function displayPath(target: string, cwd: string, home?: string | undefined): string;
|
|
109
|
+
/** `412ms` below a second, `1.2s` above, `2m 05s` past a minute. */
|
|
110
|
+
export declare function formatDuration(ms: number): string;
|
|
111
|
+
export declare function pluralize(count: number, noun: string): string;
|
package/dist/ui.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
1
2
|
import pc from 'picocolors';
|
|
2
3
|
/**
|
|
3
4
|
* The one color-on/off decision for the whole CLI. True only when the
|
|
@@ -33,8 +34,20 @@ function unicodeSupported(env) {
|
|
|
33
34
|
return false;
|
|
34
35
|
return true;
|
|
35
36
|
}
|
|
36
|
-
const UNICODE_GLYPHS = {
|
|
37
|
-
|
|
37
|
+
const UNICODE_GLYPHS = {
|
|
38
|
+
ok: '✓',
|
|
39
|
+
err: '✖',
|
|
40
|
+
warn: '▲',
|
|
41
|
+
info: '●',
|
|
42
|
+
step: '◇',
|
|
43
|
+
};
|
|
44
|
+
const ASCII_GLYPHS = {
|
|
45
|
+
ok: 'ok',
|
|
46
|
+
err: 'x',
|
|
47
|
+
warn: '!',
|
|
48
|
+
info: '*',
|
|
49
|
+
step: 'o',
|
|
50
|
+
};
|
|
38
51
|
/** 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
52
|
function padCell(text, width, align) {
|
|
40
53
|
return align === 'right' ? text.padStart(width) : text.padEnd(width);
|
|
@@ -67,6 +80,24 @@ export function createUi(probe) {
|
|
|
67
80
|
.map(([key, value]) => `${dim(key.padEnd(keyWidth))} ${value}`)
|
|
68
81
|
.join('\n');
|
|
69
82
|
},
|
|
83
|
+
result(block) {
|
|
84
|
+
var _a, _b;
|
|
85
|
+
const paint = { ok, warn, err }[(_a = block.tone) !== null && _a !== void 0 ? _a : 'ok'];
|
|
86
|
+
const lines = [`${paint(glyphs.step)} ${bold(block.title)}`];
|
|
87
|
+
const rows = (_b = block.rows) !== null && _b !== void 0 ? _b : [];
|
|
88
|
+
const labelWidth = Math.max(0, ...rows.map(([label]) => label.length));
|
|
89
|
+
for (const [label, value] of rows) {
|
|
90
|
+
const values = typeof value === 'string' ? [value] : value;
|
|
91
|
+
values.forEach((line, index) => {
|
|
92
|
+
const gutter = (index === 0 ? label : '').padEnd(labelWidth);
|
|
93
|
+
lines.push(` ${dim(gutter)} ${line}`.trimEnd());
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (block.notes && block.notes.length > 0) {
|
|
97
|
+
lines.push('', ...block.notes.map((note) => ` ${dim(note)}`));
|
|
98
|
+
}
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
},
|
|
70
101
|
table(headers, rows) {
|
|
71
102
|
const columnCount = headers.length;
|
|
72
103
|
const isNumeric = (value) => typeof value === 'number' ||
|
|
@@ -123,4 +154,48 @@ export function createUi(probe) {
|
|
|
123
154
|
},
|
|
124
155
|
};
|
|
125
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* A host URL as a person would say it: `https://x.site` -> `x.site`. The
|
|
159
|
+
* scheme only stays when it carries information (plain `http://`, as in a
|
|
160
|
+
* local sandbox) or when the string is not a URL at all.
|
|
161
|
+
*/
|
|
162
|
+
export function shortHost(host) {
|
|
163
|
+
let url;
|
|
164
|
+
try {
|
|
165
|
+
url = new URL(host);
|
|
166
|
+
}
|
|
167
|
+
catch (_a) {
|
|
168
|
+
return host;
|
|
169
|
+
}
|
|
170
|
+
const bare = url.pathname === '/' && !url.search && !url.hash;
|
|
171
|
+
return url.protocol === 'https:' && bare ? url.host : host;
|
|
172
|
+
}
|
|
173
|
+
/** A path relative to `cwd` when it sits inside it, `~/…` under home, else as given. */
|
|
174
|
+
export function displayPath(target, cwd, home = process.env.HOME) {
|
|
175
|
+
const relative = path.relative(cwd, target);
|
|
176
|
+
if (relative === '')
|
|
177
|
+
return '.';
|
|
178
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
179
|
+
return relative;
|
|
180
|
+
if (home) {
|
|
181
|
+
const underHome = path.relative(home, target);
|
|
182
|
+
if (!underHome.startsWith('..') && !path.isAbsolute(underHome)) {
|
|
183
|
+
return `~/${underHome}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return target;
|
|
187
|
+
}
|
|
188
|
+
/** `412ms` below a second, `1.2s` above, `2m 05s` past a minute. */
|
|
189
|
+
export function formatDuration(ms) {
|
|
190
|
+
if (ms < 1000)
|
|
191
|
+
return `${Math.round(ms)}ms`;
|
|
192
|
+
if (ms < 60000)
|
|
193
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
194
|
+
const minutes = Math.floor(ms / 60000);
|
|
195
|
+
const seconds = Math.round((ms % 60000) / 1000);
|
|
196
|
+
return `${minutes}m ${String(seconds).padStart(2, '0')}s`;
|
|
197
|
+
}
|
|
198
|
+
export function pluralize(count, noun) {
|
|
199
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
200
|
+
}
|
|
126
201
|
//# sourceMappingURL=ui.js.map
|
package/dist/ui.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'node:path';\nimport 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 /** The result-block title marker — the same `◇` the clack wizard draws. */\n step: string;\n}\n\nconst UNICODE_GLYPHS: Glyphs = {\n ok: '✓',\n err: '✖',\n warn: '▲',\n info: '●',\n step: '◇',\n};\nconst ASCII_GLYPHS: Glyphs = {\n ok: 'ok',\n err: 'x',\n warn: '!',\n info: '*',\n step: 'o',\n};\n\n/** One row of `ui.keyValue`. */\nexport type KeyValueRow = readonly [key: string, value: string];\n\n/** One row of `ui.result`: a dim label and a value, or several values stacked under one label. */\nexport type ResultRow = readonly [\n label: string,\n value: string | readonly string[],\n];\n\nexport type ResultTone = 'ok' | 'warn' | 'err';\n\n/** What every command's successful human-mode ending is built from. */\nexport interface ResultBlock {\n /** The one-line outcome: \"Signed in\", \"Submitted\", … Title case, no verb prefix, no period. */\n title: string;\n /** Defaults to `ok` (green marker). `warn` for a result that succeeded but needs attention. */\n tone?: ResultTone;\n rows?: readonly ResultRow[];\n /** Secondary prose, dim, set apart from the rows by a blank line. Keep it short. */\n notes?: readonly string[];\n}\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 /**\n * THE success renderer: a marker + bold title line, then aligned\n * `label value` rows indented under it, labels dim. A multi-value row\n * stacks its values under one label. Plain strings throughout, so a\n * monochrome terminal gets exactly the same layout minus the color.\n */\n result(block: ResultBlock): 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 result(block: ResultBlock): string {\n const paint = { ok, warn, err }[block.tone ?? 'ok'];\n const lines = [`${paint(glyphs.step)} ${bold(block.title)}`];\n const rows = block.rows ?? [];\n const labelWidth = Math.max(0, ...rows.map(([label]) => label.length));\n\n for (const [label, value] of rows) {\n const values = typeof value === 'string' ? [value] : value;\n values.forEach((line, index) => {\n const gutter = (index === 0 ? label : '').padEnd(labelWidth);\n lines.push(` ${dim(gutter)} ${line}`.trimEnd());\n });\n }\n\n if (block.notes && block.notes.length > 0) {\n lines.push('', ...block.notes.map((note) => ` ${dim(note)}`));\n }\n\n return lines.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\n/**\n * A host URL as a person would say it: `https://x.site` -> `x.site`. The\n * scheme only stays when it carries information (plain `http://`, as in a\n * local sandbox) or when the string is not a URL at all.\n */\nexport function shortHost(host: string): string {\n let url: URL;\n try {\n url = new URL(host);\n } catch {\n return host;\n }\n\n const bare = url.pathname === '/' && !url.search && !url.hash;\n return url.protocol === 'https:' && bare ? url.host : host;\n}\n\n/** A path relative to `cwd` when it sits inside it, `~/…` under home, else as given. */\nexport function displayPath(\n target: string,\n cwd: string,\n home: string | undefined = process.env.HOME,\n): string {\n const relative = path.relative(cwd, target);\n if (relative === '') return '.';\n if (!relative.startsWith('..') && !path.isAbsolute(relative)) return relative;\n\n if (home) {\n const underHome = path.relative(home, target);\n if (!underHome.startsWith('..') && !path.isAbsolute(underHome)) {\n return `~/${underHome}`;\n }\n }\n\n return target;\n}\n\n/** `412ms` below a second, `1.2s` above, `2m 05s` past a minute. */\nexport function formatDuration(ms: number): string {\n if (ms < 1000) return `${Math.round(ms)}ms`;\n if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;\n\n const minutes = Math.floor(ms / 60_000);\n const seconds = Math.round((ms % 60_000) / 1000);\n return `${minutes}m ${String(seconds).padStart(2, '0')}s`;\n}\n\nexport function pluralize(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? '' : 's'}`;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekanos/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@clack/prompts": "1.7.0",
|
|
36
|
-
"@ekanos/integration-schema": "0.
|
|
36
|
+
"@ekanos/integration-schema": "0.5.0",
|
|
37
37
|
"esbuild": "0.28.1",
|
|
38
38
|
"picocolors": "1.1.1"
|
|
39
39
|
},
|
package/templates/AGENTS.md.tmpl
CHANGED
|
@@ -129,6 +129,15 @@ missing key and for an entry past its `expiresAt`). Evolve a schema with a
|
|
|
129
129
|
versioned key or a `z.union` — a stored value that no longer parses throws on
|
|
130
130
|
read.
|
|
131
131
|
|
|
132
|
+
**Limits, enforced by the host and by `createMockContext()` alike** (exported
|
|
133
|
+
as `PARTNER_STORAGE_LIMITS` from `@ekanos/integration-schema`): one stored
|
|
134
|
+
value serializes to at most **64 KB**, one secret value is at most 64 KB, an
|
|
135
|
+
integration may write at most **32 distinct secret names**, and a key segment
|
|
136
|
+
is at most 100 characters. A cache of vendor records hits the 64 KB cap
|
|
137
|
+
sooner than intuition says — roughly 120–150 modest records — so budget the
|
|
138
|
+
snapshot (trim the oldest rows until `JSON.stringify` fits, and say so with a
|
|
139
|
+
`trimmed` flag) rather than discovering the cap on the host.
|
|
140
|
+
|
|
132
141
|
### Secrets and the activation contract
|
|
133
142
|
|
|
134
143
|
There is **no `secrets` block** in the definition. Secrets are *named*, not
|
|
@@ -165,6 +174,15 @@ your slug at discovery.
|
|
|
165
174
|
Declared with kebab-case ids. The transport validates `payloadSchema` before
|
|
166
175
|
your handler runs and owns signature verification (`signature: { header,
|
|
167
176
|
secretName, scheme? }` or `'none'`) — never verify a signature in the handler.
|
|
177
|
+
|
|
178
|
+
**If you must declare `'none'`** (the vendor's scheme is not yet
|
|
179
|
+
expressible), the delivery is authenticated only by the per-account endpoint
|
|
180
|
+
token in its URL. Treat the payload as a HINT: read the ids out of it, then
|
|
181
|
+
re-fetch the item from the vendor with `ctx.fetch` before writing anything
|
|
182
|
+
to storage a widget will render — never copy names, URLs or text from an
|
|
183
|
+
unverified body into a `clientReadable` key. Confirm removals the same way
|
|
184
|
+
(a 404 or a "trashed" status from the vendor), so a stale retry cannot
|
|
185
|
+
resurrect or delete the wrong row.
|
|
168
186
|
Omit `scheme` for a plain HMAC-SHA256 body digest; declare one for keyed or
|
|
169
187
|
timestamped headers (Stripe/Calendly: `{ format: 'keyed', digestKey: 'v1',
|
|
170
188
|
timestampKey: 't', signedPayload: '{timestamp}.{body}' }`) or SHA-1
|
|
@@ -248,6 +266,23 @@ logic — export the handler once and reference it from both declarations
|
|
|
248
266
|
(`run: (ctx, args) => createRecord.handler(ctx, CreateRecordInput.parse(args))`)
|
|
249
267
|
if you also want the assistant able to call it.
|
|
250
268
|
|
|
269
|
+
**Reading through an action (credential must stay server-side).** The
|
|
270
|
+
schedule → `clientReadable` storage pattern is for data that is safe to hand
|
|
271
|
+
to the browser. When the credential cannot leave the server, a widget reads
|
|
272
|
+
through an action instead: declare a read-only action (e.g. `get-overview`)
|
|
273
|
+
and wrap it in `useQuery`, whose `queryFn` calls
|
|
274
|
+
`useIntegrationAction(...).mutateAsync`. Declare ONE shared overview action
|
|
275
|
+
and have several widgets call it with the same query key, so they share a
|
|
276
|
+
single fetch rather than one request each:
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
const overview = useIntegrationAction<void, Overview>('__SLUG__', 'get-overview', { accountId });
|
|
280
|
+
const { data } = useQuery({
|
|
281
|
+
queryKey: ['__SLUG__', 'overview', accountId],
|
|
282
|
+
queryFn: () => overview.mutateAsync(),
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
251
286
|
### `onActivate`
|
|
252
287
|
|
|
253
288
|
Runs once after an activation is first persisted, and again after
|
|
@@ -298,6 +333,23 @@ harness and in production. A widget reads declared `clientReadable`
|
|
|
298
333
|
account-scope storage back with `fetchIntegrationStorage()` from
|
|
299
334
|
`@ekanos/sdk/hooks`.
|
|
300
335
|
|
|
336
|
+
Two more hooks from `@ekanos/sdk/hooks` that widgets use together:
|
|
337
|
+
|
|
338
|
+
- **Cross-widget events.** Publish with
|
|
339
|
+
`useWidgetEventBus().publish({ subtype, sourceWidget, sourceProduct, payload })`
|
|
340
|
+
and subscribe with
|
|
341
|
+
`useWidgetEvents({ subtypes: ['deal-selected'], sourceProducts: ['__SLUG__'] })`,
|
|
342
|
+
which returns `{ latestEvent, events, clear }`. `sourceProduct` is your slug;
|
|
343
|
+
`subtype` is your own kebab-case name. Both are no-ops outside a dashboard
|
|
344
|
+
(the admin preview, the harness tile view), so calling them never crashes.
|
|
345
|
+
- **Assistant hand-off.** `useAskAssistant()` returns a function that takes a
|
|
346
|
+
`WidgetAskContext` OBJECT, not a string:
|
|
347
|
+
`ask({ widgetId, title, snapshot, suggestedPrompt, suggestedPrompts? })`.
|
|
348
|
+
`snapshot` is small JSON the assistant sees as context; `suggestedPrompt`
|
|
349
|
+
is pre-filled for the user to send; `suggestedPrompts` are extra
|
|
350
|
+
`{ label, prompt }` chips. Put ids in prompts, not free text copied from
|
|
351
|
+
vendor data.
|
|
352
|
+
|
|
301
353
|
For a widget that WRITES — a form's Save button, not a read — reach for
|
|
302
354
|
`useIntegrationAction()` (see "Actions" above), not `useAskAssistant()`.
|
|
303
355
|
`useAskAssistant()` sends a conversational question to the assistant dock;
|
|
@@ -411,6 +463,17 @@ Two different fetches, two different guarantees:
|
|
|
411
463
|
`ctx.fetch`, just in the browser" — that is the one thing this section
|
|
412
464
|
means to rule out.
|
|
413
465
|
|
|
466
|
+
**Node built-ins are not available to integration code.** The gate bundles
|
|
467
|
+
your single `entry` for the browser as well as for the server, and the entry
|
|
468
|
+
reaches every handler, so `import 'node:crypto'` (or any other built-in)
|
|
469
|
+
anywhere in the graph fails the build — even in code that only ever runs
|
|
470
|
+
server-side. For HMACs, hashes, random ids and JWT signing use **WebCrypto**
|
|
471
|
+
(`globalThis.crypto.subtle`, `crypto.randomUUID()`), which is the same API in
|
|
472
|
+
Node 20+ and every browser; it needs no import and no dependency. A
|
|
473
|
+
passphrase-encrypted private key cannot be imported by WebCrypto — have the
|
|
474
|
+
admin decrypt it once (`openssl pkcs8 -nocrypt`) and store the plain PKCS#8
|
|
475
|
+
key.
|
|
476
|
+
|
|
414
477
|
In short: widgets get real network in Live API mode, gated by convenience in
|
|
415
478
|
the harness and by nothing in production; handlers get fixtures in fixtures
|
|
416
479
|
mode and the real, enforced thing in live mode — locally on the dev server,
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
"prepare": "node scripts/setup-hooks.mjs"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@ekanos/sdk": "^0.
|
|
15
|
-
"@ekanos/ui": "^0.
|
|
14
|
+
"@ekanos/sdk": "^0.5.0",
|
|
15
|
+
"@ekanos/ui": "^0.5.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@ekanos/cli": "^0.3.
|
|
19
|
-
"@ekanos/harness": "^0.4.
|
|
18
|
+
"@ekanos/cli": "^0.3.5",
|
|
19
|
+
"@ekanos/harness": "^0.4.1",
|
|
20
20
|
"@hookform/resolvers": "^5.2.2",
|
|
21
21
|
"@tailwindcss/postcss": "^4.3.3",
|
|
22
22
|
"@tanstack/react-query": "^5.101.4",
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
# pnpm 10+ refuses to run dependency build scripts it has not been told to
|
|
2
|
-
# allow
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
2
|
+
# allow. Any dependency with an install script that is not answered here gets
|
|
3
|
+
# a placeholder value written by the first `pnpm install`, and every later `ekanos dev` / `ekanos test` then fails
|
|
4
|
+
# with ERR_PNPM_IGNORED_BUILDS until a human edits it. Every build script in
|
|
5
|
+
# the scaffolded dependency tree is answered below. npm and yarn ignore this
|
|
6
|
+
# file.
|
|
7
7
|
allowBuilds:
|
|
8
|
+
# vitest -> esbuild: the install script puts the platform binary in place.
|
|
8
9
|
esbuild: true
|
|
10
|
+
# next-intl -> @parcel/watcher: only used for optional message-file
|
|
11
|
+
# watching, which falls back to a plain watcher when the native one is absent.
|
|
12
|
+
'@parcel/watcher': false
|
|
13
|
+
# next-intl -> @swc/core: Next compiles with its own bundled SWC, and the
|
|
14
|
+
# platform binary arrives as an optional dependency, not via this script.
|
|
15
|
+
'@swc/core': false
|