agentic-workflow-manager 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -9,6 +9,8 @@ const commander_1 = require("commander");
9
9
  const config_1 = require("./utils/config");
10
10
  const grouping_1 = require("./utils/grouping");
11
11
  const registry_view_1 = require("./utils/registry-view");
12
+ const tty_1 = require("./ui/tty");
13
+ const picker_1 = require("./ui/picker");
12
14
  const providers_1 = require("./providers");
13
15
  const executor_1 = require("./core/executor");
14
16
  const regenerate_1 = require("./core/context/regenerate");
@@ -73,6 +75,7 @@ program.command('add [name]')
73
75
  .option('-s, --scope <scope>', 'Scope: local or global')
74
76
  .option('-m, --method <method>', 'Install method: symlink or copy')
75
77
  .option('-y, --yes', 'Skip confirmation prompts')
78
+ .option('--all', 'install all artifacts from all packages without prompting')
76
79
  .action(async (name, options) => {
77
80
  (0, prompts_1.intro)(picocolors_1.default.bgCyan(picocolors_1.default.black(' AWM - Agentic Workflow Manager ')));
78
81
  // 1. Sync the registry
@@ -142,6 +145,10 @@ program.command('add [name]')
142
145
  (0, prompts_1.outro)(`✅ Installed bundle ${picocolors_1.default.cyan(matchedBundle.name)}:\n ${lines}${recordNote}`);
143
146
  return;
144
147
  }
148
+ // name given but no matching bundle found — report clearly regardless of TTY
149
+ console.error(picocolors_1.default.red(`Bundle "${name}" not found in registry.`));
150
+ console.error(picocolors_1.default.dim('Run `awm list` to see available packages.'));
151
+ process.exit(1);
145
152
  }
146
153
  // 2. Discover artifacts
147
154
  const skills = (0, discovery_1.discoverSkills)();
@@ -152,6 +159,103 @@ program.command('add [name]')
152
159
  process.exit(0);
153
160
  }
154
161
  const prefs = (0, config_1.getPreferences)();
162
+ // --all: install everything from every package headlessly
163
+ if (options.all) {
164
+ let targetAgents;
165
+ if (options.agent) {
166
+ const validAgents = Object.keys(providers_1.PROVIDERS);
167
+ const parsed = options.agent.split(',').map((a) => a.trim());
168
+ for (const a of parsed) {
169
+ if (!validAgents.includes(a)) {
170
+ console.error(picocolors_1.default.red(`Invalid agent "${a}". Use: ${validAgents.join(', ')}.`));
171
+ process.exit(1);
172
+ }
173
+ }
174
+ targetAgents = parsed;
175
+ }
176
+ else {
177
+ targetAgents = [prefs.defaultAgent];
178
+ }
179
+ let scopeVal;
180
+ if (options.scope) {
181
+ if (!['local', 'global'].includes(options.scope)) {
182
+ console.error(picocolors_1.default.red(`Invalid scope "${options.scope}". Use: local or global.`));
183
+ process.exit(1);
184
+ }
185
+ scopeVal = options.scope;
186
+ }
187
+ else {
188
+ scopeVal = prefs.defaultScope;
189
+ }
190
+ let methodVal;
191
+ if (options.method) {
192
+ if (!['symlink', 'copy'].includes(options.method)) {
193
+ console.error(picocolors_1.default.red(`Invalid method "${options.method}". Use: symlink or copy.`));
194
+ process.exit(1);
195
+ }
196
+ methodVal = options.method;
197
+ }
198
+ else {
199
+ methodVal = scopeVal === 'local' ? 'copy' : 'symlink';
200
+ }
201
+ const includeWorkflows = targetAgents.some((a) => providers_1.PROVIDERS[a].workflow !== null);
202
+ const includeAgents = targetAgents.some((a) => providers_1.PROVIDERS[a].agent !== null);
203
+ const allView = (0, registry_view_1.buildPackageView)(skills, includeWorkflows ? workflows : [], includeAgents ? agents : [], (0, bundles_1.discoverAllBundles)());
204
+ const allDedup = new Map();
205
+ for (const pkg of allView) {
206
+ for (const a of pkg.artifacts) {
207
+ allDedup.set((0, registry_view_1.artifactValue)(a), a);
208
+ }
209
+ }
210
+ const artifactsToInstall = Array.from(allDedup.values()).map((a) => ({
211
+ name: a.installName,
212
+ sourcePath: a.sourcePath,
213
+ type: a.type,
214
+ }));
215
+ if (artifactsToInstall.length === 0) {
216
+ (0, prompts_1.outro)(picocolors_1.default.yellow('No artifacts available.'));
217
+ return;
218
+ }
219
+ const installSpinner = (0, prompts_1.spinner)();
220
+ installSpinner.start('Installing artifacts...');
221
+ try {
222
+ const installed = [];
223
+ const skipped = [];
224
+ for (const currentAgent of targetAgents) {
225
+ for (const artifact of artifactsToInstall) {
226
+ if (providers_1.PROVIDERS[currentAgent][artifact.type] === null) {
227
+ skipped.push(`${artifact.name} (${currentAgent})`);
228
+ continue;
229
+ }
230
+ const targetDir = (0, providers_1.getTargetPath)(artifact.type, currentAgent, scopeVal);
231
+ const finalDest = path_1.default.join(targetDir, artifact.name);
232
+ (0, executor_1.installArtifact)(artifact.sourcePath, finalDest, methodVal);
233
+ installed.push(`${artifact.name} → ${currentAgent} (${scopeVal})`);
234
+ }
235
+ }
236
+ (0, config_1.savePreferences)({ defaultAgent: targetAgents[0], defaultScope: scopeVal, installMethod: methodVal });
237
+ installSpinner.stop('Installation complete!');
238
+ if (skipped.length > 0) {
239
+ for (const sk of skipped) {
240
+ console.log(picocolors_1.default.yellow(` ⚠️ Skipped: ${sk} (not supported by target agent)`));
241
+ }
242
+ }
243
+ const names = installed.map((n) => picocolors_1.default.green(n)).join('\n ');
244
+ (0, prompts_1.outro)(`✅ Installed:\n ${names}`);
245
+ }
246
+ catch (e) {
247
+ installSpinner.stop('Installation failed.');
248
+ console.error(picocolors_1.default.red(e.message));
249
+ process.exit(1);
250
+ }
251
+ return;
252
+ }
253
+ // Interactive selection requires a TTY. Bundle-by-name (handled above) works headless.
254
+ if (!(0, tty_1.isInteractive)()) {
255
+ console.error(picocolors_1.default.red('`awm add` needs an interactive terminal for selection.'));
256
+ console.error(picocolors_1.default.dim('Install a bundle non-interactively: `awm add <bundle>` (optionally with --agent/--scope).'));
257
+ process.exit(1);
258
+ }
155
259
  // 3. Agent & Scope Prompts (Moved up)
156
260
  let targetAgents;
157
261
  if (options.agent) {
@@ -207,27 +311,35 @@ program.command('add [name]')
207
311
  process.exit(0);
208
312
  }
209
313
  // 5. Level 1 — pick package(s)
210
- const pkgChoice = await (0, prompts_1.multiselect)({
211
- message: 'Select package(s)',
212
- options: (0, registry_view_1.buildLevel1Options)(view),
213
- required: true
314
+ const pickedPackages = await (0, picker_1.multiselectPicker)({
315
+ title: 'Select package(s)',
316
+ items: (0, registry_view_1.packagePickerItems)(view),
214
317
  });
215
- handleCancel(pkgChoice);
216
- const selectedPackages = pkgChoice
217
- .map(name => view.find(p => p.name === name))
318
+ if (pickedPackages === null) {
319
+ (0, prompts_1.outro)('Operation cancelled.');
320
+ process.exit(0);
321
+ }
322
+ if (pickedPackages.length === 0) {
323
+ (0, prompts_1.outro)(picocolors_1.default.yellow('No packages selected.'));
324
+ return;
325
+ }
326
+ const selectedPackages = pickedPackages
327
+ .map((name) => view.find((p) => p.name === name))
218
328
  .filter(Boolean);
219
329
  // 5b. Level 2 — pick skills within each package, in sequence
220
330
  const dedup = new Map();
221
331
  for (let i = 0; i < selectedPackages.length; i++) {
222
332
  const pkg = selectedPackages[i];
223
- const skillChoice = await (0, prompts_1.multiselect)({
224
- message: `[${i + 1}/${selectedPackages.length}] ${pkg.name} — select artifacts`,
225
- options: (0, registry_view_1.buildLevel2Options)(pkg),
226
- initialValues: [registry_view_1.ALL_SENTINEL],
227
- required: true
333
+ const picked = await (0, picker_1.multiselectPicker)({
334
+ title: `[${i + 1}/${selectedPackages.length}] ${pkg.name} — select artifacts`,
335
+ items: (0, registry_view_1.artifactPickerItems)(pkg),
336
+ initialSelected: [registry_view_1.ALL_SENTINEL, ...pkg.artifacts.map((a) => (0, registry_view_1.artifactValue)(a))],
228
337
  });
229
- handleCancel(skillChoice);
230
- for (const a of (0, registry_view_1.resolveLevel2Selection)(pkg, skillChoice)) {
338
+ if (picked === null) {
339
+ (0, prompts_1.outro)('Operation cancelled.');
340
+ process.exit(0);
341
+ }
342
+ for (const a of (0, registry_view_1.resolveLevel2Selection)(pkg, picked)) {
231
343
  dedup.set((0, registry_view_1.artifactValue)(a), a);
232
344
  }
233
345
  }
@@ -469,6 +581,7 @@ program.command('list [package]')
469
581
  if (packageName && options.all) {
470
582
  console.log(picocolors_1.default.dim('(Ignoring --all when a package name is provided.)'));
471
583
  }
584
+ const listWidth = process.stdout.isTTY ? process.stdout.columns : undefined;
472
585
  // Detail for a single package.
473
586
  if (packageName) {
474
587
  const { match, suggestion } = (0, registry_view_1.findPackage)(fullView, packageName);
@@ -480,7 +593,7 @@ program.command('list [package]')
480
593
  process.exit(1);
481
594
  }
482
595
  console.log();
483
- for (const line of (0, registry_view_1.packageDetailLines)(match))
596
+ for (const line of (0, registry_view_1.packageDetailLines)(match, listWidth))
484
597
  console.log(line);
485
598
  console.log();
486
599
  (0, prompts_1.outro)(`Run ${picocolors_1.default.green(`awm add`)} to install artifacts from ${picocolors_1.default.cyan(match.name)}.`);
@@ -490,7 +603,7 @@ program.command('list [package]')
490
603
  if (options.all) {
491
604
  for (const pkg of view) {
492
605
  console.log();
493
- for (const line of (0, registry_view_1.packageDetailLines)(pkg))
606
+ for (const line of (0, registry_view_1.packageDetailLines)(pkg, listWidth))
494
607
  console.log(line);
495
608
  }
496
609
  console.log();
@@ -499,7 +612,7 @@ program.command('list [package]')
499
612
  }
500
613
  // Default: compact summary.
501
614
  console.log();
502
- for (const line of (0, registry_view_1.packageSummaryLines)(view))
615
+ for (const line of (0, registry_view_1.packageSummaryLines)(view, listWidth))
503
616
  console.log(line);
504
617
  console.log();
505
618
  console.log(picocolors_1.default.dim(` awm list <pkg> · awm list --all`));
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TWO_PANE_MIN_COLUMNS = void 0;
7
+ exports.visibleItems = visibleItems;
8
+ exports.renderPicker = renderPicker;
9
+ const picocolors_1 = __importDefault(require("picocolors"));
10
+ const text_1 = require("./text");
11
+ exports.TWO_PANE_MIN_COLUMNS = 72;
12
+ const FOOTER = '↑↓ move · space select · tab all · ⏎ confirm · esc clear';
13
+ function visibleItems(state) {
14
+ if (!state.filter)
15
+ return state.items;
16
+ const q = state.filter.toLowerCase();
17
+ return state.items.filter((i) => i.label.toLowerCase().includes(q));
18
+ }
19
+ /** Compute a scroll window [start, end) around the cursor that fits `height` rows. */
20
+ function windowFor(cursor, total, height) {
21
+ if (total <= height)
22
+ return [0, total];
23
+ let start = cursor - Math.floor(height / 2);
24
+ start = Math.max(0, Math.min(start, total - height));
25
+ return [start, start + height];
26
+ }
27
+ function checkbox(selected) {
28
+ return selected ? picocolors_1.default.green('◼') : '◻';
29
+ }
30
+ function renderPicker(state, vp) {
31
+ const vis = visibleItems(state);
32
+ const cursor = Math.max(0, Math.min(state.cursor, vis.length - 1));
33
+ const selCount = state.selected.size;
34
+ const filterPart = state.filter ? ` filter: ${state.filter}` : '';
35
+ const header = picocolors_1.default.bold(state.title) + filterPart + (selCount ? picocolors_1.default.dim(` ${selCount} sel`) : '');
36
+ const out = [header];
37
+ if (vis.length === 0) {
38
+ out.push(picocolors_1.default.dim(' (no matches)'));
39
+ out.push(FOOTER);
40
+ return out;
41
+ }
42
+ const bodyHeight = Math.max(3, vp.rows - 4); // header + footer + breathing room
43
+ const [start, end] = windowFor(cursor, vis.length, bodyHeight);
44
+ const twoPane = vp.columns >= exports.TWO_PANE_MIN_COLUMNS;
45
+ const listWidth = twoPane ? Math.floor(vp.columns * 0.5) : vp.columns - 2;
46
+ const rows = [];
47
+ for (let i = start; i < end; i++) {
48
+ const item = vis[i];
49
+ const isCursor = i === cursor;
50
+ const marker = isCursor ? picocolors_1.default.cyan('❯') : ' ';
51
+ const box = checkbox(state.selected.has(item.value));
52
+ const labelWidth = listWidth - 5; // wide cursor marker (2 cells) + space + checkbox + space
53
+ const label = (0, text_1.truncate)(item.label, labelWidth);
54
+ rows.push(`${marker} ${box} ${label}`);
55
+ }
56
+ if (twoPane) {
57
+ const detailWidth = vp.columns - listWidth - 3; // gutter " │ "
58
+ const detail = (0, text_1.wrap)(vis[cursor].description, detailWidth).slice(0, rows.length);
59
+ const height = Math.max(rows.length, detail.length);
60
+ for (let i = 0; i < height; i++) {
61
+ const leftPadded = padToWidth(rows[i] ?? '', listWidth);
62
+ const right = detail[i] ?? '';
63
+ out.push(`${leftPadded} ${picocolors_1.default.dim('│')} ${picocolors_1.default.dim(right)}`);
64
+ }
65
+ }
66
+ else {
67
+ out.push(...rows);
68
+ out.push(picocolors_1.default.dim(' ' + ((0, text_1.wrap)(vis[cursor].description, vp.columns - 4)[0] ?? '')));
69
+ }
70
+ out.push(FOOTER);
71
+ return out;
72
+ }
73
+ /** Pad a possibly-colored string to `width` visible cells. */
74
+ function padToWidth(s, width) {
75
+ const pad = width - (0, text_1.displayWidth)(s);
76
+ return pad > 0 ? s + ' '.repeat(pad) : s;
77
+ }
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ALL_SENTINEL = void 0;
4
+ exports.parseKey = parseKey;
5
+ exports.pickerReducer = pickerReducer;
6
+ exports.multiselectPicker = multiselectPicker;
7
+ const picker_view_1 = require("./picker-view");
8
+ const registry_view_1 = require("../utils/registry-view");
9
+ // ALL_SENTINEL: single source of truth lives in registry-view.ts — re-exported here.
10
+ exports.ALL_SENTINEL = registry_view_1.ALL_SENTINEL;
11
+ function parseKey(data) {
12
+ switch (data) {
13
+ case '\x1b[A': return { action: 'up' };
14
+ case '\x1b[B': return { action: 'down' };
15
+ case '\r':
16
+ case '\n': return { action: 'confirm' };
17
+ case ' ': return { action: 'toggle' };
18
+ case '\t': return { action: 'toggleAll' };
19
+ case '\x7f':
20
+ case '\b': return { action: 'backspace' };
21
+ case '\x03': return { action: 'cancel' }; // Ctrl-C
22
+ case '\x1b': return { action: 'clearOrCancel' }; // Esc
23
+ }
24
+ if (data.length === 1 && /[a-zA-Z0-9-]/.test(data))
25
+ return { action: 'char', char: data };
26
+ return { action: 'none' };
27
+ }
28
+ function realValues(state) {
29
+ return state.items.filter((i) => i.value !== exports.ALL_SENTINEL).map((i) => i.value);
30
+ }
31
+ function pickerReducer(state, key) {
32
+ const vis = (0, picker_view_1.visibleItems)(state);
33
+ switch (key.type) {
34
+ case 'up': {
35
+ if (vis.length === 0)
36
+ return state;
37
+ return { ...state, cursor: (state.cursor - 1 + vis.length) % vis.length };
38
+ }
39
+ case 'down': {
40
+ if (vis.length === 0)
41
+ return state;
42
+ return { ...state, cursor: (state.cursor + 1) % vis.length };
43
+ }
44
+ case 'toggle': {
45
+ if (vis.length === 0)
46
+ return state;
47
+ const item = vis[Math.max(0, Math.min(state.cursor, vis.length - 1))];
48
+ const selected = new Set(state.selected);
49
+ if (item.value === exports.ALL_SENTINEL) {
50
+ const reals = realValues(state);
51
+ const allSel = reals.every((v) => selected.has(v));
52
+ if (allSel) {
53
+ reals.forEach((v) => selected.delete(v));
54
+ selected.delete(exports.ALL_SENTINEL);
55
+ }
56
+ else {
57
+ reals.forEach((v) => selected.add(v));
58
+ selected.add(exports.ALL_SENTINEL);
59
+ }
60
+ }
61
+ else {
62
+ if (selected.has(item.value)) {
63
+ selected.delete(item.value);
64
+ selected.delete(exports.ALL_SENTINEL); // no longer all-selected
65
+ }
66
+ else {
67
+ selected.add(item.value);
68
+ // if every real item is now selected, also mark the sentinel
69
+ const reals = realValues(state);
70
+ if (reals.every((v) => selected.has(v)))
71
+ selected.add(exports.ALL_SENTINEL);
72
+ }
73
+ }
74
+ return { ...state, selected };
75
+ }
76
+ case 'toggleAll': {
77
+ const reals = vis.filter((i) => i.value !== exports.ALL_SENTINEL).map((i) => i.value);
78
+ const selected = new Set(state.selected);
79
+ const allSel = reals.every((v) => selected.has(v));
80
+ if (allSel) {
81
+ reals.forEach((v) => selected.delete(v));
82
+ selected.delete(exports.ALL_SENTINEL);
83
+ }
84
+ else {
85
+ reals.forEach((v) => selected.add(v));
86
+ selected.add(exports.ALL_SENTINEL);
87
+ }
88
+ return { ...state, selected };
89
+ }
90
+ case 'filterChar':
91
+ return { ...state, filter: state.filter + key.char, cursor: 0 };
92
+ case 'backspace':
93
+ return { ...state, filter: state.filter.slice(0, -1), cursor: 0 };
94
+ }
95
+ }
96
+ function defaultIO() {
97
+ return { input: process.stdin, output: process.stdout };
98
+ }
99
+ /**
100
+ * Interactive multiselect. Returns the selected values, or null if cancelled.
101
+ * Streams are injectable for testing. Always restores terminal state on exit.
102
+ */
103
+ function multiselectPicker(opts, io = defaultIO()) {
104
+ return new Promise((resolve) => {
105
+ let state = {
106
+ title: opts.title,
107
+ items: opts.items,
108
+ selected: new Set(opts.initialSelected ?? []),
109
+ cursor: 0,
110
+ filter: '',
111
+ };
112
+ let lastLineCount = 0;
113
+ const size = () => ({
114
+ columns: io.output.columns ?? 80,
115
+ rows: io.output.rows ?? 24,
116
+ });
117
+ const draw = () => {
118
+ const lines = (0, picker_view_1.renderPicker)(state, size());
119
+ // Move up over the previous frame and clear downward.
120
+ if (lastLineCount > 0)
121
+ io.output.write(`\x1b[${lastLineCount}A`);
122
+ io.output.write('\x1b[0J');
123
+ io.output.write(lines.join('\n') + '\n');
124
+ lastLineCount = lines.length;
125
+ };
126
+ let onSigint;
127
+ const cleanup = () => {
128
+ io.input.removeListener('data', onData);
129
+ if (onSigint)
130
+ process.removeListener('SIGINT', onSigint);
131
+ try {
132
+ io.input.setRawMode?.(false);
133
+ }
134
+ catch { /* best-effort: terminal may not support raw mode */ }
135
+ io.input.pause?.();
136
+ io.output.write('\x1b[?25h'); // show cursor
137
+ };
138
+ const onData = (chunk) => {
139
+ const key = parseKey(chunk.toString());
140
+ switch (key.action) {
141
+ case 'confirm':
142
+ cleanup();
143
+ resolve(Array.from(state.selected));
144
+ return;
145
+ case 'cancel':
146
+ cleanup();
147
+ resolve(null);
148
+ return;
149
+ case 'clearOrCancel':
150
+ if (state.filter) {
151
+ state = { ...state, filter: '', cursor: 0 };
152
+ break;
153
+ }
154
+ cleanup();
155
+ resolve(null);
156
+ return;
157
+ case 'up':
158
+ state = pickerReducer(state, { type: 'up' });
159
+ break;
160
+ case 'down':
161
+ state = pickerReducer(state, { type: 'down' });
162
+ break;
163
+ case 'toggle':
164
+ state = pickerReducer(state, { type: 'toggle' });
165
+ break;
166
+ case 'toggleAll':
167
+ state = pickerReducer(state, { type: 'toggleAll' });
168
+ break;
169
+ case 'backspace':
170
+ state = pickerReducer(state, { type: 'backspace' });
171
+ break;
172
+ case 'char':
173
+ state = pickerReducer(state, { type: 'filterChar', char: key.char });
174
+ break;
175
+ case 'none': return; // ignore, no redraw
176
+ }
177
+ draw();
178
+ };
179
+ onSigint = () => {
180
+ cleanup();
181
+ resolve(null);
182
+ };
183
+ process.once('SIGINT', onSigint);
184
+ try {
185
+ io.input.setRawMode?.(true);
186
+ }
187
+ catch { /* best-effort: degrade if raw mode unsupported */ }
188
+ io.input.resume?.();
189
+ io.input.setEncoding?.('utf8');
190
+ io.output.write('\x1b[?25l'); // hide cursor
191
+ io.input.on('data', onData);
192
+ draw();
193
+ });
194
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stripAnsi = stripAnsi;
4
+ exports.displayWidth = displayWidth;
5
+ exports.truncate = truncate;
6
+ exports.wrap = wrap;
7
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
8
+ function stripAnsi(s) {
9
+ return s.replace(ANSI_RE, '');
10
+ }
11
+ function isWide(cp) {
12
+ return ((cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
13
+ (cp >= 0x2300 && cp <= 0x23ff) || // misc technical (⏎ etc.)
14
+ (cp >= 0x2600 && cp <= 0x27bf) || // misc symbols / dingbats
15
+ (cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified Ideographs
16
+ (cp >= 0xac00 && cp <= 0xd7a3) || // Hangul Syllables
17
+ (cp >= 0xff00 && cp <= 0xffef) || // Fullwidth Latin/symbols
18
+ (cp >= 0x1f000 && cp <= 0x1faff) // emoji
19
+ );
20
+ }
21
+ /** Visible terminal cells, ignoring ANSI color and treating known wide glyphs as 2. */
22
+ function displayWidth(s) {
23
+ const plain = stripAnsi(s);
24
+ let w = 0;
25
+ for (const ch of plain) {
26
+ const cp = ch.codePointAt(0);
27
+ if (cp === 0xfe0f)
28
+ continue; // variation selector — zero width
29
+ w += isWide(cp) ? 2 : 1;
30
+ }
31
+ return w;
32
+ }
33
+ /** Truncate plain text to `width` cells, appending '…'. Strips ANSI — color AFTER truncating. */
34
+ function truncate(s, width) {
35
+ if (width <= 0)
36
+ return '';
37
+ const plain = stripAnsi(s);
38
+ if (displayWidth(plain) <= width)
39
+ return plain;
40
+ if (width === 1)
41
+ return '…';
42
+ let out = '';
43
+ let w = 0;
44
+ for (const ch of plain) {
45
+ const cw = displayWidth(ch);
46
+ if (w + cw > width - 1)
47
+ break;
48
+ out += ch;
49
+ w += cw;
50
+ }
51
+ return out + '…';
52
+ }
53
+ /** Word-wrap plain text to `width` cells. Long single words are not split (acceptable). */
54
+ function wrap(s, width) {
55
+ if (width <= 0)
56
+ return [s];
57
+ const words = stripAnsi(s).split(/\s+/).filter(Boolean);
58
+ const lines = [];
59
+ let cur = '';
60
+ for (const word of words) {
61
+ if (cur === '') {
62
+ cur = word;
63
+ continue;
64
+ }
65
+ if (displayWidth(cur) + 1 + displayWidth(word) <= width)
66
+ cur += ' ' + word;
67
+ else {
68
+ lines.push(cur);
69
+ cur = word;
70
+ }
71
+ }
72
+ if (cur)
73
+ lines.push(cur);
74
+ return lines.length ? lines : [''];
75
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isInteractive = isInteractive;
4
+ exports.terminalSize = terminalSize;
5
+ /** True only when both ends are TTYs — the precondition for the interactive picker. */
6
+ function isInteractive() {
7
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY);
8
+ }
9
+ /** Current terminal size, with a safe 80x24 fallback when undefined (non-TTY). */
10
+ function terminalSize() {
11
+ return {
12
+ columns: process.stdout.columns ?? 80,
13
+ rows: process.stdout.rows ?? 24,
14
+ };
15
+ }
@@ -10,11 +10,12 @@ exports.packageSummaryLines = packageSummaryLines;
10
10
  exports.packageDetailLines = packageDetailLines;
11
11
  exports.findPackage = findPackage;
12
12
  exports.artifactValue = artifactValue;
13
- exports.buildLevel1Options = buildLevel1Options;
14
- exports.buildLevel2Options = buildLevel2Options;
13
+ exports.packagePickerItems = packagePickerItems;
14
+ exports.artifactPickerItems = artifactPickerItems;
15
15
  exports.resolveLevel2Selection = resolveLevel2Selection;
16
16
  const picocolors_1 = __importDefault(require("picocolors"));
17
17
  const registries_1 = require("../core/registries");
18
+ const text_1 = require("../ui/text");
18
19
  exports.STANDALONE_NAME = 'standalone';
19
20
  function makePackage(name, description, isStandalone, visibility, artifacts) {
20
21
  return {
@@ -73,7 +74,7 @@ function artifactCountLabel(counts) {
73
74
  function packageIcon(pkg) {
74
75
  return pkg.isStandalone ? '🔹' : '📦';
75
76
  }
76
- function packageSummaryLines(packages) {
77
+ function packageSummaryLines(packages, width) {
77
78
  const uniqueSkillNames = new Set(packages.flatMap((p) => p.artifacts.filter((a) => a.type === 'skill').map((a) => a.name)));
78
79
  const totalSkills = uniqueSkillNames.size;
79
80
  const lines = [`AWM Registry — ${plural(packages.length, 'package')}, ${plural(totalSkills, 'skill')}`, ''];
@@ -83,12 +84,17 @@ function packageSummaryLines(packages) {
83
84
  packages.forEach((p, i) => {
84
85
  const name = p.name.padEnd(nameWidth);
85
86
  const count = countLabels[i].padEnd(countWidth);
86
- const desc = p.isStandalone ? '' : p.description;
87
+ let desc = p.isStandalone ? '' : p.description;
88
+ if (width && desc) {
89
+ const used = 3 + nameWidth + 3 + countWidth + 3; // icon(2)+space(1), name, gap, count, gap
90
+ const avail = width - used;
91
+ desc = avail > 1 ? (0, text_1.truncate)(desc, avail) : '';
92
+ }
87
93
  lines.push(`${packageIcon(p)} ${name} ${count} ${desc}`.trimEnd());
88
94
  });
89
95
  return lines;
90
96
  }
91
- function packageDetailLines(pkg) {
97
+ function packageDetailLines(pkg, width) {
92
98
  const lines = [];
93
99
  const header = pkg.isStandalone
94
100
  ? `${packageIcon(pkg)} ${pkg.name} — ${plural(pkg.artifacts.length, 'artifact')}`
@@ -99,8 +105,10 @@ function packageDetailLines(pkg) {
99
105
  ? picocolors_1.default.yellow(` ← ${(0, registries_1.registryNameForPath)(a.sourcePath) ?? 'unknown'} (override)`)
100
106
  : '';
101
107
  lines.push(` ${TYPE_ICON[a.type]}${a.name}${mark}`);
102
- if (a.description)
103
- lines.push(` ${a.description}`);
108
+ if (a.description) {
109
+ const desc = width ? (0, text_1.truncate)(a.description, width - 5) : a.description;
110
+ lines.push(` ${desc}`);
111
+ }
104
112
  });
105
113
  return lines;
106
114
  }
@@ -130,21 +138,27 @@ exports.ALL_SENTINEL = '__ALL__';
130
138
  function artifactValue(a) {
131
139
  return `${a.type}:${a.name}`;
132
140
  }
133
- function buildLevel1Options(packages) {
134
- return packages.map((p) => {
135
- const count = p.isStandalone ? plural(p.artifacts.length, 'artifact') : artifactCountLabel(p.counts);
136
- const desc = p.isStandalone ? '' : ` · ${p.description}`;
137
- return { value: p.name, label: `${packageIcon(p)} ${p.name} ${picocolors_1.default.dim(`${count}${desc}`)}` };
138
- });
141
+ function packagePickerItems(packages) {
142
+ return packages.map((p) => ({
143
+ value: p.name,
144
+ label: `${packageIcon(p)} ${p.name}`,
145
+ description: p.isStandalone
146
+ ? plural(p.artifacts.length, 'artifact')
147
+ : `${artifactCountLabel(p.counts)} · ${p.description}`,
148
+ }));
139
149
  }
140
- function buildLevel2Options(pkg) {
141
- const installAll = { value: exports.ALL_SENTINEL, label: `✨ Install entire package (${pkg.artifacts.length})` };
142
- const rest = pkg.artifacts.map((a) => {
143
- const title = `${TYPE_ICON[a.type]}${a.name}`;
144
- const label = a.description ? `${title}\n ${picocolors_1.default.dim(a.description)}` : title;
145
- return { value: artifactValue(a), label };
146
- });
147
- return [installAll, ...rest];
150
+ function artifactPickerItems(pkg) {
151
+ const all = {
152
+ value: exports.ALL_SENTINEL,
153
+ label: `✨ Install entire package (${pkg.artifacts.length})`,
154
+ description: 'Select every artifact in this package.',
155
+ };
156
+ const rest = pkg.artifacts.map((a) => ({
157
+ value: artifactValue(a),
158
+ label: `${TYPE_ICON[a.type]}${a.name}`,
159
+ description: a.description || '',
160
+ }));
161
+ return [all, ...rest];
148
162
  }
149
163
  function resolveLevel2Selection(pkg, selectedValues) {
150
164
  if (selectedValues.includes(exports.ALL_SENTINEL))
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const picker_1 = require("../../src/ui/picker");
4
+ const items = [
5
+ { value: picker_1.ALL_SENTINEL, label: '✨ Install entire package (2)', description: '' },
6
+ { value: 'skill:a', label: 'alpha', description: '' },
7
+ { value: 'skill:b', label: 'beta', description: '' },
8
+ ];
9
+ const base = (over = {}) => ({
10
+ title: 't', items, selected: new Set(), cursor: 0, filter: '', ...over,
11
+ });
12
+ describe('parseKey', () => {
13
+ it('maps arrow and control sequences', () => {
14
+ expect((0, picker_1.parseKey)('\x1b[A')).toEqual({ action: 'up' });
15
+ expect((0, picker_1.parseKey)('\x1b[B')).toEqual({ action: 'down' });
16
+ expect((0, picker_1.parseKey)('\r')).toEqual({ action: 'confirm' });
17
+ expect((0, picker_1.parseKey)(' ')).toEqual({ action: 'toggle' });
18
+ expect((0, picker_1.parseKey)('\t')).toEqual({ action: 'toggleAll' });
19
+ expect((0, picker_1.parseKey)('\x03')).toEqual({ action: 'cancel' });
20
+ expect((0, picker_1.parseKey)('\x1b')).toEqual({ action: 'clearOrCancel' });
21
+ expect((0, picker_1.parseKey)('\x7f')).toEqual({ action: 'backspace' });
22
+ });
23
+ it('maps filter characters and ignores the rest', () => {
24
+ expect((0, picker_1.parseKey)('c')).toEqual({ action: 'char', char: 'c' });
25
+ expect((0, picker_1.parseKey)('-')).toEqual({ action: 'char', char: '-' });
26
+ expect((0, picker_1.parseKey)('?')).toEqual({ action: 'none' });
27
+ });
28
+ });
29
+ describe('pickerReducer', () => {
30
+ it('moves the cursor and wraps around', () => {
31
+ expect((0, picker_1.pickerReducer)(base({ cursor: 0 }), { type: 'up' }).cursor).toBe(2);
32
+ expect((0, picker_1.pickerReducer)(base({ cursor: 2 }), { type: 'down' }).cursor).toBe(0);
33
+ });
34
+ it('toggles the current item', () => {
35
+ const s = (0, picker_1.pickerReducer)(base({ cursor: 1 }), { type: 'toggle' });
36
+ expect(s.selected.has('skill:a')).toBe(true);
37
+ const s2 = (0, picker_1.pickerReducer)(s, { type: 'toggle' });
38
+ expect(s2.selected.has('skill:a')).toBe(false);
39
+ });
40
+ it('toggling the ALL sentinel selects every real item plus the sentinel', () => {
41
+ const s = (0, picker_1.pickerReducer)(base({ cursor: 0 }), { type: 'toggle' });
42
+ expect(s.selected.has('skill:a')).toBe(true);
43
+ expect(s.selected.has('skill:b')).toBe(true);
44
+ expect(s.selected.has(picker_1.ALL_SENTINEL)).toBe(true);
45
+ });
46
+ it('Tab toggles all real items without needing the sentinel row', () => {
47
+ const s = (0, picker_1.pickerReducer)(base(), { type: 'toggleAll' });
48
+ expect(s.selected.has('skill:a')).toBe(true);
49
+ expect(s.selected.has('skill:b')).toBe(true);
50
+ expect(s.selected.has(picker_1.ALL_SENTINEL)).toBe(true);
51
+ });
52
+ it('toggleAll via Tab syncs ALL_SENTINEL when all become selected', () => {
53
+ const s = (0, picker_1.pickerReducer)(base(), { type: 'toggleAll' });
54
+ expect(s.selected.has('skill:a')).toBe(true);
55
+ expect(s.selected.has('skill:b')).toBe(true);
56
+ expect(s.selected.has(picker_1.ALL_SENTINEL)).toBe(true);
57
+ });
58
+ it('typing filters and resets the cursor to 0', () => {
59
+ const s = (0, picker_1.pickerReducer)(base({ cursor: 2 }), { type: 'filterChar', char: 'b' });
60
+ expect(s.filter).toBe('b');
61
+ expect(s.cursor).toBe(0);
62
+ });
63
+ it('backspace edits the filter', () => {
64
+ expect((0, picker_1.pickerReducer)(base({ filter: 'be' }), { type: 'backspace' }).filter).toBe('b');
65
+ });
66
+ it('does not move the cursor when the filtered list is empty', () => {
67
+ const s = base({ filter: 'zzz', cursor: 0 });
68
+ expect((0, picker_1.pickerReducer)(s, { type: 'down' }).cursor).toBe(0);
69
+ });
70
+ it('toggling an individual item off removes ALL_SENTINEL from selected', () => {
71
+ // Start with all items pre-selected (the initialSelected scenario)
72
+ const all = new Set([picker_1.ALL_SENTINEL, 'skill:a', 'skill:b']);
73
+ const s = (0, picker_1.pickerReducer)(base({ cursor: 1, selected: all }), { type: 'toggle' });
74
+ // Unchecked skill:a — sentinel should be gone
75
+ expect(s.selected.has('skill:a')).toBe(false);
76
+ expect(s.selected.has(picker_1.ALL_SENTINEL)).toBe(false);
77
+ expect(s.selected.has('skill:b')).toBe(true);
78
+ });
79
+ it('toggling the last missing item on syncs the ALL_SENTINEL', () => {
80
+ // skill:a is missing, skill:b is selected — select skill:a and sentinel should appear
81
+ const partial = new Set(['skill:b']);
82
+ const s = (0, picker_1.pickerReducer)(base({ cursor: 1, selected: partial }), { type: 'toggle' });
83
+ expect(s.selected.has('skill:a')).toBe(true);
84
+ expect(s.selected.has(picker_1.ALL_SENTINEL)).toBe(true);
85
+ });
86
+ it('toggleAll with active filter only toggles visible items', () => {
87
+ // filter to 'beta' — only skill:b is visible (ALL_SENTINEL label '✨ Install entire package (2)' does not match)
88
+ const s = (0, picker_1.pickerReducer)(base({ filter: 'beta' }), { type: 'toggleAll' });
89
+ expect(s.selected.has('skill:b')).toBe(true);
90
+ expect(s.selected.has('skill:a')).toBe(false); // not visible, should NOT be selected
91
+ });
92
+ });
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const events_1 = require("events");
4
+ const picker_1 = require("../../src/ui/picker");
5
+ function fakeIO() {
6
+ const input = new events_1.EventEmitter();
7
+ input.setRawMode = jest.fn();
8
+ input.resume = jest.fn();
9
+ input.pause = jest.fn();
10
+ input.setEncoding = jest.fn();
11
+ const writes = [];
12
+ const output = { write: (s) => { writes.push(s); return true; }, columns: 100, rows: 20 };
13
+ return { io: { input, output }, writes, send: (s) => input.emit('data', s) };
14
+ }
15
+ const items = [
16
+ { value: picker_1.ALL_SENTINEL, label: '✨ all', description: '' },
17
+ { value: 'skill:a', label: 'alpha', description: 'A.' },
18
+ { value: 'skill:b', label: 'beta', description: 'B.' },
19
+ ];
20
+ it('resolves with the toggled selection on Enter', async () => {
21
+ const { io, send } = fakeIO();
22
+ const p = (0, picker_1.multiselectPicker)({ title: 't', items }, io);
23
+ send('\x1b[B'); // down → cursor on alpha
24
+ send(' '); // toggle alpha
25
+ send('\r'); // confirm
26
+ await expect(p).resolves.toEqual(['skill:a']);
27
+ expect(io.input.setRawMode).toHaveBeenLastCalledWith(false); // restored
28
+ });
29
+ it('resolves null on Ctrl-C and restores raw mode', async () => {
30
+ const { io, send } = fakeIO();
31
+ const p = (0, picker_1.multiselectPicker)({ title: 't', items }, io);
32
+ send('\x03');
33
+ await expect(p).resolves.toBeNull();
34
+ expect(io.input.setRawMode).toHaveBeenLastCalledWith(false);
35
+ });
36
+ it('seeds the initial selection', async () => {
37
+ const { io, send } = fakeIO();
38
+ const p = (0, picker_1.multiselectPicker)({ title: 't', items, initialSelected: [picker_1.ALL_SENTINEL, 'skill:a', 'skill:b'] }, io);
39
+ send('\r');
40
+ const result = await p;
41
+ expect(new Set(result)).toEqual(new Set([picker_1.ALL_SENTINEL, 'skill:a', 'skill:b']));
42
+ });
43
+ it('Esc with empty filter cancels and returns null', async () => {
44
+ const { io, send } = fakeIO();
45
+ const p = (0, picker_1.multiselectPicker)({ title: 't', items }, io);
46
+ send('\x1b'); // Esc with no active filter → cancel
47
+ await expect(p).resolves.toBeNull();
48
+ expect(io.input.setRawMode).toHaveBeenLastCalledWith(false);
49
+ });
50
+ it('Esc with active filter clears it instead of cancelling', async () => {
51
+ const { io, send } = fakeIO();
52
+ const p = (0, picker_1.multiselectPicker)({ title: 't', items }, io);
53
+ send('a'); // type 'a' → filter becomes 'a'
54
+ send('\x1b'); // Esc → should clear filter, NOT cancel
55
+ send('\r'); // Enter → confirm (with no selection)
56
+ await expect(p).resolves.toEqual([]);
57
+ // setRawMode(false) called on confirm, not on Esc
58
+ expect(io.input.setRawMode).toHaveBeenLastCalledWith(false);
59
+ });
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const picker_view_1 = require("../../src/ui/picker-view");
4
+ const items = [
5
+ { value: 'skill:architecture-advisor', label: 'architecture-advisor', description: 'Define and review system architecture.' },
6
+ { value: 'skill:brainstorming', label: 'brainstorming', description: 'Turn ideas into designs.' },
7
+ { value: 'skill:cicd-proposal-builder', label: 'cicd-proposal-builder', description: 'Specialist in CI/CD pipeline design. Use when defining a pipeline.' },
8
+ ];
9
+ const base = (over = {}) => ({
10
+ title: 'dev — select artifacts',
11
+ items,
12
+ selected: new Set(),
13
+ cursor: 0,
14
+ filter: '',
15
+ ...over,
16
+ });
17
+ describe('visibleItems', () => {
18
+ it('returns all items with an empty filter', () => {
19
+ expect((0, picker_view_1.visibleItems)(base())).toHaveLength(3);
20
+ });
21
+ it('filters by label substring, case-insensitive', () => {
22
+ expect((0, picker_view_1.visibleItems)(base({ filter: 'CIC' })).map((i) => i.value)).toEqual(['skill:cicd-proposal-builder']);
23
+ });
24
+ });
25
+ describe('renderPicker', () => {
26
+ it('shows the title in the header', () => {
27
+ const lines = (0, picker_view_1.renderPicker)(base(), { columns: 100, rows: 20 });
28
+ expect(lines[0]).toContain('dev — select artifacts');
29
+ });
30
+ it('marks the cursor row and selected rows', () => {
31
+ const lines = (0, picker_view_1.renderPicker)(base({ cursor: 1, selected: new Set(['skill:brainstorming']) }), { columns: 100, rows: 20 });
32
+ const body = lines.join('\n');
33
+ expect(body).toContain('❯'); // cursor marker
34
+ expect(body).toContain('◼'); // a selected checkbox
35
+ expect(body).toContain('◻'); // an unselected checkbox
36
+ });
37
+ it('uses two panes (a gutter) when wide', () => {
38
+ const lines = (0, picker_view_1.renderPicker)(base(), { columns: picker_view_1.TWO_PANE_MIN_COLUMNS + 10, rows: 20 });
39
+ expect(lines.some((l) => l.includes(' │ '))).toBe(true);
40
+ });
41
+ it('collapses to one pane (no gutter) when narrow', () => {
42
+ const lines = (0, picker_view_1.renderPicker)(base(), { columns: 50, rows: 20 });
43
+ expect(lines.some((l) => l.includes(' │ '))).toBe(false);
44
+ // the highlighted item's description still appears below the list
45
+ expect(lines.join('\n')).toContain('Define and review');
46
+ });
47
+ it('shows the filter text in the header when filtering', () => {
48
+ expect((0, picker_view_1.renderPicker)(base({ filter: 'cic' }), { columns: 100, rows: 20 })[0]).toContain('filter: cic');
49
+ });
50
+ it('reports no matches when the filter excludes everything', () => {
51
+ expect((0, picker_view_1.renderPicker)(base({ filter: 'zzz' }), { columns: 100, rows: 20 }).join('\n').toLowerCase()).toContain('no matches');
52
+ });
53
+ it('always ends with the key hint footer', () => {
54
+ const lines = (0, picker_view_1.renderPicker)(base(), { columns: 100, rows: 20 });
55
+ expect(lines[lines.length - 1]).toContain('confirm');
56
+ });
57
+ });
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const text_1 = require("../../src/ui/text");
7
+ const picocolors_1 = __importDefault(require("picocolors"));
8
+ describe('stripAnsi', () => {
9
+ it('removes color escape sequences', () => {
10
+ expect((0, text_1.stripAnsi)(picocolors_1.default.green('hi'))).toBe('hi');
11
+ });
12
+ });
13
+ describe('displayWidth', () => {
14
+ it('counts plain ASCII one cell each', () => {
15
+ expect((0, text_1.displayWidth)('hello')).toBe(5);
16
+ });
17
+ it('ignores color codes', () => {
18
+ expect((0, text_1.displayWidth)(picocolors_1.default.red('abc'))).toBe(3);
19
+ });
20
+ it('counts an emoji as two cells and ignores the VS16 selector', () => {
21
+ expect((0, text_1.displayWidth)('📦')).toBe(2);
22
+ expect((0, text_1.displayWidth)('✍️')).toBe(2);
23
+ });
24
+ it('counts CJK ideographs as 2 cells', () => {
25
+ expect((0, text_1.displayWidth)('你好')).toBe(4);
26
+ });
27
+ it('counts Hangul syllables as 2 cells', () => {
28
+ expect((0, text_1.displayWidth)('안녕')).toBe(4);
29
+ });
30
+ it('counts fullwidth Latin as 2 cells', () => {
31
+ // U+FF21 = A (fullwidth A)
32
+ expect((0, text_1.displayWidth)('AB')).toBe(4);
33
+ });
34
+ });
35
+ describe('truncate', () => {
36
+ it('returns the string unchanged when it fits', () => {
37
+ expect((0, text_1.truncate)('abc', 5)).toBe('abc');
38
+ });
39
+ it('truncates and appends an ellipsis, never exceeding width', () => {
40
+ const out = (0, text_1.truncate)('abcdefgh', 5);
41
+ expect(out).toBe('abcd…');
42
+ expect((0, text_1.displayWidth)(out)).toBeLessThanOrEqual(5);
43
+ });
44
+ it('returns empty for non-positive width', () => {
45
+ expect((0, text_1.truncate)('abc', 0)).toBe('');
46
+ });
47
+ });
48
+ describe('wrap', () => {
49
+ it('breaks text at word boundaries within width', () => {
50
+ expect((0, text_1.wrap)('the quick brown fox', 9)).toEqual(['the quick', 'brown fox']);
51
+ });
52
+ it('returns a single empty line for empty input', () => {
53
+ expect((0, text_1.wrap)('', 10)).toEqual(['']);
54
+ });
55
+ });
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tty_1 = require("../../src/ui/tty");
4
+ describe('isInteractive', () => {
5
+ const outTTY = process.stdout.isTTY;
6
+ const inTTY = process.stdin.isTTY;
7
+ afterEach(() => {
8
+ Object.defineProperty(process.stdout, 'isTTY', { value: outTTY, configurable: true });
9
+ Object.defineProperty(process.stdin, 'isTTY', { value: inTTY, configurable: true });
10
+ });
11
+ it('is true only when both stdin and stdout are TTYs', () => {
12
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
13
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
14
+ expect((0, tty_1.isInteractive)()).toBe(true);
15
+ });
16
+ it('is false when stdout is not a TTY (piped output)', () => {
17
+ Object.defineProperty(process.stdout, 'isTTY', { value: false, configurable: true });
18
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
19
+ expect((0, tty_1.isInteractive)()).toBe(false);
20
+ });
21
+ });
22
+ describe('terminalSize', () => {
23
+ const cols = process.stdout.columns;
24
+ const rows = process.stdout.rows;
25
+ afterEach(() => {
26
+ Object.defineProperty(process.stdout, 'columns', { value: cols, configurable: true });
27
+ Object.defineProperty(process.stdout, 'rows', { value: rows, configurable: true });
28
+ });
29
+ it('reports the stdout dimensions', () => {
30
+ Object.defineProperty(process.stdout, 'columns', { value: 120, configurable: true });
31
+ Object.defineProperty(process.stdout, 'rows', { value: 40, configurable: true });
32
+ expect((0, tty_1.terminalSize)()).toEqual({ columns: 120, rows: 40 });
33
+ });
34
+ it('falls back to 80x24 when dimensions are undefined', () => {
35
+ Object.defineProperty(process.stdout, 'columns', { value: undefined, configurable: true });
36
+ Object.defineProperty(process.stdout, 'rows', { value: undefined, configurable: true });
37
+ expect((0, tty_1.terminalSize)()).toEqual({ columns: 80, rows: 24 });
38
+ });
39
+ });
@@ -89,42 +89,6 @@ describe('findPackage', () => {
89
89
  expect(res.suggestion).toBe('core-dev');
90
90
  });
91
91
  });
92
- describe('buildLevel1Options', () => {
93
- it('produces one option per package keyed by package name', () => {
94
- const view = (0, registry_view_1.buildPackageView)([skill('brainstorming'), skill('shared')], [], [], processes);
95
- const opts = (0, registry_view_1.buildLevel1Options)(view);
96
- expect(opts.map((o) => o.value).sort()).toEqual(['core-dev', 'docs']);
97
- expect(opts.find((o) => o.value === 'core-dev').label).toContain('core-dev');
98
- expect(opts.find((o) => o.value === 'core-dev').label).toContain('Dev lifecycle');
99
- });
100
- it('uses artifact-count label for standalone packages', () => {
101
- const view = (0, registry_view_1.buildPackageView)([skill('orphan')], [], [], []);
102
- const opts = (0, registry_view_1.buildLevel1Options)(view);
103
- expect(opts[0].value).toBe(registry_view_1.STANDALONE_NAME);
104
- expect(opts[0].label).toContain('1 artifact');
105
- });
106
- });
107
- describe('buildLevel2Options', () => {
108
- it('puts an "install entire package" sentinel first, then one option per artifact', () => {
109
- const view = (0, registry_view_1.buildPackageView)([skill('brainstorming', 'explore')], [wf('exec')], [agent('plan')], processes);
110
- const core = view.find((p) => p.name === 'core-dev');
111
- const opts = (0, registry_view_1.buildLevel2Options)(core);
112
- expect(opts[0].value).toBe(registry_view_1.ALL_SENTINEL);
113
- expect(opts[0].label).toContain('Install entire package');
114
- const values = opts.slice(1).map((o) => o.value);
115
- expect(values).toContain('skill:brainstorming');
116
- expect(values).toContain('workflow:exec');
117
- expect(values).toContain('agent:plan');
118
- expect(opts.find((o) => o.value === 'skill:brainstorming').label).toContain('explore');
119
- });
120
- it('uses plain title for artifact with no description', () => {
121
- const view = (0, registry_view_1.buildPackageView)([skill('brainstorming', 'explore')], [wf('exec')], [agent('plan')], processes);
122
- const core = view.find((p) => p.name === 'core-dev');
123
- const opts = (0, registry_view_1.buildLevel2Options)(core);
124
- const wfOpt = opts.find((o) => o.value === 'workflow:exec');
125
- expect(wfOpt.label).toBe('⚡ exec');
126
- });
127
- });
128
92
  describe('resolveLevel2Selection', () => {
129
93
  const view = (0, registry_view_1.buildPackageView)([skill('brainstorming'), skill('shared')], [], [], processes);
130
94
  const core = view.find((p) => p.name === 'core-dev');
@@ -154,3 +118,42 @@ describe('visibility', () => {
154
118
  expect(view.find((p) => p.name === 'core-dev').visibility).toBe('public');
155
119
  });
156
120
  });
121
+ describe('packageSummaryLines width-awareness', () => {
122
+ it('truncates the description column when a width is given', () => {
123
+ const view = (0, registry_view_1.buildPackageView)([skill('brainstorming', 'x'.repeat(200))], [], [], [
124
+ bundle({ name: 'core-dev', description: 'x'.repeat(200), skills: [{ name: 'brainstorming', onSignal: false }] }),
125
+ ]);
126
+ const lines = (0, registry_view_1.packageSummaryLines)(view, 60);
127
+ for (const l of lines)
128
+ expect(l.length).toBeLessThanOrEqual(60);
129
+ expect(lines.some((l) => l.includes('…'))).toBe(true);
130
+ });
131
+ it('does not truncate when no width is given (piped output)', () => {
132
+ const view = (0, registry_view_1.buildPackageView)([skill('brainstorming', 'y'.repeat(200))], [], [], [
133
+ bundle({ name: 'core-dev', description: 'z'.repeat(200), skills: [{ name: 'brainstorming', onSignal: false }] }),
134
+ ]);
135
+ const lines = (0, registry_view_1.packageSummaryLines)(view); // no width
136
+ expect(lines.some((l) => l.includes('z'.repeat(200)))).toBe(true);
137
+ });
138
+ });
139
+ describe('artifactPickerItems', () => {
140
+ it('prepends an "install entire package" sentinel item, then one per artifact', () => {
141
+ const view = (0, registry_view_1.buildPackageView)([skill('a', 'desc a'), skill('b', 'desc b')], [], [], [
142
+ bundle({ name: 'p', description: 'pkg', skills: [{ name: 'a', onSignal: false }, { name: 'b', onSignal: false }] }),
143
+ ]);
144
+ const items = (0, registry_view_1.artifactPickerItems)(view.find((p) => p.name === 'p'));
145
+ expect(items[0].value).toBe(registry_view_1.ALL_SENTINEL);
146
+ expect(items.slice(1).map((i) => i.label)).toEqual(['a', 'b']);
147
+ expect(items.find((i) => i.label === 'a').description).toBe('desc a');
148
+ });
149
+ });
150
+ describe('packagePickerItems', () => {
151
+ it('builds one item per package with a count+description summary', () => {
152
+ const view = (0, registry_view_1.buildPackageView)([skill('a')], [], [], [
153
+ bundle({ name: 'p', description: 'pkg desc', skills: [{ name: 'a', onSignal: false }] }),
154
+ ]);
155
+ const items = (0, registry_view_1.packagePickerItems)(view);
156
+ expect(items[0].value).toBe('p');
157
+ expect(items[0].description).toContain('pkg desc');
158
+ });
159
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"