@handsupmin/gc-tree 0.7.0 → 0.7.2

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/dist/src/cli.js CHANGED
@@ -139,6 +139,11 @@ async function launchGuidedFlow({ provider, providerMode, preferredLanguage, tar
139
139
  return plan;
140
140
  return maybeLaunchProvider(plan);
141
141
  }
142
+ function printJsonUnlessLaunched(payload, launch) {
143
+ if (launch?.launched)
144
+ return;
145
+ console.log(JSON.stringify(payload, null, 2));
146
+ }
142
147
  async function main() {
143
148
  if (hasFlag('--version') || hasFlag('-v')) {
144
149
  stdout.write(`${await readPackageVersion()}\n`);
@@ -189,7 +194,7 @@ async function main() {
189
194
  noLaunch: hasFlag('--no-launch'),
190
195
  })
191
196
  : null;
192
- console.log(JSON.stringify({
197
+ printJsonUnlessLaunched({
193
198
  ...result,
194
199
  provider_mode: settings.provider_mode,
195
200
  preferred_provider: settings.preferred_provider,
@@ -197,7 +202,7 @@ async function main() {
197
202
  global_scaffold: globalScaffold,
198
203
  scaffold,
199
204
  launch,
200
- }, null, 2));
205
+ }, launch);
201
206
  return;
202
207
  }
203
208
  case 'checkout': {
@@ -289,7 +294,7 @@ async function main() {
289
294
  command: 'gc-onboard',
290
295
  noLaunch: hasFlag('--no-launch'),
291
296
  });
292
- console.log(JSON.stringify({ mode: 'guided_onboarding', gc_branch: gcBranch, preferred_provider: provider, scaffold, launch }, null, 2));
297
+ printJsonUnlessLaunched({ mode: 'guided_onboarding', gc_branch: gcBranch, preferred_provider: provider, scaffold, launch }, launch);
293
298
  return;
294
299
  }
295
300
  case 'verify-onboarding': {
@@ -486,7 +491,7 @@ async function main() {
486
491
  command: 'gc-update-global-context',
487
492
  noLaunch: hasFlag('--no-launch'),
488
493
  });
489
- console.log(JSON.stringify({ mode: 'guided_update', gc_branch: gcBranch, preferred_provider: provider, scaffold, launch }, null, 2));
494
+ printJsonUnlessLaunched({ mode: 'guided_update', gc_branch: gcBranch, preferred_provider: provider, scaffold, launch }, launch);
490
495
  return;
491
496
  }
492
497
  case '__apply-update': {
@@ -7,6 +7,63 @@ export function slugify(value) {
7
7
  .replace(/^-|-$/g, '');
8
8
  return normalized || 'doc';
9
9
  }
10
+ function parseDocReference(value) {
11
+ const trimmed = String(value || '').trim().replace(/^\.?\//, '');
12
+ if (!trimmed)
13
+ return null;
14
+ const normalized = trimmed.replace(/\\/g, '/');
15
+ const docsMatch = normalized.match(/^docs\/([^/]+)\/([^/]+?)(?:\.md)?$/i);
16
+ if (docsMatch) {
17
+ const category = normalizeCategory(docsMatch[1]);
18
+ const slug = slugify(docsMatch[2]);
19
+ if (slug === 'index')
20
+ return null;
21
+ return { category, slug, label: docsMatch[2].replace(/\.md$/i, '').trim() };
22
+ }
23
+ const directMatch = normalized.match(/^(role|repos|domain|workflows|conventions|infra|verification)\/([^/]+?)(?:\.md)?$/i);
24
+ if (directMatch) {
25
+ const category = normalizeCategory(directMatch[1]);
26
+ const slug = slugify(directMatch[2]);
27
+ if (slug === 'index')
28
+ return null;
29
+ return { category, slug, label: directMatch[2].replace(/\.md$/i, '').trim() };
30
+ }
31
+ return null;
32
+ }
33
+ function parseTitlePlacement(value) {
34
+ const trimmed = String(value || '').trim();
35
+ if (!trimmed)
36
+ return null;
37
+ if (/^index$/i.test(trimmed))
38
+ return { category: 'index', slug: 'index', label: 'Index' };
39
+ const match = trimmed.match(/^(role|repo|repos|repository|repositories|domain|workflow|workflows|convention|conventions|infra|verification)\s*:\s*(.+)$/i);
40
+ if (!match)
41
+ return null;
42
+ const rawCategory = match[1].toLowerCase();
43
+ const categoryMap = {
44
+ role: 'role',
45
+ repo: 'repos',
46
+ repos: 'repos',
47
+ repository: 'repos',
48
+ repositories: 'repos',
49
+ domain: 'domain',
50
+ workflow: 'workflows',
51
+ workflows: 'workflows',
52
+ convention: 'conventions',
53
+ conventions: 'conventions',
54
+ infra: 'infra',
55
+ verification: 'verification',
56
+ };
57
+ const label = match[2].trim();
58
+ const category = categoryMap[rawCategory];
59
+ if (!category || !label)
60
+ return null;
61
+ return {
62
+ category,
63
+ slug: slugify(label),
64
+ label,
65
+ };
66
+ }
10
67
  export function ensureSummary(summary) {
11
68
  const trimmed = String(summary || '').trim();
12
69
  if (!trimmed) {
@@ -17,6 +74,10 @@ export function ensureSummary(summary) {
17
74
  export function renderDocMarkdown(doc) {
18
75
  const summary = ensureSummary(doc.summary);
19
76
  const body = String(doc.body || '').trim();
77
+ const normalizedIndexEntries = [...new Set([
78
+ ...(doc.indexLabel?.trim() ? [doc.indexLabel.trim()] : []),
79
+ ...(doc.indexEntries || []).map((entry) => String(entry || '').trim()).filter(Boolean),
80
+ ])];
20
81
  return [
21
82
  `# ${doc.title.trim()}`,
22
83
  '',
@@ -27,8 +88,8 @@ export function renderDocMarkdown(doc) {
27
88
  ...(doc.tags && doc.tags.length > 0
28
89
  ? ['## Tags', '', ...doc.tags.map((tag) => `- ${tag}`), '']
29
90
  : []),
30
- ...(doc.indexEntries && doc.indexEntries.length > 0
31
- ? ['## Index Entries', '', ...doc.indexEntries.map((entry) => `- ${entry}`), '']
91
+ ...(normalizedIndexEntries.length > 0
92
+ ? ['## Index Entries', '', ...normalizedIndexEntries.map((entry) => `- ${entry}`), '']
32
93
  : []),
33
94
  '## Details',
34
95
  '',
@@ -158,6 +219,60 @@ export function extractIndexEntries(markdown) {
158
219
  .map((line) => line.slice(2).trim())
159
220
  .filter(Boolean);
160
221
  }
222
+ export function normalizeIndexEntry(value, fallback) {
223
+ const reference = parseDocReference(value);
224
+ if (reference) {
225
+ return {
226
+ category: reference.category,
227
+ label: reference.label,
228
+ };
229
+ }
230
+ const trimmed = String(value || '').trim();
231
+ if (!trimmed)
232
+ return null;
233
+ if (/^docs\/index\.md$/i.test(trimmed) || /^index\.md$/i.test(trimmed))
234
+ return null;
235
+ const titlePlacement = parseTitlePlacement(trimmed);
236
+ if (titlePlacement) {
237
+ if (titlePlacement.category === 'index')
238
+ return null;
239
+ return {
240
+ category: titlePlacement.category,
241
+ label: titlePlacement.label,
242
+ };
243
+ }
244
+ return {
245
+ category: fallback.category,
246
+ label: trimmed,
247
+ };
248
+ }
249
+ export function inferDocPlacement(input) {
250
+ for (const candidate of [input.slug, input.indexLabel]) {
251
+ const reference = parseDocReference(candidate || '');
252
+ if (reference) {
253
+ return { category: reference.category, slug: reference.slug, label: reference.label, isIndexDoc: false };
254
+ }
255
+ }
256
+ const titlePlacement = parseTitlePlacement(input.title);
257
+ const explicitLabel = String(input.indexLabel || '').trim();
258
+ const preferredLabel = explicitLabel && !parseDocReference(explicitLabel) ? explicitLabel : input.title.trim();
259
+ if (titlePlacement) {
260
+ return {
261
+ category: titlePlacement.category === 'index' ? null : titlePlacement.category,
262
+ slug: titlePlacement.slug,
263
+ label: explicitLabel && !parseDocReference(explicitLabel) ? explicitLabel : titlePlacement.label,
264
+ isIndexDoc: titlePlacement.category === 'index',
265
+ };
266
+ }
267
+ const explicitCategory = input.category ? slugify(input.category) : null;
268
+ const slug = slugify(input.slug || input.indexLabel || input.title);
269
+ return {
270
+ category: explicitCategory,
271
+ slug,
272
+ label: preferredLabel,
273
+ isIndexDoc: slug === 'index' && explicitCategory === null,
274
+ };
275
+ }
161
276
  export function extractSummary(markdown) {
162
277
  const match = String(markdown || '').match(/## Summary\s+([\s\S]*?)(?:\n## |$)/);
163
278
  return match?.[1]?.trim() || '';
@@ -1,13 +1,13 @@
1
1
  import { mkdir, rm, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
- import { renderDocMarkdown, slugify } from './markdown.js';
3
+ import { inferDocPlacement, renderDocMarkdown } from './markdown.js';
4
4
  import { branchDocsDir, DEFAULT_BRANCH } from './paths.js';
5
5
  import { ensureBranchExists, updateBranchMeta, writeIndexFromDocs } from './store.js';
6
6
  function docRelativePath(doc) {
7
7
  if (doc.slug?.includes('/'))
8
8
  return `${doc.slug.replace(/\.md$/i, '')}.md`;
9
- const fileBase = slugify(doc.slug || doc.indexLabel || doc.title);
10
- const category = doc.category ? slugify(doc.category) : '';
9
+ const { category, slug } = inferDocPlacement(doc);
10
+ const fileBase = slug;
11
11
  return category ? `${category}/${fileBase}.md` : `${fileBase}.md`;
12
12
  }
13
13
  export async function onboardBranch({ home, input, branch, }) {
@@ -17,6 +17,9 @@ export async function onboardBranch({ home, input, branch, }) {
17
17
  await mkdir(branchDocsDir(home, targetBranch), { recursive: true });
18
18
  const written = [];
19
19
  for (const doc of input.docs) {
20
+ const inferred = inferDocPlacement(doc);
21
+ if (inferred.isIndexDoc)
22
+ continue;
20
23
  const fullPath = join(branchDocsDir(home, targetBranch), docRelativePath(doc));
21
24
  await mkdir(dirname(fullPath), { recursive: true });
22
25
  await writeFile(fullPath, renderDocMarkdown(doc), 'utf8');
package/dist/src/store.js CHANGED
@@ -2,7 +2,7 @@ import { cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { branchDir, branchDocsDir, branchIndexPath, branchMetaPath, branchesRoot, DEFAULT_BRANCH, headPath, } from './paths.js';
5
- import { extractIndexEntries, renderIndexMarkdown } from './markdown.js';
5
+ import { extractIndexEntries, inferDocPlacement, normalizeIndexEntry, renderIndexMarkdown } from './markdown.js';
6
6
  async function listDocRelativePaths(dir, prefix = '') {
7
7
  const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
8
8
  const files = [];
@@ -109,18 +109,24 @@ export async function writeIndexFromDocs(home, branch) {
109
109
  const docsDir = branchDocsDir(home, branch);
110
110
  await mkdir(docsDir, { recursive: true });
111
111
  const files = await listDocRelativePaths(docsDir);
112
- const docs = await Promise.all(files.map(async (file) => {
112
+ const docs = await Promise.all(files.filter((file) => !/(^|\/)index\.md$/i.test(file)).map(async (file) => {
113
113
  const raw = await readFile(join(docsDir, file), 'utf8');
114
114
  const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() || file.replace(/\.md$/i, '').replace(/-/g, ' ');
115
115
  const parts = file.replace(/\.md$/i, '').split('/').filter(Boolean);
116
- const category = parts.length > 1 ? parts[0] : 'general';
117
- const label = parts.length > 1 ? parts[parts.length - 1] : title;
118
- const indexEntries = extractIndexEntries(raw);
119
- const entryLabels = indexEntries.length > 0 ? indexEntries : [label];
120
- return entryLabels.map((entryLabel) => ({
116
+ const fallbackCategory = parts.length > 1 ? parts[0] : 'general';
117
+ const fallbackLabel = parts.length > 1 ? parts[parts.length - 1] : title;
118
+ const inferred = inferDocPlacement({ title });
119
+ const docCategory = inferred.category || fallbackCategory;
120
+ const docLabel = inferred.label || fallbackLabel;
121
+ const indexEntries = extractIndexEntries(raw)
122
+ .map((entry) => normalizeIndexEntry(entry, { category: docCategory, label: docLabel }))
123
+ .filter((entry) => Boolean(entry));
124
+ const entryLabels = indexEntries.length > 0 ? indexEntries : [{ category: docCategory, label: docLabel }];
125
+ const uniqueEntries = [...new Map(entryLabels.map((entry) => [`${entry.category}::${entry.label}`, entry])).values()];
126
+ return uniqueEntries.map((entry) => ({
121
127
  title,
122
- label: entryLabel,
123
- category,
128
+ label: entry.label,
129
+ category: entry.category,
124
130
  path: `docs/${file}`,
125
131
  }));
126
132
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handsupmin/gc-tree",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Global Context Tree, a lightweight branch-aware global context orchestrator for AI coding tools",
5
5
  "type": "module",
6
6
  "private": false,