sprag-cli 3.43.0 → 3.45.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.ko.md +54 -571
- package/README.md +105 -1259
- package/bin/cli.js +18 -25
- package/package.json +1 -1
- package/presets/model-rules.json +50 -9
- package/presets/ratchet-rules.json +2 -2
- package/src/brief.js +1 -1
- package/src/commands/brief.js +14 -2
- package/src/commands/mode.js +22 -2
- package/src/commands/route-scan.js +3 -3
- package/src/commands/upgrade.js +27 -3
- package/src/config.js +22 -3
- package/src/formatters/statusline.js +255 -100
- package/src/glyphs.js +78 -0
- package/src/harness-templates.js +5 -4
- package/src/litellm-budget.js +1 -1
- package/src/model-rules.js +53 -17
- package/src/route-inject.js +86 -0
- package/src/route-scan.js +1 -1
- package/src/seed-rules.js +4 -0
- package/src/statusline-mode.js +95 -0
- package/src/update-check.js +20 -3
- package/src/window-labels.js +17 -7
package/bin/cli.js
CHANGED
|
@@ -209,7 +209,7 @@ async function main() {
|
|
|
209
209
|
// sprag route-scan --hook # SessionStart hook mode (context injection)
|
|
210
210
|
// sprag route-scan dismiss <N> # mute candidate R<N>
|
|
211
211
|
// Promote a candidate to a ratchet rule (scope is always explicit):
|
|
212
|
-
// sprag harness promote R<N> --project
|
|
212
|
+
// sprag harness promote R<N> --global|--project
|
|
213
213
|
// brief --hook — UserPromptSubmit hook mode: per-session, change-triggered
|
|
214
214
|
// briefing of state the statusline can only chip (ctx tier crossings,
|
|
215
215
|
// mid-session route/rule-health changes). Silent when nothing changed.
|
|
@@ -397,9 +397,10 @@ async function main() {
|
|
|
397
397
|
process.exit(1);
|
|
398
398
|
}
|
|
399
399
|
const { formatReport } = await import('../src/formatters/statusline.js');
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
400
|
+
// Same resolver as the real statusline path: a demo that renders emoji
|
|
401
|
+
// where the real line would not is worse than no demo at all.
|
|
402
|
+
const { resolveLabelMode } = await import('../src/statusline-mode.js');
|
|
403
|
+
const { mode: labelMode } = resolveLabelMode({ hasFlag, cfg });
|
|
403
404
|
const isVerbose = hasFlag('--verbose')
|
|
404
405
|
? true
|
|
405
406
|
: (hasFlag('--no-verbose') || hasFlag('--compact') ? false : cfg.verbose);
|
|
@@ -413,7 +414,7 @@ async function main() {
|
|
|
413
414
|
color: colorOk,
|
|
414
415
|
verbose: isVerbose,
|
|
415
416
|
timer: showTimer,
|
|
416
|
-
mode:
|
|
417
|
+
mode: labelMode,
|
|
417
418
|
segments,
|
|
418
419
|
singleLine: hasFlag('--single-line'),
|
|
419
420
|
});
|
|
@@ -480,9 +481,11 @@ async function main() {
|
|
|
480
481
|
const { statuslineDefaults } = await import('../src/config.js');
|
|
481
482
|
const cfg = statuslineDefaults();
|
|
482
483
|
const colorOk = !hasFlag('--no-color') && !process.env.NO_COLOR && cfg.color;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
484
|
+
// Third caller of the same decision — the no-session line has to agree
|
|
485
|
+
// with the populated one, or IntelliJ users watch icons appear and
|
|
486
|
+
// disappear as the analysis window empties and fills.
|
|
487
|
+
const { resolveLabelMode } = await import('../src/statusline-mode.js');
|
|
488
|
+
const { mode: labelMode } = resolveLabelMode({ hasFlag, cfg });
|
|
486
489
|
const stdinJson = readStdinJson();
|
|
487
490
|
const caps = extractCaps(stdinJson);
|
|
488
491
|
const model = extractModel(stdinJson);
|
|
@@ -494,7 +497,7 @@ async function main() {
|
|
|
494
497
|
}
|
|
495
498
|
console.log(formatNoSession(
|
|
496
499
|
{ caps, model, windowLabel, version: PKG_VERSION, update: readUpdateChip() },
|
|
497
|
-
{ color: colorOk, mode:
|
|
500
|
+
{ color: colorOk, mode: labelMode },
|
|
498
501
|
));
|
|
499
502
|
return;
|
|
500
503
|
}
|
|
@@ -749,21 +752,11 @@ async function main() {
|
|
|
749
752
|
const { statuslineDefaults } = await import('../src/config.js');
|
|
750
753
|
const cfg = statuslineDefaults();
|
|
751
754
|
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
// Force text mode unconditionally inside IntelliJ — even past an explicit
|
|
758
|
-
// `--icon` flag, since wrappers commonly hardcode `--icon` and the user
|
|
759
|
-
// can't easily edit them; icon mode is just broken there.
|
|
760
|
-
const isIntelliJ = process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm';
|
|
761
|
-
// CLI flags take precedence; otherwise fall back to persisted config.
|
|
762
|
-
const isIcon = isIntelliJ
|
|
763
|
-
? false
|
|
764
|
-
: (hasFlag('--icon')
|
|
765
|
-
? true
|
|
766
|
-
: (hasFlag('--no-icon') || hasFlag('--text') ? false : cfg.icon));
|
|
755
|
+
// Icon vs text is decided in one module shared with the `--demo` path, so
|
|
756
|
+
// the two cannot drift apart again. It also owns the IntelliJ downgrade and
|
|
757
|
+
// the escape hatch out of it (SPRAG_ICON=force / `sprag mode icon-force`).
|
|
758
|
+
const { resolveLabelMode } = await import('../src/statusline-mode.js');
|
|
759
|
+
const { mode: labelMode } = resolveLabelMode({ hasFlag, cfg });
|
|
767
760
|
const isVerbose = hasFlag('--verbose')
|
|
768
761
|
? true
|
|
769
762
|
: (hasFlag('--no-verbose') || hasFlag('--compact') ? false : cfg.verbose);
|
|
@@ -779,7 +772,7 @@ async function main() {
|
|
|
779
772
|
color: colorOk,
|
|
780
773
|
verbose: isVerbose,
|
|
781
774
|
timer: showTimer,
|
|
782
|
-
mode:
|
|
775
|
+
mode: labelMode,
|
|
783
776
|
segments,
|
|
784
777
|
// macOS builds of Claude Code have rendered only the first line of a
|
|
785
778
|
// multi-line statusline in some versions (anthropics/claude-code#35176)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.45.0",
|
|
4
4
|
"description": "Sprag - a quality ratchet harness for AI coding agents: model-fitting delegation, ratchet rules, doc2md, Korean style gate. Same tool as claude-token-saver, under its new name.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/presets/model-rules.json
CHANGED
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
"agent": "haiku-runner",
|
|
22
22
|
"example": "npm test 돌려봐",
|
|
23
23
|
"exampleEn": "run npm test",
|
|
24
|
-
"budget": {
|
|
24
|
+
"budget": {
|
|
25
|
+
"calls": 8,
|
|
26
|
+
"out": 1500
|
|
27
|
+
}
|
|
25
28
|
},
|
|
26
29
|
{
|
|
27
30
|
"id": "run-t1",
|
|
@@ -32,7 +35,10 @@
|
|
|
32
35
|
"agent": "sonnet",
|
|
33
36
|
"example": "빌드하고 실패한 테스트만 보여줘",
|
|
34
37
|
"exampleEn": "build it and show only the failing tests",
|
|
35
|
-
"budget": {
|
|
38
|
+
"budget": {
|
|
39
|
+
"calls": null,
|
|
40
|
+
"out": 6000
|
|
41
|
+
}
|
|
36
42
|
},
|
|
37
43
|
{
|
|
38
44
|
"id": "explore-t2",
|
|
@@ -43,7 +49,10 @@
|
|
|
43
49
|
"agent": "haiku-explore",
|
|
44
50
|
"example": "이 설정값 어느 파일에 있어?",
|
|
45
51
|
"exampleEn": "which file holds this config value?",
|
|
46
|
-
"budget": {
|
|
52
|
+
"budget": {
|
|
53
|
+
"calls": 8,
|
|
54
|
+
"out": 1500
|
|
55
|
+
}
|
|
47
56
|
},
|
|
48
57
|
{
|
|
49
58
|
"id": "explore-t1",
|
|
@@ -54,7 +63,10 @@
|
|
|
54
63
|
"agent": "sonnet",
|
|
55
64
|
"example": "이 함수를 호출하는 곳 전부 찾아줘",
|
|
56
65
|
"exampleEn": "find every caller of this function",
|
|
57
|
-
"budget": {
|
|
66
|
+
"budget": {
|
|
67
|
+
"calls": null,
|
|
68
|
+
"out": 6000
|
|
69
|
+
}
|
|
58
70
|
},
|
|
59
71
|
{
|
|
60
72
|
"id": "check-t2",
|
|
@@ -65,7 +77,10 @@
|
|
|
65
77
|
"agent": "haiku-explore",
|
|
66
78
|
"example": "지금 배포된 버전이 뭔지 확인해줘",
|
|
67
79
|
"exampleEn": "check which version is deployed right now",
|
|
68
|
-
"budget": {
|
|
80
|
+
"budget": {
|
|
81
|
+
"calls": 8,
|
|
82
|
+
"out": 1500
|
|
83
|
+
}
|
|
69
84
|
},
|
|
70
85
|
{
|
|
71
86
|
"id": "check-t1",
|
|
@@ -76,7 +91,10 @@
|
|
|
76
91
|
"agent": "sonnet",
|
|
77
92
|
"example": "마이그레이션이 실제로 적용됐는지 확인해줘",
|
|
78
93
|
"exampleEn": "verify the migration actually applied",
|
|
79
|
-
"budget": {
|
|
94
|
+
"budget": {
|
|
95
|
+
"calls": null,
|
|
96
|
+
"out": 6000
|
|
97
|
+
}
|
|
80
98
|
},
|
|
81
99
|
{
|
|
82
100
|
"id": "paste-t2",
|
|
@@ -87,7 +105,10 @@
|
|
|
87
105
|
"agent": "haiku-explore",
|
|
88
106
|
"example": "이 에러 로그는 무슨 뜻이야?",
|
|
89
107
|
"exampleEn": "what does this error log mean?",
|
|
90
|
-
"budget": {
|
|
108
|
+
"budget": {
|
|
109
|
+
"calls": 8,
|
|
110
|
+
"out": 1500
|
|
111
|
+
}
|
|
91
112
|
},
|
|
92
113
|
{
|
|
93
114
|
"id": "paste-t1",
|
|
@@ -98,7 +119,10 @@
|
|
|
98
119
|
"agent": "sonnet",
|
|
99
120
|
"example": "이 스택트레이스에서 어느 모듈이 죽은 건지 짚어줘",
|
|
100
121
|
"exampleEn": "point out which module failed in this stack trace",
|
|
101
|
-
"budget": {
|
|
122
|
+
"budget": {
|
|
123
|
+
"calls": null,
|
|
124
|
+
"out": 8000
|
|
125
|
+
}
|
|
102
126
|
},
|
|
103
127
|
{
|
|
104
128
|
"id": "read-t2",
|
|
@@ -109,7 +133,24 @@
|
|
|
109
133
|
"agent": "haiku-explore",
|
|
110
134
|
"example": "이 README 요약해줘",
|
|
111
135
|
"exampleEn": "summarize this README",
|
|
112
|
-
"budget": {
|
|
136
|
+
"budget": {
|
|
137
|
+
"calls": 8,
|
|
138
|
+
"out": 1900
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "read-t1",
|
|
143
|
+
"tier": "T1",
|
|
144
|
+
"category": "read",
|
|
145
|
+
"label": "읽기·요약·설명",
|
|
146
|
+
"labelEn": "reading / summarizing / explaining",
|
|
147
|
+
"agent": "sonnet",
|
|
148
|
+
"example": "이 함수가 하는 일을 단계별로 정리해줘",
|
|
149
|
+
"exampleEn": "walk through what this function does, step by step",
|
|
150
|
+
"budget": {
|
|
151
|
+
"calls": null,
|
|
152
|
+
"out": 8000
|
|
153
|
+
}
|
|
113
154
|
}
|
|
114
155
|
]
|
|
115
156
|
}
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"en": "When a web fetch/search is blocked by robots or user-agent checks ('unable to fetch' and similar) → retry with curl carrying a browser UA header (Mozilla/5.0 ... Chrome/...). Sites like reddit also answer on their .json endpoints with that UA"
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
|
-
"ko": "ratchet 룰 promote 호출 직전 사용자에게 scope(project
|
|
19
|
-
"en": "Ask the user for the scope (
|
|
18
|
+
"ko": "ratchet 룰 promote 호출 직전 사용자에게 scope(global/project)를 먼저 묻는다 — Bash 환경은 non-TTY라 CLI readline 프롬프트가 안 뜨므로, 호출자(LLM)가 대신 묻고 --global/--project 플래그를 명시해 실행",
|
|
19
|
+
"en": "Ask the user for the scope (global or project) before calling ratchet promote — the Bash environment is non-TTY, so the CLI's readline prompt never appears; the caller (the model) has to ask and pass --global/--project explicitly"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"ko": "자동화 도구가 grep·정규식으로 찾는 마커 텍스트(헤더·앵커·키워드 라인)는 리팩터링·압축·수정 금지 — 정확 매칭 검출은 한 글자만 바뀌어도 깨짐",
|
package/src/brief.js
CHANGED
|
@@ -226,7 +226,7 @@ export async function runBrief({ sessionId, transcriptPath, now = Date.now() })
|
|
|
226
226
|
}
|
|
227
227
|
for (const [kind, x] of fresh) {
|
|
228
228
|
items.push(kind === 'route'
|
|
229
|
-
? `새 위임 후보가 감지되었습니다 — "${x.label}" 유형 ${x.count}회 반복(statusline의 route? R${x.id} 칩). 등록: sprag harness promote R${x.id} --project
|
|
229
|
+
? `새 위임 후보가 감지되었습니다 — "${x.label}" 유형 ${x.count}회 반복(statusline의 route? R${x.id} 칩). 등록: sprag harness promote R${x.id} --global|--project (적용 범위는 사용자에게 확인) / 무시: route-scan dismiss ${x.id}`
|
|
230
230
|
: `승인된 위임 룰의 최근 에러율이 기준(20%)을 넘었습니다 — "${x.label}" (${x.healthSource === 'delegated' ? `위임 실행 ${x.delegatedRuns}건 중 에러율 ${Math.round((x.delegatedErrRate || 0) * 100)}%` : `유사 에피소드 기준 에러율 ${Math.round((x.errRate || 0) * 100)}%`}, statusline의 rule-health 칩). 조건 좁히기/제거를 사용자와 상의하세요: sprag route-scan rules`);
|
|
231
231
|
}
|
|
232
232
|
s.briefed = [...briefed];
|
package/src/commands/brief.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* sprag route-scan --hook # SessionStart hook mode (context injection)
|
|
8
8
|
* sprag route-scan dismiss <N> # mute candidate R<N>
|
|
9
9
|
* Promote a candidate to a ratchet rule (scope is always explicit):
|
|
10
|
-
* sprag harness promote R<N> --project
|
|
10
|
+
* sprag harness promote R<N> --global|--project
|
|
11
11
|
* brief --hook — UserPromptSubmit hook mode: per-session, change-triggered
|
|
12
12
|
* briefing of state the statusline can only chip (ctx tier crossings,
|
|
13
13
|
* mid-session route/rule-health changes). Silent when nothing changed.
|
|
@@ -22,10 +22,22 @@ export async function run({ hasFlag }) {
|
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
24
|
const ctx = readStdinJson() || {};
|
|
25
|
+
const parts = [];
|
|
25
26
|
try {
|
|
26
27
|
const { runBrief } = await import('../brief.js');
|
|
27
28
|
const out = await runBrief({ sessionId: ctx.session_id, transcriptPath: ctx.transcript_path });
|
|
28
|
-
if (out)
|
|
29
|
+
if (out) parts.push(out);
|
|
29
30
|
} catch (e) { debug('brief:hook', e); /* briefing is best-effort — never block a prompt */ }
|
|
31
|
+
// State the matching delegation rule as a fact about this request. Registered
|
|
32
|
+
// rules were going unused because applying one meant scanning a list and
|
|
33
|
+
// resolving it against a general "don't spawn subagents" instruction; naming
|
|
34
|
+
// the match here removes both steps. Silent unless it is sure — see
|
|
35
|
+
// src/route-inject.js.
|
|
36
|
+
try {
|
|
37
|
+
const { routeHint } = await import('../route-inject.js');
|
|
38
|
+
const hint = routeHint(ctx.prompt);
|
|
39
|
+
if (hint) parts.push(hint);
|
|
40
|
+
} catch (e) { debug('brief:route-hint', e); }
|
|
41
|
+
if (parts.length) console.log(parts.join('\n'));
|
|
30
42
|
return;
|
|
31
43
|
}
|
package/src/commands/mode.js
CHANGED
|
@@ -10,12 +10,28 @@
|
|
|
10
10
|
export async function run({ args }) {
|
|
11
11
|
const { applyMode, loadConfig, configPath, statuslineDefaults, userLanguage, VALID_KEYWORDS } =
|
|
12
12
|
await import('../config.js');
|
|
13
|
+
const { resolveLabelMode, isJetBrainsTerminal } = await import('../statusline-mode.js');
|
|
13
14
|
const words = args.slice(1);
|
|
14
15
|
if (words.length === 0) {
|
|
15
16
|
const eff = statuslineDefaults();
|
|
16
17
|
const raw = loadConfig();
|
|
17
18
|
console.log('Statusline (effective):');
|
|
18
|
-
console.log(`
|
|
19
|
+
console.log(` labels: ${eff.labels}`);
|
|
20
|
+
// What renders can differ from the stored preference: inside IntelliJ the
|
|
21
|
+
// emoji set steps down to narrow glyphs, because the IDE font lacks the
|
|
22
|
+
// emoji and the fallback breaks the cell grid. Saying so here is the
|
|
23
|
+
// difference between a two-second answer and a hunt through the source for
|
|
24
|
+
// why `--icon` appeared to do nothing.
|
|
25
|
+
const { mode: labelMode, reason } = resolveLabelMode({ cfg: eff });
|
|
26
|
+
if (labelMode !== eff.labels) {
|
|
27
|
+
const why = reason === 'intellij-narrow'
|
|
28
|
+
? ' (IntelliJ: emoji garble in the IDE font. `sprag mode icon-force` keeps them anyway)'
|
|
29
|
+
: reason === 'intellij-forced'
|
|
30
|
+
? ' (icon-force)'
|
|
31
|
+
: '';
|
|
32
|
+
console.log(` renders: ${labelMode}${why}`);
|
|
33
|
+
}
|
|
34
|
+
console.log(` icon-force: ${eff.iconForce}`);
|
|
19
35
|
console.log(` verbose: ${eff.verbose}`);
|
|
20
36
|
console.log(` timer: ${eff.timer}`);
|
|
21
37
|
console.log(` color: ${eff.color}`);
|
|
@@ -40,7 +56,11 @@ export async function run({ args }) {
|
|
|
40
56
|
}
|
|
41
57
|
const eff = statuslineDefaults();
|
|
42
58
|
console.log(`Updated: ${applied.join(', ')}`);
|
|
43
|
-
console.log(`Now: icon=${eff.
|
|
59
|
+
console.log(`Now: labels=${eff.labels} icon-force=${eff.iconForce} verbose=${eff.verbose} timer=${eff.timer} color=${eff.color} window=${eff.windowLabel} language=${userLanguage()}`);
|
|
60
|
+
const { mode: renders } = resolveLabelMode({ cfg: eff });
|
|
61
|
+
if (renders !== eff.labels) {
|
|
62
|
+
console.log(`Renders as: ${renders}${isJetBrainsTerminal() ? ' (IntelliJ detected)' : ''}`);
|
|
63
|
+
}
|
|
44
64
|
console.log('Statusline picks up the change on the next refresh (~1s).');
|
|
45
65
|
return;
|
|
46
66
|
}
|
|
@@ -356,11 +356,11 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
356
356
|
}
|
|
357
357
|
if (lang === 'ko') {
|
|
358
358
|
lines.push('등록하면 다음 세션부터 자동 위임됩니다. 사용자에게 등록 여부를 물을 때 위 룰 원문을 그대로 보여주고, 적용 범위까지 확인한 뒤 실행하세요:');
|
|
359
|
-
lines.push(' sprag harness promote R<N> --project
|
|
359
|
+
lines.push(' sprag harness promote R<N> --global|--project # 적용 범위는 반드시 사용자에게 확인');
|
|
360
360
|
lines.push(' sprag route-scan dismiss <N> # 사용자가 원치 않으면');
|
|
361
361
|
} else {
|
|
362
362
|
lines.push('Once registered, delegation happens automatically from the next session. Show the user the rule text verbatim, confirm the scope with them, then run:');
|
|
363
|
-
lines.push(' sprag harness promote R<N> --project
|
|
363
|
+
lines.push(' sprag harness promote R<N> --global|--project # ALWAYS confirm the scope with the user first');
|
|
364
364
|
lines.push(' sprag route-scan dismiss <N> # if they do not want it');
|
|
365
365
|
}
|
|
366
366
|
}
|
|
@@ -446,7 +446,7 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
446
446
|
}
|
|
447
447
|
console.log('');
|
|
448
448
|
console.log(lang === 'ko' ? '등록 / 무시:' : 'Promote / dismiss:');
|
|
449
|
-
console.log(' sprag harness promote R<N> --project
|
|
449
|
+
console.log(' sprag harness promote R<N> --global|--project');
|
|
450
450
|
console.log(' sprag route-scan dismiss <N>');
|
|
451
451
|
// 최초 1회만 — 매번 찍으면 도구가 광고판이 된다 (CTS_NO_NOTE=1 로 끔)
|
|
452
452
|
(await import('../first-run-note.js')).printOnce('route-scan', lang);
|
package/src/commands/upgrade.js
CHANGED
|
@@ -25,7 +25,8 @@ function runCommand(cmd) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export async function run({ hasFlag, version }) {
|
|
28
|
-
const { updateStatus, refreshUpdateState, upgradeCommand } =
|
|
28
|
+
const { updateStatus, refreshUpdateState, upgradeCommand, INSTALLED_UNDER_LEGACY_NAME } =
|
|
29
|
+
await import('../update-check.js');
|
|
29
30
|
const { userLanguage } = await import('../config.js');
|
|
30
31
|
const lang = userLanguage();
|
|
31
32
|
const cmd = upgradeCommand();
|
|
@@ -40,6 +41,25 @@ export async function run({ hasFlag, version }) {
|
|
|
40
41
|
// network round trip is fine here.
|
|
41
42
|
await refreshUpdateState(version);
|
|
42
43
|
const s = updateStatus(version);
|
|
44
|
+
// A legacy-name copy has somewhere to go even on the newest version: the
|
|
45
|
+
// package was renamed, and only the new name keeps receiving releases.
|
|
46
|
+
if (!s.available && INSTALLED_UNDER_LEGACY_NAME) {
|
|
47
|
+
console.log(lang === 'ko'
|
|
48
|
+
? `패키지 이름이 claude-token-saver 에서 sprag-cli 로 바뀌었습니다. 버전은 최신(v${version})이지만, 앞으로의 릴리스를 계속 받으려면 새 이름으로 옮겨야 합니다.`
|
|
49
|
+
: `The package was renamed from claude-token-saver to sprag-cli. You are on the latest version (v${version}), but future releases only ship under the new name.`);
|
|
50
|
+
console.log(`$ ${cmd}`);
|
|
51
|
+
const migrateCode = await runCommand(cmd);
|
|
52
|
+
if (migrateCode !== 0) {
|
|
53
|
+
console.error(lang === 'ko'
|
|
54
|
+
? `이전 명령이 종료 코드 ${migrateCode}로 실패했습니다. 위 출력을 확인하십시오.`
|
|
55
|
+
: `Migration command failed with exit code ${migrateCode}. Check the output above.`);
|
|
56
|
+
process.exit(migrateCode);
|
|
57
|
+
}
|
|
58
|
+
console.log(lang === 'ko'
|
|
59
|
+
? '이전이 끝났습니다. 새 셸에서 sprag --version 으로 확인하십시오.'
|
|
60
|
+
: 'Migration done. Confirm with sprag --version in a fresh shell.');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
43
63
|
if (!s.available) {
|
|
44
64
|
console.log(lang === 'ko'
|
|
45
65
|
? `이미 최신 버전입니다 (v${version}). 설치할 것이 없습니다.`
|
|
@@ -48,8 +68,12 @@ export async function run({ hasFlag, version }) {
|
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
console.log(lang === 'ko'
|
|
51
|
-
?
|
|
52
|
-
|
|
71
|
+
? (INSTALLED_UNDER_LEGACY_NAME
|
|
72
|
+
? `v${version} → ${s.latest} 로 업그레이드하면서 새 패키지 이름(sprag-cli)으로 함께 옮깁니다.`
|
|
73
|
+
: `v${version} → ${s.latest} 로 업그레이드합니다.`)
|
|
74
|
+
: (INSTALLED_UNDER_LEGACY_NAME
|
|
75
|
+
? `Upgrading v${version} → ${s.latest} and moving to the new package name (sprag-cli).`
|
|
76
|
+
: `Upgrading v${version} → ${s.latest}.`));
|
|
53
77
|
console.log(`$ ${cmd}`);
|
|
54
78
|
const code = await runCommand(cmd);
|
|
55
79
|
if (code !== 0) {
|
package/src/config.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { userDataDir } from './paths.js';
|
|
15
|
+
import { labelPreference } from './statusline-mode.js';
|
|
15
16
|
|
|
16
17
|
const CONFIG_DIR = userDataDir();
|
|
17
18
|
const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
|
|
@@ -38,8 +39,17 @@ export function saveConfig(cfg) {
|
|
|
38
39
|
// Orthogonal — `mode icon verbose` flips both without resetting the rest.
|
|
39
40
|
// Statusline-only toggles. Stored under `cfg.statusline`.
|
|
40
41
|
const KEYWORDS = {
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
// Label style is one setting with three values, so the keywords write one key
|
|
43
|
+
// rather than a pair of booleans that could contradict each other.
|
|
44
|
+
icon: { key: 'labels', value: 'icon' },
|
|
45
|
+
narrow: { key: 'labels', value: 'narrow' },
|
|
46
|
+
text: { key: 'labels', value: 'text' },
|
|
47
|
+
// Opt back into icons inside IntelliJ, where they are downgraded by default
|
|
48
|
+
// for the plugin's statusline widget. Scoped to that guard only: outside
|
|
49
|
+
// IntelliJ it changes nothing, because `icon` / `text` already decide there.
|
|
50
|
+
// See src/statusline-mode.js.
|
|
51
|
+
'icon-force': { key: 'iconForce', value: true },
|
|
52
|
+
'no-icon-force': { key: 'iconForce', value: false },
|
|
43
53
|
verbose: { key: 'verbose', value: true },
|
|
44
54
|
compact: { key: 'verbose', value: false },
|
|
45
55
|
timer: { key: 'timer', value: true },
|
|
@@ -171,8 +181,17 @@ export function statuslineDefaults() {
|
|
|
171
181
|
} else {
|
|
172
182
|
windowHours = 24; // default: last 1 day
|
|
173
183
|
}
|
|
184
|
+
// Three-valued label style, with the legacy boolean honored for configs
|
|
185
|
+
// written before it existed. labelPreference() owns that fallback so the
|
|
186
|
+
// resolver and this function cannot disagree about what an old config means.
|
|
187
|
+
const labels = labelPreference(s);
|
|
174
188
|
return {
|
|
175
|
-
|
|
189
|
+
labels,
|
|
190
|
+
// Kept for callers that only ask "emoji or not".
|
|
191
|
+
icon: labels === 'icon',
|
|
192
|
+
// Off unless asked for: narrow labels are the IntelliJ default now, and only
|
|
193
|
+
// the user knows whether they would rather have garbled emoji than glyphs.
|
|
194
|
+
iconForce: s.iconForce === true,
|
|
176
195
|
verbose: s.verbose !== false,
|
|
177
196
|
timer: s.timer !== false,
|
|
178
197
|
color: s.color !== false,
|