novahiz 0.2.2 → 0.2.3
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 +355 -355
- package/adapters/opencode/commands/novahiz-init.md +10 -0
- package/adapters/opencode/novahiz.ts +181 -3
- package/docs/CONFIGURATION.md +2 -2
- package/docs/PLUGIN.md +24 -6
- package/docs/RULES.md +1 -1
- package/install/install.mjs +2 -0
- package/mcp/novahiz-tools/index.mjs +35 -6
- package/package.json +6 -6
- package/skills/novahiz-gate/SKILL.md +1 -1
- package/skills/novahiz-init/SKILL.md +112 -0
- package/src/autodocs.ts +192 -0
- package/src/cli.ts +8 -1
- package/src/commands/autodocs.ts +250 -0
- package/src/commands/doctor.ts +226 -217
- package/src/commands/init.ts +356 -0
- package/src/commands/task.ts +20 -7
- package/src/ledger.ts +23 -0
- package/src/targets.ts +43 -7
package/src/autodocs.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const NOVAHIZ_DIR = ".novahiz";
|
|
5
|
+
export const CONFIG_NAME = "config.json";
|
|
6
|
+
export const STATE_NAME = "state.json";
|
|
7
|
+
|
|
8
|
+
export type ProjectAutoDocsConfig = {
|
|
9
|
+
autoDocs?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type AutoDocsState = {
|
|
13
|
+
dirty: boolean;
|
|
14
|
+
pending: string[];
|
|
15
|
+
lastSync: string | null;
|
|
16
|
+
sessions: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const EMPTY_STATE: AutoDocsState = {
|
|
20
|
+
dirty: false,
|
|
21
|
+
pending: [],
|
|
22
|
+
lastSync: null,
|
|
23
|
+
sessions: 0
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const MAJOR_DIRS = /^(src|lib|app|routes|pages|api|server|internal|pkg|cmd)\//;
|
|
27
|
+
const MAJOR_FILES = new Set([
|
|
28
|
+
"package.json",
|
|
29
|
+
"pyproject.toml",
|
|
30
|
+
"Cargo.toml",
|
|
31
|
+
"go.mod",
|
|
32
|
+
"composer.json",
|
|
33
|
+
"Gemfile"
|
|
34
|
+
]);
|
|
35
|
+
const MAJOR_EXT = new Set([
|
|
36
|
+
".ts",
|
|
37
|
+
".tsx",
|
|
38
|
+
".js",
|
|
39
|
+
".jsx",
|
|
40
|
+
".mjs",
|
|
41
|
+
".cjs",
|
|
42
|
+
".py",
|
|
43
|
+
".rs",
|
|
44
|
+
".go",
|
|
45
|
+
".php",
|
|
46
|
+
".rb",
|
|
47
|
+
".java",
|
|
48
|
+
".kt",
|
|
49
|
+
".swift",
|
|
50
|
+
".dart",
|
|
51
|
+
".sql"
|
|
52
|
+
]);
|
|
53
|
+
const SKIP_DIRS = new Set([
|
|
54
|
+
"node_modules",
|
|
55
|
+
".git",
|
|
56
|
+
"project-memory",
|
|
57
|
+
"novahiz-docs",
|
|
58
|
+
".novahiz",
|
|
59
|
+
"dist",
|
|
60
|
+
"build",
|
|
61
|
+
"coverage",
|
|
62
|
+
".next"
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
export function projectDir(cwd: string): string {
|
|
66
|
+
return join(cwd, NOVAHIZ_DIR);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function configPath(cwd: string): string {
|
|
70
|
+
return join(projectDir(cwd), CONFIG_NAME);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function statePath(cwd: string): string {
|
|
74
|
+
return join(projectDir(cwd), STATE_NAME);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ensureDir(path: string): void {
|
|
78
|
+
if (!existsSync(path)) mkdirSync(path, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function autoDocsEnabled(cwd: string): boolean {
|
|
82
|
+
const escape = (process.env.NOVAHIZ_AUTODOCS ?? "").toLowerCase();
|
|
83
|
+
if (["off", "0", "false", "no", "disabled"].includes(escape)) return false;
|
|
84
|
+
const config = readProjectConfig(cwd);
|
|
85
|
+
return config.autoDocs === true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function readProjectConfig(cwd: string): ProjectAutoDocsConfig {
|
|
89
|
+
try {
|
|
90
|
+
const raw = readFileSync(configPath(cwd), "utf8");
|
|
91
|
+
const parsed = JSON.parse(raw) as Partial<ProjectAutoDocsConfig>;
|
|
92
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
93
|
+
return { autoDocs: parsed.autoDocs === true };
|
|
94
|
+
} catch {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function writeProjectConfig(cwd: string, config: ProjectAutoDocsConfig): void {
|
|
100
|
+
ensureDir(projectDir(cwd));
|
|
101
|
+
writeFileSync(configPath(cwd), `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function readState(cwd: string): AutoDocsState {
|
|
105
|
+
try {
|
|
106
|
+
const raw = readFileSync(statePath(cwd), "utf8");
|
|
107
|
+
const parsed = JSON.parse(raw) as Partial<AutoDocsState>;
|
|
108
|
+
if (!parsed || typeof parsed !== "object") return { ...EMPTY_STATE };
|
|
109
|
+
return {
|
|
110
|
+
dirty: parsed.dirty === true,
|
|
111
|
+
pending: Array.isArray(parsed.pending)
|
|
112
|
+
? parsed.pending.filter((entry): entry is string => typeof entry === "string").slice(0, 64)
|
|
113
|
+
: [],
|
|
114
|
+
lastSync: typeof parsed.lastSync === "string" ? parsed.lastSync : null,
|
|
115
|
+
sessions: typeof parsed.sessions === "number" && Number.isFinite(parsed.sessions) ? parsed.sessions : 0
|
|
116
|
+
};
|
|
117
|
+
} catch {
|
|
118
|
+
return { ...EMPTY_STATE };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function writeState(cwd: string, state: AutoDocsState): void {
|
|
123
|
+
ensureDir(projectDir(cwd));
|
|
124
|
+
writeFileSync(statePath(cwd), `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function normalizePath(filePath: string): string {
|
|
128
|
+
return filePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function isMajorPath(filePath: string): boolean {
|
|
132
|
+
const path = normalizePath(filePath);
|
|
133
|
+
const parts = path.split("/");
|
|
134
|
+
if (parts.some((part) => SKIP_DIRS.has(part))) return false;
|
|
135
|
+
const base = parts[parts.length - 1] ?? "";
|
|
136
|
+
if (MAJOR_FILES.has(base)) return true;
|
|
137
|
+
const dot = base.lastIndexOf(".");
|
|
138
|
+
const ext = dot >= 0 ? base.slice(dot).toLowerCase() : "";
|
|
139
|
+
if (MAJOR_EXT.has(ext)) return true;
|
|
140
|
+
return MAJOR_DIRS.test(path);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function markDirty(cwd: string, filePath: string): AutoDocsState {
|
|
144
|
+
const path = normalizePath(filePath);
|
|
145
|
+
const state = readState(cwd);
|
|
146
|
+
const pending = state.pending.includes(path) ? state.pending : [...state.pending, path].slice(-64);
|
|
147
|
+
const next: AutoDocsState = {
|
|
148
|
+
...state,
|
|
149
|
+
dirty: true,
|
|
150
|
+
pending,
|
|
151
|
+
sessions: state.sessions + 1
|
|
152
|
+
};
|
|
153
|
+
writeState(cwd, next);
|
|
154
|
+
return next;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function clearDirty(cwd: string): AutoDocsState {
|
|
158
|
+
const state = readState(cwd);
|
|
159
|
+
const next: AutoDocsState = {
|
|
160
|
+
...state,
|
|
161
|
+
dirty: false,
|
|
162
|
+
pending: [],
|
|
163
|
+
lastSync: new Date().toISOString()
|
|
164
|
+
};
|
|
165
|
+
writeState(cwd, next);
|
|
166
|
+
return next;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function dateStamp(): string {
|
|
170
|
+
return new Date().toISOString().slice(0, 10);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function upsertChangesSection(doc: string, lines: string[]): string {
|
|
174
|
+
const header = "## Recent changes";
|
|
175
|
+
const stamp = `*Last updated: ${dateStamp()}*`;
|
|
176
|
+
const bullet = lines.map((line) => `- ${line}`).join("\n");
|
|
177
|
+
const footerMatch = doc.match(/\n---\n\n\*Last updated:.*$/s);
|
|
178
|
+
const footer = footerMatch ? footerMatch[0] : `\n---\n\n${stamp}\n`;
|
|
179
|
+
|
|
180
|
+
if (!doc.includes(header)) {
|
|
181
|
+
const base = doc.replace(/\n---\n\n\*Last updated:.*$/s, "").replace(/\s+$/, "");
|
|
182
|
+
return `${base}\n\n${header}\n\n${bullet}\n${footer}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const start = doc.indexOf(header);
|
|
186
|
+
const after = doc.slice(start + header.length);
|
|
187
|
+
const nextHeader = after.search(/\n#{1,6} /);
|
|
188
|
+
const sectionEnd = nextHeader === -1 ? doc.length : start + header.length + nextHeader;
|
|
189
|
+
const before = doc.slice(0, start);
|
|
190
|
+
const rest = doc.slice(sectionEnd);
|
|
191
|
+
return `${before}${header}\n\n${bullet}\n${rest}`;
|
|
192
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { commandClean } from "./commands/clean.ts";
|
|
|
13
13
|
import { commandDoctor } from "./commands/doctor.ts";
|
|
14
14
|
import { commandTokens } from "./commands/tokens.ts";
|
|
15
15
|
import { graftCommand } from "./commands/graft.ts";
|
|
16
|
+
import { commandInit } from "./commands/init.ts";
|
|
17
|
+
import { commandAutodocs } from "./commands/autodocs.ts";
|
|
16
18
|
|
|
17
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
20
|
|
|
@@ -41,7 +43,9 @@ function usage(): void {
|
|
|
41
43
|
print({
|
|
42
44
|
name: "novahiz",
|
|
43
45
|
commands: [
|
|
44
|
-
"init
|
|
46
|
+
"init Initialize Novahiz in the current project",
|
|
47
|
+
"setup Install Novahiz (config, skills, catalog)",
|
|
48
|
+
"autodocs [--flush] Sync docs and memory after major project changes",
|
|
45
49
|
"doctor Check that everything works",
|
|
46
50
|
"status Show current classification and gate state",
|
|
47
51
|
"task new <title> Start a new tracked task",
|
|
@@ -131,8 +135,11 @@ function main(argv: string[]): void {
|
|
|
131
135
|
}
|
|
132
136
|
switch (command) {
|
|
133
137
|
case "init":
|
|
138
|
+
return commandInit(parsed);
|
|
134
139
|
case "setup":
|
|
135
140
|
return runInit();
|
|
141
|
+
case "autodocs":
|
|
142
|
+
return commandAutodocs(parsed);
|
|
136
143
|
case "doctor":
|
|
137
144
|
return commandDoctor(parsed);
|
|
138
145
|
case "status":
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
autoDocsEnabled,
|
|
5
|
+
clearDirty,
|
|
6
|
+
dateStamp,
|
|
7
|
+
isMajorPath,
|
|
8
|
+
markDirty,
|
|
9
|
+
readProjectConfig,
|
|
10
|
+
readState,
|
|
11
|
+
upsertChangesSection,
|
|
12
|
+
writeProjectConfig
|
|
13
|
+
} from "../autodocs.ts";
|
|
14
|
+
import { emit, flagOn, type Parsed } from "./context.ts";
|
|
15
|
+
import { memoryRoot, writeEntry } from "../memory.ts";
|
|
16
|
+
import * as ui from "../render.ts";
|
|
17
|
+
|
|
18
|
+
const MAX_DOC_PATHS = 12;
|
|
19
|
+
const ARCHITECTURE = "novahiz-docs/ARCHITECTURE.md";
|
|
20
|
+
|
|
21
|
+
export type EnsureResult = "created" | "existing" | "failed";
|
|
22
|
+
|
|
23
|
+
export type FlushOutcome = {
|
|
24
|
+
status: "flushed" | "marked" | "skipped" | "failed";
|
|
25
|
+
reason: string;
|
|
26
|
+
docsPath: string | null;
|
|
27
|
+
docsUpdated: boolean;
|
|
28
|
+
memorySlot: string | null;
|
|
29
|
+
paths: string[];
|
|
30
|
+
pending: number;
|
|
31
|
+
lastSync: string | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function ensureProjectAutoDocsConfig(cwd: string): EnsureResult {
|
|
35
|
+
try {
|
|
36
|
+
const config = readProjectConfig(cwd);
|
|
37
|
+
if (config.autoDocs === true) return "existing";
|
|
38
|
+
writeProjectConfig(cwd, { ...config, autoDocs: true });
|
|
39
|
+
return "created";
|
|
40
|
+
} catch {
|
|
41
|
+
return "failed";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function skipResult(reason: string, paths: string[] = []): FlushOutcome {
|
|
46
|
+
const state = readState(process.cwd());
|
|
47
|
+
return {
|
|
48
|
+
status: "skipped",
|
|
49
|
+
reason,
|
|
50
|
+
docsPath: null,
|
|
51
|
+
docsUpdated: false,
|
|
52
|
+
memorySlot: null,
|
|
53
|
+
paths,
|
|
54
|
+
pending: state.pending.length,
|
|
55
|
+
lastSync: state.lastSync
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function flushDocs(cwd: string, paths: string[]): { path: string; updated: boolean } {
|
|
60
|
+
const docPath = join(cwd, "novahiz-docs", "ARCHITECTURE.md");
|
|
61
|
+
if (!existsSync(docPath)) return { path: ARCHITECTURE, updated: false };
|
|
62
|
+
const sample = paths.slice(0, MAX_DOC_PATHS);
|
|
63
|
+
if (sample.length === 0) return { path: ARCHITECTURE, updated: false };
|
|
64
|
+
const doc = readFileSync(docPath, "utf8");
|
|
65
|
+
const lines = sample.map((path) => `${path} (${dateStamp()})`);
|
|
66
|
+
const next = upsertChangesSection(doc, lines);
|
|
67
|
+
if (next === doc) return { path: ARCHITECTURE, updated: false };
|
|
68
|
+
writeFileSync(docPath, next, "utf8");
|
|
69
|
+
return { path: ARCHITECTURE, updated: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function flushMemory(cwd: string, paths: string[]): string | null {
|
|
73
|
+
const sample = paths.slice(0, MAX_DOC_PATHS);
|
|
74
|
+
if (sample.length === 0) return null;
|
|
75
|
+
try {
|
|
76
|
+
const result = writeEntry({
|
|
77
|
+
title: "Auto docs sync",
|
|
78
|
+
description: "Change log written by novahiz autodocs",
|
|
79
|
+
tags: ["autodocs", "sync"],
|
|
80
|
+
root: memoryRoot(cwd),
|
|
81
|
+
content: [`Sync: ${new Date().toISOString()}`, ...sample.map((path) => `- ${path}`)].join("\n")
|
|
82
|
+
});
|
|
83
|
+
return result.slot.id;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function flushAutoDocs(cwd: string): FlushOutcome {
|
|
90
|
+
if (!autoDocsEnabled(cwd)) {
|
|
91
|
+
return skipResult("autoDocs disabled (set autoDocs=true in .novahiz/config.json or unset NOVAHIZ_AUTODOCS)");
|
|
92
|
+
}
|
|
93
|
+
const state = readState(cwd);
|
|
94
|
+
if (!state.dirty && state.pending.length === 0) {
|
|
95
|
+
return skipResult("nothing dirty", state.pending);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const paths = state.pending;
|
|
99
|
+
const docs = flushDocs(cwd, paths);
|
|
100
|
+
const memorySlot = flushMemory(cwd, paths);
|
|
101
|
+
const next = clearDirty(cwd);
|
|
102
|
+
return {
|
|
103
|
+
status: "flushed",
|
|
104
|
+
reason: `${paths.length} path(s) recorded`,
|
|
105
|
+
docsPath: docs.path,
|
|
106
|
+
docsUpdated: docs.updated,
|
|
107
|
+
memorySlot,
|
|
108
|
+
paths,
|
|
109
|
+
pending: next.pending.length,
|
|
110
|
+
lastSync: next.lastSync
|
|
111
|
+
};
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
status: "failed",
|
|
115
|
+
reason: (error as Error).message,
|
|
116
|
+
docsPath: null,
|
|
117
|
+
docsUpdated: false,
|
|
118
|
+
memorySlot: null,
|
|
119
|
+
paths: state.pending,
|
|
120
|
+
pending: state.pending.length,
|
|
121
|
+
lastSync: state.lastSync
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function markFromArgs(cwd: string, mark: string | boolean, positionals: string[]): FlushOutcome {
|
|
127
|
+
const raw =
|
|
128
|
+
typeof mark === "string" && mark.length > 0
|
|
129
|
+
? mark
|
|
130
|
+
: positionals.join(",");
|
|
131
|
+
const list = raw
|
|
132
|
+
.split(",")
|
|
133
|
+
.map((entry) => entry.trim())
|
|
134
|
+
.filter(Boolean);
|
|
135
|
+
if (list.length === 0) {
|
|
136
|
+
return {
|
|
137
|
+
status: "failed",
|
|
138
|
+
reason: "--mark needs at least one path",
|
|
139
|
+
docsPath: null,
|
|
140
|
+
docsUpdated: false,
|
|
141
|
+
memorySlot: null,
|
|
142
|
+
paths: [],
|
|
143
|
+
pending: readState(cwd).pending.length,
|
|
144
|
+
lastSync: readState(cwd).lastSync
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const accepted: string[] = [];
|
|
148
|
+
const rejected: string[] = [];
|
|
149
|
+
for (const path of list) {
|
|
150
|
+
if (isMajorPath(path)) {
|
|
151
|
+
markDirty(cwd, path);
|
|
152
|
+
accepted.push(path);
|
|
153
|
+
} else {
|
|
154
|
+
rejected.push(path);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const state = readState(cwd);
|
|
158
|
+
if (accepted.length === 0) {
|
|
159
|
+
return {
|
|
160
|
+
status: "skipped",
|
|
161
|
+
reason: `no major path among ${rejected.join(", ")}`,
|
|
162
|
+
docsPath: null,
|
|
163
|
+
docsUpdated: false,
|
|
164
|
+
memorySlot: null,
|
|
165
|
+
paths: rejected,
|
|
166
|
+
pending: state.pending.length,
|
|
167
|
+
lastSync: state.lastSync
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
status: "marked",
|
|
172
|
+
reason:
|
|
173
|
+
rejected.length > 0
|
|
174
|
+
? `marked ${accepted.length}, ignored ${rejected.length} non-major`
|
|
175
|
+
: `marked ${accepted.length}`,
|
|
176
|
+
docsPath: null,
|
|
177
|
+
docsUpdated: false,
|
|
178
|
+
memorySlot: null,
|
|
179
|
+
paths: accepted,
|
|
180
|
+
pending: state.pending.length,
|
|
181
|
+
lastSync: state.lastSync
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function statusView(cwd: string): Record<string, unknown> {
|
|
186
|
+
const config = readProjectConfig(cwd);
|
|
187
|
+
const state = readState(cwd);
|
|
188
|
+
return {
|
|
189
|
+
cwd,
|
|
190
|
+
autoDocs: config.autoDocs === true,
|
|
191
|
+
enabled: autoDocsEnabled(cwd),
|
|
192
|
+
dirty: state.dirty,
|
|
193
|
+
pending: state.pending,
|
|
194
|
+
lastSync: state.lastSync,
|
|
195
|
+
sessions: state.sessions
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function commandAutodocs(parsed: Parsed): void {
|
|
200
|
+
const cwd = process.cwd();
|
|
201
|
+
const wantFlush = flagOn(parsed, "flush");
|
|
202
|
+
const markFlag = parsed.flags.mark;
|
|
203
|
+
const isMark = markFlag !== undefined && markFlag !== false;
|
|
204
|
+
|
|
205
|
+
let outcome: FlushOutcome | null = null;
|
|
206
|
+
if (isMark) {
|
|
207
|
+
outcome = markFromArgs(cwd, markFlag, parsed.positionals.slice(1));
|
|
208
|
+
} else if (wantFlush) {
|
|
209
|
+
outcome = flushAutoDocs(cwd);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const value = outcome
|
|
213
|
+
? { ...statusView(cwd), action: isMark ? "mark" : "flush", ...outcome }
|
|
214
|
+
: { ...statusView(cwd), action: "status" };
|
|
215
|
+
|
|
216
|
+
emit(parsed, value, () => {
|
|
217
|
+
const view = statusView(cwd);
|
|
218
|
+
const lines = [
|
|
219
|
+
ui.heading("novahiz autodocs"),
|
|
220
|
+
ui.kv([
|
|
221
|
+
["Cwd", String(view.cwd)],
|
|
222
|
+
["autoDocs", String(view.autoDocs)],
|
|
223
|
+
["Enabled", String(view.enabled)],
|
|
224
|
+
["Dirty", String(view.dirty)],
|
|
225
|
+
["Pending", String(view.pending)],
|
|
226
|
+
["Last sync", String(view.lastSync)]
|
|
227
|
+
])
|
|
228
|
+
];
|
|
229
|
+
if (outcome) {
|
|
230
|
+
lines.push(
|
|
231
|
+
"",
|
|
232
|
+
ui.kv([
|
|
233
|
+
["Action", outcome.status],
|
|
234
|
+
["Reason", outcome.reason],
|
|
235
|
+
["Docs", outcome.docsPath ? `${outcome.docsPath}${outcome.docsUpdated ? " updated" : " unchanged"}` : "n/a"],
|
|
236
|
+
["Memory", outcome.memorySlot ?? "n/a"]
|
|
237
|
+
])
|
|
238
|
+
);
|
|
239
|
+
lines.push(
|
|
240
|
+
"",
|
|
241
|
+
outcome.status === "failed" ? ui.style("red", outcome.reason) : ui.style("green", outcome.reason)
|
|
242
|
+
);
|
|
243
|
+
} else {
|
|
244
|
+
lines.push("", ui.style("dim", "Use --flush to sync, --mark <path> to record a change."));
|
|
245
|
+
}
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (outcome?.status === "failed") process.exitCode = 1;
|
|
250
|
+
}
|