@gitset-dev/cli 2.3.2 → 2.4.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.
@@ -0,0 +1,266 @@
1
+ 'use strict';
2
+
3
+ const DOC_SPECS = [
4
+ {
5
+ id: 'architecture',
6
+ filename: 'architecture.md',
7
+ title: 'Architecture',
8
+ llm: true,
9
+ sections: [
10
+ 'System Overview',
11
+ 'Entry Points',
12
+ 'Core Components',
13
+ 'Data Flow',
14
+ 'External Dependencies',
15
+ ],
16
+ guidance: 'Core Components: one bullet per module with a one-line role — no per-file listings and no tables (module-map.md already covers per-file detail). Data Flow: a numbered list of at most 8 steps.',
17
+ },
18
+ {
19
+ id: 'developer-guide',
20
+ filename: 'developer-guide.md',
21
+ title: 'Developer Guide',
22
+ llm: true,
23
+ sections: [
24
+ 'Prerequisites',
25
+ 'Setup',
26
+ 'Project Layout',
27
+ 'Testing',
28
+ 'Release & Deployment',
29
+ ],
30
+ guidance: 'Project Layout: at most 6 bullet lines describing the top-level directories — never a module table, module-map.md already covers that. Every section must be short and practical; this is an onboarding page, not a reference.',
31
+ },
32
+ {
33
+ id: 'commands-and-workflows',
34
+ filename: 'commands-and-workflows.md',
35
+ title: 'Commands & Workflows',
36
+ llm: true,
37
+ sections: [
38
+ 'Package Scripts',
39
+ 'CLI Commands',
40
+ 'CI Workflows',
41
+ 'Common Tasks',
42
+ ],
43
+ guidance: 'CLI Commands: bullets in the form `command` — one-line description, using only registered command names from the digest. Do not cite source file paths in this document.',
44
+ },
45
+ ];
46
+
47
+ const AGENTS_START = '<!-- gitset-knowledge:start -->';
48
+ const AGENTS_END = '<!-- gitset-knowledge:end -->';
49
+
50
+ function buildStructuralDigest(map, files) {
51
+ const lines = [];
52
+ if (map.manifest) {
53
+ const m = map.manifest;
54
+ lines.push(`Project: ${m.name || 'unknown'}${m.version ? ` v${m.version}` : ''}`);
55
+ if (m.description) lines.push(`Description: ${m.description}`);
56
+ if (m.bin) lines.push(`Binaries: ${typeof m.bin === 'string' ? m.bin : Object.entries(m.bin).map(([k, v]) => `${k} -> ${v}`).join(', ')}`);
57
+ if (m.engines) lines.push(`Engines: ${JSON.stringify(m.engines)}`);
58
+ if (m.packageManager) lines.push(`Package manager: ${m.packageManager}`);
59
+ const scripts = Object.entries(m.scripts || {});
60
+ if (scripts.length) {
61
+ lines.push('Package scripts:');
62
+ for (const [name, cmd] of scripts) lines.push(` ${name}: ${cmd}`);
63
+ }
64
+ }
65
+ if (map.entryPoints.length) lines.push(`Entry points: ${map.entryPoints.join(', ')}`);
66
+ if (map.registeredCommands && map.registeredCommands.length) {
67
+ lines.push(`CLI commands registered in the entry point (authoritative names): ${map.registeredCommands.join(', ')}`);
68
+ }
69
+
70
+ lines.push('Modules:');
71
+ for (const mod of map.modules) {
72
+ lines.push(` ${mod.name} — ${mod.sourceCount} source, ${mod.testCount} test, ${mod.docCount} doc files`);
73
+ }
74
+
75
+ if (map.moduleEdges.length) {
76
+ lines.push('Module dependencies (imports between modules):');
77
+ for (const { edge, count } of map.moduleEdges) lines.push(` ${edge} (${count})`);
78
+ }
79
+
80
+ if (map.externalDeps.length) {
81
+ const top = map.externalDeps.slice(0, 15).map((d) => `${d.name} (${d.count})`).join(', ');
82
+ lines.push(`External packages imported in code: ${top}`);
83
+ }
84
+
85
+ const workflows = files.filter((f) => /^\.github\/workflows\//.test(f.path)).map((f) => f.path);
86
+ if (workflows.length) lines.push(`CI workflow files: ${workflows.join(', ')}`);
87
+
88
+ const central = [...map.centrality.entries()]
89
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
90
+ .slice(0, 10)
91
+ .map(([p, n]) => `${p} (imported by ${n})`);
92
+ if (central.length) lines.push(`Most imported files: ${central.join(', ')}`);
93
+
94
+ return lines.join('\n');
95
+ }
96
+
97
+ function buildSummarizeFilesBlock(batchFiles) {
98
+ return batchFiles
99
+ .map((f) => `FILE: ${f.path} [${f.mode}${f.centrality ? `, imported by ${f.centrality}` : ''}]\n${f.content}`)
100
+ .join('\n\n---\n\n');
101
+ }
102
+
103
+ function parseSummarizeResponse(text) {
104
+ let cleaned = String(text || '').trim()
105
+ .replace(/^```(?:json)?\n?/i, '')
106
+ .replace(/\n?```$/i, '')
107
+ .trim();
108
+ const start = cleaned.indexOf('[');
109
+ const end = cleaned.lastIndexOf(']');
110
+ if (start === -1 || end === -1 || end <= start) return null;
111
+ try {
112
+ const parsed = JSON.parse(cleaned.slice(start, end + 1));
113
+ if (!Array.isArray(parsed)) return null;
114
+ const entries = parsed
115
+ .filter((e) => e && typeof e.path === 'string')
116
+ .map((e) => ({
117
+ path: e.path,
118
+ purpose: typeof e.purpose === 'string' ? e.purpose : '',
119
+ exports: Array.isArray(e.exports) ? e.exports.map(String).slice(0, 20) : [],
120
+ dependencies: Array.isArray(e.dependencies) ? e.dependencies.map(String).slice(0, 20) : [],
121
+ notes: typeof e.notes === 'string' ? e.notes : '',
122
+ }));
123
+ return entries.length ? entries : null;
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ function summariesToText(moduleSummaries) {
130
+ const blocks = [];
131
+ for (const modName of Object.keys(moduleSummaries).sort()) {
132
+ const entries = moduleSummaries[modName];
133
+ blocks.push(`MODULE: ${modName}`);
134
+ if (typeof entries === 'string') {
135
+ blocks.push(entries);
136
+ continue;
137
+ }
138
+ for (const e of entries) {
139
+ const parts = [`- ${e.path}: ${e.purpose}`];
140
+ if (e.exports && e.exports.length) parts.push(` exports: ${e.exports.join(', ')}`);
141
+ if (e.notes) parts.push(` notes: ${e.notes}`);
142
+ blocks.push(parts.join('\n'));
143
+ }
144
+ }
145
+ return blocks.join('\n');
146
+ }
147
+
148
+ function renderIndexDoc({ map, files, tag, validationSummary }) {
149
+ const stats = {
150
+ total: files.length,
151
+ source: files.filter((f) => f.kind === 'source').length,
152
+ test: files.filter((f) => f.kind === 'test').length,
153
+ config: files.filter((f) => f.kind === 'config').length,
154
+ doc: files.filter((f) => f.kind === 'doc').length,
155
+ };
156
+ const name = (map.manifest && map.manifest.name) || '(unnamed project)';
157
+ const description = (map.manifest && map.manifest.description) || '';
158
+ const lines = [
159
+ `# ${name} — Knowledge Base`,
160
+ '',
161
+ description,
162
+ '',
163
+ `> Generated by [Gitset](https://gitset.dev) Knowledge Mapper${tag ? ` at ${tag}` : ''}. Derived from source code, manifests and CI configuration — not from existing prose docs.`,
164
+ '',
165
+ '## Contents',
166
+ '',
167
+ '- [Architecture](architecture.md) — system design, entry points, data flow',
168
+ '- [Developer Guide](developer-guide.md) — setup, testing, release',
169
+ '- [Commands & Workflows](commands-and-workflows.md) — scripts, CLI commands, CI',
170
+ '- [Module Map](module-map.md) — per-module summaries and dependency edges',
171
+ '',
172
+ '## Repository Stats',
173
+ '',
174
+ `| Files | Source | Tests | Config | Docs |`,
175
+ `| ---: | ---: | ---: | ---: | ---: |`,
176
+ `| ${stats.total} | ${stats.source} | ${stats.test} | ${stats.config} | ${stats.doc} |`,
177
+ '',
178
+ map.entryPoints.length ? `Entry points: ${map.entryPoints.map((e) => `\`${e}\``).join(', ')}` : '',
179
+ '',
180
+ ];
181
+ if (validationSummary) {
182
+ lines.push(`Validation: ${validationSummary}`, '');
183
+ }
184
+ return `${lines.filter((l) => l !== null).join('\n').replace(/\n{3,}/g, '\n\n').trim()}\n`;
185
+ }
186
+
187
+ function renderModuleMapDoc({ map, moduleSummaries }) {
188
+ const lines = ['# Module Map', ''];
189
+ lines.push('## Modules', '');
190
+ for (const mod of map.modules) {
191
+ lines.push(`### \`${mod.name}\``, '');
192
+ const summary = moduleSummaries[mod.name];
193
+ if (typeof summary === 'string') {
194
+ lines.push(summary.trim(), '');
195
+ } else if (Array.isArray(summary)) {
196
+ for (const e of summary) {
197
+ lines.push(`- \`${e.path}\` — ${e.purpose || 'no summary'}`);
198
+ if (e.exports && e.exports.length) lines.push(` - exports: ${e.exports.map((x) => `\`${x}\``).join(', ')}`);
199
+ if (e.notes) lines.push(` - ${e.notes}`);
200
+ }
201
+ lines.push('');
202
+ } else {
203
+ const shown = mod.files.filter((f) => f.kind !== 'doc').slice(0, 30);
204
+ for (const f of shown) lines.push(`- \`${f.path}\` (${f.kind})`);
205
+ lines.push('');
206
+ }
207
+ }
208
+ if (map.moduleEdges.length) {
209
+ lines.push('## Dependency Edges', '');
210
+ lines.push('| From | To | Imports |', '| :--- | :--- | ---: |');
211
+ for (const { edge, count } of map.moduleEdges) {
212
+ const [from, to] = edge.split(' -> ');
213
+ lines.push(`| \`${from}\` | \`${to}\` | ${count} |`);
214
+ }
215
+ lines.push('');
216
+ }
217
+ if (map.externalDeps.length) {
218
+ lines.push('## External Packages (imported in code)', '');
219
+ for (const dep of map.externalDeps.slice(0, 25)) {
220
+ lines.push(`- \`${dep.name}\` (${dep.count} import${dep.count === 1 ? '' : 's'})`);
221
+ }
222
+ lines.push('');
223
+ }
224
+ return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trim()}\n`;
225
+ }
226
+
227
+ function buildAgentsBlock() {
228
+ return [
229
+ AGENTS_START,
230
+ '## Repository knowledge base',
231
+ '',
232
+ 'Structural context for this repository is maintained by [Gitset](https://gitset.dev) Knowledge Mapper.',
233
+ 'Before making changes, read:',
234
+ '',
235
+ '- [docs/gitset-knowledge/index.md](docs/gitset-knowledge/index.md)',
236
+ '- [docs/gitset-knowledge/architecture.md](docs/gitset-knowledge/architecture.md)',
237
+ '- [docs/gitset-knowledge/module-map.md](docs/gitset-knowledge/module-map.md)',
238
+ AGENTS_END,
239
+ ].join('\n');
240
+ }
241
+
242
+ function applyAgentsPointer(existingContent) {
243
+ const block = buildAgentsBlock();
244
+ if (!existingContent || !existingContent.trim()) {
245
+ return { content: `# AGENTS.md\n\n${block}\n`, action: 'created' };
246
+ }
247
+ const markerRe = new RegExp(`${AGENTS_START}[\\s\\S]*?${AGENTS_END}`);
248
+ if (markerRe.test(existingContent)) {
249
+ return { content: existingContent.replace(markerRe, block), action: 'updated' };
250
+ }
251
+ return { content: `${existingContent.replace(/\n*$/, '\n\n')}${block}\n`, action: 'appended' };
252
+ }
253
+
254
+ module.exports = {
255
+ DOC_SPECS,
256
+ AGENTS_START,
257
+ AGENTS_END,
258
+ buildStructuralDigest,
259
+ buildSummarizeFilesBlock,
260
+ parseSummarizeResponse,
261
+ summariesToText,
262
+ renderIndexDoc,
263
+ renderModuleMapDoc,
264
+ buildAgentsBlock,
265
+ applyAgentsPointer,
266
+ };
@@ -98,7 +98,13 @@ const release = {
98
98
  'headings, ordering, and tone closely — treat it as the format to reproduce. ' +
99
99
  'Never invent a repository path (e.g. placeholders like "your-project/your-repo" ' +
100
100
  'or "user/repo") for a "Full Changelog" or compare link — only include such a link ' +
101
- 'when the real repository is given below, using that exact path; otherwise omit it.',
101
+ 'when the real repository is given below, using that exact path; otherwise omit it. ' +
102
+ 'Never attribute a change to a person (e.g. "by @username") — a commit author\'s ' +
103
+ 'display name (e.g. "Jane Smith") is NOT their GitHub username and must never be ' +
104
+ 'guessed into one (e.g. never invent "@jane-smith"). If a commit message already ' +
105
+ 'ends in a real PR reference like "(#123)", you may link directly to that PR using ' +
106
+ 'the given repository path (e.g. https://github.com/<repo>/pull/123), but state only ' +
107
+ 'what changed — no author mention.',
102
108
  user: [
103
109
  repo && `Repository: ${clip(repo, 200)}`,
104
110
  tag && `Release: ${clip(tag, 100)}`,
@@ -129,6 +135,74 @@ const about = {
129
135
  },
130
136
  };
131
137
 
138
+ const knowledgeSummarize = {
139
+ id: 'knowledgeSummarize',
140
+ build(ctx = {}) {
141
+ const { repo = '', module = '', files = '' } = ctx;
142
+ return {
143
+ system:
144
+ 'You are a code cartographer producing structured file summaries for a ' +
145
+ 'repository knowledge base consumed by AI agents and developers. Describe ' +
146
+ 'ONLY what is present in the provided file contents — if something is not ' +
147
+ 'visible in the input, do not claim it. Never invent file paths, exports, ' +
148
+ 'or behavior. Respond with ONLY a valid JSON array, no prose, no code ' +
149
+ 'fences: [{"path": string, "purpose": string (<= 220 chars, concrete and ' +
150
+ 'specific), "exports": string[] (ONLY values the file actually exports/' +
151
+ 'exposes — module.exports, export statements, __all__; an executable or ' +
152
+ 'script with no exports gets [], never its internal functions; max 12), ' +
153
+ '"dependencies": string[] (notable internal files or external ' +
154
+ 'packages this file relies on, max 10), "notes": string (<= 160 chars — ' +
155
+ 'gotchas, side effects, or "" if none)}]. One entry per FILE block in the ' +
156
+ 'input, using the exact path given. For a dispatcher/entry-point file, ' +
157
+ 'note in "notes" the exact command or route names it registers.',
158
+ user: [
159
+ repo && `Repository: ${clip(repo, 200)}`,
160
+ module && `Module: ${clip(module, 200)}`,
161
+ `Files:\n\n${clip(files, 30000)}`,
162
+ ].filter(Boolean).join('\n\n'),
163
+ };
164
+ },
165
+ };
166
+
167
+ const knowledgeWrite = {
168
+ id: 'knowledgeWrite',
169
+ build(ctx = {}) {
170
+ const { repo = '', doc = '', sections = '', guidance = '', digest = '', summaries = '' } = ctx;
171
+ return {
172
+ system:
173
+ 'You write one Markdown document of a repository knowledge base optimized ' +
174
+ 'for AI agents and developers: dense, factual, skimmable. Ground every ' +
175
+ 'statement in the structural digest and file summaries provided — never ' +
176
+ 'cite a file path, command, script, or dependency that does not appear in ' +
177
+ 'the input. If the input lacks information for a section, write what is ' +
178
+ 'known and omit speculation entirely. Use the exact section headings ' +
179
+ 'requested, as "## " headings, in the given order — every requested ' +
180
+ 'section must appear. Start directly with ' +
181
+ 'the "# " title line — no preamble, no sign-off, no code fences around ' +
182
+ 'the document. Keep it concise: prefer tables and short bullet lists ' +
183
+ 'over paragraphs. In Markdown tables, keep every cell compact with a ' +
184
+ 'single space around each pipe (| Cell | Cell |) — never pad columns ' +
185
+ 'with runs of spaces to align them. When referring to a repository file, format it as a ' +
186
+ 'relative markdown link from docs/gitset-knowledge/ (e.g. ' +
187
+ '[src/x.js](../../src/x.js)) — the link target is the file path prefixed ' +
188
+ 'with ../../, never a repeated or altered filename. When documenting CLI ' +
189
+ 'commands or routes, use the exact names the dispatcher/entry point ' +
190
+ 'registers (from the summaries/digest), never names derived from file ' +
191
+ 'names. End the document immediately after the last line of the final ' +
192
+ 'requested section — no trailing blank lines, separators, or repeated ' +
193
+ 'characters of any kind.',
194
+ user: [
195
+ repo && `Repository: ${clip(repo, 200)}`,
196
+ `Document to write: ${clip(doc, 200)}`,
197
+ sections && `Required sections, in order: ${clip(sections, 500)}`,
198
+ guidance && `Section guidance (follow strictly): ${clip(guidance, 1000)}`,
199
+ digest && `Structural digest (ground truth):\n${clip(digest, 12000)}`,
200
+ summaries && `File summaries (ground truth):\n${clip(summaries, 40000)}`,
201
+ ].filter(Boolean).join('\n\n'),
202
+ };
203
+ },
204
+ };
205
+
132
206
  const gitignore = {
133
207
  id: 'gitignore',
134
208
  build(ctx = {}) {
@@ -192,4 +266,4 @@ const labelDescriptions = {
192
266
  },
193
267
  };
194
268
 
195
- module.exports = { commit, issue, pr, readme, release, about, gitignore, labels, labelDescriptions };
269
+ module.exports = { commit, issue, pr, readme, release, about, gitignore, labels, labelDescriptions, knowledgeSummarize, knowledgeWrite };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitset-dev/cli",
3
- "version": "2.3.2",
3
+ "version": "2.4.0",
4
4
  "description": "Gitset CLI — drafts commits, PRs, issues, READMEs and release notes with your own AI key. Fully local: your code and keys never touch a Gitset server. Draft. Refine. Ship.",
5
5
  "type": "commonjs",
6
6
  "bin": {