@skilly-hand/skilly-hand 0.1.1 → 0.2.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/README.md +2 -1
- package/package.json +4 -1
- package/packages/cli/src/bin.js +323 -50
- package/packages/core/src/terminal.js +196 -0
package/README.md
CHANGED
|
@@ -58,7 +58,8 @@ npx skilly-hand
|
|
|
58
58
|
4. Run publish gate: `npm run verify:publish`.
|
|
59
59
|
5. Inspect package payload: `npm pack --dry-run --json`.
|
|
60
60
|
6. Bump version intentionally: `npm version patch|minor|major` (this auto-rotates `CHANGELOG.md`, creates a dated release section, and inserts a version-specific npm link).
|
|
61
|
-
7. Publish
|
|
61
|
+
7. Publish with assisted 2FA flow: `npm run publish:otp` (or `npm run publish:next` for prereleases).
|
|
62
|
+
- The script runs the publish gate, asks for OTP with hidden input, and if left blank lets npm trigger your default security method.
|
|
62
63
|
8. Smoke test after publish: `npx @skilly-hand/skilly-hand@<version> --help`.
|
|
63
64
|
9. Verify npm metadata (README render, changelog, license, executable bin).
|
|
64
65
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skilly-hand/skilly-hand",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "CC-BY-NC-4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
"test": "node --test tests/*.test.js",
|
|
31
31
|
"verify:packlist": "node ./scripts/verify-packlist.mjs",
|
|
32
32
|
"verify:publish": "npm run catalog:check && npm test && npm run verify:packlist",
|
|
33
|
+
"publish:prepare": "npm run verify:publish && npm pack --dry-run --json",
|
|
34
|
+
"publish:otp": "node ./scripts/publish-with-otp.mjs",
|
|
35
|
+
"publish:next": "node ./scripts/publish-with-otp.mjs --tag next",
|
|
33
36
|
"prepublishOnly": "npm run verify:publish",
|
|
34
37
|
"version": "node ./scripts/release-changelog.mjs",
|
|
35
38
|
"changelog:release": "node ./scripts/release-changelog.mjs",
|
package/packages/cli/src/bin.js
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { loadAllSkills } from "../../catalog/src/index.js";
|
|
4
4
|
import { installProject, runDoctor, uninstallProject } from "../../core/src/index.js";
|
|
5
|
+
import { createTerminalRenderer } from "../../core/src/terminal.js";
|
|
5
6
|
import { detectProject } from "../../detectors/src/index.js";
|
|
6
7
|
|
|
8
|
+
const renderer = createTerminalRenderer();
|
|
9
|
+
|
|
7
10
|
function parseArgs(argv) {
|
|
8
11
|
const args = [...argv];
|
|
9
12
|
const positional = [];
|
|
@@ -11,11 +14,20 @@ function parseArgs(argv) {
|
|
|
11
14
|
dryRun: false,
|
|
12
15
|
yes: false,
|
|
13
16
|
verbose: false,
|
|
17
|
+
json: false,
|
|
14
18
|
agents: [],
|
|
15
19
|
include: [],
|
|
16
20
|
exclude: []
|
|
17
21
|
};
|
|
18
22
|
|
|
23
|
+
function takeFlagValue(flagName) {
|
|
24
|
+
const value = args.shift();
|
|
25
|
+
if (!value || value.startsWith("-")) {
|
|
26
|
+
throw new Error(`Missing value for ${flagName}`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
while (args.length > 0) {
|
|
20
32
|
const token = args.shift();
|
|
21
33
|
if (!token.startsWith("-")) {
|
|
@@ -26,10 +38,11 @@ function parseArgs(argv) {
|
|
|
26
38
|
if (token === "--dry-run") flags.dryRun = true;
|
|
27
39
|
else if (token === "--yes" || token === "-y") flags.yes = true;
|
|
28
40
|
else if (token === "--verbose" || token === "-v") flags.verbose = true;
|
|
29
|
-
else if (token === "--
|
|
30
|
-
else if (token === "--
|
|
31
|
-
else if (token === "--
|
|
32
|
-
else if (token === "--
|
|
41
|
+
else if (token === "--json") flags.json = true;
|
|
42
|
+
else if (token === "--agent" || token === "-a") flags.agents.push(takeFlagValue(token));
|
|
43
|
+
else if (token === "--cwd") flags.cwd = takeFlagValue(token);
|
|
44
|
+
else if (token === "--include") flags.include.push(takeFlagValue(token));
|
|
45
|
+
else if (token === "--exclude") flags.exclude.push(takeFlagValue(token));
|
|
33
46
|
else if (token === "--help" || token === "-h") flags.help = true;
|
|
34
47
|
else throw new Error(`Unknown flag: ${token}`);
|
|
35
48
|
}
|
|
@@ -37,57 +50,251 @@ function parseArgs(argv) {
|
|
|
37
50
|
return { command: positional[0], flags };
|
|
38
51
|
}
|
|
39
52
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
function buildHelpText() {
|
|
54
|
+
const usage = renderer.section("Usage", renderer.list([
|
|
55
|
+
"npx skilly-hand [install]",
|
|
56
|
+
"npx skilly-hand detect",
|
|
57
|
+
"npx skilly-hand list",
|
|
58
|
+
"npx skilly-hand doctor",
|
|
59
|
+
"npx skilly-hand uninstall"
|
|
60
|
+
], { bullet: "-" }));
|
|
61
|
+
|
|
62
|
+
const flags = renderer.section("Flags", renderer.list([
|
|
63
|
+
"--dry-run Show install plan without writing files",
|
|
64
|
+
"--json Emit stable JSON output for automation",
|
|
65
|
+
"--yes, -y Reserved for future non-interactive confirmations",
|
|
66
|
+
"--verbose, -v Reserved for future debug detail",
|
|
67
|
+
"--agent, -a <name> codex|claude|cursor|gemini|copilot (repeatable)",
|
|
68
|
+
"--cwd <path> Project root (defaults to current directory)",
|
|
69
|
+
"--include <tag> Include only skills matching all tags",
|
|
70
|
+
"--exclude <tag> Exclude skills matching any tag",
|
|
71
|
+
"--help, -h Show help"
|
|
72
|
+
], { bullet: "-" }));
|
|
73
|
+
|
|
74
|
+
const examples = renderer.section("Examples", renderer.list([
|
|
75
|
+
"npx skilly-hand install --dry-run",
|
|
76
|
+
"npx skilly-hand detect --json",
|
|
77
|
+
"npx skilly-hand install --agent codex --agent claude",
|
|
78
|
+
"npx skilly-hand list --include workflow"
|
|
79
|
+
], { bullet: "-" }));
|
|
80
|
+
|
|
81
|
+
return renderer.joinBlocks([
|
|
82
|
+
renderer.status("info", "skilly-hand", "Portable AI skill orchestration for coding assistants."),
|
|
83
|
+
usage,
|
|
84
|
+
flags,
|
|
85
|
+
examples
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function detectionRows(detections) {
|
|
90
|
+
return detections.map((item) => ({
|
|
91
|
+
technology: item.technology,
|
|
92
|
+
confidence: item.confidence.toFixed(2),
|
|
93
|
+
reasons: item.reasons.join("; "),
|
|
94
|
+
recommended: item.recommendedSkillIds.join(", ")
|
|
95
|
+
}));
|
|
59
96
|
}
|
|
60
97
|
|
|
61
|
-
function
|
|
98
|
+
function renderDetections(detections) {
|
|
62
99
|
if (detections.length === 0) {
|
|
63
|
-
|
|
64
|
-
return;
|
|
100
|
+
return renderer.status("warn", "No technology signals were detected.", "Only core skills will be selected.");
|
|
65
101
|
}
|
|
66
102
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
103
|
+
return renderer.table(
|
|
104
|
+
[
|
|
105
|
+
{ key: "technology", header: "Technology" },
|
|
106
|
+
{ key: "confidence", header: "Confidence" },
|
|
107
|
+
{ key: "reasons", header: "Reasons" },
|
|
108
|
+
{ key: "recommended", header: "Recommended Skills" }
|
|
109
|
+
],
|
|
110
|
+
detectionRows(detections)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderSkillTable(skills) {
|
|
115
|
+
if (skills.length === 0) {
|
|
116
|
+
return renderer.status("warn", "No skills selected.");
|
|
71
117
|
}
|
|
118
|
+
|
|
119
|
+
return renderer.table(
|
|
120
|
+
[
|
|
121
|
+
{ key: "id", header: "Skill ID" },
|
|
122
|
+
{ key: "title", header: "Title" },
|
|
123
|
+
{ key: "tags", header: "Tags" }
|
|
124
|
+
],
|
|
125
|
+
skills.map((skill) => ({
|
|
126
|
+
id: skill.id,
|
|
127
|
+
title: skill.title,
|
|
128
|
+
tags: skill.tags.join(", ")
|
|
129
|
+
}))
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function printInstallResult(result, flags) {
|
|
134
|
+
const mode = flags.dryRun ? "dry-run" : "apply";
|
|
135
|
+
const preflight = renderer.section(
|
|
136
|
+
"Install Preflight",
|
|
137
|
+
renderer.kv([
|
|
138
|
+
["Project", result.plan.cwd],
|
|
139
|
+
["Install root", result.plan.installRoot],
|
|
140
|
+
["Agents", result.plan.agents.join(", ") || "none"],
|
|
141
|
+
["Include tags", flags.include.join(", ") || "none"],
|
|
142
|
+
["Exclude tags", flags.exclude.join(", ") || "none"],
|
|
143
|
+
["Mode", mode]
|
|
144
|
+
])
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const detections = renderer.section("Detected Technologies", renderDetections(result.plan.detections));
|
|
148
|
+
const skills = renderer.section("Skill Plan", renderSkillTable(result.plan.skills));
|
|
149
|
+
|
|
150
|
+
const status = result.applied
|
|
151
|
+
? renderer.status("success", "Installation completed.", "Managed files and symlinks are in place.")
|
|
152
|
+
: renderer.status("info", "Dry run complete.", "No files were written.");
|
|
153
|
+
|
|
154
|
+
const nextSteps = result.applied
|
|
155
|
+
? renderer.nextSteps([
|
|
156
|
+
"Review generated AGENTS and assistant instruction files.",
|
|
157
|
+
"Run `npx skilly-hand doctor` to validate installation health.",
|
|
158
|
+
"Use `npx skilly-hand uninstall` to restore backed-up files if needed."
|
|
159
|
+
])
|
|
160
|
+
: renderer.nextSteps([
|
|
161
|
+
"Run `npx skilly-hand install` to apply this plan.",
|
|
162
|
+
"Adjust `--include` and `--exclude` tags to tune skill selection."
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
renderer.write(renderer.joinBlocks([preflight, detections, skills, status, nextSteps]));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function printDetectResult(cwd, detections) {
|
|
169
|
+
const summary = renderer.section(
|
|
170
|
+
"Detection Summary",
|
|
171
|
+
renderer.kv([
|
|
172
|
+
["Project", cwd],
|
|
173
|
+
["Signals found", String(detections.length)]
|
|
174
|
+
])
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const details = renderer.section("Findings", renderDetections(detections));
|
|
178
|
+
renderer.write(renderer.joinBlocks([summary, details]));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function printListResult(skills) {
|
|
182
|
+
const summary = renderer.section(
|
|
183
|
+
"Catalog Summary",
|
|
184
|
+
renderer.kv([["Skills available", String(skills.length)]])
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const table = renderer.section(
|
|
188
|
+
"Skills",
|
|
189
|
+
renderer.table(
|
|
190
|
+
[
|
|
191
|
+
{ key: "id", header: "Skill ID" },
|
|
192
|
+
{ key: "title", header: "Title" },
|
|
193
|
+
{ key: "tags", header: "Tags" },
|
|
194
|
+
{ key: "agents", header: "Agents" }
|
|
195
|
+
],
|
|
196
|
+
skills.map((skill) => ({
|
|
197
|
+
id: skill.id,
|
|
198
|
+
title: skill.title,
|
|
199
|
+
tags: skill.tags.join(", "),
|
|
200
|
+
agents: skill.agentSupport.join(", ")
|
|
201
|
+
}))
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
renderer.write(renderer.joinBlocks([summary, table]));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function printDoctorResult(result) {
|
|
209
|
+
const health = result.installed
|
|
210
|
+
? renderer.status("success", "Installation detected.")
|
|
211
|
+
: renderer.status("warn", "No installation detected.");
|
|
212
|
+
|
|
213
|
+
const summary = renderer.section(
|
|
214
|
+
"Doctor Summary",
|
|
215
|
+
renderer.kv([
|
|
216
|
+
["Project", result.cwd],
|
|
217
|
+
["Installed", result.installed ? "yes" : "no"],
|
|
218
|
+
["Catalog issues", String(result.catalogIssues.length)]
|
|
219
|
+
])
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const lock = result.lock
|
|
223
|
+
? renderer.section(
|
|
224
|
+
"Lock Metadata",
|
|
225
|
+
renderer.kv([
|
|
226
|
+
["Generated at", result.lock.generatedAt],
|
|
227
|
+
["Agents", result.lock.agents.join(", ")],
|
|
228
|
+
["Skills", result.lock.skills.join(", ")]
|
|
229
|
+
])
|
|
230
|
+
)
|
|
231
|
+
: "";
|
|
232
|
+
|
|
233
|
+
const issues = result.catalogIssues.length
|
|
234
|
+
? renderer.section("Catalog Issues", renderer.list(result.catalogIssues))
|
|
235
|
+
: renderer.section("Catalog Issues", renderer.status("success", "No catalog issues found."));
|
|
236
|
+
|
|
237
|
+
const probes = renderer.section(
|
|
238
|
+
"Project Probes",
|
|
239
|
+
renderer.table(
|
|
240
|
+
[
|
|
241
|
+
{ key: "path", header: "Path" },
|
|
242
|
+
{ key: "exists", header: "Exists" },
|
|
243
|
+
{ key: "type", header: "Type" }
|
|
244
|
+
],
|
|
245
|
+
result.fileStatus.map((item) => ({
|
|
246
|
+
path: item.path,
|
|
247
|
+
exists: item.exists ? "yes" : "no",
|
|
248
|
+
type: item.type || "-"
|
|
249
|
+
}))
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
renderer.write(renderer.joinBlocks([health, summary, lock, issues, probes]));
|
|
72
254
|
}
|
|
73
255
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
256
|
+
function printUninstallResult(result) {
|
|
257
|
+
if (result.removed) {
|
|
258
|
+
renderer.write(
|
|
259
|
+
renderer.joinBlocks([
|
|
260
|
+
renderer.status("success", "skilly-hand installation removed."),
|
|
261
|
+
renderer.nextSteps([
|
|
262
|
+
"Run `npx skilly-hand install` if you want to reinstall managed files.",
|
|
263
|
+
"Run `npx skilly-hand doctor` to confirm the project state."
|
|
264
|
+
])
|
|
265
|
+
])
|
|
266
|
+
);
|
|
267
|
+
return;
|
|
83
268
|
}
|
|
269
|
+
|
|
270
|
+
renderer.write(
|
|
271
|
+
renderer.joinBlocks([
|
|
272
|
+
renderer.status("warn", "Nothing to uninstall.", result.reason),
|
|
273
|
+
renderer.nextSteps(["Run `npx skilly-hand install` to create a managed installation first."])
|
|
274
|
+
])
|
|
275
|
+
);
|
|
84
276
|
}
|
|
85
277
|
|
|
86
278
|
async function main() {
|
|
87
279
|
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
88
280
|
|
|
89
281
|
if (flags.help) {
|
|
90
|
-
|
|
282
|
+
if (flags.json) {
|
|
283
|
+
renderer.writeJson({
|
|
284
|
+
command: command || "install",
|
|
285
|
+
help: true,
|
|
286
|
+
usage: [
|
|
287
|
+
"npx skilly-hand [install]",
|
|
288
|
+
"npx skilly-hand detect",
|
|
289
|
+
"npx skilly-hand list",
|
|
290
|
+
"npx skilly-hand doctor",
|
|
291
|
+
"npx skilly-hand uninstall"
|
|
292
|
+
]
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
renderer.write(buildHelpText());
|
|
91
298
|
return;
|
|
92
299
|
}
|
|
93
300
|
|
|
@@ -95,29 +302,57 @@ async function main() {
|
|
|
95
302
|
const effectiveCommand = command || "install";
|
|
96
303
|
|
|
97
304
|
if (effectiveCommand === "detect") {
|
|
98
|
-
|
|
305
|
+
const detections = await detectProject(cwd);
|
|
306
|
+
if (flags.json) {
|
|
307
|
+
renderer.writeJson({
|
|
308
|
+
command: "detect",
|
|
309
|
+
cwd,
|
|
310
|
+
count: detections.length,
|
|
311
|
+
detections
|
|
312
|
+
});
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
printDetectResult(cwd, detections);
|
|
99
316
|
return;
|
|
100
317
|
}
|
|
101
318
|
|
|
102
319
|
if (effectiveCommand === "list") {
|
|
103
320
|
const skills = await loadAllSkills();
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
321
|
+
if (flags.json) {
|
|
322
|
+
renderer.writeJson({
|
|
323
|
+
command: "list",
|
|
324
|
+
count: skills.length,
|
|
325
|
+
skills
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
108
328
|
}
|
|
329
|
+
printListResult(skills);
|
|
109
330
|
return;
|
|
110
331
|
}
|
|
111
332
|
|
|
112
333
|
if (effectiveCommand === "doctor") {
|
|
113
334
|
const result = await runDoctor(cwd);
|
|
114
|
-
|
|
335
|
+
if (flags.json) {
|
|
336
|
+
renderer.writeJson({
|
|
337
|
+
command: "doctor",
|
|
338
|
+
...result
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
printDoctorResult(result);
|
|
115
343
|
return;
|
|
116
344
|
}
|
|
117
345
|
|
|
118
346
|
if (effectiveCommand === "uninstall") {
|
|
119
347
|
const result = await uninstallProject(cwd);
|
|
120
|
-
|
|
348
|
+
if (flags.json) {
|
|
349
|
+
renderer.writeJson({
|
|
350
|
+
command: "uninstall",
|
|
351
|
+
...result
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
printUninstallResult(result);
|
|
121
356
|
return;
|
|
122
357
|
}
|
|
123
358
|
|
|
@@ -130,15 +365,53 @@ async function main() {
|
|
|
130
365
|
excludeTags: flags.exclude
|
|
131
366
|
});
|
|
132
367
|
|
|
133
|
-
|
|
134
|
-
|
|
368
|
+
if (flags.json) {
|
|
369
|
+
renderer.writeJson({
|
|
370
|
+
command: "install",
|
|
371
|
+
applied: result.applied,
|
|
372
|
+
plan: result.plan,
|
|
373
|
+
lockPath: result.lockPath || null
|
|
374
|
+
});
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
printInstallResult(result, flags);
|
|
135
379
|
return;
|
|
136
380
|
}
|
|
137
381
|
|
|
138
382
|
throw new Error(`Unknown command: ${effectiveCommand}`);
|
|
139
383
|
}
|
|
140
384
|
|
|
385
|
+
const jsonRequested = process.argv.includes("--json");
|
|
386
|
+
|
|
141
387
|
main().catch((error) => {
|
|
142
|
-
|
|
388
|
+
const hint =
|
|
389
|
+
error.message.startsWith("Unknown command:")
|
|
390
|
+
? "Run `npx skilly-hand --help` to see available commands."
|
|
391
|
+
: error.message.startsWith("Unknown flag:") || error.message.startsWith("Missing value")
|
|
392
|
+
? "Check command flags with `npx skilly-hand --help`."
|
|
393
|
+
: "Retry with `--verbose` for expanded context if needed.";
|
|
394
|
+
|
|
395
|
+
if (jsonRequested) {
|
|
396
|
+
renderer.writeErrorJson({
|
|
397
|
+
ok: false,
|
|
398
|
+
error: {
|
|
399
|
+
what: "skilly-hand command failed",
|
|
400
|
+
why: error.message,
|
|
401
|
+
hint
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
process.exitCode = 1;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
renderer.writeError(
|
|
409
|
+
renderer.error({
|
|
410
|
+
what: "skilly-hand command failed",
|
|
411
|
+
why: error.message,
|
|
412
|
+
hint,
|
|
413
|
+
exitCode: 1
|
|
414
|
+
})
|
|
415
|
+
);
|
|
143
416
|
process.exitCode = 1;
|
|
144
417
|
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
2
|
+
|
|
3
|
+
function asString(value) {
|
|
4
|
+
if (value === null || value === undefined) return "";
|
|
5
|
+
return String(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function stripAnsi(value) {
|
|
9
|
+
return asString(value).replace(ANSI_PATTERN, "");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function padEndAnsi(value, width) {
|
|
13
|
+
const clean = stripAnsi(value);
|
|
14
|
+
if (clean.length >= width) return value;
|
|
15
|
+
return value + " ".repeat(width - clean.length);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeBooleanEnv(value) {
|
|
19
|
+
if (value === undefined || value === null) return null;
|
|
20
|
+
const normalized = String(value).trim().toLowerCase();
|
|
21
|
+
if (normalized === "" || normalized === "0" || normalized === "false" || normalized === "no") return false;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function detectColorSupport({ env = process.env, stream = process.stdout } = {}) {
|
|
26
|
+
const noColor = normalizeBooleanEnv(env.NO_COLOR);
|
|
27
|
+
if (noColor) return false;
|
|
28
|
+
|
|
29
|
+
const forceColor = normalizeBooleanEnv(env.FORCE_COLOR);
|
|
30
|
+
if (forceColor === true) return true;
|
|
31
|
+
if (forceColor === false) return false;
|
|
32
|
+
|
|
33
|
+
if (!stream?.isTTY) return false;
|
|
34
|
+
if (env.CI) return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function detectUnicodeSupport({ env = process.env, stream = process.stdout, platform = process.platform } = {}) {
|
|
39
|
+
if (!stream?.isTTY) return false;
|
|
40
|
+
if (env.TERM === "dumb") return false;
|
|
41
|
+
if (env.NO_UNICODE) return false;
|
|
42
|
+
if (platform === "win32") {
|
|
43
|
+
return Boolean(env.WT_SESSION || env.TERM_PROGRAM || env.ConEmuTask || env.ANSICON);
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createStyler(enabled) {
|
|
49
|
+
if (!enabled) {
|
|
50
|
+
const passthrough = (value) => asString(value);
|
|
51
|
+
return {
|
|
52
|
+
reset: passthrough,
|
|
53
|
+
bold: passthrough,
|
|
54
|
+
dim: passthrough,
|
|
55
|
+
cyan: passthrough,
|
|
56
|
+
green: passthrough,
|
|
57
|
+
yellow: passthrough,
|
|
58
|
+
red: passthrough,
|
|
59
|
+
magenta: passthrough
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const wrap = (code) => (value) => `\u001b[${code}m${asString(value)}\u001b[0m`;
|
|
64
|
+
return {
|
|
65
|
+
reset: wrap("0"),
|
|
66
|
+
bold: wrap("1"),
|
|
67
|
+
dim: wrap("2"),
|
|
68
|
+
cyan: wrap("36"),
|
|
69
|
+
green: wrap("32"),
|
|
70
|
+
yellow: wrap("33"),
|
|
71
|
+
red: wrap("31"),
|
|
72
|
+
magenta: wrap("35")
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderKeyValue(entries, style) {
|
|
77
|
+
if (!entries || entries.length === 0) return "";
|
|
78
|
+
const normalized = entries.map(([key, value]) => [asString(key), asString(value)]);
|
|
79
|
+
const width = normalized.reduce((max, [key]) => Math.max(max, key.length), 0);
|
|
80
|
+
return normalized
|
|
81
|
+
.map(([key, value]) => `${style.dim(padEndAnsi(key, width))} : ${value}`)
|
|
82
|
+
.join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderList(items, { bullet }) {
|
|
86
|
+
if (!items || items.length === 0) return "";
|
|
87
|
+
return items.map((item) => `${bullet} ${asString(item)}`).join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderTable(columns, rows) {
|
|
91
|
+
if (!columns || columns.length === 0) return "";
|
|
92
|
+
const header = columns.map((column) => column.header);
|
|
93
|
+
const matrix = [header, ...rows.map((row) => columns.map((column) => asString(row[column.key] ?? "")))];
|
|
94
|
+
const widths = header.map((_, index) =>
|
|
95
|
+
matrix.reduce((max, line) => Math.max(max, stripAnsi(line[index]).length), 0)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const headerLine = header.map((value, index) => padEndAnsi(value, widths[index])).join(" ");
|
|
99
|
+
const separatorLine = widths.map((width) => "-".repeat(Math.max(3, width))).join(" ");
|
|
100
|
+
const body = rows.map((row) =>
|
|
101
|
+
columns.map((column, index) => padEndAnsi(asString(row[column.key] ?? ""), widths[index])).join(" ")
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return [headerLine, separatorLine, ...body].join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function joinBlocks(blocks) {
|
|
108
|
+
const filtered = blocks.map((block) => asString(block).trimEnd()).filter(Boolean);
|
|
109
|
+
if (filtered.length === 0) return "";
|
|
110
|
+
return filtered.join("\n\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createTerminalRenderer({
|
|
114
|
+
stdout = process.stdout,
|
|
115
|
+
stderr = process.stderr,
|
|
116
|
+
env = process.env,
|
|
117
|
+
platform = process.platform
|
|
118
|
+
} = {}) {
|
|
119
|
+
const colorEnabled = detectColorSupport({ env, stream: stdout });
|
|
120
|
+
const unicodeEnabled = detectUnicodeSupport({ env, stream: stdout, platform });
|
|
121
|
+
const style = createStyler(colorEnabled);
|
|
122
|
+
const symbols = unicodeEnabled
|
|
123
|
+
? { info: "i", success: "✓", warn: "!", error: "x", bullet: "•", section: "■" }
|
|
124
|
+
: { info: "i", success: "+", warn: "!", error: "x", bullet: "-", section: "#" };
|
|
125
|
+
|
|
126
|
+
const statusStyles = {
|
|
127
|
+
info: style.cyan,
|
|
128
|
+
success: style.green,
|
|
129
|
+
warn: style.yellow,
|
|
130
|
+
error: style.red
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const renderer = {
|
|
134
|
+
colorEnabled,
|
|
135
|
+
unicodeEnabled,
|
|
136
|
+
symbols,
|
|
137
|
+
style,
|
|
138
|
+
json(value) {
|
|
139
|
+
return JSON.stringify(value, null, 2);
|
|
140
|
+
},
|
|
141
|
+
section(title, body = "") {
|
|
142
|
+
const heading = `${style.cyan(symbols.section)} ${style.bold(asString(title))}`;
|
|
143
|
+
const bodyText = asString(body).trimEnd();
|
|
144
|
+
return bodyText ? `${heading}\n${bodyText}` : heading;
|
|
145
|
+
},
|
|
146
|
+
kv(entries) {
|
|
147
|
+
return renderKeyValue(entries, style);
|
|
148
|
+
},
|
|
149
|
+
list(items, options = {}) {
|
|
150
|
+
return renderList(items, { bullet: options.bullet || symbols.bullet });
|
|
151
|
+
},
|
|
152
|
+
table(columns, rows) {
|
|
153
|
+
return renderTable(columns, rows);
|
|
154
|
+
},
|
|
155
|
+
status(level, message, detail = "") {
|
|
156
|
+
const applied = statusStyles[level] || ((value) => value);
|
|
157
|
+
const icon = symbols[level] || symbols.info;
|
|
158
|
+
const main = `${applied(icon)} ${asString(message)}`;
|
|
159
|
+
if (!detail) return main;
|
|
160
|
+
return `${main}\n${style.dim(" " + asString(detail))}`;
|
|
161
|
+
},
|
|
162
|
+
summary(title, items = []) {
|
|
163
|
+
return renderer.section(title, renderer.list(items));
|
|
164
|
+
},
|
|
165
|
+
nextSteps(steps = []) {
|
|
166
|
+
if (!steps.length) return "";
|
|
167
|
+
return renderer.section("Next Steps", renderer.list(steps));
|
|
168
|
+
},
|
|
169
|
+
error({ what = "Command failed", why = "", hint = "", exitCode = 1 } = {}) {
|
|
170
|
+
const blocks = [
|
|
171
|
+
renderer.status("error", what),
|
|
172
|
+
why ? renderer.kv([["Why", why]]) : "",
|
|
173
|
+
hint ? renderer.kv([["How to recover", hint]]) : "",
|
|
174
|
+
renderer.kv([["Exit code", asString(exitCode)]])
|
|
175
|
+
];
|
|
176
|
+
return joinBlocks(blocks);
|
|
177
|
+
},
|
|
178
|
+
joinBlocks
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
...renderer,
|
|
183
|
+
write(block = "") {
|
|
184
|
+
if (block) stdout.write(`${block}\n`);
|
|
185
|
+
},
|
|
186
|
+
writeError(block = "") {
|
|
187
|
+
if (block) stderr.write(`${block}\n`);
|
|
188
|
+
},
|
|
189
|
+
writeJson(value) {
|
|
190
|
+
stdout.write(`${renderer.json(value)}\n`);
|
|
191
|
+
},
|
|
192
|
+
writeErrorJson(value) {
|
|
193
|
+
stderr.write(`${renderer.json(value)}\n`);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|