hafez 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -5
- package/dist/apply.d.ts +14 -1
- package/dist/apply.js +94 -32
- package/dist/batch-ops.d.ts +31 -27
- package/dist/batch-ops.js +83 -16
- package/dist/cli/block-hook.d.ts +6 -0
- package/dist/cli/block-hook.js +27 -12
- package/dist/cli/commands.d.ts +3 -1
- package/dist/cli/commands.js +183 -73
- package/dist/cli/format.d.ts +11 -3
- package/dist/cli/format.js +61 -4
- package/dist/cli/index.js +40 -19
- package/dist/cli/install.d.ts +9 -0
- package/dist/cli/install.js +36 -15
- package/dist/cli/onboard.js +30 -9
- package/dist/cli/upgrade.js +5 -4
- package/dist/contracts.d.ts +9 -0
- package/dist/contracts.js +9 -0
- package/dist/dates.d.ts +7 -0
- package/dist/dates.js +21 -1
- package/dist/db.d.ts +50 -10
- package/dist/db.js +251 -65
- package/dist/digest.js +14 -7
- package/dist/document.d.ts +2 -0
- package/dist/document.js +2 -2
- package/dist/git.d.ts +13 -2
- package/dist/git.js +121 -14
- package/dist/graph/activity.d.ts +5 -0
- package/dist/graph/activity.js +70 -0
- package/dist/graph/client.js +326 -0
- package/dist/graph/copy-fonts.d.ts +1 -0
- package/dist/graph/copy-fonts.js +13 -0
- package/dist/graph/fields.d.ts +8 -0
- package/dist/graph/fields.js +12 -0
- package/dist/graph/fonts/geist-OFL.txt +93 -0
- package/dist/graph/fonts/geist-mono-OFL.txt +93 -0
- package/dist/graph/fonts/geist-mono.woff2 +0 -0
- package/dist/graph/fonts/geist.woff2 +0 -0
- package/dist/graph/fonts/outfit-OFL.txt +93 -0
- package/dist/graph/fonts/outfit.woff2 +0 -0
- package/dist/graph/fonts.d.ts +14 -0
- package/dist/graph/fonts.js +17 -0
- package/dist/graph/live.d.ts +16 -0
- package/dist/graph/live.js +99 -0
- package/dist/graph/open.d.ts +1 -0
- package/dist/graph/open.js +20 -0
- package/dist/graph/server.d.ts +17 -0
- package/dist/graph/server.js +189 -0
- package/dist/health.d.ts +6 -0
- package/dist/health.js +310 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +66 -18
- package/dist/internal.d.ts +21 -0
- package/dist/internal.js +9 -0
- package/dist/knowledge-index.d.ts +1 -1
- package/dist/read-json.d.ts +26 -0
- package/dist/read-json.js +53 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +11 -0
- package/dist/stale.d.ts +2 -2
- package/dist/stale.js +4 -9
- package/dist/surfaces/items.d.ts +100 -1
- package/dist/surfaces/items.js +148 -10
- package/dist/surfaces/surfaces.d.ts +8 -5
- package/dist/surfaces/surfaces.js +34 -17
- package/dist/templates.d.ts +2 -0
- package/dist/templates.js +6 -1
- package/dist/types.d.ts +239 -25
- package/dist/validate.js +44 -56
- package/dist/vault.d.ts +29 -9
- package/dist/vault.js +39 -8
- package/dist/workstream.d.ts +17 -0
- package/dist/workstream.js +78 -0
- package/package.json +10 -4
- package/skill/hafez/SKILL.md +16 -7
package/dist/cli/commands.js
CHANGED
|
@@ -7,49 +7,69 @@ import { BatchOperationSchema, normalizeAliases, opErrorLine, batchInvalidHeader
|
|
|
7
7
|
import { formatBatchError } from './batch-errors.js';
|
|
8
8
|
import { parseDigestInput, digest } from '../digest.js';
|
|
9
9
|
import { detectAgent } from '../harness.js';
|
|
10
|
-
import { getActionItems } from '../document.js';
|
|
11
10
|
import { resemblances } from '../near-match.js';
|
|
12
|
-
import { actionAge,
|
|
11
|
+
import { actionAge, staleHandles, stalenessDaysOf } from '../stale.js';
|
|
13
12
|
import { slugify } from '../vault.js';
|
|
14
|
-
import {
|
|
13
|
+
import { GRAPH_LAYERS } from '../contracts.js';
|
|
14
|
+
import { ITEMS } from '../surfaces/items.js';
|
|
15
|
+
import { footerFor, pointersFor, readJsonOf } from '../read-json.js';
|
|
15
16
|
import { placeIn } from '../surfaces/surfaces.js';
|
|
16
|
-
import { formatEntityHeader, formatKnowledgeHeader, formatQueryTable, formatKnowledgeTable, formatSearchResults, formatValidation, formatStats, formatChangelog } from './format.js';
|
|
17
|
+
import { formatEntityHeader, formatKnowledgeHeader, formatSessionHeader, formatQueryTable, formatKnowledgeTable, formatSessionTable, formatSearchResults, formatValidation, formatHealth, formatStats, formatChangelog } from './format.js';
|
|
17
18
|
function jsonOut(data) {
|
|
18
19
|
return JSON.stringify(data, null, 2);
|
|
19
20
|
}
|
|
20
|
-
const READ_USAGE = 'Usage: hafez read <slug> [--depth frontmatter|summary|full]';
|
|
21
|
-
const READ_FLAGS = { depth: 'string' };
|
|
21
|
+
const READ_USAGE = 'Usage: hafez read <slug> [--depth frontmatter|summary|full] [--archive]';
|
|
22
|
+
const READ_FLAGS = { depth: 'string', archive: 'boolean' };
|
|
22
23
|
export async function cmdRead(os, args, opts = {}) {
|
|
23
24
|
const { flags, positionals } = parseArgs(args, READ_FLAGS, READ_USAGE);
|
|
24
25
|
const slug = positionals[0];
|
|
25
26
|
if (!slug)
|
|
26
27
|
throw new UsageError(READ_USAGE);
|
|
27
28
|
const depth = (str(flags.depth) ?? 'summary');
|
|
28
|
-
const result = await os.read(slug, depth).catch(err => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (opts.json) {
|
|
38
|
-
const writes = footerOp(footer);
|
|
39
|
-
// A frontmatter-depth read carries no body, so it has no actions to list.
|
|
40
|
-
if (result.kind !== 'entity' || depth === 'frontmatter')
|
|
41
|
-
return jsonOut({ ...result, writes });
|
|
42
|
-
return jsonOut({ ...result, actions: agedActions(result.body, result.frontmatter), writes });
|
|
29
|
+
const result = await os.read(slug, depth).catch(err => { throw readMiss(err, slug, opts); });
|
|
30
|
+
if (flags.archive) {
|
|
31
|
+
// The archive is an entity's: reached from it on purpose (#34).
|
|
32
|
+
if (result.kind !== 'entity')
|
|
33
|
+
throw new UsageError(`--archive reads an entity's archived log entries; '${slug}' is a ${result.kind === 'session' ? 'session note' : 'knowledge note'}\n${READ_USAGE}`);
|
|
34
|
+
const archive = await os.archive(slug);
|
|
35
|
+
if (opts.json)
|
|
36
|
+
return jsonOut({ slug, archive });
|
|
37
|
+
return archive ?? `no archive for ${slug}`;
|
|
43
38
|
}
|
|
39
|
+
if (opts.json)
|
|
40
|
+
return jsonOut(await readJsonOf(os, slug, depth, result));
|
|
41
|
+
// A session note is read, never written: no Footer (#35).
|
|
42
|
+
if (result.kind === 'session')
|
|
43
|
+
return formatSessionHeader(result.frontmatter, result.body);
|
|
44
|
+
// The Footer and the pointers are the same at every depth: they read the
|
|
45
|
+
// whole file, not the body this depth shows.
|
|
46
|
+
const whole = depth === 'full' ? result : await os.read(slug, 'full');
|
|
47
|
+
const footer = footerFor(slug, whole);
|
|
48
|
+
const pointers = whole.kind === 'entity' ? await pointersFor(os, slug, whole.body) : null;
|
|
44
49
|
const fm = result.frontmatter;
|
|
45
50
|
const days = stalenessDaysOf(fm);
|
|
46
51
|
const staleMark = (added) => ITEMS.staleMarker.render(actionAge(added, days));
|
|
47
52
|
const text = result.kind === 'entity'
|
|
48
|
-
? formatEntityHeader(fm, ITEMS.handles.render(result.body, staleMark), await parentLine(os, fm.parent)
|
|
53
|
+
? formatEntityHeader(fm, ITEMS.handles.render(result.body, staleMark), await parentLine(os, fm.parent), [
|
|
54
|
+
ITEMS.childrenBlock.render({ slug, children: pointers.children }),
|
|
55
|
+
...pointers.workstreams.map(w => ITEMS.workstreamLine.render(w)),
|
|
56
|
+
ITEMS.sessionsLine.render({ slug, ...pointers.sessions }),
|
|
57
|
+
ITEMS.archivePointer.render({ slug, entries: pointers.archive_entries }),
|
|
58
|
+
].filter(Boolean))
|
|
49
59
|
: formatKnowledgeHeader(fm, result.body);
|
|
50
60
|
// Trailing newlines only: the Footer brings its own blank line.
|
|
51
61
|
return `${text.replace(/\n+$/, '')}\n${ITEMS.writeLine.render(footer)}`;
|
|
52
62
|
}
|
|
63
|
+
/** A `read` miss as the CLI renders it: the closest slugs on stderr (`--json`:
|
|
64
|
+
* `{slug, closest}` on stdout), exit 1. Any other error passes through. */
|
|
65
|
+
function readMiss(err, slug, opts) {
|
|
66
|
+
if (!(err instanceof HafezError) || err.code !== 'NOT_FOUND')
|
|
67
|
+
return err;
|
|
68
|
+
const closest = err.details ?? [];
|
|
69
|
+
return opts.json
|
|
70
|
+
? new RenderedError('NOT_FOUND', jsonOut({ slug, closest }), 'stdout')
|
|
71
|
+
: new RenderedError('NOT_FOUND', ITEMS.slugMissRecovery.render({ slug, closest }), 'stderr');
|
|
72
|
+
}
|
|
53
73
|
/** The `parent:` line's value: the slug with the parent's description. The
|
|
54
74
|
* one thing `read` takes from another file; a parent that cannot be read
|
|
55
75
|
* (a broken slug) is named by its slug alone. */
|
|
@@ -73,25 +93,16 @@ async function staleAfterWrite(os, slug) {
|
|
|
73
93
|
return [];
|
|
74
94
|
}
|
|
75
95
|
}
|
|
76
|
-
|
|
77
|
-
* at every depth, so its Handle comes from the whole file, not from the body
|
|
78
|
-
* this depth shows. */
|
|
79
|
-
async function footerFor(os, slug, result, depth) {
|
|
80
|
-
return { slug, agent: detectAgent(), ...(result.kind === 'entity'
|
|
81
|
-
? {
|
|
82
|
-
target: { kind: 'entity', type: result.frontmatter.type },
|
|
83
|
-
handle: getActionItems(depth === 'full' ? result.body : (await os.read(slug, 'full')).body).find(a => a.handle)?.handle,
|
|
84
|
-
}
|
|
85
|
-
: { target: { kind: 'knowledge', subtype: result.frontmatter.subtype } }) };
|
|
86
|
-
}
|
|
87
|
-
const QUERY_USAGE = `Usage: hafez query [--kind entity|knowledge|all] [--filter ...] [--type t] [--parent slug] [--related-to slug] [--domain d] [--confidence c] [--subtype s] [--tag t] [--since d] [--before d] [--created-since d] [--created-before d] [--sort-by f] [--sort-order asc|desc] [--limit n] [--offset n]
|
|
96
|
+
const QUERY_USAGE = `Usage: hafez query [--kind entity|knowledge|session|all] [--filter ...] [--type t] [--parent slug] [--related-to slug] [--domain d] [--confidence c] [--subtype s] [--tag t] [--since d] [--before d] [--created-since d] [--created-before d] [--sort-by f] [--sort-order asc|desc] [--limit n] [--offset n]
|
|
88
97
|
|
|
89
|
-
--filter active|paused|done matches entity status; knowledge
|
|
90
|
-
status, so under --kind all they are always included
|
|
91
|
-
entities. --filter stale|capture are entity-only
|
|
92
|
-
knowledge. --filter/--type/--parent are rejected with
|
|
93
|
-
--confidence/--subtype are rejected with --kind
|
|
94
|
-
silently dropped. --
|
|
98
|
+
--filter active|paused|done matches entity status; knowledge and session
|
|
99
|
+
notes carry no status, so under --kind all they are always included
|
|
100
|
+
alongside the matching entities. --filter stale|capture are entity-only
|
|
101
|
+
views and return no knowledge. --filter/--type/--parent are rejected with
|
|
102
|
+
--kind knowledge, and --confidence/--subtype are rejected with --kind
|
|
103
|
+
entity, rather than being silently dropped. --kind session lists session
|
|
104
|
+
notes newest first and takes the time window, sort and paging flags only.
|
|
105
|
+
--related-to takes --kind to narrow to one kind and no other query flag.`;
|
|
95
106
|
const QUERY_FLAGS = {
|
|
96
107
|
kind: 'string', filter: 'string', type: 'string', parent: 'string', 'related-to': 'string',
|
|
97
108
|
domain: 'string', confidence: 'string', subtype: 'string', tag: 'string',
|
|
@@ -107,8 +118,9 @@ export const QUERY_FLAG_NAMES = Object.keys(QUERY_FLAGS);
|
|
|
107
118
|
export const QUERY_FLAG_SUPPORT = {
|
|
108
119
|
entity: ['kind', 'filter', 'type', 'parent', 'domain', 'tag', 'since', 'before', 'created-since', 'created-before', 'sort-by', 'sort-order', 'limit', 'offset'],
|
|
109
120
|
knowledge: ['kind', 'domain', 'confidence', 'subtype', 'tag', 'since', 'before', 'created-since', 'created-before', 'sort-by', 'sort-order', 'limit', 'offset'],
|
|
121
|
+
session: ['kind', 'since', 'before', 'created-since', 'created-before', 'sort-by', 'sort-order', 'limit', 'offset'],
|
|
110
122
|
all: ['kind', 'filter', 'type', 'parent', 'domain', 'confidence', 'subtype', 'tag', 'since', 'before', 'created-since', 'created-before', 'sort-by', 'sort-order', 'limit', 'offset'],
|
|
111
|
-
'related-to': ['related-to'],
|
|
123
|
+
'related-to': ['related-to', 'kind'],
|
|
112
124
|
};
|
|
113
125
|
const FLAG_SCOPE_REASON = {
|
|
114
126
|
filter: 'it selects entity status (or the entity-only stale/capture views); knowledge notes carry no status',
|
|
@@ -117,16 +129,17 @@ const FLAG_SCOPE_REASON = {
|
|
|
117
129
|
confidence: 'knowledge notes only',
|
|
118
130
|
subtype: 'knowledge notes only',
|
|
119
131
|
};
|
|
120
|
-
const QUERY_KINDS = ['entity', 'knowledge', 'all'];
|
|
132
|
+
const QUERY_KINDS = ['entity', 'knowledge', 'session', 'all'];
|
|
121
133
|
function rejectUnsupportedFlags(branch, flags) {
|
|
122
134
|
const supported = new Set(QUERY_FLAG_SUPPORT[branch]);
|
|
123
135
|
for (const [name, value] of Object.entries(flags)) {
|
|
124
136
|
if (value === undefined || supported.has(name))
|
|
125
137
|
continue;
|
|
126
138
|
const where = branch === 'related-to'
|
|
127
|
-
? 'with --related-to (it returns every entity and
|
|
139
|
+
? 'with --related-to (it returns every entity, knowledge note and session note linked to the slug)'
|
|
128
140
|
: `with --kind ${branch}`;
|
|
129
|
-
const
|
|
141
|
+
const scope = branch === 'session' && (name === 'domain' || name === 'tag') ? 'a session note carries none' : FLAG_SCOPE_REASON[name];
|
|
142
|
+
const reason = scope ? ` — ${scope}` : '';
|
|
130
143
|
throw new UsageError(`--${name} is not supported ${where}${reason}\n${QUERY_USAGE}`);
|
|
131
144
|
}
|
|
132
145
|
}
|
|
@@ -142,9 +155,10 @@ function intFlag(raw, name) {
|
|
|
142
155
|
export async function cmdQuery(os, args, opts = {}) {
|
|
143
156
|
const { flags } = parseArgs(args, QUERY_FLAGS, QUERY_USAGE);
|
|
144
157
|
const relatedTo = str(flags['related-to']);
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
const kindFlag = str(flags.kind);
|
|
159
|
+
const branch = relatedTo !== undefined ? 'related-to' : (kindFlag ?? 'entity');
|
|
160
|
+
if (kindFlag !== undefined && !QUERY_KINDS.includes(kindFlag)) {
|
|
161
|
+
throw new UsageError(`--kind must be one of ${QUERY_KINDS.join(', ')} (got "${kindFlag}")\n${QUERY_USAGE}`);
|
|
148
162
|
}
|
|
149
163
|
rejectUnsupportedFlags(branch, flags);
|
|
150
164
|
const filter = (str(flags.filter) ?? 'active');
|
|
@@ -165,20 +179,16 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
165
179
|
const offset = intFlag(str(flags.offset), 'offset');
|
|
166
180
|
const sortOpts = { sort_by: sortBy, sort_order: sortOrder, limit, offset };
|
|
167
181
|
const timeOpts = { since, before, createdSince, createdBefore };
|
|
168
|
-
// --related-to:
|
|
182
|
+
// --related-to: every kind linked to the slug, the sessions that touched
|
|
183
|
+
// an entity included; --kind narrows to one (the entity header's
|
|
184
|
+
// `--kind session --related-to` line)
|
|
169
185
|
if (relatedTo) {
|
|
170
|
-
const
|
|
186
|
+
const linked = await os.related_to(relatedTo);
|
|
187
|
+
const items = kindFlag && kindFlag !== 'all' ? linked.items.filter(i => i.kind === kindFlag) : linked.items;
|
|
188
|
+
const result = { items, total: items.length };
|
|
171
189
|
if (opts.json)
|
|
172
190
|
return jsonOut(result);
|
|
173
|
-
|
|
174
|
-
const entities = results.filter(r => r.kind === 'entity');
|
|
175
|
-
const knowledge = results.filter(r => r.kind === 'knowledge');
|
|
176
|
-
const sections = [];
|
|
177
|
-
if (entities.length > 0)
|
|
178
|
-
sections.push('## Entities\n\n' + formatQueryTable(entities, total));
|
|
179
|
-
if (knowledge.length > 0)
|
|
180
|
-
sections.push('## Knowledge\n\n' + formatKnowledgeTable(knowledge, total));
|
|
181
|
-
return sections.length > 0 ? sections.join('\n\n') : 'No results.';
|
|
191
|
+
return formatUnifiedSections(result.items, result.total);
|
|
182
192
|
}
|
|
183
193
|
const queryOpts = { filter, type, parent, domain, ...timeOpts, tags, ...sortOpts };
|
|
184
194
|
// --kind knowledge: query knowledge only. The time window applies here too —
|
|
@@ -190,22 +200,19 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
190
200
|
return jsonOut(result);
|
|
191
201
|
return formatKnowledgeTable(result.items, result.total);
|
|
192
202
|
}
|
|
193
|
-
// --kind
|
|
203
|
+
// --kind session: the session notes alone, newest first
|
|
204
|
+
if (branch === 'session') {
|
|
205
|
+
const result = await os.query_sessions({ ...timeOpts, ...sortOpts });
|
|
206
|
+
if (opts.json)
|
|
207
|
+
return jsonOut(result);
|
|
208
|
+
return formatSessionTable(result.items, result.total);
|
|
209
|
+
}
|
|
210
|
+
// --kind all: every kind via the unified query, sessions included
|
|
194
211
|
if (branch === 'all') {
|
|
195
212
|
const result = await os.queryUnified({ ...queryOpts, kind: 'all', confidence, subtype });
|
|
196
213
|
if (opts.json)
|
|
197
214
|
return jsonOut(result);
|
|
198
|
-
|
|
199
|
-
const entities = items.filter(i => i.kind === 'entity');
|
|
200
|
-
const knowledge = items.filter(i => i.kind === 'knowledge');
|
|
201
|
-
const sections = [];
|
|
202
|
-
if (entities.length > 0)
|
|
203
|
-
sections.push('## Entities\n\n' + formatQueryTable(entities));
|
|
204
|
-
if (knowledge.length > 0)
|
|
205
|
-
sections.push('## Knowledge\n\n' + formatKnowledgeTable(knowledge));
|
|
206
|
-
if (items.length < total)
|
|
207
|
-
sections.push(`\nShowing ${items.length} of ${total}`);
|
|
208
|
-
return sections.length > 0 ? sections.join('\n\n') : 'No results.';
|
|
215
|
+
return formatUnifiedSections(result.items, result.total);
|
|
209
216
|
}
|
|
210
217
|
// --kind entity (default): query entities
|
|
211
218
|
const result = await os.query(queryOpts);
|
|
@@ -213,14 +220,38 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
213
220
|
return jsonOut(result);
|
|
214
221
|
return formatQueryTable(result.items, result.total);
|
|
215
222
|
}
|
|
216
|
-
|
|
223
|
+
/** One section per kind present, in the order entities, knowledge,
|
|
224
|
+
* sessions; a page line when the page is short of the total. */
|
|
225
|
+
function formatUnifiedSections(items, total) {
|
|
226
|
+
const entities = items.filter((i) => i.kind === 'entity');
|
|
227
|
+
const knowledge = items.filter((i) => i.kind === 'knowledge');
|
|
228
|
+
const sessions = items.filter((i) => i.kind === 'session');
|
|
229
|
+
const sections = [];
|
|
230
|
+
if (entities.length > 0)
|
|
231
|
+
sections.push('## Entities\n\n' + formatQueryTable(entities));
|
|
232
|
+
if (knowledge.length > 0)
|
|
233
|
+
sections.push('## Knowledge\n\n' + formatKnowledgeTable(knowledge));
|
|
234
|
+
if (sessions.length > 0)
|
|
235
|
+
sections.push('## Sessions\n\n' + formatSessionTable(sessions));
|
|
236
|
+
if (items.length < total)
|
|
237
|
+
sections.push(`\nShowing ${items.length} of ${total}`);
|
|
238
|
+
return sections.length > 0 ? sections.join('\n\n') : 'No results.';
|
|
239
|
+
}
|
|
240
|
+
const SEARCH_USAGE = `Usage: hafez search <terms> [--kind entity|knowledge|session|all]
|
|
241
|
+
|
|
242
|
+
Without --kind, entities and knowledge notes are searched. --kind all adds
|
|
243
|
+
the session notes; --kind session searches them alone.`;
|
|
217
244
|
const SEARCH_FLAGS = { kind: 'string' };
|
|
245
|
+
const SEARCH_KINDS = ['entity', 'knowledge', 'session', 'all'];
|
|
218
246
|
export async function cmdSearch(os, args, opts = {}) {
|
|
219
247
|
const { flags, positionals } = parseArgs(args, SEARCH_FLAGS, SEARCH_USAGE);
|
|
220
248
|
const terms = positionals.join(' ');
|
|
221
249
|
if (!terms)
|
|
222
250
|
throw new UsageError(SEARCH_USAGE);
|
|
223
|
-
const kind =
|
|
251
|
+
const kind = str(flags.kind);
|
|
252
|
+
if (kind !== undefined && !SEARCH_KINDS.includes(kind)) {
|
|
253
|
+
throw new UsageError(`--kind must be one of ${SEARCH_KINDS.join(', ')} (got "${kind}")\n${SEARCH_USAGE}`);
|
|
254
|
+
}
|
|
224
255
|
const results = await os.search(terms, kind);
|
|
225
256
|
if (opts.json)
|
|
226
257
|
return jsonOut(results);
|
|
@@ -462,7 +493,14 @@ async function updateFailure(os, slug, err, opts) {
|
|
|
462
493
|
return err;
|
|
463
494
|
let retry;
|
|
464
495
|
try {
|
|
465
|
-
|
|
496
|
+
const file = await os.read(slug, 'full');
|
|
497
|
+
// The Op Spec resolves no session note, so the update missed; the slug
|
|
498
|
+
// names one all the same. Say what it is instead of offering a Footer
|
|
499
|
+
// it will never have.
|
|
500
|
+
if (file.kind === 'session') {
|
|
501
|
+
return new RenderedError('VALIDATION_FAILED', `'${slug}' is a session note: written once and read, never updated. What matters from it belongs on the entity it touched.`, 'stderr');
|
|
502
|
+
}
|
|
503
|
+
retry = { footer: footerFor(slug, file) };
|
|
466
504
|
}
|
|
467
505
|
catch (readErr) {
|
|
468
506
|
if (!(readErr instanceof HafezError) || readErr.code !== 'NOT_FOUND')
|
|
@@ -542,6 +580,78 @@ export async function cmdValidate(os, args, opts = {}) {
|
|
|
542
580
|
return jsonOut(report);
|
|
543
581
|
return formatValidation(report);
|
|
544
582
|
}
|
|
583
|
+
export async function cmdHealth(os, args, opts = {}) {
|
|
584
|
+
helpIfAsked(args, 'Usage: hafez health (what placement would change; applies nothing)');
|
|
585
|
+
const report = await os.health();
|
|
586
|
+
if (opts.json)
|
|
587
|
+
return jsonOut(report);
|
|
588
|
+
return formatHealth(report);
|
|
589
|
+
}
|
|
590
|
+
const GRAPH_USAGE = "Usage: hafez graph [slug] [--port N] [--no-open] (serve the vault's graph on 127.0.0.1 and open it in the browser; a slug opens it focused on that note)\n hafez graph --json [--layers tag,domain,session] (the vault's structure as {nodes, edges})";
|
|
591
|
+
const GRAPH_FLAGS = { layers: 'string', port: 'string', 'no-open': 'boolean' };
|
|
592
|
+
/** The local-graph depth `hafez graph <slug>` opens at (#256). */
|
|
593
|
+
const GRAPH_FOCUS_DEPTH = 2;
|
|
594
|
+
export async function cmdGraph(os, args, opts = {}) {
|
|
595
|
+
const { flags, positionals } = parseArgs(args, GRAPH_FLAGS, GRAPH_USAGE);
|
|
596
|
+
if (positionals.length > 1)
|
|
597
|
+
throw new UsageError(GRAPH_USAGE);
|
|
598
|
+
const slug = positionals[0];
|
|
599
|
+
if (!opts.json) {
|
|
600
|
+
if (flags.layers !== undefined)
|
|
601
|
+
throw new UsageError('--layers requires --json', GRAPH_USAGE);
|
|
602
|
+
// A miss fails as `read` does, before any server starts.
|
|
603
|
+
if (slug !== undefined)
|
|
604
|
+
await os.read(slug, 'frontmatter').catch(err => { throw readMiss(err, slug, opts); });
|
|
605
|
+
return serveGraph(opts.vaultPath, flags, slug);
|
|
606
|
+
}
|
|
607
|
+
if (slug !== undefined)
|
|
608
|
+
throw new UsageError('a slug opens the page focused on it; --json prints the whole Graph', GRAPH_USAGE);
|
|
609
|
+
if (flags.port !== undefined || flags['no-open'])
|
|
610
|
+
throw new UsageError('--port and --no-open serve the page; --json prints the Graph and exits', GRAPH_USAGE);
|
|
611
|
+
const layers = splitList(flags.layers);
|
|
612
|
+
const unknown = layers?.find(l => !GRAPH_LAYERS.includes(l));
|
|
613
|
+
if (unknown !== undefined)
|
|
614
|
+
throw new UsageError(`Unknown layer: ${unknown}. Valid layers: ${GRAPH_LAYERS.join(', ')}`, GRAPH_USAGE);
|
|
615
|
+
return jsonOut(await os.graph(layers ? { layers: layers } : undefined));
|
|
616
|
+
}
|
|
617
|
+
const STOP_SIGNALS = ['SIGINT', 'SIGTERM'];
|
|
618
|
+
/** `hafez graph` without --json: serve the page until Ctrl-C or SIGTERM, then close the
|
|
619
|
+
* server and the index. The server gets its own instance whose reads trust the
|
|
620
|
+
* index: it syncs the index when its poll sees a commit (#271). */
|
|
621
|
+
async function serveGraph(vaultPath, flags, focus) {
|
|
622
|
+
const portFlag = str(flags.port);
|
|
623
|
+
if (portFlag !== undefined && !(/^\d+$/.test(portFlag) && Number(portFlag) <= 65535)) {
|
|
624
|
+
throw new UsageError(`--port takes a port number (0-65535), got "${portFlag}"`, GRAPH_USAGE);
|
|
625
|
+
}
|
|
626
|
+
const port = portFlag === undefined ? undefined : Number(portFlag);
|
|
627
|
+
const { startGraphServer, DEFAULT_PORT } = await import('../graph/server.js');
|
|
628
|
+
const { createInternalHafez } = await import('../internal.js');
|
|
629
|
+
const os = createInternalHafez({ vaultPath, git: { push: false }, indexSync: 'manual' });
|
|
630
|
+
const server = await startGraphServer({ os, port }).catch(err => {
|
|
631
|
+
os.close();
|
|
632
|
+
if (err.code !== 'EADDRINUSE' || port === undefined)
|
|
633
|
+
throw err;
|
|
634
|
+
throw new UsageError(`port ${port} is in use: pick another with --port, or leave it off for the first free port from ${DEFAULT_PORT}`);
|
|
635
|
+
});
|
|
636
|
+
// The page reads its view from the URL: a focus opens local-graph mode around it.
|
|
637
|
+
const url = focus === undefined ? server.url : `${server.url}/?focus=${encodeURIComponent(focus)}&depth=${GRAPH_FOCUS_DEPTH}`;
|
|
638
|
+
process.stdout.write(`Serving the graph at ${url} (Ctrl-C to stop)\n`);
|
|
639
|
+
if (!flags['no-open'])
|
|
640
|
+
(await import('../graph/open.js')).openBrowser(url);
|
|
641
|
+
// SIGTERM (a supervisor's kill) stops it as Ctrl-C does, so the index is
|
|
642
|
+
// closed either way (#280). A second signal during the close is not caught.
|
|
643
|
+
await new Promise(resolve => {
|
|
644
|
+
const stop = () => {
|
|
645
|
+
for (const signal of STOP_SIGNALS)
|
|
646
|
+
process.off(signal, stop);
|
|
647
|
+
resolve();
|
|
648
|
+
};
|
|
649
|
+
for (const signal of STOP_SIGNALS)
|
|
650
|
+
process.on(signal, stop);
|
|
651
|
+
});
|
|
652
|
+
await server.close();
|
|
653
|
+
return 'Graph server stopped.';
|
|
654
|
+
}
|
|
545
655
|
export async function cmdIndexRebuild(os) {
|
|
546
656
|
await os.rebuildIndex();
|
|
547
657
|
return 'Index rebuilt.';
|
package/dist/cli/format.d.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import type { QueryResult, KnowledgeQueryResult, ValidationReport, SearchResult, VaultStats, ChangelogEntry } from '../types.js';
|
|
1
|
+
import type { QueryResult, KnowledgeQueryResult, SessionQueryResult, ValidationReport, HealthReport, SearchResult, VaultStats, ChangelogEntry } from '../types.js';
|
|
2
2
|
/** `parent` is the rendered value of the `parent:` line (the slug with the
|
|
3
|
-
* parent's description); absent, the slug alone.
|
|
4
|
-
|
|
3
|
+
* parent's description); absent, the slug alone. `pointers` are the lines
|
|
4
|
+
* naming what hangs off the entity elsewhere (its sessions, its archive). */
|
|
5
|
+
export declare function formatEntityHeader(fm: Record<string, any>, body: string, parent?: string | undefined, pointers?: string[]): string;
|
|
6
|
+
/** A session note: its day, then the entities it touched. */
|
|
7
|
+
export declare function formatSessionHeader(fm: Record<string, any>, body: string): string;
|
|
5
8
|
export declare function formatKnowledgeHeader(fm: Record<string, any>, body: string): string;
|
|
6
9
|
export type QueryTableRow = Pick<QueryResult, 'slug' | 'name' | 'type' | 'status' | 'next_action' | 'next_action_count'>;
|
|
7
10
|
export declare function formatQueryTable(results: QueryTableRow[], total?: number): string;
|
|
8
11
|
export type KnowledgeTableRow = Pick<KnowledgeQueryResult, 'slug' | 'name' | 'confidence' | 'domain' | 'reinforcement_count'>;
|
|
9
12
|
export declare function formatKnowledgeTable(results: KnowledgeTableRow[], total?: number): string;
|
|
13
|
+
export type SessionTableRow = Pick<SessionQueryResult, 'slug' | 'name' | 'session_date' | 'related'>;
|
|
14
|
+
export declare function formatSessionTable(results: SessionTableRow[], total?: number): string;
|
|
10
15
|
export declare function formatSearchResults(results: SearchResult[]): string;
|
|
11
16
|
export declare function formatStats(stats: VaultStats): string;
|
|
12
17
|
export declare function formatChangelog(entries: ChangelogEntry[]): string;
|
|
18
|
+
/** The Health pass as markdown: one section per check, every section
|
|
19
|
+
* printed, so an empty report still shows what was checked. */
|
|
20
|
+
export declare function formatHealth(report: HealthReport): string;
|
|
13
21
|
export declare function formatValidation(report: ValidationReport): string;
|
package/dist/cli/format.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { ITEMS } from '../surfaces/items.js';
|
|
1
2
|
/** `parent` is the rendered value of the `parent:` line (the slug with the
|
|
2
|
-
* parent's description); absent, the slug alone.
|
|
3
|
-
|
|
3
|
+
* parent's description); absent, the slug alone. `pointers` are the lines
|
|
4
|
+
* naming what hangs off the entity elsewhere (its sessions, its archive). */
|
|
5
|
+
export function formatEntityHeader(fm, body, parent = fm.parent, pointers = []) {
|
|
4
6
|
const lines = [`# ${fm.name}`];
|
|
5
7
|
if (fm.description)
|
|
6
8
|
lines.push(`> ${fm.description}`);
|
|
@@ -29,6 +31,23 @@ export function formatEntityHeader(fm, body, parent = fm.parent) {
|
|
|
29
31
|
lines.push(`tags: ${fm.tags.join(', ')}`);
|
|
30
32
|
if (fm.resource)
|
|
31
33
|
lines.push(`resource: ${fm.resource}`);
|
|
34
|
+
lines.push(...pointers);
|
|
35
|
+
lines.push('');
|
|
36
|
+
lines.push(body);
|
|
37
|
+
return lines.join('\n');
|
|
38
|
+
}
|
|
39
|
+
/** A session note: its day, then the entities it touched. */
|
|
40
|
+
export function formatSessionHeader(fm, body) {
|
|
41
|
+
const lines = [`# ${fm.name}`];
|
|
42
|
+
const meta = [];
|
|
43
|
+
if (fm['session-date'])
|
|
44
|
+
meta.push(`session-date: ${fm['session-date']}`);
|
|
45
|
+
if (fm.created)
|
|
46
|
+
meta.push(`created: ${fm.created}`);
|
|
47
|
+
if (meta.length)
|
|
48
|
+
lines.push(meta.join(' | '));
|
|
49
|
+
if (fm.related?.length)
|
|
50
|
+
lines.push(`related: ${fm.related.join(', ')}`);
|
|
32
51
|
lines.push('');
|
|
33
52
|
lines.push(body);
|
|
34
53
|
return lines.join('\n');
|
|
@@ -100,11 +119,24 @@ export function formatKnowledgeTable(results, total) {
|
|
|
100
119
|
}
|
|
101
120
|
return lines.join('\n');
|
|
102
121
|
}
|
|
122
|
+
export function formatSessionTable(results, total) {
|
|
123
|
+
if (results.length === 0)
|
|
124
|
+
return 'No results.';
|
|
125
|
+
const lines = ['| slug | name | session-date | related |', '|------|------|--------------|---------|'];
|
|
126
|
+
for (const r of results) {
|
|
127
|
+
lines.push(`| ${r.slug} | ${r.name} | ${r.session_date} | ${r.related.join(', ')} |`);
|
|
128
|
+
}
|
|
129
|
+
if (total != null && results.length < total) {
|
|
130
|
+
lines.push(`\nShowing ${results.length} of ${total}`);
|
|
131
|
+
}
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
103
134
|
export function formatSearchResults(results) {
|
|
104
135
|
if (results.length === 0)
|
|
105
136
|
return 'No results.';
|
|
106
137
|
const entities = results.filter(r => r.kind === 'entity');
|
|
107
138
|
const knowledge = results.filter(r => r.kind === 'knowledge');
|
|
139
|
+
const sessions = results.filter(r => r.kind === 'session');
|
|
108
140
|
const sections = [];
|
|
109
141
|
if (entities.length > 0) {
|
|
110
142
|
const lines = ['## Entities', '', '| slug | name | type | match |', '|------|------|------|-------|'];
|
|
@@ -118,6 +150,12 @@ export function formatSearchResults(results) {
|
|
|
118
150
|
lines.push(`| ${r.slug} | ${r.name} | ${r.confidence ?? ''} | ${r.snippet} |`);
|
|
119
151
|
sections.push(lines.join('\n'));
|
|
120
152
|
}
|
|
153
|
+
if (sessions.length > 0) {
|
|
154
|
+
const lines = ['## Sessions', '', '| slug | name | match |', '|------|------|-------|'];
|
|
155
|
+
for (const r of sessions)
|
|
156
|
+
lines.push(`| ${r.slug} | ${r.name} | ${r.snippet} |`);
|
|
157
|
+
sections.push(lines.join('\n'));
|
|
158
|
+
}
|
|
121
159
|
return sections.join('\n\n');
|
|
122
160
|
}
|
|
123
161
|
export function formatStats(stats) {
|
|
@@ -159,13 +197,32 @@ export function formatChangelog(entries) {
|
|
|
159
197
|
const rows = entries.map(e => `| ${e.slug} | ${e.kind} | ${e.operation} | ${e.timestamp.slice(0, 10)} | ${e.commit_message} |`);
|
|
160
198
|
return [header, sep, ...rows].join('\n');
|
|
161
199
|
}
|
|
200
|
+
/** The Health pass as markdown: one section per check, every section
|
|
201
|
+
* printed, so an empty report still shows what was checked. */
|
|
202
|
+
export function formatHealth(report) {
|
|
203
|
+
const out = ['## Health'];
|
|
204
|
+
const section = (title, items) => {
|
|
205
|
+
out.push('', `### ${title} (${items.length})`);
|
|
206
|
+
for (const item of items)
|
|
207
|
+
out.push(`- ${item}`);
|
|
208
|
+
};
|
|
209
|
+
// The workstream Item's own text (ADR 0001), prefixed with the entity.
|
|
210
|
+
section('Workstreams', report.workstreams.map(w => `${w.entity} ${ITEMS.workstreamLine.render(w)}`));
|
|
211
|
+
section('Recurring names', report.recurring_names.map(r => `${r.entity}: \`${r.term}\` in ${r.units} units across ${r.days.length} days (${r.days[0]} .. ${r.days[r.days.length - 1]})`));
|
|
212
|
+
section('Leakage', report.leakage.map(l => `${l.parent} → ${l.child} (${l.relation}): ${l.hits} of ${l.entries} entries (${Math.round(100 * l.hits / l.entries)}%)`));
|
|
213
|
+
section('One-way edges', report.one_way_edges.map(e => `${e.from} → ${e.to}: \`hafez link ${e.to} ${e.from} --relation related\``));
|
|
214
|
+
section('Unlinked mentions', report.unlinked_mentions.map(e => `${e.from} mentions [[${e.to}]]: \`hafez link ${e.from} ${e.to} --relation related\``));
|
|
215
|
+
section('Restated decisions', report.restated_decisions.map(r => `${r.day} ${r.entity} → ${r.note} (${Math.round(100 * r.share)}%): ${r.summary}`));
|
|
216
|
+
return out.join('\n');
|
|
217
|
+
}
|
|
162
218
|
export function formatValidation(report) {
|
|
163
219
|
const issues = report.broken_slugs.length + report.orphaned_knowledge.length +
|
|
164
220
|
report.missing_fields.length;
|
|
221
|
+
const counts = `${report.total_entities} entities, ${report.total_knowledge} knowledge, ${report.total_sessions} sessions`;
|
|
165
222
|
if (issues === 0) {
|
|
166
|
-
return `Vault OK — ${
|
|
223
|
+
return `Vault OK — ${counts}, 0 issues`;
|
|
167
224
|
}
|
|
168
|
-
const sections = [`## Vault Validation — ${
|
|
225
|
+
const sections = [`## Vault Validation — ${counts}`];
|
|
169
226
|
const addSection = (title, items) => {
|
|
170
227
|
if (items.length === 0)
|
|
171
228
|
return;
|
package/dist/cli/index.js
CHANGED
|
@@ -41,6 +41,8 @@ export const COMMANDS = {
|
|
|
41
41
|
delete: { phase: 'vault', mutating: true, run: lazy(m => m.cmdDelete) },
|
|
42
42
|
batch: { phase: 'vault', mutating: true, run: lazy(m => m.cmdBatch) },
|
|
43
43
|
validate: { phase: 'vault', run: lazy(m => m.cmdValidate) },
|
|
44
|
+
health: { phase: 'vault', run: lazy(m => m.cmdHealth) },
|
|
45
|
+
graph: { phase: 'vault', run: lazy(m => m.cmdGraph) },
|
|
44
46
|
index: {
|
|
45
47
|
phase: 'vault',
|
|
46
48
|
run: async (os, args) => {
|
|
@@ -68,6 +70,7 @@ export const EXIT_TABLE = [
|
|
|
68
70
|
{ exit: 5, meaning: 'slug already exists', errorCodes: ['SLUG_EXISTS'] },
|
|
69
71
|
{ exit: 6, meaning: 'git commit failed — nothing was written to history', errorCodes: ['GIT_COMMIT_FAILED'] },
|
|
70
72
|
{ exit: 7, meaning: 'vault locked by another hafez process — retry when it finishes', errorCodes: ['VAULT_LOCKED'] },
|
|
73
|
+
{ exit: 8, meaning: "git read failed (changelog, stats) — the vault's git is unusable", errorCodes: ['GIT_READ_FAILED'] },
|
|
71
74
|
];
|
|
72
75
|
export const EXIT_CODES = Object.fromEntries(EXIT_TABLE.flatMap(row => (row.errorCodes ?? []).map(code => [code, row.exit])));
|
|
73
76
|
/** Exported so a guard test can assert every registered command is documented
|
|
@@ -75,11 +78,14 @@ export const EXIT_CODES = Object.fromEntries(EXIT_TABLE.flatMap(row => (row.erro
|
|
|
75
78
|
export const USAGE = `Usage: hafez <command> [args]
|
|
76
79
|
|
|
77
80
|
Commands:
|
|
78
|
-
read <slug> Read entity or
|
|
81
|
+
read <slug> Read an entity, knowledge note or session note
|
|
82
|
+
--archive An entity's archived log entries
|
|
79
83
|
query [--filter ...] ... Filter entities/knowledge
|
|
80
|
-
--kind all
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
--kind all Every kind, sessions included; --filter matches
|
|
85
|
+
entity status only, so the rest always show
|
|
86
|
+
--kind session Session notes alone, newest first
|
|
87
|
+
search <terms> Full-text search of entities and knowledge
|
|
88
|
+
--kind session|all Session notes alone, or every kind
|
|
83
89
|
create <kind> <name> Create entity or knowledge note
|
|
84
90
|
capture <name> [--notes] Quick inbox capture
|
|
85
91
|
promote <slug> <target> Promote capture→entity/project/knowledge, entity→project
|
|
@@ -105,15 +111,24 @@ Commands:
|
|
|
105
111
|
changelog --since <when> Git-derived change history (e.g. --since 7.days.ago)
|
|
106
112
|
schema [op] [--examples] Machine-readable JSON schema for batch operations
|
|
107
113
|
validate Check vault integrity
|
|
114
|
+
health The Health pass: what placement would change (applies nothing)
|
|
115
|
+
graph --json The vault's structure: entities and knowledge, parent/related/wiki-link edges
|
|
116
|
+
--layers tag,domain,session Add tag and domain nodes, and sessions
|
|
117
|
+
graph [slug] [--port N] [--no-open]
|
|
118
|
+
Serve the graph on 127.0.0.1 (first free port from 4747) and open the browser;
|
|
119
|
+
a slug opens it focused on that note, local graph at depth 2
|
|
108
120
|
index rebuild Rebuild the SQLite index
|
|
109
121
|
init --register <path> Register an existing vault
|
|
110
122
|
init Show current vault resolution
|
|
111
123
|
onboard Setup status, then the script your agent follows to set Hafez up
|
|
112
|
-
--install --mode <always-on|invoke-only> [--harness <h>] [--confirm]
|
|
124
|
+
--install --mode <always-on|invoke-only> [--harness <h>] [--vault-repo <o/r>] [--confirm]
|
|
113
125
|
Write both skills and, always-on, the Block
|
|
126
|
+
(--vault-repo: the cloud Block, which attaches that repo)
|
|
114
127
|
--remove [--confirm] Remove the skills and the Block region
|
|
115
128
|
--block [--harness <h>] Print the Block and the entities matching this folder
|
|
116
129
|
(the SessionStart hook; prints nothing on any failure)
|
|
130
|
+
--cloud --vault-repo <o/r>
|
|
131
|
+
Print the setup script for Claude Code cloud sessions
|
|
117
132
|
upgrade Check whether hafez is outdated and print the update steps
|
|
118
133
|
help --agent The hafez skill body: the reference when the skill is not installed
|
|
119
134
|
|
|
@@ -278,22 +293,28 @@ export async function main(argv) {
|
|
|
278
293
|
// First touch of the core — and so of the native SQLite binding. A
|
|
279
294
|
// failure to load it lands in the catch below and is recognised by
|
|
280
295
|
// isBindingsFailure, exactly as it was when this import was static.
|
|
281
|
-
const {
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
296
|
+
const { createInternalHafez } = await import('../internal.js');
|
|
297
|
+
// One index scan per process (#269): the first read syncs, later reads trust it.
|
|
298
|
+
const os = createInternalHafez({ vaultPath, git: { push: false }, indexSync: 'once' });
|
|
299
|
+
try {
|
|
300
|
+
const result = await spec.run(os, args, { json, vaultPath });
|
|
301
|
+
// Auto-sync after mutating commands
|
|
302
|
+
if (spec.mutating) {
|
|
303
|
+
try {
|
|
304
|
+
await os.sync();
|
|
305
|
+
}
|
|
306
|
+
catch (syncErr) {
|
|
307
|
+
// Operation succeeded but sync failed — emit result with warning, exit 4
|
|
308
|
+
process.stdout.write(result + '\n');
|
|
309
|
+
process.stderr.write(`Warning: ${syncErr.message ?? syncErr.message}\n`);
|
|
310
|
+
process.exit(4);
|
|
311
|
+
}
|
|
294
312
|
}
|
|
313
|
+
process.stdout.write(result + '\n');
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
os.close();
|
|
295
317
|
}
|
|
296
|
-
process.stdout.write(result + '\n');
|
|
297
318
|
}
|
|
298
319
|
catch (err) {
|
|
299
320
|
if (hook)
|
package/dist/cli/install.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare const SKILL_DIRS: string[];
|
|
|
6
6
|
export type HashState = 'absent' | 'current' | 'stale' | 'hand-edited';
|
|
7
7
|
/** What to do about broken markers: a hand edit, then `rerun`. */
|
|
8
8
|
export declare const fixBrokenMarkers: (path: string, rerun: string) => string;
|
|
9
|
+
/** The flag that keeps an install a cloud one, for every printed install line. */
|
|
10
|
+
export declare const vaultRepoFlag: (vaultRepo: string | undefined) => string;
|
|
9
11
|
/** What to do about a 1.x section: Hafez cannot tell where it ends. */
|
|
10
12
|
export declare const removeLegacy: (path: string, line: number) => string;
|
|
11
13
|
/** The status line for a 1.x section. */
|
|
@@ -24,13 +26,18 @@ export interface InstallState {
|
|
|
24
26
|
onboarded: boolean;
|
|
25
27
|
/** The line of a 1.x section in the instructions file, else null. */
|
|
26
28
|
legacy: number | null;
|
|
29
|
+
/** The Block is a cloud Block, for this vault repo. */
|
|
30
|
+
vaultRepo?: string;
|
|
27
31
|
}
|
|
28
32
|
export declare function installState(home: string, harness: HarnessId): InstallState;
|
|
33
|
+
/** `vaultRepo`: the install is for a cloud container, whose Block carries
|
|
34
|
+
* the rule that attaches that repo as the vault (#160). */
|
|
29
35
|
export declare function install(opts: {
|
|
30
36
|
home: string;
|
|
31
37
|
harness: HarnessId;
|
|
32
38
|
mode: Mode;
|
|
33
39
|
confirm: boolean;
|
|
40
|
+
vaultRepo?: string;
|
|
34
41
|
}): string;
|
|
35
42
|
export interface SkillCheck {
|
|
36
43
|
path: string;
|
|
@@ -43,6 +50,8 @@ export interface BlockCheck {
|
|
|
43
50
|
state: Exclude<HashState, 'absent'> | 'broken';
|
|
44
51
|
/** The Block Hafez writes has changed since this one was written. */
|
|
45
52
|
renderChanged: boolean;
|
|
53
|
+
/** A cloud Block: the vault repo it was written for. */
|
|
54
|
+
vaultRepo?: string;
|
|
46
55
|
}
|
|
47
56
|
export interface LegacyCheck {
|
|
48
57
|
harness: HarnessId;
|