pi-compact-tools 0.9.3 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -13
- package/extensions/compact-tools-activity.ts +351 -0
- package/extensions/compact-tools-clock.ts +29 -0
- package/extensions/compact-tools-config.ts +47 -5
- package/extensions/compact-tools-custom.ts +77 -0
- package/extensions/compact-tools-hook.ts +47 -0
- package/extensions/compact-tools-invocation.ts +72 -0
- package/extensions/compact-tools-layout.ts +10 -0
- package/extensions/compact-tools-palette.ts +18 -0
- package/extensions/compact-tools-progress.ts +9 -9
- package/extensions/compact-tools-release.ts +138 -0
- package/extensions/compact-tools-runtime.ts +38 -11
- package/extensions/compact-tools-silent.ts +334 -0
- package/extensions/compact-tools-thinking.ts +7 -7
- package/extensions/compact-tools-types.ts +14 -1
- package/extensions/compact-tools-viewport.ts +234 -0
- package/extensions/compact-tools.ts +156 -12
- package/package.json +3 -2
- package/release-notes.json +11 -0
- package/schemas/compact-tools.schema.json +34 -2
- package/scripts/check-resources.mjs +9 -0
|
@@ -29,8 +29,10 @@ import {
|
|
|
29
29
|
} from "./compact-tools-core.ts";
|
|
30
30
|
import { colorizeRgb, interpolateRgb } from "./compact-tools-color.ts";
|
|
31
31
|
import { loadConfig } from "./compact-tools-config.ts";
|
|
32
|
+
import { installToolRowPatch, setRowResolver, type RowRenderers, type ToolRow } from "./compact-tools-custom.ts";
|
|
32
33
|
import {
|
|
33
34
|
formatResultLineSummary,
|
|
35
|
+
countEditChanges,
|
|
34
36
|
getArgumentDetails,
|
|
35
37
|
getCallDetails,
|
|
36
38
|
getEditDiff,
|
|
@@ -39,12 +41,15 @@ import {
|
|
|
39
41
|
getTextResult,
|
|
40
42
|
isReadTextResult,
|
|
41
43
|
splitReadFooter,
|
|
44
|
+
summarizeCustomArguments,
|
|
45
|
+
summarizeFailure,
|
|
42
46
|
summarizeShellCommand,
|
|
43
47
|
} from "./compact-tools-invocation.ts";
|
|
44
48
|
import {
|
|
45
49
|
CachedContainer,
|
|
46
50
|
limitComponentLines,
|
|
47
51
|
prefixedText,
|
|
52
|
+
railComponent,
|
|
48
53
|
renderArguments,
|
|
49
54
|
renderCodeDiff,
|
|
50
55
|
renderCodeView,
|
|
@@ -54,15 +59,19 @@ import {
|
|
|
54
59
|
} from "./compact-tools-layout.ts";
|
|
55
60
|
import { chromePainter, indicatorPulse } from "./compact-tools-palette.ts";
|
|
56
61
|
import { ProgressController } from "./compact-tools-progress.ts";
|
|
62
|
+
import { showReleaseNotice } from "./compact-tools-release.ts";
|
|
57
63
|
import { ToolRuntime } from "./compact-tools-runtime.ts";
|
|
64
|
+
import { SilentModeController } from "./compact-tools-silent.ts";
|
|
58
65
|
import { ThinkingCycleController } from "./compact-tools-thinking.ts";
|
|
59
|
-
import
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
import { ViewportKeeper } from "./compact-tools-viewport.ts";
|
|
67
|
+
import {
|
|
68
|
+
SUPPORTED_TOOL_SET,
|
|
69
|
+
type BuiltInDefinition,
|
|
70
|
+
type CompactToolName,
|
|
71
|
+
type RenderContext,
|
|
72
|
+
type RowState,
|
|
73
|
+
type ShellToolName,
|
|
74
|
+
type ToolArgs,
|
|
66
75
|
} from "./compact-tools-types.ts";
|
|
67
76
|
|
|
68
77
|
/** Context lines kept around each change while an edit result is collapsed. */
|
|
@@ -134,6 +143,8 @@ function renderControls(
|
|
|
134
143
|
running: boolean,
|
|
135
144
|
isError: boolean,
|
|
136
145
|
lineSummary?: string,
|
|
146
|
+
failureReason?: string,
|
|
147
|
+
changes?: { added: number; removed: number },
|
|
137
148
|
): Component {
|
|
138
149
|
const duration = running ? undefined : formatDuration(state);
|
|
139
150
|
const status = running
|
|
@@ -143,8 +154,24 @@ function renderControls(
|
|
|
143
154
|
// background, so the status word stays chrome rather than repeating that signal.
|
|
144
155
|
const chrome = chromePainter(theme);
|
|
145
156
|
let details = chrome(status);
|
|
146
|
-
if (
|
|
147
|
-
|
|
157
|
+
if (failureReason && isError && !running) details += chrome(" · ") + theme.fg("error", failureReason);
|
|
158
|
+
// An edit reports what it changed the way a diff does: lines added and removed.
|
|
159
|
+
else if (changes && !running && !isError) {
|
|
160
|
+
details += chrome(" (") + theme.fg("toolDiffAdded", `+${changes.added}`) + chrome(" ")
|
|
161
|
+
+ theme.fg("toolDiffRemoved", `-${changes.removed}`) + chrome(")");
|
|
162
|
+
}
|
|
163
|
+
// A failed call produced no result worth counting; "(0 lines)" would only mislead.
|
|
164
|
+
else if (lineSummary && !running && !isError) details += chrome(` (${lineSummary})`);
|
|
165
|
+
return prefixedText(details, chrome(" └ "), " ");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A failed row whose output is hidden gets its reason on the status line. When
|
|
170
|
+
* the output shows, expanded or as a preview, it already says why, and repeating
|
|
171
|
+
* it there only doubles the error.
|
|
172
|
+
*/
|
|
173
|
+
function hiddenFailureReason(name: string, state: RowState, isError: boolean, output: string): string | undefined {
|
|
174
|
+
return isError && !state.expanded && !state.preview ? summarizeFailure(name, output) : undefined;
|
|
148
175
|
}
|
|
149
176
|
|
|
150
177
|
function renderCallTitle(name: string, theme: Theme, ctx: RenderContext, state: RowState): string {
|
|
@@ -234,7 +261,8 @@ function renderFileResult(
|
|
|
234
261
|
state.resultLineSummary = formatResultLineSummary(name, ctx.args, result, output);
|
|
235
262
|
state.resultLineSummaryComputed = true;
|
|
236
263
|
}
|
|
237
|
-
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary
|
|
264
|
+
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary,
|
|
265
|
+
hiddenFailureReason(name, state, ctx.isError, output), name === "edit" ? countEditChanges(result) : undefined));
|
|
238
266
|
rememberResult(state, result, options, ctx);
|
|
239
267
|
return container;
|
|
240
268
|
}
|
|
@@ -265,13 +293,14 @@ function renderShellResult(
|
|
|
265
293
|
runtime.syncExpansion(state, ctx.expanded, name);
|
|
266
294
|
if (canReuseResult(state, result, options, ctx)) return ctx.lastComponent;
|
|
267
295
|
const output = getTextResult(result);
|
|
296
|
+
const failureReason = hiddenFailureReason(name, state, ctx.isError, output);
|
|
268
297
|
runtime.setResultAvailable(state, name, output.length > 0);
|
|
269
298
|
if (!options.isPartial && !state.resultLineSummaryComputed) {
|
|
270
299
|
state.resultLineSummary = formatResultLineSummary(name, ctx.args, result, output);
|
|
271
300
|
state.resultLineSummaryComputed = true;
|
|
272
301
|
}
|
|
273
302
|
if (!state.expanded && !state.preview) {
|
|
274
|
-
const controls = renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary);
|
|
303
|
+
const controls = renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary, failureReason);
|
|
275
304
|
rememberResult(state, result, options, ctx);
|
|
276
305
|
return controls;
|
|
277
306
|
}
|
|
@@ -280,11 +309,107 @@ function renderShellResult(
|
|
|
280
309
|
if (component) {
|
|
281
310
|
container.addChild(state.expanded ? component : limitComponentLines(component, runtime.config.previewLines, theme));
|
|
282
311
|
}
|
|
283
|
-
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary));
|
|
312
|
+
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary, failureReason));
|
|
313
|
+
rememberResult(state, result, options, ctx);
|
|
314
|
+
return container;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function renderCustomCall(name: string, args: ToolArgs, theme: Theme, ctx: RenderContext): Component {
|
|
318
|
+
const state = runtime.syncRow(ctx, ctx.state.endedAt === undefined);
|
|
319
|
+
runtime.syncExpansion(state, ctx.expanded, name);
|
|
320
|
+
const summary = summarizeCustomArguments(args);
|
|
321
|
+
const container = new CachedContainer();
|
|
322
|
+
container.addChild(renderToolCall(
|
|
323
|
+
renderCallTitle(name, theme, ctx, state),
|
|
324
|
+
summary ? theme.fg("toolOutput", summary) : undefined,
|
|
325
|
+
theme,
|
|
326
|
+
));
|
|
327
|
+
if (state.expanded && Object.keys(args).length > 0) container.addChild(renderArguments(args, theme));
|
|
328
|
+
return container;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
type AuthorResultRenderer = (expanded: boolean) => Component | undefined;
|
|
332
|
+
|
|
333
|
+
function renderCustomBody(
|
|
334
|
+
output: string,
|
|
335
|
+
expanded: boolean,
|
|
336
|
+
theme: Theme,
|
|
337
|
+
isError: boolean,
|
|
338
|
+
renderAuthorResult: AuthorResultRenderer | undefined,
|
|
339
|
+
): Component | undefined {
|
|
340
|
+
if (renderAuthorResult) {
|
|
341
|
+
try {
|
|
342
|
+
const component = renderAuthorResult(expanded);
|
|
343
|
+
if (component) return railComponent(component, theme);
|
|
344
|
+
} catch {
|
|
345
|
+
// A failing third-party renderer degrades to the plain text result.
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return renderOutput(output, theme, isError);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function renderCustomResult(
|
|
352
|
+
name: string,
|
|
353
|
+
result: AgentToolResult<unknown>,
|
|
354
|
+
options: ToolRenderResultOptions,
|
|
355
|
+
theme: Theme,
|
|
356
|
+
ctx: RenderContext,
|
|
357
|
+
renderAuthorResult: AuthorResultRenderer | undefined,
|
|
358
|
+
): Component {
|
|
359
|
+
const state = runtime.syncRow(ctx, options.isPartial, !options.isPartial);
|
|
360
|
+
runtime.syncExpansion(state, ctx.expanded, name);
|
|
361
|
+
if (canReuseResult(state, result, options, ctx)) return ctx.lastComponent;
|
|
362
|
+
const output = getTextResult(result);
|
|
363
|
+
runtime.setResultAvailable(state, name, output.length > 0 || renderAuthorResult !== undefined);
|
|
364
|
+
if (!options.isPartial && !state.resultLineSummaryComputed) {
|
|
365
|
+
state.resultLineSummary = output ? formatResultLineSummary(name, ctx.args, result, output) : undefined;
|
|
366
|
+
state.resultLineSummaryComputed = true;
|
|
367
|
+
}
|
|
368
|
+
const container = new CachedContainer();
|
|
369
|
+
if (state.expanded || state.preview) {
|
|
370
|
+
const body = renderCustomBody(output, state.expanded === true, theme, ctx.isError, renderAuthorResult);
|
|
371
|
+
if (body) container.addChild(state.expanded ? body : limitComponentLines(body, runtime.config.previewLines, theme));
|
|
372
|
+
}
|
|
373
|
+
const failureReason = hiddenFailureReason(name, state, ctx.isError, output);
|
|
374
|
+
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary, failureReason));
|
|
284
375
|
rememberResult(state, result, options, ctx);
|
|
285
376
|
return container;
|
|
286
377
|
}
|
|
287
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Compact renderers for one row of a tool this extension did not register. The
|
|
381
|
+
* author's result renderer still draws the expanded body; the row chrome, status,
|
|
382
|
+
* and collapse behavior match the built-ins.
|
|
383
|
+
*/
|
|
384
|
+
function createCustomRenderers(name: string, author: ToolDefinition<any, any, any> | undefined): RowRenderers {
|
|
385
|
+
// Pi hands both renderers the row's own state object, which belongs to the tool's
|
|
386
|
+
// author. Compact bookkeeping is kept here so neither side overwrites the other.
|
|
387
|
+
const state: RowState = {};
|
|
388
|
+
let authorResult: Component | undefined;
|
|
389
|
+
const own = (ctx: RenderContext<any>): RenderContext => ({ ...ctx, args: ctx.args ?? {}, state });
|
|
390
|
+
return {
|
|
391
|
+
renderCall: (args, theme, ctx) => renderCustomCall(name, (args ?? {}) as ToolArgs, theme, own(ctx)),
|
|
392
|
+
renderResult: (result, options, theme, ctx) => {
|
|
393
|
+
const authorRenderer = author?.renderResult;
|
|
394
|
+
const renderAuthorResult = authorRenderer
|
|
395
|
+
? (expanded: boolean) => {
|
|
396
|
+
authorResult = authorRenderer(result, { ...options, expanded }, theme, { ...ctx, lastComponent: authorResult });
|
|
397
|
+
return authorResult;
|
|
398
|
+
}
|
|
399
|
+
: undefined;
|
|
400
|
+
return renderCustomResult(name, result, options, theme, own(ctx), renderAuthorResult);
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function resolveCustomRow(row: ToolRow): RowRenderers | undefined {
|
|
406
|
+
const customTools = runtime.config.custom_tools;
|
|
407
|
+
// Built-ins are governed by `tools`: ones left out keep Pi's own renderer.
|
|
408
|
+
if (!customTools.enabled || SUPPORTED_TOOL_SET.has(row.toolName)) return undefined;
|
|
409
|
+
if (customTools.exclude.includes(row.toolName)) return undefined;
|
|
410
|
+
return createCustomRenderers(row.toolName, row.toolDefinition);
|
|
411
|
+
}
|
|
412
|
+
|
|
288
413
|
const toolFactories: Record<CompactToolName, (cwd: string) => BuiltInDefinition> = {
|
|
289
414
|
read: createReadToolDefinition,
|
|
290
415
|
write: createWriteToolDefinition,
|
|
@@ -369,6 +494,11 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
369
494
|
|
|
370
495
|
const thinkingCycle = new ThinkingCycleController(pi);
|
|
371
496
|
const progress = new ProgressController(pi);
|
|
497
|
+
const viewport = new ViewportKeeper();
|
|
498
|
+
const silent = new SilentModeController(pi, showReleaseNotice);
|
|
499
|
+
const customRowsAvailable = installToolRowPatch();
|
|
500
|
+
setRowResolver(resolveCustomRow);
|
|
501
|
+
let customRowsWarned = false;
|
|
372
502
|
// Register once while the extension runtime is being built. In particular, this
|
|
373
503
|
// makes the overrides available before Pi restores the active tool set on /reload.
|
|
374
504
|
configure(pi, process.cwd());
|
|
@@ -376,16 +506,30 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
376
506
|
if (event.reason !== "reload") runtime.clearTimings();
|
|
377
507
|
configure(pi, ctx.cwd, ctx.isProjectTrusted());
|
|
378
508
|
if (ctx.mode === "tui") {
|
|
509
|
+
// First, so its input listener sees relayout keys before the thinking controller consumes them.
|
|
510
|
+
viewport.bind(ctx);
|
|
379
511
|
progress.bind(ctx);
|
|
380
512
|
thinkingCycle.bind(ctx);
|
|
513
|
+
silent.bind(ctx, runtime.config.mode, event.reason === "reload");
|
|
514
|
+
if (!silent.isEnabled()) showReleaseNotice(ctx);
|
|
515
|
+
if (runtime.config.custom_tools.enabled && !customRowsAvailable && !customRowsWarned) {
|
|
516
|
+
customRowsWarned = true;
|
|
517
|
+
ctx.ui.notify("Compact rendering for custom tools is unavailable in this version of Pi", "warning");
|
|
518
|
+
}
|
|
381
519
|
} else {
|
|
520
|
+
viewport.dispose();
|
|
382
521
|
progress.dispose();
|
|
383
522
|
thinkingCycle.dispose();
|
|
523
|
+
silent.dispose();
|
|
384
524
|
}
|
|
385
525
|
});
|
|
526
|
+
pi.on("tool_execution_start", (event) => runtime.noteExecutionStart(event.toolCallId));
|
|
527
|
+
pi.on("tool_execution_end", (event) => runtime.noteExecutionEnd(event.toolCallId));
|
|
386
528
|
pi.on("session_shutdown", (event) => {
|
|
387
529
|
thinkingCycle.dispose();
|
|
388
530
|
progress.dispose();
|
|
531
|
+
silent.dispose();
|
|
532
|
+
viewport.dispose();
|
|
389
533
|
runtime.reset(event.reason !== "reload");
|
|
390
534
|
});
|
|
391
535
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-compact-tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Compact, expandable UI for pi's built-in tools, bundled with a polished GitHub Dark theme.",
|
|
3
|
+
"version": "0.10.1",
|
|
4
|
+
"description": "Compact, expandable UI for pi's built-in and custom tools, bundled with a polished GitHub Dark theme.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Dong Hyeong Lee",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"examples",
|
|
27
27
|
"scripts",
|
|
28
28
|
"schemas",
|
|
29
|
+
"release-notes.json",
|
|
29
30
|
"README.md",
|
|
30
31
|
"LICENSE"
|
|
31
32
|
],
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"0.10.1": [],
|
|
3
|
+
"0.10.0": [
|
|
4
|
+
"Silent mode shows only prompts and final answers (Ctrl+' or /silent)",
|
|
5
|
+
"Tools from other packages get the same compact rows as the built-ins",
|
|
6
|
+
"Edits show the lines they added and removed, in the diff colors",
|
|
7
|
+
"Thinking summaries are shown in full instead of cut off",
|
|
8
|
+
"Fullscreen scrolling stays in place when expanding rows or cycling thinking",
|
|
9
|
+
"Failed rows show a short reason while their output is hidden"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
"$schema": {
|
|
9
9
|
"type": "string"
|
|
10
10
|
},
|
|
11
|
+
"mode": {
|
|
12
|
+
"description": "normal shows every tool row. silent leaves only your prompts and the final answers: tool rows, in-between assistant turns, thinking, error and abort notices, extension notices, and Pi's own status lines are hidden while a light animates beneath the prompt. Ctrl+' or /silent toggles it for the current session.",
|
|
13
|
+
"type": "string",
|
|
14
|
+
"enum": ["normal", "silent"],
|
|
15
|
+
"default": "normal"
|
|
16
|
+
},
|
|
11
17
|
"tools": {
|
|
12
18
|
"description": "Built-in tools that use compact rendering. Omitted tools keep Pi's default renderer.",
|
|
13
19
|
"type": "array",
|
|
@@ -18,6 +24,32 @@
|
|
|
18
24
|
},
|
|
19
25
|
"default": ["read", "write", "edit", "bash"]
|
|
20
26
|
},
|
|
27
|
+
"custom_tools": {
|
|
28
|
+
"description": "Compact rendering for tools registered by other extensions. true/false toggles it; the object form also sets whether rows start hidden and which tools keep their own renderer.",
|
|
29
|
+
"oneOf": [
|
|
30
|
+
{ "type": "boolean" },
|
|
31
|
+
{
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"properties": {
|
|
35
|
+
"enabled": { "type": "boolean", "default": true },
|
|
36
|
+
"auto_compact": {
|
|
37
|
+
"description": "True starts custom tool rows hidden; false starts with a bounded preview.",
|
|
38
|
+
"type": "boolean",
|
|
39
|
+
"default": true
|
|
40
|
+
},
|
|
41
|
+
"exclude": {
|
|
42
|
+
"description": "Tool names that keep their own renderer.",
|
|
43
|
+
"type": "array",
|
|
44
|
+
"uniqueItems": true,
|
|
45
|
+
"items": { "type": "string" },
|
|
46
|
+
"default": []
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"default": { "enabled": true, "auto_compact": true, "exclude": [] }
|
|
52
|
+
},
|
|
21
53
|
"previewLines": {
|
|
22
54
|
"description": "Maximum rendered lines shown initially when auto_compact is false. Explicit expansion always shows the complete result.",
|
|
23
55
|
"type": "integer",
|
|
@@ -26,9 +58,9 @@
|
|
|
26
58
|
"default": 10
|
|
27
59
|
},
|
|
28
60
|
"auto_compact": {
|
|
29
|
-
"description": "Whether each compact-rendered tool starts hidden. False starts with a bounded preview; Pi's normal click and Ctrl+O flow can expand either state.",
|
|
61
|
+
"description": "Whether each compact-rendered tool starts hidden. False starts with a bounded preview; Pi's normal click and Ctrl+O flow can expand either state. Custom tools can be listed by name too, which overrides custom_tools.auto_compact for that tool.",
|
|
30
62
|
"type": "object",
|
|
31
|
-
"additionalProperties":
|
|
63
|
+
"additionalProperties": { "type": "boolean" },
|
|
32
64
|
"properties": {
|
|
33
65
|
"read": { "type": "boolean", "default": true },
|
|
34
66
|
"write": { "type": "boolean", "default": true },
|
|
@@ -5,6 +5,7 @@ const resources = [
|
|
|
5
5
|
"examples/compact-tools.json",
|
|
6
6
|
"examples/compact-tools-2.json",
|
|
7
7
|
"schemas/compact-tools.schema.json",
|
|
8
|
+
"release-notes.json",
|
|
8
9
|
];
|
|
9
10
|
|
|
10
11
|
for (const path of resources) {
|
|
@@ -70,4 +71,12 @@ if (schema.properties?.previewLines?.default !== 10) {
|
|
|
70
71
|
throw new Error("schema must expose the bounded previewLines setting");
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
75
|
+
const releaseNotes = JSON.parse(await readFile(new URL("../release-notes.json", import.meta.url), "utf8"));
|
|
76
|
+
// Every published version needs an entry. An empty list is a quiet release that shows no notice.
|
|
77
|
+
if (!Array.isArray(releaseNotes[manifest.version]) ||
|
|
78
|
+
releaseNotes[manifest.version].some((note) => typeof note !== "string" || !note.trim())) {
|
|
79
|
+
throw new Error(`release-notes.json must have an entry for v${manifest.version} before publishing; use [] for no notice`);
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
console.log("Resource checks passed");
|