agentic-workflow-manager 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.js +130 -17
- package/dist/src/release/core.js +83 -0
- package/dist/src/release/index.js +94 -0
- package/dist/src/release/orchestrator.js +116 -0
- package/dist/src/ui/picker-view.js +77 -0
- package/dist/src/ui/picker.js +194 -0
- package/dist/src/ui/text.js +75 -0
- package/dist/src/ui/tty.js +15 -0
- package/dist/src/utils/registry-view.js +35 -21
- package/dist/tests/release/core.test.js +106 -0
- package/dist/tests/release/orchestrator.test.js +188 -0
- package/dist/tests/ui/picker-reducer.test.js +92 -0
- package/dist/tests/ui/picker-shell.test.js +59 -0
- package/dist/tests/ui/picker-view.test.js +57 -0
- package/dist/tests/ui/text.test.js +55 -0
- package/dist/tests/ui/tty.test.js +39 -0
- package/dist/tests/utils/registry-view.test.js +39 -36
- package/package.json +3 -1
- package/dist/tests/core/registry-versioned-sync.test.js +0 -113
- package/dist/tests/core/registry.test.js +0 -51
- package/dist/tests/registry/b3-ledger-wiring.test.js +0 -74
- package/dist/tests/registry/catalog-consistency.test.js +0 -47
- package/dist/tests/registry/design-skills.test.js +0 -146
- package/dist/tests/registry/prose-agnostic.test.js +0 -18
- package/dist/tests/registry/sensor-packs.test.js +0 -45
- package/dist/tests/registry/skill-versions.test.js +0 -26
- package/dist/tests/registry/using-awm.test.js +0 -39
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// cli/tests/release/orchestrator.test.ts
|
|
4
|
+
const orchestrator_1 = require("../../src/release/orchestrator");
|
|
5
|
+
const core_1 = require("../../src/release/core");
|
|
6
|
+
function makeIO(over = {}) {
|
|
7
|
+
const calls = [];
|
|
8
|
+
let pkgVersion = '2.1.1';
|
|
9
|
+
const io = {
|
|
10
|
+
run(cmd, args) {
|
|
11
|
+
const full = `${cmd} ${args.join(' ')}`;
|
|
12
|
+
calls.push(full);
|
|
13
|
+
if (cmd === 'git' && args[0] === 'rev-parse' && args.includes('--abbrev-ref'))
|
|
14
|
+
return 'main';
|
|
15
|
+
if (cmd === 'git' && args[0] === 'status')
|
|
16
|
+
return '';
|
|
17
|
+
if (cmd === 'git' && args[0] === 'tag' && args[1] === '--list' && args[2] === 'v*')
|
|
18
|
+
return over.tags ?? '';
|
|
19
|
+
if (cmd === 'git' && args[0] === 'tag' && args[1] === '--list')
|
|
20
|
+
return ''; // gate: tag puntual no existe
|
|
21
|
+
if (cmd === 'git' && args[0] === 'log')
|
|
22
|
+
return over.commits ?? `feat: nueva${core_1.US}${core_1.RS}`;
|
|
23
|
+
if (cmd === 'npm' && args[0] === 'view')
|
|
24
|
+
return over.npmView ?? '';
|
|
25
|
+
return '';
|
|
26
|
+
},
|
|
27
|
+
readPackageVersion: () => pkgVersion,
|
|
28
|
+
writePackageVersion: (v) => { pkgVersion = v; calls.push(`WRITE_PKG ${v}`); },
|
|
29
|
+
readChangelog: () => '',
|
|
30
|
+
writeChangelog: (c) => calls.push(`WRITE_CHANGELOG ${c.split('\n')[0]}`),
|
|
31
|
+
writeNpmrc: () => calls.push('WRITE_NPMRC'),
|
|
32
|
+
removeNpmrc: () => calls.push('REMOVE_NPMRC'),
|
|
33
|
+
today: () => '2026-06-25',
|
|
34
|
+
log: () => { },
|
|
35
|
+
env: { NPM_TOKEN: 'tok' },
|
|
36
|
+
...over,
|
|
37
|
+
};
|
|
38
|
+
return { io, calls };
|
|
39
|
+
}
|
|
40
|
+
const opts = (o = {}) => ({ dryRun: false, force: null, push: true, branch: 'main', cliDir: '/cli', ...o });
|
|
41
|
+
describe('release — happy path', () => {
|
|
42
|
+
it('feat → minor: escribe versión, commitea, taggea, publica y pushea en orden', () => {
|
|
43
|
+
const { io, calls } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
44
|
+
const res = (0, orchestrator_1.release)(opts(), io);
|
|
45
|
+
expect(res).toEqual({ released: true, version: '2.2.0' });
|
|
46
|
+
expect(calls).toContain('WRITE_PKG 2.2.0');
|
|
47
|
+
const joined = calls.join('\n');
|
|
48
|
+
expect(joined).toMatch(/git commit .*chore\(release\): v2.2.0/);
|
|
49
|
+
expect(joined).toMatch(/git tag -a v2.2.0/);
|
|
50
|
+
expect(joined).toMatch(/npm publish/);
|
|
51
|
+
expect(joined).toMatch(/git push origin main/);
|
|
52
|
+
expect(joined).toMatch(/git push origin v2.2.0/);
|
|
53
|
+
// npmrc creado y SIEMPRE removido
|
|
54
|
+
expect(calls).toContain('WRITE_NPMRC');
|
|
55
|
+
expect(calls).toContain('REMOVE_NPMRC');
|
|
56
|
+
});
|
|
57
|
+
it('sin commits releasables → no publica (exit 0 lógico)', () => {
|
|
58
|
+
const { io, calls } = makeIO({ commits: `docs: solo docs${core_1.US}${core_1.RS}` });
|
|
59
|
+
const res = (0, orchestrator_1.release)(opts(), io);
|
|
60
|
+
expect(res.released).toBe(false);
|
|
61
|
+
expect(res.reason).toMatch(/nada que publicar|no releasable/i);
|
|
62
|
+
expect(calls).not.toContain('WRITE_PKG 2.1.2');
|
|
63
|
+
expect(calls.join('\n')).not.toMatch(/npm publish/);
|
|
64
|
+
});
|
|
65
|
+
it('--force patch publica aunque no haya commits releasables', () => {
|
|
66
|
+
const { io } = makeIO({ commits: `docs: x${core_1.US}${core_1.RS}` });
|
|
67
|
+
expect((0, orchestrator_1.release)(opts({ force: 'patch' }), io)).toEqual({ released: true, version: '2.1.2' });
|
|
68
|
+
});
|
|
69
|
+
it('--dry-run calcula la versión pero NO escribe ni publica', () => {
|
|
70
|
+
const { io, calls } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
71
|
+
const res = (0, orchestrator_1.release)(opts({ dryRun: true }), io);
|
|
72
|
+
expect(res).toEqual({ released: false, version: '2.2.0', reason: 'dry-run' });
|
|
73
|
+
expect(calls.join('\n')).not.toMatch(/npm publish|WRITE_PKG|git commit/);
|
|
74
|
+
});
|
|
75
|
+
it('OIDC via ACTIONS_ID_TOKEN_REQUEST_URL: usa --provenance y omite .npmrc', () => {
|
|
76
|
+
const { io, calls } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
77
|
+
io.env = { ACTIONS_ID_TOKEN_REQUEST_URL: 'https://token.actions.githubusercontent.com/...' };
|
|
78
|
+
const res = (0, orchestrator_1.release)(opts(), io);
|
|
79
|
+
expect(res).toEqual({ released: true, version: '2.2.0' });
|
|
80
|
+
expect(calls.join('\n')).toMatch(/npm publish --provenance/);
|
|
81
|
+
expect(calls).not.toContain('WRITE_NPMRC');
|
|
82
|
+
expect(calls).not.toContain('REMOVE_NPMRC');
|
|
83
|
+
});
|
|
84
|
+
it('OIDC via NODE_AUTH_TOKEN sin NPM_TOKEN: usa --provenance y omite .npmrc', () => {
|
|
85
|
+
const { io, calls } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
86
|
+
io.env = { NODE_AUTH_TOKEN: 'oidc-tok' };
|
|
87
|
+
const res = (0, orchestrator_1.release)(opts(), io);
|
|
88
|
+
expect(res).toEqual({ released: true, version: '2.2.0' });
|
|
89
|
+
expect(calls.join('\n')).toMatch(/npm publish --provenance/);
|
|
90
|
+
expect(calls).not.toContain('WRITE_NPMRC');
|
|
91
|
+
});
|
|
92
|
+
it('--no-push omite los push', () => {
|
|
93
|
+
const { io, calls } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
94
|
+
(0, orchestrator_1.release)(opts({ push: false }), io);
|
|
95
|
+
expect(calls.join('\n')).not.toMatch(/git push/);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('release — gates de preflight', () => {
|
|
99
|
+
it('aborta si la rama no es la esperada', () => {
|
|
100
|
+
const fake = makeIO().io;
|
|
101
|
+
const origRun = fake.run;
|
|
102
|
+
fake.run = (cmd, args, o) => cmd === 'git' && args[0] === 'rev-parse' ? 'feature/x' : origRun(cmd, args, o);
|
|
103
|
+
expect(() => (0, orchestrator_1.release)(opts(), fake)).toThrow(/rama|branch/i);
|
|
104
|
+
});
|
|
105
|
+
it('--force relaja el gate de rama', () => {
|
|
106
|
+
const fake = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` }).io;
|
|
107
|
+
const origRun = fake.run;
|
|
108
|
+
fake.run = (cmd, args, o) => cmd === 'git' && args[0] === 'rev-parse' ? 'feature/x' : origRun(cmd, args, o);
|
|
109
|
+
expect(() => (0, orchestrator_1.release)(opts({ force: 'patch' }), fake)).not.toThrow();
|
|
110
|
+
});
|
|
111
|
+
it('aborta si el working tree está sucio', () => {
|
|
112
|
+
const fake = makeIO().io;
|
|
113
|
+
const origRun = fake.run;
|
|
114
|
+
fake.run = (cmd, args, o) => cmd === 'git' && args[0] === 'status' ? ' M cli/src/x.ts' : origRun(cmd, args, o);
|
|
115
|
+
expect(() => (0, orchestrator_1.release)(opts(), fake)).toThrow(/working tree|sin commitear|dirty/i);
|
|
116
|
+
});
|
|
117
|
+
it('aborta si no hay credencial de publicación', () => {
|
|
118
|
+
const fake = makeIO().io;
|
|
119
|
+
fake.env = {};
|
|
120
|
+
expect(() => (0, orchestrator_1.release)(opts(), fake)).toThrow(/NPM_TOKEN|NODE_AUTH_TOKEN|OIDC/i);
|
|
121
|
+
});
|
|
122
|
+
it('el gate de credencial corre ANTES del early-exit de "nada que publicar"', () => {
|
|
123
|
+
const fake = makeIO({ commits: `docs: x${core_1.US}${core_1.RS}` }).io; // sin commits releasables
|
|
124
|
+
fake.env = {};
|
|
125
|
+
expect(() => (0, orchestrator_1.release)(opts(), fake)).toThrow(/NPM_TOKEN|NODE_AUTH_TOKEN|OIDC/i); // no devuelve {released:false}
|
|
126
|
+
});
|
|
127
|
+
it('--dry-run no exige NPM_TOKEN ni working tree limpio', () => {
|
|
128
|
+
const fake = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` }).io;
|
|
129
|
+
fake.env = {};
|
|
130
|
+
const origRun = fake.run;
|
|
131
|
+
fake.run = (cmd, args, o) => cmd === 'git' && args[0] === 'status' ? ' M x' : origRun(cmd, args, o);
|
|
132
|
+
expect((0, orchestrator_1.release)(opts({ dryRun: true }), fake).version).toBe('2.2.0');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('release — rollback', () => {
|
|
136
|
+
it('si npm publish falla, hace rollback del commit y del tag', () => {
|
|
137
|
+
const { io, calls } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
138
|
+
const origRun = io.run;
|
|
139
|
+
io.run = (cmd, args, o) => {
|
|
140
|
+
if (cmd === 'npm' && args[0] === 'publish')
|
|
141
|
+
throw new Error('network error');
|
|
142
|
+
return origRun(cmd, args, o);
|
|
143
|
+
};
|
|
144
|
+
expect(() => (0, orchestrator_1.release)(opts(), io)).toThrow(/network error/i);
|
|
145
|
+
const joined = calls.join('\n');
|
|
146
|
+
expect(joined).toMatch(/git tag -d v2.2.0/);
|
|
147
|
+
expect(joined).toMatch(/git reset --hard HEAD~1/);
|
|
148
|
+
expect(calls).toContain('REMOVE_NPMRC'); // .npmrc siempre se limpia
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe('release — gates de idempotencia', () => {
|
|
152
|
+
it('aborta si el tag v2.2.0 ya existe en git', () => {
|
|
153
|
+
const { io } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
154
|
+
const origRun = io.run;
|
|
155
|
+
io.run = (cmd, args, o) => {
|
|
156
|
+
if (cmd === 'git' && args[0] === 'tag' && args[1] === '--list' && args[2] === 'v2.2.0')
|
|
157
|
+
return 'v2.2.0';
|
|
158
|
+
return origRun(cmd, args, o);
|
|
159
|
+
};
|
|
160
|
+
expect(() => (0, orchestrator_1.release)(opts(), io)).toThrow(/ya existe|v2\.2\.0/i);
|
|
161
|
+
});
|
|
162
|
+
it('aborta si agentic-workflow-manager@2.2.0 ya está publicado en npm', () => {
|
|
163
|
+
const { io } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
164
|
+
const origRun = io.run;
|
|
165
|
+
io.run = (cmd, args, o) => {
|
|
166
|
+
if (cmd === 'npm' && args[0] === 'view' && args[1] === 'agentic-workflow-manager@2.2.0')
|
|
167
|
+
return '2.2.0';
|
|
168
|
+
return origRun(cmd, args, o);
|
|
169
|
+
};
|
|
170
|
+
expect(() => (0, orchestrator_1.release)(opts(), io)).toThrow(/ya está publicado|2\.2\.0/i);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
const index_1 = require("../../src/release/index");
|
|
174
|
+
describe('parseArgs', () => {
|
|
175
|
+
it('defaults', () => {
|
|
176
|
+
expect((0, index_1.parseArgs)([])).toMatchObject({ dryRun: false, force: null, push: true, branch: 'main' });
|
|
177
|
+
});
|
|
178
|
+
it('--dry-run, --no-push, --force minor, --branch release', () => {
|
|
179
|
+
expect((0, index_1.parseArgs)(['--dry-run', '--no-push', '--force', 'minor', '--branch', 'release']))
|
|
180
|
+
.toMatchObject({ dryRun: true, push: false, force: 'minor', branch: 'release' });
|
|
181
|
+
});
|
|
182
|
+
it('rechaza --force con nivel inválido', () => {
|
|
183
|
+
expect(() => (0, index_1.parseArgs)(['--force', 'huge'])).toThrow(/force/i);
|
|
184
|
+
});
|
|
185
|
+
it('--branch sin valor lanza error', () => {
|
|
186
|
+
expect(() => (0, index_1.parseArgs)(['--branch'])).toThrow(/--branch requiere/i);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -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": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"build": "tsc && chmod +x dist/src/index.js",
|
|
21
21
|
"test": "jest --runInBand",
|
|
22
22
|
"start": "ts-node src/index.ts",
|
|
23
|
+
"prerelease": "npm run build",
|
|
24
|
+
"release": "node dist/src/release/index.js",
|
|
23
25
|
"prepack": "npm run build",
|
|
24
26
|
"prepublishOnly": "npm run build"
|
|
25
27
|
},
|