@litfamily/litgrok 1.0.18 → 1.0.19
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/.grok/hooks/lit-status-line.mjs +194 -15
- package/.grok/hooks/plan-progress.mjs +36 -0
- package/.grok/hooks/post-tool-use-failure.mjs +2 -1
- package/.grok/hooks/post-tool-use.json +0 -1
- package/.grok/hooks/post-tool-use.mjs +2 -2
- package/.grok/hooks/pre-tool-use.json +15 -0
- package/.grok/hooks/pre-tool-use.mjs +7 -0
- package/.grok/hooks/record-passive-event.mjs +32 -1
- package/.grok/hooks/stop.mjs +19 -1
- package/.grok/hooks/workflow-stage.mjs +189 -0
- package/.grok/skills/litgrok/SKILL.md +4 -2
- package/.grok-plugin/plugin.json +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +9 -9
- package/README_ko-KR.md +9 -9
- package/bin/litgrok.mjs +26 -8
- package/bin/status-line-config.mjs +71 -40
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/privacy.md +1 -1
- package/docs/reference.md +6 -2
- package/docs/reference_ko-KR.md +6 -2
- package/hooks/hooks.json +9 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Where the session is in the LIT workflow, for the status line's third row.
|
|
2
|
+
//
|
|
3
|
+
// The ledger keeps the history: a prompt that names a workflow skill carries its `stage`, and a test
|
|
4
|
+
// run's PostToolUse or PostToolUseFailure record carries its `test` verdict. The status line reads only
|
|
5
|
+
// the end of the ledger, so the hooks also keep a small per-session snapshot beside it,
|
|
6
|
+
// `<session-hash>.workflow.json`, written only once there is something to show. Neither file holds a
|
|
7
|
+
// prompt, a command line, a tool argument or tool output: only stage names, plan checkbox counts and a
|
|
8
|
+
// pass or fail verdict with one count. It depends on nothing outside this folder, so the status line
|
|
9
|
+
// can import it.
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
export const WORKFLOW_SCHEMA = 'litgrok.workflow/v1';
|
|
15
|
+
export const STAGES = Object.freeze(['interview', 'plan', 'work', 'review', 'done']);
|
|
16
|
+
const MAX_STATE_BYTES = 4096;
|
|
17
|
+
const MAX_COUNT = 1_000_000;
|
|
18
|
+
// The end of a long test log carries its summary.
|
|
19
|
+
const RESULT_TAIL_CHARACTERS = 64 * 1024;
|
|
20
|
+
|
|
21
|
+
// The first workflow skill a prompt names sets the stage. A bare `lit` starts litwork, as on row one.
|
|
22
|
+
const STAGE_PATTERN = /(?:^|[\s/])(deep-interview|lit-plan|start-work|review-work|litwork|lit)(?=$|[^\w-])/iu;
|
|
23
|
+
const STAGE_OF = { 'deep-interview': 'interview', 'lit-plan': 'plan', 'start-work': 'work', litwork: 'work', lit: 'work', 'review-work': 'review' };
|
|
24
|
+
|
|
25
|
+
// A command counts as a test run when it calls a test runner; the command text itself is never stored.
|
|
26
|
+
const TEST_COMMAND = /(?:^|[\s;&|(])(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test\b|node\s+(?:[^;&|\n]*\s)?--test\b|(?:npx\s+|bunx\s+)?(?:vitest|jest|mocha)\b|(?:python3?\s+-m\s+)?(?:pytest|unittest)\b|cargo\s+(?:test|nextest)\b|go\s+test\b|deno\s+test\b|make\s+(?:test|check)\b|(?:\.\/)?gradlew?\s+test\b|mvn\s+(?:-\S+\s+)*test\b|rspec\b|ctest\b|swift\s+test\b|dotnet\s+test\b)/u;
|
|
27
|
+
const TAP_PASS = /^\s*[#ℹ]\s*pass\s+(\d+)\s*$/gmu;
|
|
28
|
+
const TAP_FAIL = /^\s*[#ℹ]\s*fail\s+(\d+)\s*$/gmu;
|
|
29
|
+
const PASSED = /\b(\d+)\s+(?:tests?\s+)?(?:passed|passing)\b/gu;
|
|
30
|
+
const FAILED = /\b(\d+)\s+(?:tests?\s+)?(?:failed|failing)\b/gu;
|
|
31
|
+
const EXIT_TEXT = /\bexit(?:ed with)?\s+(?:code|status)[\s:=]+(-?\d+)/giu;
|
|
32
|
+
const EXIT_KEY = /^(?:exit_?code|exitCode|exit_?status|returncode)$/iu;
|
|
33
|
+
const FAIL_MARK = /^(?:FAIL\b|not ok \d)/mu;
|
|
34
|
+
|
|
35
|
+
// Heredoc bodies and quoted strings are data: a script that only mentions a test runner, such as a plan
|
|
36
|
+
// check that embeds the plan's test command, is not a test run.
|
|
37
|
+
function commandWords(command) {
|
|
38
|
+
const kept = [];
|
|
39
|
+
let delimiter = null;
|
|
40
|
+
for (const line of command.split('\n')) {
|
|
41
|
+
if (delimiter !== null) {
|
|
42
|
+
if (line.trim() === delimiter) delimiter = null;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
kept.push(line);
|
|
46
|
+
const heredoc = /<<-?\s*(['"]?)([A-Za-z_][\w-]*)\1/u.exec(line);
|
|
47
|
+
if (heredoc) delimiter = heredoc[2];
|
|
48
|
+
}
|
|
49
|
+
return kept.join('\n').replace(/'[^'\n]*'|"(?:[^"\\\n]|\\.)*"/gu, "''");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function stageFromPrompt(visibleText) {
|
|
53
|
+
if (typeof visibleText !== 'string') return null;
|
|
54
|
+
const match = STAGE_PATTERN.exec(visibleText);
|
|
55
|
+
return match ? STAGE_OF[match[1].toLowerCase()] : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function lastCount(text, pattern) {
|
|
59
|
+
let count = null;
|
|
60
|
+
for (const match of text.matchAll(pattern)) count = Number(match[1]);
|
|
61
|
+
return count !== null && Number.isSafeInteger(count) && count <= MAX_COUNT ? count : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resultText(result) {
|
|
65
|
+
if (typeof result === 'string') return { text: result.slice(-RESULT_TAIL_CHARACTERS), exitCode: null };
|
|
66
|
+
const parts = [];
|
|
67
|
+
let exitCode = null;
|
|
68
|
+
const walk = (value, key, depth) => {
|
|
69
|
+
if (depth > 4) return;
|
|
70
|
+
if (typeof value === 'string') parts.push(value);
|
|
71
|
+
else if (typeof value === 'number' && Number.isInteger(value) && EXIT_KEY.test(key)) exitCode = value;
|
|
72
|
+
else if (value && typeof value === 'object') {
|
|
73
|
+
for (const [childKey, child] of Object.entries(value)) walk(child, childKey, depth + 1);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
walk(result, '', 0);
|
|
77
|
+
return { text: parts.join('\n').slice(-RESULT_TAIL_CHARACTERS), exitCode };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The verdict of a test run, or null when the tool call was not a test run or its outcome cannot be
|
|
82
|
+
* read. Returns { verdict: 'pass' | 'fail', count? }: the passing count for a pass, the failing count
|
|
83
|
+
* for a fail, each only when the runner printed it.
|
|
84
|
+
*/
|
|
85
|
+
export function testVerdict({ toolInput, toolResult }, failed = false) {
|
|
86
|
+
const command = toolInput?.command ?? toolInput?.cmd;
|
|
87
|
+
if (typeof command !== 'string' || command.length > 16 * 1024 || !TEST_COMMAND.test(commandWords(command))) return null;
|
|
88
|
+
const { text, exitCode: exitKey } = resultText(toolResult);
|
|
89
|
+
const tapPassed = lastCount(text, TAP_PASS);
|
|
90
|
+
const tapFailed = lastCount(text, TAP_FAIL);
|
|
91
|
+
const tap = tapPassed !== null || tapFailed !== null;
|
|
92
|
+
const passed = tap ? tapPassed : lastCount(text, PASSED);
|
|
93
|
+
const failedCount = tap ? tapFailed : lastCount(text, FAILED);
|
|
94
|
+
const exitCode = exitKey ?? lastCount(text, EXIT_TEXT);
|
|
95
|
+
let verdict = null;
|
|
96
|
+
if (failed || (failedCount ?? 0) > 0) verdict = 'fail';
|
|
97
|
+
else if (exitCode !== null) verdict = exitCode === 0 ? 'pass' : 'fail';
|
|
98
|
+
else if (passed !== null) verdict = 'pass';
|
|
99
|
+
else if (FAIL_MARK.test(text)) verdict = 'fail';
|
|
100
|
+
if (verdict === null) return null;
|
|
101
|
+
const count = verdict === 'pass' ? passed : failedCount;
|
|
102
|
+
return count !== null && (verdict === 'pass' || count > 0) ? { verdict, count } : { verdict };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function workflowStatePath(ledgerFile) {
|
|
106
|
+
return ledgerFile.replace(/\.jsonl$/u, '.workflow.json');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const isCount = (value) => Number.isSafeInteger(value) && value >= 0 && value <= MAX_COUNT;
|
|
110
|
+
const isTime = (value) => typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
111
|
+
|
|
112
|
+
function validState(state, sessionId) {
|
|
113
|
+
return state !== null && typeof state === 'object' && !Array.isArray(state)
|
|
114
|
+
&& state.schema === WORKFLOW_SCHEMA && state.sessionId === sessionId
|
|
115
|
+
&& (state.stage === null || STAGES.includes(state.stage))
|
|
116
|
+
&& Array.isArray(state.visited) && state.visited.length <= STAGES.length && state.visited.every((stage) => STAGES.includes(stage))
|
|
117
|
+
&& typeof state.ended === 'boolean'
|
|
118
|
+
&& (state.plan === null || (isCount(state.plan?.done) && isCount(state.plan?.total) && state.plan.done <= state.plan.total))
|
|
119
|
+
&& (state.test === null || (['pass', 'fail'].includes(state.test?.verdict) && (state.test.count === undefined || isCount(state.test.count)) && isTime(state.test.at)))
|
|
120
|
+
&& isTime(state.updatedAt);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** This session's snapshot, or null when it is missing, malformed, oversized, a symlink or another session's. */
|
|
124
|
+
export function readWorkflowState(path, sessionId) {
|
|
125
|
+
let descriptor;
|
|
126
|
+
try {
|
|
127
|
+
descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const status = fstatSync(descriptor);
|
|
133
|
+
if (!status.isFile() || status.size > MAX_STATE_BYTES) return null;
|
|
134
|
+
const buffer = Buffer.alloc(status.size);
|
|
135
|
+
const state = JSON.parse(buffer.subarray(0, readSync(descriptor, buffer, 0, status.size, 0)).toString('utf8'));
|
|
136
|
+
return validState(state, sessionId) ? state : null;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
} finally {
|
|
140
|
+
closeSync(descriptor);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Applies one hook's news to the snapshot: { stage } when a prompt names a workflow skill, { ended }
|
|
146
|
+
* when a turn stops (true) or is held (false), { plan } and { test }. A turn that ends a review with
|
|
147
|
+
* every plan task checked moves the stage to done. Nothing is written for a session with nothing to show.
|
|
148
|
+
*/
|
|
149
|
+
export function updateWorkflowState(ledgerFile, sessionId, { stage, ended, plan, test, now = new Date() }) {
|
|
150
|
+
const path = workflowStatePath(ledgerFile);
|
|
151
|
+
try {
|
|
152
|
+
if (lstatSync(path).isSymbolicLink()) throw new Error('WORKFLOW_STATE_UNSAFE');
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
155
|
+
}
|
|
156
|
+
const previous = readWorkflowState(path, sessionId);
|
|
157
|
+
const state = previous ?? { schema: WORKFLOW_SCHEMA, sessionId, stage: null, visited: [], ended: false, plan: null, test: null };
|
|
158
|
+
if (stage !== undefined && stage !== null) {
|
|
159
|
+
state.stage = stage;
|
|
160
|
+
state.ended = false;
|
|
161
|
+
if (!state.visited.includes(stage)) state.visited = STAGES.filter((name) => name === stage || state.visited.includes(name));
|
|
162
|
+
}
|
|
163
|
+
if (plan !== undefined) state.plan = plan;
|
|
164
|
+
if (test) state.test = { ...test, at: now.toISOString() };
|
|
165
|
+
if (typeof ended === 'boolean' && state.stage !== null) {
|
|
166
|
+
state.ended = ended;
|
|
167
|
+
if (ended && state.stage === 'review' && state.plan && state.plan.total > 0 && state.plan.done === state.plan.total) {
|
|
168
|
+
state.stage = 'done';
|
|
169
|
+
state.visited = STAGES.filter((name) => name === 'done' || state.visited.includes(name));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (previous === null && state.stage === null && state.test === null) return null;
|
|
173
|
+
state.updatedAt = now.toISOString();
|
|
174
|
+
if (!validState(state, sessionId)) throw new Error('WORKFLOW_STATE_INVALID');
|
|
175
|
+
const content = `${JSON.stringify(state)}\n`;
|
|
176
|
+
if (Buffer.byteLength(content) > MAX_STATE_BYTES) throw new Error('WORKFLOW_STATE_TOO_LARGE');
|
|
177
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
178
|
+
try {
|
|
179
|
+
writeFileSync(temporary, content, { flag: 'wx', mode: 0o600 });
|
|
180
|
+
renameSync(temporary, path);
|
|
181
|
+
} finally {
|
|
182
|
+
try {
|
|
183
|
+
unlinkSync(temporary);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return state;
|
|
189
|
+
}
|
|
@@ -34,13 +34,15 @@ The `lit-pptx` and `lit-docx` skill folders also ship explicitly invoked engines
|
|
|
34
34
|
|
|
35
35
|
The `lit-typographic-motion` skill folder ships a film director (a treatment validator, a type engine for films that are the words themselves, a stage capture for authored films, a generated sound bed, look rounds), its QA gate and pinned locks; it never installs on first use: run `npm exec --yes --package @litfamily/litgrok@<installed version> -- litgrok motion-runtime install` once outside a Grok session to pre-warm its cache (`npm exec --yes --package @litfamily/litgrok@<installed version> -- litgrok motion-runtime status` shows the five pre-render probes). Typographic-motion engine adapted from mexicat/pdoom-video (MIT, Giacomo Magnanini), commit `ca251e3`.
|
|
36
36
|
|
|
37
|
-
The skills, agents, and rule are static guidance loaded from documented Grok Build paths. Hook commands and the opt-in status-line command are the package code executed by the host. The installer discovers, compares, copies, and removes package-owned `.grok/` files; only `install --user --status-line` also
|
|
37
|
+
The skills, agents, and rule are static guidance loaded from documented Grok Build paths. Hook commands and the opt-in status-line command are the package code executed by the host. The installer discovers, compares, copies, and removes package-owned `.grok/` files; only `install --user --status-line` and `install --user --notify` also merge a value into user config after preserving a backup.
|
|
38
38
|
|
|
39
39
|
## Install destinations
|
|
40
40
|
|
|
41
41
|
The project install writes the packaged tree into `<cwd>/.grok/`. The user install writes the whole packaged tree into `~/.grok/`, including skills, rules, and hooks. Use the dry-run first to preview exact destinations without mutation.
|
|
42
42
|
|
|
43
|
-
`install --user --status-line` is an opt-in that adds a user-level `[ui.status_line]` command in `~/.grok/config.toml` with a two-second refresh. Grok accepts this setting only from user or administrator config, not project config or a plugin. The installer backs up an existing config before changing it and leaves any existing `ui.status_line` untouched. `uninstall --user` removes only unchanged LitGrok-managed values. This status command does not add a hook registration.
|
|
43
|
+
`install --user --status-line` is an opt-in that adds a user-level `[ui.status_line]` command in `~/.grok/config.toml` with a two-second refresh. Grok accepts this setting only from user or administrator config, not project config or a plugin. The installer backs up an existing config before changing it and leaves any existing `ui.status_line` untouched. `uninstall --user` removes only unchanged LitGrok-managed values. This status command does not add a hook registration. Once a prompt names `deep-interview`, `lit-plan`, `start-work`, `litwork` or `review-work`, the command adds a workflow line (stage path, checked plan tasks, the tool in flight, the last test verdict) from the stage records the passive hooks keep; `LITGROK_HUD_LINES=1` hides it.
|
|
44
|
+
|
|
45
|
+
`install --user --notify` is a separate opt-in that adds a managed `[ui.notifications]` table so Grok's own terminal notification fires when a turn completes, needs approval or fails, even while the terminal has focus. It is off by default, leaves any existing notification setting untouched, and `uninstall --user` removes only the unchanged table.
|
|
44
46
|
|
|
45
47
|
Project hooks do not run merely because files exist. The user must trust the project through `/hooks-trust` or the `--trust` flow; Grok records trusted folders in `~/.grok/trusted_folders.toml`. Installation never grants trust or edits that file.
|
|
46
48
|
|
package/.grok-plugin/plugin.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.0.19 — 2026-10-13
|
|
6
|
+
|
|
7
|
+
- The status row shows where a session is in the LIT workflow. Once a prompt names `deep-interview`, `lit-plan`, `start-work`, `litwork`, a bare `lit` or `review-work`, a line such as `plan ✓ ━━━━ [WORK 3/7] ──── review · ──── done · ⠹ Bash · 0:31 tests ✓ 214` shows the stages passed, the current stage with the checked tasks of the newest plan, the tool running now and for how long, and the last test run. A command that only mentions a test runner inside a heredoc or a quoted string is not counted as a test run. LitGrok keeps only stage names, task counts and the test verdict, never a prompt, a command or its output.
|
|
8
|
+
- `litgrok install --user --notify` turns on Grok's own terminal notification for a finished turn, an approval request or an error, even while you are looking at the terminal. It is off until you ask for it, and a user uninstall removes only the unchanged table it added.
|
|
9
|
+
- The running ribbon and the live tool step now show in Grok Build 1.0.13, which sends the status line no turn start. LitGrok reads a turn in flight from its own session log instead, and stops showing it once the turn ends, fails or has been quiet for ten minutes.
|
|
10
|
+
- The bright cell that moves along the workflow rail is cyan, so it stays visible on a light terminal.
|
|
11
|
+
|
|
5
12
|
## 1.0.18 — 2026-10-12
|
|
6
13
|
|
|
7
14
|
- The README shows automatic handoff step by step. Each `litgrok auto-handoff` command (`on 60`, `status`, `off`) and the environment switches appear as terminal pictures with their real output, followed by the status row as the context nears your percent and the handoff is requested, and the `/compact` step that comes after.
|
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/cover-motion-still.webp" /><source media="(prefers-reduced-motion: no-preference)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/cover-motion.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five armored robots power on one by one, the LitGrok robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
|
|
2
2
|
|
|
3
3
|
<h1 align="center">LitGrok</h1>
|
|
4
4
|
<p align="center"><strong>Keep the work lit.</strong></p>
|
|
5
5
|
|
|
6
6
|
Skills, a project rule and hooks for xAI's Grok Build that help every session plan the change, build it, check it, and leave a handoff the next session can pick up.
|
|
7
7
|
|
|
8
|
-
**[Full README on GitHub](https://github.com/wjgoarxiv/litgrok#readme)** · [Guide](https://github.com/wjgoarxiv/litgrok/blob/main/docs/guide.md) · [All skills](https://github.com/wjgoarxiv/litgrok/blob/main/docs/skills.md) · [한국어](https://github.com/wjgoarxiv/litgrok/blob/main/README_ko-KR.md) · [Install](#install-in-30-seconds) · [Reference](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
8
|
+
**[Full README on GitHub](https://github.com/wjgoarxiv/litgrok#readme)** · [Guide](https://github.com/wjgoarxiv/litgrok/blob/main/docs/guide.md) · [All skills](https://github.com/wjgoarxiv/litgrok/blob/main/docs/skills.md) · [한국어](https://github.com/wjgoarxiv/litgrok/blob/main/README_ko-KR.md) · [Install](#install-in-30-seconds) · [Reference](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/reference.md)
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
|
-
<a href="#install-in-30-seconds"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
12
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
11
|
+
<a href="#install-in-30-seconds"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/readme/badge-version.svg" alt="1.0.19" /></a>
|
|
12
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
<p align="center"><a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
15
|
+
<p align="center"><a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/reference.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/readme/lucide-book-open.svg" width="16" alt="" /> Docs</a> <a href="#install-in-30-seconds">Install</a> <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/cover-motion.webp"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/readme/lucide-play.svg" width="16" alt="" /> Cover motion</a> <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/readme/lucide-shield-check.svg" width="16" alt="" /> MIT</a></p>
|
|
16
16
|
|
|
17
17
|
Long Grok Build sessions get compacted, a new session starts without what the last one knew, and "done" often means the model said so. LitGrok gives each session a routine to follow: a written plan before the first edit, checks against that plan, and a handoff file the next session can read. It ships 38 skills, 11 agents, one project rule, and eleven hook registrations. Grok Build still runs the session and chooses the model.
|
|
18
18
|
|
|
@@ -24,7 +24,7 @@ You need Node.js and Grok Build. In an interactive terminal, from the project yo
|
|
|
24
24
|
npm exec --yes --package @litfamily/litgrok@latest -- litgrok install
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The files land in `<project>/.grok/`. Add `--user` to install under `~/.grok/` instead, or use `--package @litfamily/litgrok@1.0.
|
|
27
|
+
The files land in `<project>/.grok/`. Add `--user` to install under `~/.grok/` instead, or use `--package @litfamily/litgrok@1.0.19` to pin this release. Add `--dry-run` to see every destination before anything is written.
|
|
28
28
|
|
|
29
29
|
Trying a local package instead? Put its real absolute path in a variable, then run these two lines separately:
|
|
30
30
|
|
|
@@ -72,9 +72,9 @@ Beyond code, `lit-pptx` makes slide decks and `lit-docx` makes reports and Word
|
|
|
72
72
|
|
|
73
73
|
## What changes after install
|
|
74
74
|
|
|
75
|
-
The installer copies the skills, agents, project rule, and hooks into `.grok/`. The eleven hook registrations in [`hooks/hooks.json`](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
75
|
+
The installer copies the skills, agents, project rule, and hooks into `.grok/`. The eleven hook registrations in [`hooks/hooks.json`](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/hooks/hooks.json) run only in a trusted Git root, and they log the order of events in `.grok/litgrok/session-ledger/`. Everything runs inside your Grok session, and nothing runs in the background; when the session ends, the work waits for the next one.
|
|
76
76
|
|
|
77
|
-
If you'd like a status row showing the active LitGrok skill, the model and how much context is used, add it with `install --user --status-line`. It writes `[ui.status_line]` into `~/.grok/config.toml` and backs up an existing file first.
|
|
77
|
+
If you'd like a status row showing the active LitGrok skill, the model and how much context is used, add it with `install --user --status-line`. It writes `[ui.status_line]` into `~/.grok/config.toml` and backs up an existing file first. Once a prompt starts a workflow skill, the row also shows the workflow stage, the tool running now and the last test result. Add `--notify` for Grok's terminal notification when a turn finishes.
|
|
78
78
|
|
|
79
79
|
## Safety and uninstall
|
|
80
80
|
|
|
@@ -109,6 +109,6 @@ The installer never runs `git init`, trusts hooks, signs in or picks a model for
|
|
|
109
109
|
|
|
110
110
|
If LitGrok saves you some re-explaining, a [star on GitHub](https://github.com/wjgoarxiv/litgrok) helps other Grok Build users find it.
|
|
111
111
|
|
|
112
|
-
[Reference](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
112
|
+
[Reference](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/reference.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/CHANGELOG.md) · [Privacy](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/privacy.md) · [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/LICENSE)
|
|
113
113
|
|
|
114
114
|
The cover is brand motion made with the LitFamily motion skill: animated artwork, with no Grok session recorded in it.
|
package/README_ko-KR.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/cover-motion-still.webp" /><source media="(prefers-reduced-motion: no-preference)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/cover-motion.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 다섯 로봇 패널이 차례로 켜지고, LitGrok 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
|
|
2
2
|
|
|
3
3
|
<h1 align="center">LitGrok</h1>
|
|
4
4
|
<p align="center"><strong>Keep the work lit.</strong></p>
|
|
5
5
|
|
|
6
6
|
xAI Grok Build에 스킬, 프로젝트 규칙, 훅을 더해 세션마다 계획하고, 만들고, 확인하고, 다음 세션이 이어받을 인수인계 문서를 남기게 합니다.
|
|
7
7
|
|
|
8
|
-
**[GitHub에서 README 전체 보기](https://github.com/wjgoarxiv/litgrok/blob/main/README_ko-KR.md)** · [안내서](https://github.com/wjgoarxiv/litgrok/blob/main/docs/guide_ko-KR.md) · [전체 스킬](https://github.com/wjgoarxiv/litgrok/blob/main/docs/skills_ko-KR.md) · [English](https://github.com/wjgoarxiv/litgrok#readme) · [설치](#30초-설치) · [상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
8
|
+
**[GitHub에서 README 전체 보기](https://github.com/wjgoarxiv/litgrok/blob/main/README_ko-KR.md)** · [안내서](https://github.com/wjgoarxiv/litgrok/blob/main/docs/guide_ko-KR.md) · [전체 스킬](https://github.com/wjgoarxiv/litgrok/blob/main/docs/skills_ko-KR.md) · [English](https://github.com/wjgoarxiv/litgrok#readme) · [설치](#30초-설치) · [상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/reference_ko-KR.md)
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
|
-
<a href="#30초-설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
12
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
11
|
+
<a href="#30초-설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/readme/badge-version.svg" alt="1.0.19" /></a>
|
|
12
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/readme/badge-license.svg" alt="MIT 라이선스" /></a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
<p align="center"><a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
15
|
+
<p align="center"><a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/reference_ko-KR.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/readme/lucide-book-open.svg" width="16" alt="" /> 상세 안내</a> <a href="#30초-설치">설치</a> <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/cover-motion.webp"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/readme/lucide-play.svg" width="16" alt="" /> 커버 모션</a> <a href="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/assets/readme/lucide-shield-check.svg" width="16" alt="" /> MIT</a></p>
|
|
16
16
|
|
|
17
17
|
Grok Build 세션이 길어지면 대화가 압축되고, 새 세션은 앞 세션이 알던 내용을 모른 채 시작하며, "다 됐다"는 말도 모델이 그렇게 말했다는 뜻일 때가 많습니다. LitGrok은 세션마다 따라갈 순서를 줍니다. 첫 수정 전에 계획을 글로 남기고, 그 계획에 비추어 확인하고, 다음 세션이 읽을 인수인계 문서를 남깁니다. 스킬 38개, 에이전트 11개, 프로젝트 규칙 하나, 훅 등록 열한 개가 들어 있고, 세션 실행과 모델 선택은 계속 Grok Build가 맡습니다.
|
|
18
18
|
|
|
@@ -24,7 +24,7 @@ Node.js와 Grok Build가 있으면 됩니다. 써 보고 싶은 프로젝트에
|
|
|
24
24
|
npm exec --yes --package @litfamily/litgrok@latest -- litgrok install
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
파일은 `<project>/.grok/`에 들어갑니다. `~/.grok/`에 설치하려면 `--user`를, 이 릴리스로 고정하려면 `--package @litfamily/litgrok@1.0.
|
|
27
|
+
파일은 `<project>/.grok/`에 들어갑니다. `~/.grok/`에 설치하려면 `--user`를, 이 릴리스로 고정하려면 `--package @litfamily/litgrok@1.0.19`을 쓰세요. `--dry-run`을 붙이면 파일을 쓰기 전에 들어갈 경로를 모두 보여 줍니다.
|
|
28
28
|
|
|
29
29
|
로컬 패키지로 써 보려면 실제 절대 경로를 변수에 넣고 두 줄을 따로 실행하세요.
|
|
30
30
|
|
|
@@ -72,9 +72,9 @@ npm exec --yes --package "$LITGROK_PACK" -- litgrok install
|
|
|
72
72
|
|
|
73
73
|
## 설치하면 달라지는 것
|
|
74
74
|
|
|
75
|
-
설치 프로그램이 스킬, 에이전트, 프로젝트 규칙, 훅을 `.grok/`에 복사합니다. [`hooks/hooks.json`](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
75
|
+
설치 프로그램이 스킬, 에이전트, 프로젝트 규칙, 훅을 `.grok/`에 복사합니다. [`hooks/hooks.json`](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/hooks/hooks.json)에 있는 훅 등록 열한 개는 신뢰한 Git 루트에서만 동작하고, `.grok/litgrok/session-ledger/`에 이벤트 순서를 남깁니다. 모든 작업은 Grok 세션 안에서 돌고 백그라운드 작업은 없습니다. 세션이 끝나면 다음 세션이 이어받을 때까지 그 자리에 멈춰 있습니다.
|
|
76
76
|
|
|
77
|
-
지금 어떤 LitGrok 스킬이 동작 중인지, 어떤 모델을 쓰는지, 컨텍스트를 얼마나 썼는지 보고 싶으면 `install --user --status-line`으로 상태 행을 켜세요. `~/.grok/config.toml`에 `[ui.status_line]`을 넣고, 파일이 이미 있으면 먼저 백업합니다.
|
|
77
|
+
지금 어떤 LitGrok 스킬이 동작 중인지, 어떤 모델을 쓰는지, 컨텍스트를 얼마나 썼는지 보고 싶으면 `install --user --status-line`으로 상태 행을 켜세요. `~/.grok/config.toml`에 `[ui.status_line]`을 넣고, 파일이 이미 있으면 먼저 백업합니다. 프롬프트가 작업 흐름 스킬을 시작하면 지금 단계, 돌고 있는 도구, 마지막 테스트 결과도 함께 보입니다. 턴이 끝날 때 Grok의 터미널 알림을 받으려면 `--notify`를 붙이세요.
|
|
78
78
|
|
|
79
79
|
## 안전과 제거
|
|
80
80
|
|
|
@@ -109,6 +109,6 @@ npm exec --yes --package @litfamily/litgrok@latest -- litgrok uninstall --user
|
|
|
109
109
|
|
|
110
110
|
LitGrok 덕분에 같은 설명을 덜 반복하게 됐다면, [GitHub 스타](https://github.com/wjgoarxiv/litgrok) 하나가 다른 Grok Build 사용자에게 이 프로젝트를 알리는 데 도움이 됩니다.
|
|
111
111
|
|
|
112
|
-
[상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.
|
|
112
|
+
[상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/reference_ko-KR.md) · [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/CHANGELOG.md) · [개인정보](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/docs/privacy.md) · [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litgrok@1.0.19/LICENSE)
|
|
113
113
|
|
|
114
114
|
커버의 모션은 LitFamily 모션 스킬로 만든 브랜드 연출입니다. 그려서 움직인 그림이라 실제 Grok 세션을 녹화한 장면은 들어 있지 않습니다.
|
package/bin/litgrok.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { homedir } from 'node:os';
|
|
|
6
6
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { renderMark, terminalMode } from '../.grok/hooks/lit-mark.mjs';
|
|
9
|
-
import { installStatusLine, removeStatusLine } from './status-line-config.mjs';
|
|
9
|
+
import { installNotifications, installStatusLine, removeNotifications, removeStatusLine } from './status-line-config.mjs';
|
|
10
10
|
|
|
11
11
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
12
12
|
const PAYLOAD_ROOT = join(PACKAGE_ROOT, '.grok');
|
|
@@ -52,6 +52,9 @@ const PRE_MANIFEST_PAYLOAD_HASHES = Object.freeze({
|
|
|
52
52
|
'hooks/deliverable-hedge-guard.json': '65a0008a8c51be203a385f0ca55a5e7eb9e60e487c701c08d6d2407f0a3a1170',
|
|
53
53
|
'hooks/deliverable-hedge-guard.mjs': '78f997d793e94853d1ef9073fbc9dd8b1577fe756d5e8a89d4fc3859d4bde5a9',
|
|
54
54
|
'hooks/plan-gate.mjs': '0477520c23776d4031e0703fc0d776a53a7e2938f5ec1a41ac0f3388bc91d7a3',
|
|
55
|
+
'hooks/post-tool-use-failure.mjs': 'b6d290bb93909a4a1a5abbba25ccc87a3ccd806a3d2810868297bab9db18b784',
|
|
56
|
+
'hooks/post-tool-use.json': 'ddccda78e32ca3f072cba48ce57a413e54e9f9da36376fadbb08eeae1fe5c95b',
|
|
57
|
+
'hooks/post-tool-use.mjs': '8984c47025285cfe2bc4a4f34edcba5921c96071703306a8c39fbc7874b73cdb',
|
|
55
58
|
'hooks/record-passive-event.mjs': '9f5bd1d6c8b8e6c0be4cc61cf31f0e26f4f8a41fceb826e88567f22efa2b0090',
|
|
56
59
|
'hooks/session-start.mjs': '5706ccce58340e78e435b9c1fd2224e101872ce1d0f51a9d187feb31b67c3b5d',
|
|
57
60
|
'hooks/stop.mjs': 'c796f02ac3dc99716d0c2f478bf93ec35cd8a9c725de18d12895957ee261b9c3',
|
|
@@ -198,7 +201,7 @@ const PRE_MANIFEST_LEGACY_PAYLOAD_HASHES = Object.freeze({
|
|
|
198
201
|
'skills/text-naturalization/SKILL.md': 'aa1ff8f68ab0bf71cb47818b536c4a2934da65adb413ee43ec5839b540b2df8e',
|
|
199
202
|
'skills/lit-korean/SKILL.md': 'dc2e4b0ea2ff6acca904f05767a62448387571e4183a615d5e53b230704ee828',
|
|
200
203
|
});
|
|
201
|
-
const USAGE = 'Usage: litgrok [install|uninstall] [--user|--project] [--status-line] [--dry-run] [--no-color] [--yes]\n litgrok auto-handoff on [percent]|off|status\n litgrok doctor [--no-color]\n litgrok motion-runtime install|status\nTypographic-motion engine adapted from mexicat/pdoom-video (MIT, Giacomo Magnanini), commit ca251e3.\n';
|
|
204
|
+
const USAGE = 'Usage: litgrok [install|uninstall] [--user|--project] [--status-line] [--notify] [--dry-run] [--no-color] [--yes]\n litgrok auto-handoff on [percent]|off|status\n litgrok doctor [--no-color]\n litgrok motion-runtime install|status\nTypographic-motion engine adapted from mexicat/pdoom-video (MIT, Giacomo Magnanini), commit ca251e3.\n';
|
|
202
205
|
const BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
203
206
|
const SPINNER_INTERVAL_MS = 80;
|
|
204
207
|
const CLEAR_LINE = '\r\u001b[2K';
|
|
@@ -207,19 +210,26 @@ function parseInstall(args) {
|
|
|
207
210
|
const remaining = [...args];
|
|
208
211
|
let command = 'install';
|
|
209
212
|
if (['install', 'uninstall'].includes(remaining[0])) command = remaining.shift();
|
|
210
|
-
if (remaining.some((argument) => !['--user', '--project', '--status-line', '--dry-run', '--no-color', '--yes'].includes(argument))) return null;
|
|
213
|
+
if (remaining.some((argument) => !['--user', '--project', '--status-line', '--notify', '--dry-run', '--no-color', '--yes'].includes(argument))) return null;
|
|
211
214
|
if (remaining.includes('--user') && remaining.includes('--project')) return null;
|
|
212
|
-
|
|
215
|
+
// Both opt-ins change ~/.grok/config.toml, which only a user install may touch.
|
|
216
|
+
if ((remaining.includes('--status-line') || remaining.includes('--notify')) && (command !== 'install' || !remaining.includes('--user'))) return null;
|
|
213
217
|
return {
|
|
214
218
|
command,
|
|
215
219
|
dryRun: remaining.includes('--dry-run'),
|
|
216
220
|
noColor: remaining.includes('--no-color'),
|
|
217
221
|
yes: remaining.includes('--yes'),
|
|
218
222
|
statusLine: remaining.includes('--status-line'),
|
|
223
|
+
notify: remaining.includes('--notify'),
|
|
219
224
|
scope: remaining.includes('--user') ? 'user' : 'project',
|
|
220
225
|
};
|
|
221
226
|
}
|
|
222
227
|
|
|
228
|
+
// What the user-config opt-ins did, appended to the WRITE step.
|
|
229
|
+
function configDetail(...results) {
|
|
230
|
+
return results.filter(Boolean).map((result) => ` · ${result.detail}`).join('');
|
|
231
|
+
}
|
|
232
|
+
|
|
223
233
|
function packageVersion() {
|
|
224
234
|
try {
|
|
225
235
|
const version = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
|
|
@@ -908,6 +918,7 @@ export async function run(args, context = {}) {
|
|
|
908
918
|
stdout.write(`${reasons.join(' / ')} — no files written\n`);
|
|
909
919
|
for (const { target } of files) stdout.write(`would ${options.command} ${target}\n`);
|
|
910
920
|
if (options.statusLine) stdout.write(`would configure user status line ${join(root, 'config.toml')}\n`);
|
|
921
|
+
if (options.notify) stdout.write(`would configure user finish notification ${join(root, 'config.toml')}\n`);
|
|
911
922
|
printHookTrustRequirement(files);
|
|
912
923
|
return 0;
|
|
913
924
|
}
|
|
@@ -988,8 +999,12 @@ export async function run(args, context = {}) {
|
|
|
988
999
|
progress.start('WRITE');
|
|
989
1000
|
let removed = 0;
|
|
990
1001
|
let statusLineResult = null;
|
|
1002
|
+
let notifyResult = null;
|
|
991
1003
|
try {
|
|
992
|
-
if (options.scope === 'user')
|
|
1004
|
+
if (options.scope === 'user') {
|
|
1005
|
+
statusLineResult = removeStatusLine(root);
|
|
1006
|
+
notifyResult = removeNotifications(root);
|
|
1007
|
+
}
|
|
993
1008
|
for (let index = 0; index < files.length; index += 1) {
|
|
994
1009
|
if (!['current', 'owned'].includes(states.states[index])) continue;
|
|
995
1010
|
unlinkSync(files[index].target);
|
|
@@ -999,7 +1014,7 @@ export async function run(args, context = {}) {
|
|
|
999
1014
|
removeRetiredDirectories(states.retiredDirectories);
|
|
1000
1015
|
removeEmptyPayloadDirectories(payloadDirectories(root, files));
|
|
1001
1016
|
removeOwnershipManifest(root, states.manifest);
|
|
1002
|
-
const statusDetail = statusLineResult
|
|
1017
|
+
const statusDetail = configDetail(statusLineResult, notifyResult);
|
|
1003
1018
|
progress.finish('ok', 'WRITE', `${removed} removed · ${states.counts.missing} already absent${statusDetail}`, elapsedSince(writeStarted));
|
|
1004
1019
|
} catch (error) {
|
|
1005
1020
|
progress.finish('failed', 'WRITE', errorText(error), elapsedSince(writeStarted));
|
|
@@ -1014,14 +1029,16 @@ export async function run(args, context = {}) {
|
|
|
1014
1029
|
|
|
1015
1030
|
if (states.states.every((state) => state === 'current') && states.legacyStates.every((state) => state === 'missing') && states.retiredDirectories.length === 0) {
|
|
1016
1031
|
let statusLineResult = null;
|
|
1032
|
+
let notifyResult = null;
|
|
1017
1033
|
try {
|
|
1018
1034
|
writeOwnershipManifest(root, files, states.manifest);
|
|
1019
1035
|
if (options.statusLine) statusLineResult = installStatusLine(root);
|
|
1036
|
+
if (options.notify) notifyResult = installNotifications(root);
|
|
1020
1037
|
} catch (error) {
|
|
1021
1038
|
if (error && typeof error === 'object') error.step = 'WRITE';
|
|
1022
1039
|
throw error;
|
|
1023
1040
|
}
|
|
1024
|
-
const statusDetail = statusLineResult
|
|
1041
|
+
const statusDetail = configDetail(statusLineResult, notifyResult);
|
|
1025
1042
|
skipStep(progress, 'WRITE', `SKIPPED · 0 written · ${states.counts.current} already-current${statusDetail}`);
|
|
1026
1043
|
const receipt = receiptCard(options, displayRoot, colors, { status: 'Already current', written: 0, current: states.counts.current });
|
|
1027
1044
|
progress.close();
|
|
@@ -1045,8 +1062,9 @@ export async function run(args, context = {}) {
|
|
|
1045
1062
|
removeRetiredDirectories(states.retiredDirectories);
|
|
1046
1063
|
writeOwnershipManifest(root, files, states.manifest);
|
|
1047
1064
|
const statusLineResult = options.statusLine ? installStatusLine(root) : null;
|
|
1065
|
+
const notifyResult = options.notify ? installNotifications(root) : null;
|
|
1048
1066
|
const legacyDetail = removedLegacy === 0 ? '' : ` · ${removedLegacy} legacy removed`;
|
|
1049
|
-
const statusDetail = statusLineResult
|
|
1067
|
+
const statusDetail = configDetail(statusLineResult, notifyResult);
|
|
1050
1068
|
progress.finish('ok', 'WRITE', `${installed} written · ${states.counts.current} already-current · ${states.counts.owned} owned${legacyDetail}${statusDetail}`, elapsedSince(writeStarted));
|
|
1051
1069
|
} catch (error) {
|
|
1052
1070
|
progress.finish('failed', 'WRITE', errorText(error), elapsedSince(writeStarted));
|