agentic-workflow-manager 2.0.1 → 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/commands/doctor.js +2 -0
- package/dist/src/commands/hooks/install.js +10 -4
- package/dist/src/commands/hooks/uninstall.js +2 -2
- package/dist/src/commands/init.js +2 -0
- package/dist/src/commands/sensors/formatters/eslint.js +7 -1
- package/dist/src/commands/sensors/install.js +3 -4
- package/dist/src/core/context/materializer.js +2 -5
- package/dist/src/core/diagnostics/context.js +2 -4
- package/dist/src/core/paths.js +56 -0
- package/dist/src/core/registries.js +3 -3
- package/dist/src/core/update-check.js +2 -3
- package/dist/src/index.js +132 -17
- package/dist/src/providers/index.js +4 -4
- 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/config.js +2 -2
- package/dist/src/utils/registry-view.js +35 -21
- package/dist/tests/commands/doctor-platform.test.js +22 -0
- package/dist/tests/commands/hooks/install-symlink-fallback.test.js +59 -0
- package/dist/tests/commands/sensors/eslint-formatter.test.js +34 -0
- package/dist/tests/core/paths.test.js +78 -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 +4 -2
|
@@ -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
|
+
}
|
package/dist/src/utils/config.js
CHANGED
|
@@ -8,14 +8,14 @@ exports.savePreferences = savePreferences;
|
|
|
8
8
|
// src/utils/config.ts
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
|
-
const
|
|
11
|
+
const paths_1 = require("../core/paths");
|
|
12
12
|
const DEFAULT_PREFS = {
|
|
13
13
|
defaultAgent: 'antigravity',
|
|
14
14
|
installMethod: 'symlink',
|
|
15
15
|
defaultScope: 'local'
|
|
16
16
|
};
|
|
17
17
|
function prefsDir() {
|
|
18
|
-
return
|
|
18
|
+
return (0, paths_1.awmHome)();
|
|
19
19
|
}
|
|
20
20
|
function getPreferences() {
|
|
21
21
|
const file = path_1.default.join(prefsDir(), 'preferences.json');
|
|
@@ -10,11 +10,12 @@ exports.packageSummaryLines = packageSummaryLines;
|
|
|
10
10
|
exports.packageDetailLines = packageDetailLines;
|
|
11
11
|
exports.findPackage = findPackage;
|
|
12
12
|
exports.artifactValue = artifactValue;
|
|
13
|
-
exports.
|
|
14
|
-
exports.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
134
|
-
return packages.map((p) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const doctor_1 = require("../../src/commands/doctor");
|
|
4
|
+
describe('doctor renderReport — platform line', () => {
|
|
5
|
+
const realPlatform = process.platform;
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
8
|
+
});
|
|
9
|
+
function emptyReport() {
|
|
10
|
+
return { overall: 'healthy', hasProject: false, projectName: undefined, results: [] };
|
|
11
|
+
}
|
|
12
|
+
it('renders the platform label under the Machine header', () => {
|
|
13
|
+
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
14
|
+
const out = (0, doctor_1.renderReport)(emptyReport());
|
|
15
|
+
expect(out).toContain('platform: Linux');
|
|
16
|
+
});
|
|
17
|
+
it('flags native Windows with a WSL hint', () => {
|
|
18
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
19
|
+
const out = (0, doctor_1.renderReport)(emptyReport());
|
|
20
|
+
expect(out).toContain('WSL');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
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 fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
describe('hooks/install — skill symlink fallback to copy', () => {
|
|
10
|
+
let tmpHome;
|
|
11
|
+
let origHome;
|
|
12
|
+
let origAwmHome;
|
|
13
|
+
let symlinkSpy;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-symlink-fb-'));
|
|
16
|
+
origHome = process.env.HOME;
|
|
17
|
+
origAwmHome = process.env.AWM_HOME;
|
|
18
|
+
process.env.HOME = tmpHome;
|
|
19
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
20
|
+
jest.resetModules();
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
symlinkSpy?.mockRestore();
|
|
24
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
25
|
+
if (origHome === undefined)
|
|
26
|
+
delete process.env.HOME;
|
|
27
|
+
else
|
|
28
|
+
process.env.HOME = origHome;
|
|
29
|
+
if (origAwmHome === undefined)
|
|
30
|
+
delete process.env.AWM_HOME;
|
|
31
|
+
else
|
|
32
|
+
process.env.AWM_HOME = origAwmHome;
|
|
33
|
+
});
|
|
34
|
+
function seedRegistry(root) {
|
|
35
|
+
const hooksDir = path_1.default.join(root, 'hooks');
|
|
36
|
+
const skillDir = path_1.default.join(root, 'skills', 'using-awm');
|
|
37
|
+
fs_1.default.mkdirSync(hooksDir, { recursive: true });
|
|
38
|
+
fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
39
|
+
fs_1.default.writeFileSync(path_1.default.join(hooksDir, 'session-start'), '#!/bin/sh\n');
|
|
40
|
+
fs_1.default.writeFileSync(path_1.default.join(hooksDir, 'run-hook.cmd'), '#!/bin/sh\n');
|
|
41
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'SKILL.md'), '# using-awm\n');
|
|
42
|
+
}
|
|
43
|
+
it('copies the skill when symlink throws (EPERM), preserving content', () => {
|
|
44
|
+
const registryRoot = path_1.default.join(tmpHome, 'registry');
|
|
45
|
+
seedRegistry(registryRoot);
|
|
46
|
+
// Force symlinkSync to fail like a platform without symlink permission.
|
|
47
|
+
symlinkSpy = jest.spyOn(fs_1.default, 'symlinkSync').mockImplementation(() => {
|
|
48
|
+
const err = new Error('EPERM: operation not permitted, symlink');
|
|
49
|
+
err.code = 'EPERM';
|
|
50
|
+
throw err;
|
|
51
|
+
});
|
|
52
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
53
|
+
const result = installHook({ agent: 'claude-code', registryRoot, installMethod: 'copy' });
|
|
54
|
+
const skillDest = path_1.default.join(result.scriptsDir, 'using-awm.md');
|
|
55
|
+
expect(fs_1.default.existsSync(skillDest)).toBe(true);
|
|
56
|
+
expect(fs_1.default.lstatSync(skillDest).isSymbolicLink()).toBe(false); // it was copied, not linked
|
|
57
|
+
expect(fs_1.default.readFileSync(skillDest, 'utf-8')).toContain('using-awm');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
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 path_1 = __importDefault(require("path"));
|
|
7
|
+
const eslint_1 = require("../../../src/commands/sensors/formatters/eslint");
|
|
8
|
+
describe('parseEslintOutput — relative path normalization', () => {
|
|
9
|
+
it('produces a cwd-relative path using the OS separator', () => {
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
const abs = path_1.default.join(cwd, 'src', 'foo.ts');
|
|
12
|
+
const raw = JSON.stringify([
|
|
13
|
+
{ filePath: abs, messages: [{ ruleId: 'no-eval', severity: 2, message: 'no eval', line: 3, column: 1 }] },
|
|
14
|
+
]);
|
|
15
|
+
const errors = (0, eslint_1.parseEslintOutput)(raw);
|
|
16
|
+
expect(errors).toHaveLength(1);
|
|
17
|
+
const first = errors[0];
|
|
18
|
+
if (!first)
|
|
19
|
+
throw new Error('Expected at least one error');
|
|
20
|
+
// path.join('src','foo.ts') uses the OS separator; on POSIX this is 'src/foo.ts'
|
|
21
|
+
expect(first.file).toBe(path_1.default.join('src', 'foo.ts'));
|
|
22
|
+
const file = first.file ?? '';
|
|
23
|
+
expect(file.startsWith(path_1.default.sep)).toBe(false); // genuinely relative
|
|
24
|
+
});
|
|
25
|
+
it('preserves the absolute path for files outside cwd', () => {
|
|
26
|
+
const abs = '/some/other/project/file.ts';
|
|
27
|
+
const raw = JSON.stringify([
|
|
28
|
+
{ filePath: abs, messages: [{ ruleId: 'no-eval', severity: 2, message: 'no eval', line: 1, column: 1 }] },
|
|
29
|
+
]);
|
|
30
|
+
const errors = (0, eslint_1.parseEslintOutput)(raw);
|
|
31
|
+
expect(errors).toHaveLength(1);
|
|
32
|
+
expect(errors[0]?.file).toBe(abs); // absolute path preserved, no traversal strings
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
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 os_1 = __importDefault(require("os"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const paths_1 = require("../../src/core/paths");
|
|
9
|
+
describe('core/paths', () => {
|
|
10
|
+
let origHome;
|
|
11
|
+
let origAwmHome;
|
|
12
|
+
const realPlatform = process.platform;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
origHome = process.env.HOME;
|
|
15
|
+
origAwmHome = process.env.AWM_HOME;
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
if (origHome === undefined)
|
|
19
|
+
delete process.env.HOME;
|
|
20
|
+
else
|
|
21
|
+
process.env.HOME = origHome;
|
|
22
|
+
if (origAwmHome === undefined)
|
|
23
|
+
delete process.env.AWM_HOME;
|
|
24
|
+
else
|
|
25
|
+
process.env.AWM_HOME = origAwmHome;
|
|
26
|
+
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
27
|
+
});
|
|
28
|
+
function setPlatform(p) {
|
|
29
|
+
Object.defineProperty(process, 'platform', { value: p, configurable: true });
|
|
30
|
+
}
|
|
31
|
+
it('homeDir uses process.env.HOME when set', () => {
|
|
32
|
+
process.env.HOME = '/tmp/fake-home';
|
|
33
|
+
expect((0, paths_1.homeDir)()).toBe('/tmp/fake-home');
|
|
34
|
+
});
|
|
35
|
+
it('homeDir falls back to os.homedir() when HOME is unset', () => {
|
|
36
|
+
delete process.env.HOME;
|
|
37
|
+
expect((0, paths_1.homeDir)()).toBe(os_1.default.homedir());
|
|
38
|
+
});
|
|
39
|
+
it('awmHome honors AWM_HOME override', () => {
|
|
40
|
+
process.env.AWM_HOME = '/tmp/custom-awm';
|
|
41
|
+
expect((0, paths_1.awmHome)()).toBe('/tmp/custom-awm');
|
|
42
|
+
});
|
|
43
|
+
it('awmHome defaults to <home>/.awm when AWM_HOME is unset', () => {
|
|
44
|
+
delete process.env.AWM_HOME;
|
|
45
|
+
process.env.HOME = '/tmp/fake-home';
|
|
46
|
+
expect((0, paths_1.awmHome)()).toBe(path_1.default.join('/tmp/fake-home', '.awm'));
|
|
47
|
+
});
|
|
48
|
+
it('platform reflects process.platform', () => {
|
|
49
|
+
setPlatform('linux');
|
|
50
|
+
expect((0, paths_1.platform)()).toBe('linux');
|
|
51
|
+
});
|
|
52
|
+
it('isWindowsNative is true only on win32', () => {
|
|
53
|
+
setPlatform('win32');
|
|
54
|
+
expect((0, paths_1.isWindowsNative)()).toBe(true);
|
|
55
|
+
setPlatform('linux');
|
|
56
|
+
expect((0, paths_1.isWindowsNative)()).toBe(false);
|
|
57
|
+
setPlatform('darwin');
|
|
58
|
+
expect((0, paths_1.isWindowsNative)()).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
it('platformLabel describes each known platform', () => {
|
|
61
|
+
setPlatform('linux');
|
|
62
|
+
expect((0, paths_1.platformLabel)()).toBe('Linux');
|
|
63
|
+
setPlatform('darwin');
|
|
64
|
+
expect((0, paths_1.platformLabel)()).toBe('macOS');
|
|
65
|
+
setPlatform('win32');
|
|
66
|
+
expect((0, paths_1.platformLabel)()).toContain('WSL');
|
|
67
|
+
});
|
|
68
|
+
it('warnIfUnsupportedPlatform calls the logger only on win32', () => {
|
|
69
|
+
const calls = [];
|
|
70
|
+
const log = (m) => calls.push(m);
|
|
71
|
+
setPlatform('linux');
|
|
72
|
+
(0, paths_1.warnIfUnsupportedPlatform)(log);
|
|
73
|
+
expect(calls).toHaveLength(0);
|
|
74
|
+
setPlatform('win32');
|
|
75
|
+
(0, paths_1.warnIfUnsupportedPlatform)(log);
|
|
76
|
+
expect(calls).toEqual([paths_1.WINDOWS_NATIVE_WARNING]);
|
|
77
|
+
});
|
|
78
|
+
});
|