@skilly-hand/skilly-hand 0.21.0 → 0.22.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/CHANGELOG.md +32 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/packages/catalog/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/bin.js +339 -140
- package/packages/cli/src/ink-ui.js +304 -27
- package/packages/cli/src/result-doc.js +58 -0
- package/packages/core/package.json +1 -1
- package/packages/core/src/index.js +312 -14
- package/packages/core/src/terminal.js +5 -3
- package/packages/core/src/ui/layout.js +11 -7
- package/packages/core/src/ui/theme.js +16 -2
- package/packages/detectors/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { detectProject, inspectProjectFiles } from "../../detectors/src/index.js
|
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_AGENTS = ["standard", "codex", "claude", "cursor", "gemini", "copilot", "antigravity", "windsurf", "trae"];
|
|
7
7
|
const MANAGED_MARKER = "<!-- Managed by skilly-hand.";
|
|
8
|
+
const NATIVE_SETUP_MARKER = "<!-- Managed by skilly-hand native setup.";
|
|
8
9
|
const AGENT_INSTALL_PROFILES = {
|
|
9
10
|
standard: {
|
|
10
11
|
instructionFiles: [["AGENTS.md"]],
|
|
@@ -43,6 +44,61 @@ const AGENT_INSTALL_PROFILES = {
|
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
export const NATIVE_ADAPTER_REGISTRY = {
|
|
48
|
+
standard: {
|
|
49
|
+
supported: false,
|
|
50
|
+
reason: "No dedicated native instruction or hook surface is available for the standard profile."
|
|
51
|
+
},
|
|
52
|
+
codex: {
|
|
53
|
+
supported: true,
|
|
54
|
+
mode: "rule_file",
|
|
55
|
+
targetPath: [".codex", "rules", "skilly-hand.md"],
|
|
56
|
+
remediation: "Run `npx skilly-hand native setup --agent codex`."
|
|
57
|
+
},
|
|
58
|
+
claude: {
|
|
59
|
+
supported: true,
|
|
60
|
+
mode: "rule_file",
|
|
61
|
+
targetPath: [".claude", "rules", "skilly-hand.md"],
|
|
62
|
+
remediation: "Run `npx skilly-hand native setup --agent claude`."
|
|
63
|
+
},
|
|
64
|
+
cursor: {
|
|
65
|
+
supported: true,
|
|
66
|
+
mode: "rule_file",
|
|
67
|
+
targetPath: [".cursor", "rules", "skilly-hand.mdc"],
|
|
68
|
+
remediation: "Run `npx skilly-hand native setup --agent cursor`."
|
|
69
|
+
},
|
|
70
|
+
gemini: {
|
|
71
|
+
supported: true,
|
|
72
|
+
mode: "rule_file",
|
|
73
|
+
targetPath: [".gemini", "rules", "skilly-hand.md"],
|
|
74
|
+
remediation: "Run `npx skilly-hand native setup --agent gemini`."
|
|
75
|
+
},
|
|
76
|
+
copilot: {
|
|
77
|
+
supported: true,
|
|
78
|
+
mode: "rule_file",
|
|
79
|
+
targetPath: [".github", "instructions", "skilly-hand.md"],
|
|
80
|
+
remediation: "Run `npx skilly-hand native setup --agent copilot`."
|
|
81
|
+
},
|
|
82
|
+
antigravity: {
|
|
83
|
+
supported: true,
|
|
84
|
+
mode: "rule_file",
|
|
85
|
+
targetPath: [".agents", "rules", "skilly-hand-native.md"],
|
|
86
|
+
remediation: "Run `npx skilly-hand native setup --agent antigravity`."
|
|
87
|
+
},
|
|
88
|
+
windsurf: {
|
|
89
|
+
supported: true,
|
|
90
|
+
mode: "rule_file",
|
|
91
|
+
targetPath: [".windsurf", "rules", "skilly-hand.md"],
|
|
92
|
+
remediation: "Run `npx skilly-hand native setup --agent windsurf`."
|
|
93
|
+
},
|
|
94
|
+
trae: {
|
|
95
|
+
supported: true,
|
|
96
|
+
mode: "rule_file",
|
|
97
|
+
targetPath: [".trae", "rules", "skilly-hand.md"],
|
|
98
|
+
remediation: "Run `npx skilly-hand native setup --agent trae`."
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
46
102
|
function uniq(values) {
|
|
47
103
|
return [...new Set(values)];
|
|
48
104
|
}
|
|
@@ -69,6 +125,63 @@ function nowIso() {
|
|
|
69
125
|
return new Date().toISOString();
|
|
70
126
|
}
|
|
71
127
|
|
|
128
|
+
function createLockData({ cwd, generatedAt, agents = [], skills = [], detections = [] }) {
|
|
129
|
+
return {
|
|
130
|
+
version: 1,
|
|
131
|
+
generatedAt,
|
|
132
|
+
cwd,
|
|
133
|
+
agents,
|
|
134
|
+
skills,
|
|
135
|
+
detections,
|
|
136
|
+
managedFiles: [],
|
|
137
|
+
managedSymlinks: [],
|
|
138
|
+
managedNativeFiles: [],
|
|
139
|
+
nativeProfiles: {},
|
|
140
|
+
backups: {}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function toRelativePath(cwd, targetPath) {
|
|
145
|
+
return path.relative(cwd, targetPath) || ".";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolveNativeAdapterTarget(cwd, adapter) {
|
|
149
|
+
return path.join(cwd, ...adapter.targetPath);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildNativeRuleContent(agent, adapter) {
|
|
153
|
+
const trigger = "Run token-optimizer before review-rangers when doing risk-heavy review passes.";
|
|
154
|
+
return [
|
|
155
|
+
`${NATIVE_SETUP_MARKER} Re-run \`npx skilly-hand native setup\` to regenerate. -->`,
|
|
156
|
+
`# skilly-hand Native Bootstrap (${agent})`,
|
|
157
|
+
"",
|
|
158
|
+
`This file is managed by skilly-hand to keep native ${adapter.mode.replace("_", " ")} behavior consistent.`,
|
|
159
|
+
"",
|
|
160
|
+
"## Always-On Defaults",
|
|
161
|
+
"- Apply AGENTS guidance from the repository root before task routing.",
|
|
162
|
+
"- Enforce optimizer gate order: `token-optimizer` then `output-optimizer`.",
|
|
163
|
+
"- Keep output concise by default (`step-brief`) unless user asks otherwise.",
|
|
164
|
+
"",
|
|
165
|
+
"## Token-Safe Review Stage",
|
|
166
|
+
`- ${trigger}`,
|
|
167
|
+
"- Keep review verdicts concise unless a blocker requires expanded rationale.",
|
|
168
|
+
"",
|
|
169
|
+
"## Sync",
|
|
170
|
+
"- Regenerate this file via `npx skilly-hand native setup` after workflow updates.",
|
|
171
|
+
""
|
|
172
|
+
].join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function retainNativeProfilesForAgents(previousLock, selectedAgents) {
|
|
176
|
+
const previousProfiles = previousLock?.nativeProfiles || {};
|
|
177
|
+
const retained = {};
|
|
178
|
+
for (const agent of selectedAgents) {
|
|
179
|
+
if (!previousProfiles[agent]) continue;
|
|
180
|
+
retained[agent] = previousProfiles[agent];
|
|
181
|
+
}
|
|
182
|
+
return retained;
|
|
183
|
+
}
|
|
184
|
+
|
|
72
185
|
function normalizeAgentList(agents) {
|
|
73
186
|
if (!agents || agents.length === 0) {
|
|
74
187
|
return [...DEFAULT_AGENTS];
|
|
@@ -167,11 +280,11 @@ async function backupPathIfNeeded(targetPath, backupsDir, lockData) {
|
|
|
167
280
|
return backupPath;
|
|
168
281
|
}
|
|
169
282
|
|
|
170
|
-
async function ensureManagedTextFile(targetPath, content, backupsDir, lockData) {
|
|
283
|
+
async function ensureManagedTextFile(targetPath, content, backupsDir, lockData, collectionKey = "managedFiles") {
|
|
171
284
|
if (await exists(targetPath)) {
|
|
172
285
|
const current = await readFile(targetPath, "utf8");
|
|
173
286
|
if (current === content) {
|
|
174
|
-
lockData.
|
|
287
|
+
lockData[collectionKey].push(targetPath);
|
|
175
288
|
return;
|
|
176
289
|
}
|
|
177
290
|
|
|
@@ -184,7 +297,7 @@ async function ensureManagedTextFile(targetPath, content, backupsDir, lockData)
|
|
|
184
297
|
|
|
185
298
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
186
299
|
await writeFile(targetPath, content, "utf8");
|
|
187
|
-
lockData.
|
|
300
|
+
lockData[collectionKey].push(targetPath);
|
|
188
301
|
}
|
|
189
302
|
|
|
190
303
|
async function ensureSymlink(targetPath, sourcePath, backupsDir, lockData) {
|
|
@@ -238,6 +351,7 @@ async function reconcileManagedTargets({
|
|
|
238
351
|
previousLock,
|
|
239
352
|
selectedInstructionTargets,
|
|
240
353
|
selectedSkillTargets,
|
|
354
|
+
selectedNativeTargets = [],
|
|
241
355
|
lockData
|
|
242
356
|
}) {
|
|
243
357
|
if (!previousLock) {
|
|
@@ -246,7 +360,8 @@ async function reconcileManagedTargets({
|
|
|
246
360
|
|
|
247
361
|
const selectedInstructions = new Set(selectedInstructionTargets);
|
|
248
362
|
const selectedSkills = new Set(selectedSkillTargets);
|
|
249
|
-
const
|
|
363
|
+
const selectedNatives = new Set(selectedNativeTargets);
|
|
364
|
+
const selectedTargets = new Set([...selectedInstructions, ...selectedSkills, ...selectedNatives]);
|
|
250
365
|
const previousBackups = previousLock.backups || {};
|
|
251
366
|
|
|
252
367
|
for (const [targetPath, backupPath] of Object.entries(previousBackups)) {
|
|
@@ -295,6 +410,170 @@ async function reconcileManagedTargets({
|
|
|
295
410
|
await rm(filePath, { force: true });
|
|
296
411
|
}
|
|
297
412
|
}
|
|
413
|
+
|
|
414
|
+
for (const filePath of previousLock.managedNativeFiles || []) {
|
|
415
|
+
if (selectedNatives.has(filePath)) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const backupPath = previousBackups[filePath];
|
|
420
|
+
if (backupPath && await exists(backupPath)) {
|
|
421
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
422
|
+
await cp(backupPath, filePath, { recursive: true, force: true });
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!(await exists(filePath))) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const content = await readFile(filePath, "utf8");
|
|
431
|
+
if (content.includes(MANAGED_MARKER) || content.includes(NATIVE_SETUP_MARKER)) {
|
|
432
|
+
await rm(filePath, { force: true });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export async function evaluateNativeCoverage({ cwd, agents }) {
|
|
438
|
+
const selectedAgents = normalizeAgentList(agents);
|
|
439
|
+
const rows = [];
|
|
440
|
+
|
|
441
|
+
for (const agent of selectedAgents) {
|
|
442
|
+
const adapter = NATIVE_ADAPTER_REGISTRY[agent];
|
|
443
|
+
if (!adapter || adapter.supported === false) {
|
|
444
|
+
rows.push({
|
|
445
|
+
agent,
|
|
446
|
+
status: "not-supported",
|
|
447
|
+
mode: adapter?.mode || null,
|
|
448
|
+
target: null,
|
|
449
|
+
remediation: adapter?.reason || "No native setup adapter available for this agent."
|
|
450
|
+
});
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const target = resolveNativeAdapterTarget(cwd, adapter);
|
|
455
|
+
const relativeTarget = toRelativePath(cwd, target);
|
|
456
|
+
if (!(await exists(target))) {
|
|
457
|
+
rows.push({
|
|
458
|
+
agent,
|
|
459
|
+
status: "missing",
|
|
460
|
+
mode: adapter.mode,
|
|
461
|
+
target: relativeTarget,
|
|
462
|
+
remediation: adapter.remediation
|
|
463
|
+
});
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const content = await readFile(target, "utf8");
|
|
468
|
+
const hasMarker = content.includes(NATIVE_SETUP_MARKER);
|
|
469
|
+
rows.push({
|
|
470
|
+
agent,
|
|
471
|
+
status: hasMarker ? "configured" : "partial",
|
|
472
|
+
mode: adapter.mode,
|
|
473
|
+
target: relativeTarget,
|
|
474
|
+
remediation: hasMarker ? "No action needed." : adapter.remediation
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return rows;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export async function setupNativeProject({
|
|
482
|
+
cwd,
|
|
483
|
+
agents,
|
|
484
|
+
dryRun = false
|
|
485
|
+
}) {
|
|
486
|
+
const selectedAgents = normalizeAgentList(agents);
|
|
487
|
+
const generatedAt = nowIso();
|
|
488
|
+
const installRoot = path.join(cwd, ".skilly-hand");
|
|
489
|
+
const backupsDir = path.join(installRoot, "backups");
|
|
490
|
+
const lockPath = path.join(installRoot, "manifest.lock.json");
|
|
491
|
+
const previousLock = await exists(lockPath) ? await readJson(lockPath) : null;
|
|
492
|
+
const selectedNativeTargets = selectedAgents
|
|
493
|
+
.map((agent) => NATIVE_ADAPTER_REGISTRY[agent])
|
|
494
|
+
.filter((adapter) => adapter && adapter.supported)
|
|
495
|
+
.map((adapter) => resolveNativeAdapterTarget(cwd, adapter));
|
|
496
|
+
|
|
497
|
+
const nativeStatusBefore = await evaluateNativeCoverage({ cwd, agents: selectedAgents });
|
|
498
|
+
|
|
499
|
+
const plan = {
|
|
500
|
+
cwd,
|
|
501
|
+
generatedAt,
|
|
502
|
+
agents: selectedAgents,
|
|
503
|
+
installRoot,
|
|
504
|
+
nativeStatus: nativeStatusBefore
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
if (dryRun) {
|
|
508
|
+
return {
|
|
509
|
+
plan,
|
|
510
|
+
applied: false
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
await mkdir(installRoot, { recursive: true });
|
|
515
|
+
await mkdir(backupsDir, { recursive: true });
|
|
516
|
+
|
|
517
|
+
const lockData = createLockData({
|
|
518
|
+
cwd,
|
|
519
|
+
generatedAt,
|
|
520
|
+
agents: previousLock?.agents || selectedAgents,
|
|
521
|
+
skills: previousLock?.skills || [],
|
|
522
|
+
detections: previousLock?.detections || []
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
await reconcileManagedTargets({
|
|
526
|
+
previousLock,
|
|
527
|
+
selectedInstructionTargets: previousLock?.managedFiles || [],
|
|
528
|
+
selectedSkillTargets: previousLock?.managedSymlinks || [],
|
|
529
|
+
selectedNativeTargets,
|
|
530
|
+
lockData
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
lockData.managedFiles = [...(previousLock?.managedFiles || [])];
|
|
534
|
+
lockData.managedSymlinks = [...(previousLock?.managedSymlinks || [])];
|
|
535
|
+
|
|
536
|
+
const retainedProfiles = retainNativeProfilesForAgents(previousLock, selectedAgents);
|
|
537
|
+
lockData.nativeProfiles = retainedProfiles;
|
|
538
|
+
lockData.managedNativeFiles = uniq(
|
|
539
|
+
Object.values(retainedProfiles)
|
|
540
|
+
.flatMap((profile) => profile?.managedFiles || [])
|
|
541
|
+
.filter(Boolean)
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
for (const agent of selectedAgents) {
|
|
545
|
+
const adapter = NATIVE_ADAPTER_REGISTRY[agent];
|
|
546
|
+
if (!adapter || adapter.supported === false) {
|
|
547
|
+
lockData.nativeProfiles[agent] = {
|
|
548
|
+
status: "not-supported",
|
|
549
|
+
mode: adapter?.mode || null,
|
|
550
|
+
managedFiles: [],
|
|
551
|
+
target: null
|
|
552
|
+
};
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const targetPath = resolveNativeAdapterTarget(cwd, adapter);
|
|
557
|
+
const content = buildNativeRuleContent(agent, adapter);
|
|
558
|
+
await ensureManagedTextFile(targetPath, content, backupsDir, lockData, "managedNativeFiles");
|
|
559
|
+
lockData.nativeProfiles[agent] = {
|
|
560
|
+
status: "configured",
|
|
561
|
+
mode: adapter.mode,
|
|
562
|
+
managedFiles: [targetPath],
|
|
563
|
+
target: toRelativePath(cwd, targetPath)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
lockData.managedNativeFiles = uniq(lockData.managedNativeFiles);
|
|
568
|
+
|
|
569
|
+
await writeJson(lockPath, lockData);
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
plan,
|
|
573
|
+
applied: true,
|
|
574
|
+
lockPath,
|
|
575
|
+
nativeStatus: await evaluateNativeCoverage({ cwd, agents: selectedAgents })
|
|
576
|
+
};
|
|
298
577
|
}
|
|
299
578
|
|
|
300
579
|
export async function installProject({
|
|
@@ -327,17 +606,13 @@ export async function installProject({
|
|
|
327
606
|
const backupsDir = path.join(installRoot, "backups");
|
|
328
607
|
const lockPath = path.join(installRoot, "manifest.lock.json");
|
|
329
608
|
const previousLock = await exists(lockPath) ? await readJson(lockPath) : null;
|
|
330
|
-
const lockData = {
|
|
331
|
-
version: 1,
|
|
332
|
-
generatedAt: plan.generatedAt,
|
|
609
|
+
const lockData = createLockData({
|
|
333
610
|
cwd,
|
|
611
|
+
generatedAt: plan.generatedAt,
|
|
334
612
|
agents: selectedAgents,
|
|
335
613
|
skills: skills.map((skill) => skill.id),
|
|
336
|
-
detections
|
|
337
|
-
|
|
338
|
-
managedSymlinks: [],
|
|
339
|
-
backups: {}
|
|
340
|
-
};
|
|
614
|
+
detections
|
|
615
|
+
});
|
|
341
616
|
|
|
342
617
|
await mkdir(installRoot, { recursive: true });
|
|
343
618
|
await mkdir(targetCatalogDir, { recursive: true });
|
|
@@ -359,14 +634,24 @@ export async function installProject({
|
|
|
359
634
|
const { instructionTargets, skillTargets } = buildInstallTargets(selectedAgents);
|
|
360
635
|
const absoluteInstructionTargets = instructionTargets.map((pathParts) => path.join(cwd, ...pathParts));
|
|
361
636
|
const absoluteSkillTargets = skillTargets.map((pathParts) => path.join(cwd, ...pathParts));
|
|
637
|
+
const retainedNativeProfiles = retainNativeProfilesForAgents(previousLock, selectedAgents);
|
|
638
|
+
const retainedNativeTargets = uniq(
|
|
639
|
+
Object.values(retainedNativeProfiles)
|
|
640
|
+
.flatMap((profile) => profile?.managedFiles || [])
|
|
641
|
+
.filter(Boolean)
|
|
642
|
+
);
|
|
362
643
|
|
|
363
644
|
await reconcileManagedTargets({
|
|
364
645
|
previousLock,
|
|
365
646
|
selectedInstructionTargets: absoluteInstructionTargets,
|
|
366
647
|
selectedSkillTargets: absoluteSkillTargets,
|
|
648
|
+
selectedNativeTargets: retainedNativeTargets,
|
|
367
649
|
lockData
|
|
368
650
|
});
|
|
369
651
|
|
|
652
|
+
lockData.nativeProfiles = retainedNativeProfiles;
|
|
653
|
+
lockData.managedNativeFiles = retainedNativeTargets;
|
|
654
|
+
|
|
370
655
|
for (const targetPath of absoluteInstructionTargets) {
|
|
371
656
|
await ensureManagedTextFile(targetPath, agentsMarkdown, backupsDir, lockData);
|
|
372
657
|
}
|
|
@@ -407,6 +692,15 @@ export async function uninstallProject(cwd) {
|
|
|
407
692
|
}
|
|
408
693
|
}
|
|
409
694
|
|
|
695
|
+
for (const filePath of lockData.managedNativeFiles || []) {
|
|
696
|
+
if (await exists(filePath)) {
|
|
697
|
+
const content = await readFile(filePath, "utf8");
|
|
698
|
+
if (content.includes(NATIVE_SETUP_MARKER) || content.includes(MANAGED_MARKER)) {
|
|
699
|
+
await rm(filePath, { force: true });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
410
704
|
for (const [targetPath, backupPath] of Object.entries(lockData.backups || {})) {
|
|
411
705
|
if (await exists(backupPath)) {
|
|
412
706
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
@@ -428,15 +722,19 @@ export async function runDoctor(cwd) {
|
|
|
428
722
|
cwd,
|
|
429
723
|
installed,
|
|
430
724
|
catalogIssues,
|
|
431
|
-
fileStatus
|
|
725
|
+
fileStatus,
|
|
726
|
+
nativeStatus: await evaluateNativeCoverage({ cwd, agents: installed ? undefined : DEFAULT_AGENTS })
|
|
432
727
|
};
|
|
433
728
|
|
|
434
729
|
if (installed) {
|
|
435
730
|
const lock = await readJson(lockPath);
|
|
731
|
+
result.nativeStatus = await evaluateNativeCoverage({ cwd, agents: lock.agents });
|
|
436
732
|
result.lock = {
|
|
437
733
|
agents: lock.agents,
|
|
438
734
|
skills: lock.skills,
|
|
439
|
-
generatedAt: lock.generatedAt
|
|
735
|
+
generatedAt: lock.generatedAt,
|
|
736
|
+
managedNativeFiles: lock.managedNativeFiles || [],
|
|
737
|
+
nativeProfiles: lock.nativeProfiles || {}
|
|
440
738
|
};
|
|
441
739
|
}
|
|
442
740
|
|
|
@@ -86,15 +86,17 @@ export function createTerminalRenderer({
|
|
|
86
86
|
stdout = process.stdout,
|
|
87
87
|
stderr = process.stderr,
|
|
88
88
|
env = process.env,
|
|
89
|
-
platform = process.platform
|
|
89
|
+
platform = process.platform,
|
|
90
|
+
colorProfile = "mono-accent",
|
|
91
|
+
density = "balanced"
|
|
90
92
|
} = {}) {
|
|
91
93
|
const colorLevel = detectColorLevel({ env, stream: stdout });
|
|
92
94
|
const colorEnabled = colorLevel > 0;
|
|
93
95
|
const unicodeEnabled = detectUnicodeSupport({ env, stream: stdout, platform });
|
|
94
96
|
const viewportWidth = detectViewportWidth({ stdout, env });
|
|
95
97
|
|
|
96
|
-
const theme = createTheme(colorLevel);
|
|
97
|
-
const layout = createLayout(theme, unicodeEnabled);
|
|
98
|
+
const theme = createTheme(colorLevel, { colorProfile });
|
|
99
|
+
const layout = createLayout(theme, unicodeEnabled, { density });
|
|
98
100
|
|
|
99
101
|
// Backward-compat style object (callers in tests may use renderer.style.*)
|
|
100
102
|
const style = {
|
|
@@ -123,9 +123,10 @@ const BOX_ASCII = {
|
|
|
123
123
|
},
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
-
export function createLayout(theme, unicodeEnabled) {
|
|
126
|
+
export function createLayout(theme, unicodeEnabled, options = {}) {
|
|
127
127
|
const box = unicodeEnabled ? BOX : BOX_ASCII;
|
|
128
128
|
const brand = getBrand();
|
|
129
|
+
const density = options.density || "balanced";
|
|
129
130
|
|
|
130
131
|
// ── Banner ────────────────────────────────────────────────────────────────
|
|
131
132
|
|
|
@@ -154,8 +155,10 @@ export function createLayout(theme, unicodeEnabled) {
|
|
|
154
155
|
const d = box.d;
|
|
155
156
|
const hLine = d.h.repeat(innerW + 2); // +2 for padding spaces inside border
|
|
156
157
|
|
|
157
|
-
const
|
|
158
|
-
|
|
158
|
+
const rowCount = Math.max(logo.length, rightLines.length);
|
|
159
|
+
const rows = Array.from({ length: rowCount }, (_, i) => {
|
|
160
|
+
const logoLine = logo[i] ?? "";
|
|
161
|
+
const logoColored = logoLine ? theme.primary(theme.bold(logoLine)) : "";
|
|
159
162
|
const rightLine = rightLines[i] || "";
|
|
160
163
|
const logoLen = logoW;
|
|
161
164
|
const logoPadded = padEnd(logoColored, logoLen + (visLen(logoColored) - stripAnsi(logoColored).length));
|
|
@@ -192,8 +195,8 @@ export function createLayout(theme, unicodeEnabled) {
|
|
|
192
195
|
if (!unicodeEnabled) {
|
|
193
196
|
return theme.bold(title);
|
|
194
197
|
}
|
|
195
|
-
const
|
|
196
|
-
const decorated = theme.primary(
|
|
198
|
+
const rightRuleLength = density === "compact" ? Math.max(2, 28 - title.length) : Math.max(4, 36 - title.length);
|
|
199
|
+
const decorated = `${theme.primary("─")} ${theme.bold(title)} ${theme.muted("─".repeat(rightRuleLength))}`;
|
|
197
200
|
return decorated;
|
|
198
201
|
}
|
|
199
202
|
|
|
@@ -201,7 +204,7 @@ export function createLayout(theme, unicodeEnabled) {
|
|
|
201
204
|
|
|
202
205
|
function borderedTable(columns, rows, opts = {}) {
|
|
203
206
|
if (!columns || columns.length === 0) return "";
|
|
204
|
-
const maxColW = opts.maxColWidth ||
|
|
207
|
+
const maxColW = opts.maxColWidth || (density === "compact" ? 32 : 40);
|
|
205
208
|
const viewportWidth = Math.max(40, Number(opts.viewportWidth) || 80);
|
|
206
209
|
const headers = columns.map((c) => String(c.header));
|
|
207
210
|
const detailMarker = unicodeEnabled ? "↳ " : "-> ";
|
|
@@ -302,7 +305,8 @@ export function createLayout(theme, unicodeEnabled) {
|
|
|
302
305
|
return cardRows.join("\n");
|
|
303
306
|
}
|
|
304
307
|
|
|
305
|
-
const
|
|
308
|
+
const borderSlack = density === "compact" ? 0 : 2;
|
|
309
|
+
const canUseBordered = unicodeEnabled && (preferredSum + borderedOverhead <= (viewportWidth - borderSlack));
|
|
306
310
|
if (canUseBordered) {
|
|
307
311
|
const widths = preferredWidths;
|
|
308
312
|
const topBorder = makeDivider(widths, s.tl, s.mt, s.tr, s.h);
|
|
@@ -69,7 +69,8 @@ const PALETTE_BG = {
|
|
|
69
69
|
|
|
70
70
|
const identity = (value) => String(value);
|
|
71
71
|
|
|
72
|
-
export function createTheme(level) {
|
|
72
|
+
export function createTheme(level, options = {}) {
|
|
73
|
+
const colorProfile = options.colorProfile || "mono-accent";
|
|
73
74
|
if (level === 0) {
|
|
74
75
|
const noop = identity;
|
|
75
76
|
return {
|
|
@@ -98,7 +99,7 @@ export function createTheme(level) {
|
|
|
98
99
|
return identity;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
|
|
102
|
+
const baseTheme = {
|
|
102
103
|
level,
|
|
103
104
|
primary: makeColor("primary"),
|
|
104
105
|
accent: makeColor("accent"),
|
|
@@ -117,4 +118,17 @@ export function createTheme(level) {
|
|
|
117
118
|
bgWarn: makeBg("warn"),
|
|
118
119
|
bgError: makeBg("error"),
|
|
119
120
|
};
|
|
121
|
+
|
|
122
|
+
if (colorProfile === "mono-accent") {
|
|
123
|
+
return {
|
|
124
|
+
...baseTheme,
|
|
125
|
+
success: baseTheme.accent,
|
|
126
|
+
warn: identity,
|
|
127
|
+
error: identity,
|
|
128
|
+
info: baseTheme.accent,
|
|
129
|
+
magenta: baseTheme.accent
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return baseTheme;
|
|
120
134
|
}
|