@handsupmin/gc-tree 0.4.2 → 0.4.5
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.es.md +12 -0
- package/README.ja.md +12 -0
- package/README.ko.md +12 -0
- package/README.md +12 -0
- package/README.zh.md +12 -0
- package/ascii-logo.txt +9 -0
- package/ascii-tree.txt +44 -0
- package/dist/src/ascii.js +25 -0
- package/dist/src/cli.js +49 -1
- package/dist/src/integration-files.js +167 -0
- package/dist/src/markdown.js +70 -12
- package/dist/src/onboard.js +13 -15
- package/dist/src/onboarding-protocol.js +5 -1
- package/dist/src/resolve.js +8 -2
- package/dist/src/scaffold.js +24 -2
- package/dist/src/store.js +22 -4
- package/dist/src/uninstall.js +82 -0
- package/dist/src/update.js +10 -3
- package/package.json +3 -1
- package/skills/onboard/SKILL.md +16 -11
package/README.es.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
+
```text
|
|
6
|
+
__
|
|
7
|
+
/\ \__
|
|
8
|
+
__ ___\ \ ,_\ _ __ __ __
|
|
9
|
+
/'_ `\ /'___\ \ \/ /\`'__\/'__`\ /'__`\
|
|
10
|
+
/\ \L\ \/\ \__/\ \ \_\ \ \//\ __//\ __/
|
|
11
|
+
\ \____ \ \____\\ \__\\ \_\\ \____\ \____\
|
|
12
|
+
\/___L\ \/____/ \/__/ \/_/ \/____/\/____/
|
|
13
|
+
/\____/
|
|
14
|
+
\_/__/
|
|
15
|
+
```
|
|
16
|
+
|
|
5
17
|
<img src="./logo.png" alt="logo de gc-tree" width="260" />
|
|
6
18
|
|
|
7
19
|
### Contexto global, más allá del proyecto.
|
package/README.ja.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
+
```text
|
|
6
|
+
__
|
|
7
|
+
/\ \__
|
|
8
|
+
__ ___\ \ ,_\ _ __ __ __
|
|
9
|
+
/'_ `\ /'___\ \ \/ /\`'__\/'__`\ /'__`\
|
|
10
|
+
/\ \L\ \/\ \__/\ \ \_\ \ \//\ __//\ __/
|
|
11
|
+
\ \____ \ \____\\ \__\\ \_\\ \____\ \____\
|
|
12
|
+
\/___L\ \/____/ \/__/ \/_/ \/____/\/____/
|
|
13
|
+
/\____/
|
|
14
|
+
\_/__/
|
|
15
|
+
```
|
|
16
|
+
|
|
5
17
|
<img src="./logo.png" alt="gc-tree logo" width="260" />
|
|
6
18
|
|
|
7
19
|
### プロジェクトの外まで届くグローバルコンテキスト。
|
package/README.ko.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
+
```text
|
|
6
|
+
__
|
|
7
|
+
/\ \__
|
|
8
|
+
__ ___\ \ ,_\ _ __ __ __
|
|
9
|
+
/'_ `\ /'___\ \ \/ /\`'__\/'__`\ /'__`\
|
|
10
|
+
/\ \L\ \/\ \__/\ \ \_\ \ \//\ __//\ __/
|
|
11
|
+
\ \____ \ \____\\ \__\\ \_\\ \____\ \____\
|
|
12
|
+
\/___L\ \/____/ \/__/ \/_/ \/____/\/____/
|
|
13
|
+
/\____/
|
|
14
|
+
\_/__/
|
|
15
|
+
```
|
|
16
|
+
|
|
5
17
|
<img src="./logo.png" alt="gc-tree 로고" width="260" />
|
|
6
18
|
|
|
7
19
|
### 프로젝트 밖까지 이어지는 글로벌 컨텍스트.
|
package/README.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
+
```text
|
|
6
|
+
__
|
|
7
|
+
/\ \__
|
|
8
|
+
__ ___\ \ ,_\ _ __ __ __
|
|
9
|
+
/'_ `\ /'___\ \ \/ /\`'__\/'__`\ /'__`\
|
|
10
|
+
/\ \L\ \/\ \__/\ \ \_\ \ \//\ __//\ __/
|
|
11
|
+
\ \____ \ \____\\ \__\\ \_\\ \____\ \____\
|
|
12
|
+
\/___L\ \/____/ \/__/ \/_/ \/____/\/____/
|
|
13
|
+
/\____/
|
|
14
|
+
\_/__/
|
|
15
|
+
```
|
|
16
|
+
|
|
5
17
|
<img src="./logo.png" alt="gc-tree logo" width="260" />
|
|
6
18
|
|
|
7
19
|
### Global context, beyond the project.
|
package/README.zh.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
+
```text
|
|
6
|
+
__
|
|
7
|
+
/\ \__
|
|
8
|
+
__ ___\ \ ,_\ _ __ __ __
|
|
9
|
+
/'_ `\ /'___\ \ \/ /\`'__\/'__`\ /'__`\
|
|
10
|
+
/\ \L\ \/\ \__/\ \ \_\ \ \//\ __//\ __/
|
|
11
|
+
\ \____ \ \____\\ \__\\ \_\\ \____\ \____\
|
|
12
|
+
\/___L\ \/____/ \/__/ \/_/ \/____/\/____/
|
|
13
|
+
/\____/
|
|
14
|
+
\_/__/
|
|
15
|
+
```
|
|
16
|
+
|
|
5
17
|
<img src="./logo.png" alt="gc-tree logo" width="260" />
|
|
6
18
|
|
|
7
19
|
### 不止于项目内部的全局上下文。
|
package/ascii-logo.txt
ADDED
package/ascii-tree.txt
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
QzczXO
|
|
3
|
+
Yvczzvvz
|
|
4
|
+
Juvw cvc
|
|
5
|
+
JzXY YvvXXuvc zXXY
|
|
6
|
+
CvvccvcU zuvX zvvcvvcY
|
|
7
|
+
cvU QvcO \uu: vX: JvX
|
|
8
|
+
Yvv0:zvc \uu: vvY ZcvC
|
|
9
|
+
XvvvvcU UcvvvX YX \uu: 0vc cvvccL XvvvvcY
|
|
10
|
+
Uvc: Ycvv vvv bcu \uv: Ovc vvU Qvc cvC
|
|
11
|
+
UvcO Ocvv cvvX Ycu \uuO Zvc XvvJ Zvca OvvC
|
|
12
|
+
UvvvvvQ vuu LcvvuUcu \uuO ZvczvvvcO cuY QvvvvvJ
|
|
13
|
+
\zcccX Quv cvv Ycvvu \uuO XvvvcX zvY XvU zzzzX
|
|
14
|
+
zvucucvcL Quv cvv Ycu \uuzuvvz zvU XvU cvvcucvcY
|
|
15
|
+
cvU zvc JvvU zvvvQ acu \uvvvY JvvvY CvvU JvY cvX
|
|
16
|
+
UvvzczvvJ 0cvvcXOYvvvzJ cvvYX\uvc Yzvvvz YcvvvC zvvzczvcJ
|
|
17
|
+
LvvvU XcvvzXXcvvcJ Uvvvvvvc JcvvvXXzvvcXO zvvuO
|
|
18
|
+
Cvc OXzzzzzXuvvz zUvvvc OXvvuXXzzzzXO OccO
|
|
19
|
+
Yvvvvvz zvvuU zvvvvvc vvvd nvvc OvvvY wvvvvvY OUvvvX 0vvvvvY
|
|
20
|
+
cvX QvvzXYUcvvvvJ CvvY Mvva uvvc ZvvO cvz: OzvvvvXUUXXvza Ycvz
|
|
21
|
+
cvY acvuuuuuuuvvC UvvcX Mcvh uvvc mvva OXvvz Xvuuuuuuuuvz Xvz
|
|
22
|
+
CcvvvvvY zzvvvvvvvvvcL zvvk nvvc Ovva OvvvcYvvvvvcO Uvvcvvz\
|
|
23
|
+
zzuvvXz Yuvvcv avvZ nvvv mvvY zzvvuU Jzuvvzz
|
|
24
|
+
UvvYYUvv0obhhhhoh*hoXvvvzUvv0 nvvc ZvvJXvvvzhao##h##hhhQvvUJUcvU
|
|
25
|
+
Uvz Yvvvvvvvvvvvvvvvvvvvvv0 uvvv muvvvvvvvvvvvvvvvvvvvvY cvY
|
|
26
|
+
JcvcCcvcb zXvvvvvvvvvvvvvvJO bvvcLcvcC
|
|
27
|
+
CzvzL YuvvvvvvvvvQ CXvYL
|
|
28
|
+
Jcvvvvvm
|
|
29
|
+
CvvvvvvZ
|
|
30
|
+
zvvvvvvY
|
|
31
|
+
cvvvvvvJ
|
|
32
|
+
nuvvvvuc
|
|
33
|
+
uuvvvvuc
|
|
34
|
+
vvvvvvuu
|
|
35
|
+
Jvvvvvvu
|
|
36
|
+
zvvvvvvv
|
|
37
|
+
JvvvvvvU
|
|
38
|
+
vvvvvvvU
|
|
39
|
+
Ovvvvvvvc
|
|
40
|
+
JuvvvvvvuL
|
|
41
|
+
zvvvvvvvvvvY
|
|
42
|
+
OXzvvvvzvvvzcvvvvzXO
|
|
43
|
+
YYcvvvvvvXCzvvvvX:XvvvvvvcXQ
|
|
44
|
+
YYYYYYYYUXO QUUUYUUYC: OcYUYUYYYYz
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
async function readAsciiAsset(fileName) {
|
|
5
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const candidates = [
|
|
7
|
+
join(here, '..', fileName),
|
|
8
|
+
join(here, '..', '..', fileName),
|
|
9
|
+
];
|
|
10
|
+
for (const candidate of candidates) {
|
|
11
|
+
try {
|
|
12
|
+
return await readFile(candidate, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`ascii asset not found: ${fileName}`);
|
|
19
|
+
}
|
|
20
|
+
export async function readAsciiLogo() {
|
|
21
|
+
return readAsciiAsset('ascii-logo.txt');
|
|
22
|
+
}
|
|
23
|
+
export async function readAsciiTree() {
|
|
24
|
+
return readAsciiAsset('ascii-tree.txt');
|
|
25
|
+
}
|
package/dist/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { stdin } from 'node:process';
|
|
3
|
+
import { stdin, stderr, stdout } from 'node:process';
|
|
4
|
+
import { readAsciiLogo, readAsciiTree } from './ascii.js';
|
|
4
5
|
import { dispatchGcTreeHook } from './hook.js';
|
|
5
6
|
import { onboardBranch } from './onboard.js';
|
|
6
7
|
import { buildProviderLaunchPlan, maybeLaunchProvider, promptLanguageSelection, promptLaunchProviderSelection, promptProviderSelection, } from './provider.js';
|
|
@@ -11,6 +12,7 @@ import { scaffoldHostIntegration } from './scaffold.js';
|
|
|
11
12
|
import { requirePreferredProvider, writeSettings, readSettings } from './settings.js';
|
|
12
13
|
import { checkoutBranch, initHome, listBranches, readHead, resetBranchContext, statusForBranch, ensureBranchExists, isBranchContextEmpty } from './store.js';
|
|
13
14
|
import { updateBranchContext } from './update.js';
|
|
15
|
+
import { uninstallGcTree } from './uninstall.js';
|
|
14
16
|
import { verifyOnboarding } from './verify-onboarding.js';
|
|
15
17
|
function readArg(flag) {
|
|
16
18
|
const index = process.argv.indexOf(flag);
|
|
@@ -19,8 +21,23 @@ function readArg(flag) {
|
|
|
19
21
|
function hasFlag(flag) {
|
|
20
22
|
return process.argv.includes(flag);
|
|
21
23
|
}
|
|
24
|
+
async function readPackageVersion() {
|
|
25
|
+
const candidates = ['../package.json', '../../package.json'];
|
|
26
|
+
for (const relative of candidates) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await readFile(new URL(relative, import.meta.url), 'utf8');
|
|
29
|
+
return JSON.parse(raw).version || '0.0.0';
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return '0.0.0';
|
|
36
|
+
}
|
|
22
37
|
function usage() {
|
|
23
38
|
console.error(`Usage:
|
|
39
|
+
gctree --version
|
|
40
|
+
gctree plant
|
|
24
41
|
gctree init [--home DIR] [--provider <claude-code|codex|both>] [--language TEXT] [--target DIR] [--no-launch]
|
|
25
42
|
gctree checkout <branch> [--home DIR]
|
|
26
43
|
gctree checkout -b <branch> [--home DIR]
|
|
@@ -31,6 +48,7 @@ function usage() {
|
|
|
31
48
|
gctree onboard [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
|
|
32
49
|
gctree verify-onboarding [--home DIR] [--branch NAME]
|
|
33
50
|
gctree reset-gc-branch [--home DIR] [--branch NAME] --yes
|
|
51
|
+
gctree uninstall [--home DIR] [--target DIR] [--host <codex|claude-code|both>] [--keep-home] --yes
|
|
34
52
|
gctree resolve --query TEXT [--home DIR] [--branch NAME] [--cwd DIR]
|
|
35
53
|
gctree show-doc --id ID [--home DIR] [--branch NAME]
|
|
36
54
|
gctree related --id ID [--home DIR] [--branch NAME]
|
|
@@ -121,13 +139,28 @@ async function launchGuidedFlow({ provider, providerMode, preferredLanguage, tar
|
|
|
121
139
|
return maybeLaunchProvider(plan);
|
|
122
140
|
}
|
|
123
141
|
async function main() {
|
|
142
|
+
if (hasFlag('--version') || hasFlag('-v')) {
|
|
143
|
+
stdout.write(`${await readPackageVersion()}\n`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
124
146
|
const rawCommand = process.argv[2];
|
|
125
147
|
if (!rawCommand || rawCommand === '--help' || rawCommand === 'help')
|
|
126
148
|
usage();
|
|
127
149
|
const command = rawCommand === 'update-gc' || rawCommand === 'ugc' ? 'update-global-context' : rawCommand;
|
|
128
150
|
const home = resolveHome(readArg('--home'));
|
|
129
151
|
switch (command) {
|
|
152
|
+
case 'plant': {
|
|
153
|
+
const asciiTree = await readAsciiTree();
|
|
154
|
+
stdout.write(asciiTree);
|
|
155
|
+
if (!asciiTree.endsWith('\n'))
|
|
156
|
+
stdout.write('\n');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
130
159
|
case 'init': {
|
|
160
|
+
const asciiLogo = await readAsciiLogo();
|
|
161
|
+
stderr.write(asciiLogo);
|
|
162
|
+
if (!asciiLogo.endsWith('\n'))
|
|
163
|
+
stderr.write('\n');
|
|
131
164
|
const providerMode = await maybePromptProviderMode(readArg('--provider'));
|
|
132
165
|
const provider = await resolveLaunchProvider(providerMode);
|
|
133
166
|
const preferredLanguage = await maybePromptLanguage(readArg('--language'));
|
|
@@ -276,6 +309,21 @@ async function main() {
|
|
|
276
309
|
console.log(JSON.stringify(result, null, 2));
|
|
277
310
|
return;
|
|
278
311
|
}
|
|
312
|
+
case 'uninstall': {
|
|
313
|
+
if (!hasFlag('--yes')) {
|
|
314
|
+
throw new Error('uninstall is destructive. Re-run with --yes to confirm.');
|
|
315
|
+
}
|
|
316
|
+
const host = normalizeProviderMode(readArg('--host')) || 'both';
|
|
317
|
+
const targetDir = readArg('--target') || process.cwd();
|
|
318
|
+
const result = await uninstallGcTree({
|
|
319
|
+
home,
|
|
320
|
+
targetDir,
|
|
321
|
+
host,
|
|
322
|
+
keepHome: hasFlag('--keep-home'),
|
|
323
|
+
});
|
|
324
|
+
console.log(JSON.stringify(result, null, 2));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
279
327
|
case 'resolve': {
|
|
280
328
|
const query = readArg('--query');
|
|
281
329
|
if (!query)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { access, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { mkdir } from 'node:fs/promises';
|
|
5
|
+
const MARKERS = {
|
|
6
|
+
codex: {
|
|
7
|
+
start: '<!-- gctree:codex:start -->',
|
|
8
|
+
end: '<!-- gctree:codex:end -->',
|
|
9
|
+
},
|
|
10
|
+
claude: {
|
|
11
|
+
start: '<!-- gctree:claude:start -->',
|
|
12
|
+
end: '<!-- gctree:claude:end -->',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export async function pathExists(target) {
|
|
16
|
+
try {
|
|
17
|
+
await access(target, constants.F_OK);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function injectManagedBlock(existing, content, start, end) {
|
|
25
|
+
const block = `${start}\n${content.trimEnd()}\n${end}`;
|
|
26
|
+
const startIndex = existing.indexOf(start);
|
|
27
|
+
const endIndex = existing.indexOf(end);
|
|
28
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
29
|
+
return `${existing.slice(0, startIndex).trimEnd()}\n\n${block}\n${existing.slice(endIndex + end.length).trimStart()}`.trimEnd() + '\n';
|
|
30
|
+
}
|
|
31
|
+
const trimmed = existing.trimEnd();
|
|
32
|
+
return `${trimmed ? `${trimmed}\n\n` : ''}${block}\n`;
|
|
33
|
+
}
|
|
34
|
+
function removeManagedBlock(existing, start, end) {
|
|
35
|
+
const startIndex = existing.indexOf(start);
|
|
36
|
+
const endIndex = existing.indexOf(end);
|
|
37
|
+
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex)
|
|
38
|
+
return existing;
|
|
39
|
+
const before = existing.slice(0, startIndex).trimEnd();
|
|
40
|
+
const after = existing.slice(endIndex + end.length).trimStart();
|
|
41
|
+
const merged = [before, after].filter(Boolean).join('\n\n');
|
|
42
|
+
return merged ? `${merged}\n` : '';
|
|
43
|
+
}
|
|
44
|
+
export async function upsertManagedMarkdownBlock({ filePath, content, marker, }) {
|
|
45
|
+
const { start, end } = MARKERS[marker];
|
|
46
|
+
const existing = (await pathExists(filePath)) ? await readFile(filePath, 'utf8') : '';
|
|
47
|
+
const next = injectManagedBlock(existing, content, start, end);
|
|
48
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
49
|
+
await writeFile(filePath, next, 'utf8');
|
|
50
|
+
return existing ? 'updated' : 'created';
|
|
51
|
+
}
|
|
52
|
+
export async function removeManagedMarkdownBlock({ filePath, marker, }) {
|
|
53
|
+
if (!(await pathExists(filePath)))
|
|
54
|
+
return 'missing';
|
|
55
|
+
const { start, end } = MARKERS[marker];
|
|
56
|
+
const existing = await readFile(filePath, 'utf8');
|
|
57
|
+
const next = removeManagedBlock(existing, start, end);
|
|
58
|
+
if (!next.trim()) {
|
|
59
|
+
await rm(filePath, { force: true });
|
|
60
|
+
return 'deleted';
|
|
61
|
+
}
|
|
62
|
+
if (next === existing)
|
|
63
|
+
return 'missing';
|
|
64
|
+
await writeFile(filePath, next, 'utf8');
|
|
65
|
+
return 'removed';
|
|
66
|
+
}
|
|
67
|
+
function buildHookEntry(event) {
|
|
68
|
+
return {
|
|
69
|
+
type: 'command',
|
|
70
|
+
command: `gctree __hook --event ${event}`,
|
|
71
|
+
metadata: {
|
|
72
|
+
owner: 'gctree',
|
|
73
|
+
},
|
|
74
|
+
...(event === 'UserPromptSubmit' ? { timeout: 10 } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function ensureObject(value) {
|
|
78
|
+
return value && typeof value === 'object' ? value : {};
|
|
79
|
+
}
|
|
80
|
+
function isGcTreeHook(entry, event) {
|
|
81
|
+
const command = String(entry.command || '');
|
|
82
|
+
const owner = ensureObject(entry.metadata).owner;
|
|
83
|
+
return owner === 'gctree' || command === `gctree __hook --event ${event}`;
|
|
84
|
+
}
|
|
85
|
+
function mergeHookJson(raw, target) {
|
|
86
|
+
const parsed = raw ? ensureObject(JSON.parse(raw)) : {};
|
|
87
|
+
const hooks = ensureObject(parsed.hooks);
|
|
88
|
+
const sessionEvent = 'SessionStart';
|
|
89
|
+
const promptEvent = 'UserPromptSubmit';
|
|
90
|
+
const ensureEventArray = (event) => {
|
|
91
|
+
const current = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
92
|
+
hooks[event] = current;
|
|
93
|
+
return current;
|
|
94
|
+
};
|
|
95
|
+
const upsertGroup = (event, matcher) => {
|
|
96
|
+
const groups = ensureEventArray(event);
|
|
97
|
+
let group = groups.find((candidate) => {
|
|
98
|
+
const hooksArr = Array.isArray(candidate?.hooks) ? candidate.hooks : [];
|
|
99
|
+
return hooksArr.some((entry) => isGcTreeHook(ensureObject(entry), event));
|
|
100
|
+
});
|
|
101
|
+
if (!group) {
|
|
102
|
+
group = matcher ? { matcher, hooks: [] } : { hooks: [] };
|
|
103
|
+
groups.push(group);
|
|
104
|
+
}
|
|
105
|
+
if (!Array.isArray(group.hooks))
|
|
106
|
+
group.hooks = [];
|
|
107
|
+
group.hooks = group.hooks.filter((entry) => !isGcTreeHook(ensureObject(entry), event));
|
|
108
|
+
group.hooks.push(buildHookEntry(event));
|
|
109
|
+
};
|
|
110
|
+
upsertGroup(sessionEvent, target === 'codex' ? 'startup|resume' : '*');
|
|
111
|
+
upsertGroup(promptEvent, target === 'codex' ? undefined : '*');
|
|
112
|
+
return `${JSON.stringify({ hooks }, null, 2)}\n`;
|
|
113
|
+
}
|
|
114
|
+
function unmergeHookJson(raw, events) {
|
|
115
|
+
const parsed = ensureObject(JSON.parse(raw));
|
|
116
|
+
const hooks = ensureObject(parsed.hooks);
|
|
117
|
+
for (const event of events) {
|
|
118
|
+
const groups = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
119
|
+
const nextGroups = groups
|
|
120
|
+
.map((group) => {
|
|
121
|
+
const hooksArr = Array.isArray(group.hooks) ? group.hooks : [];
|
|
122
|
+
const nextHooks = hooksArr.filter((entry) => !isGcTreeHook(ensureObject(entry), event));
|
|
123
|
+
return nextHooks.length > 0 ? { ...group, hooks: nextHooks } : null;
|
|
124
|
+
})
|
|
125
|
+
.filter(Boolean);
|
|
126
|
+
if (nextGroups.length > 0)
|
|
127
|
+
hooks[event] = nextGroups;
|
|
128
|
+
else
|
|
129
|
+
delete hooks[event];
|
|
130
|
+
}
|
|
131
|
+
if (Object.keys(hooks).length === 0)
|
|
132
|
+
return '';
|
|
133
|
+
return `${JSON.stringify({ hooks }, null, 2)}\n`;
|
|
134
|
+
}
|
|
135
|
+
export async function mergeGcTreeHooksJson({ filePath, target, }) {
|
|
136
|
+
const existing = (await pathExists(filePath)) ? await readFile(filePath, 'utf8') : null;
|
|
137
|
+
const next = mergeHookJson(existing, target);
|
|
138
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
139
|
+
await writeFile(filePath, next, 'utf8');
|
|
140
|
+
return existing ? 'updated' : 'created';
|
|
141
|
+
}
|
|
142
|
+
export async function unmergeGcTreeHooksJson(filePath) {
|
|
143
|
+
if (!(await pathExists(filePath)))
|
|
144
|
+
return 'missing';
|
|
145
|
+
const existing = await readFile(filePath, 'utf8');
|
|
146
|
+
const next = unmergeHookJson(existing, ['SessionStart', 'UserPromptSubmit']);
|
|
147
|
+
if (!next.trim()) {
|
|
148
|
+
await rm(filePath, { force: true });
|
|
149
|
+
return 'deleted';
|
|
150
|
+
}
|
|
151
|
+
if (next === existing)
|
|
152
|
+
return 'missing';
|
|
153
|
+
await writeFile(filePath, next, 'utf8');
|
|
154
|
+
return 'removed';
|
|
155
|
+
}
|
|
156
|
+
export function gctreeManagedMarkdownTargets(targetDir) {
|
|
157
|
+
return {
|
|
158
|
+
codex: join(targetDir, 'AGENTS.md'),
|
|
159
|
+
claude: join(targetDir, 'CLAUDE.md'),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export function gctreeHookJsonTargets(targetDir) {
|
|
163
|
+
return {
|
|
164
|
+
codex: join(targetDir, '.codex', 'hooks.json'),
|
|
165
|
+
claude: join(targetDir, '.claude', 'hooks', 'hooks.json'),
|
|
166
|
+
};
|
|
167
|
+
}
|
package/dist/src/markdown.js
CHANGED
|
@@ -45,35 +45,93 @@ export function renderIndexMarkdown(input) {
|
|
|
45
45
|
lines.push('- No source docs yet.', '');
|
|
46
46
|
}
|
|
47
47
|
else {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const categoryOrder = ['role', 'repos', 'domain', 'workflows', 'conventions', 'infra', 'verification', 'general'];
|
|
49
|
+
const grouped = new Map();
|
|
50
|
+
for (const doc of input.docs) {
|
|
51
|
+
const category = normalizeCategory(doc.category || deriveCategoryFromPath(doc.path));
|
|
52
|
+
if (!grouped.has(category))
|
|
53
|
+
grouped.set(category, []);
|
|
54
|
+
grouped.get(category).push(doc);
|
|
55
|
+
}
|
|
56
|
+
const categories = [...grouped.keys()].sort((a, b) => {
|
|
57
|
+
const ai = categoryOrder.indexOf(a);
|
|
58
|
+
const bi = categoryOrder.indexOf(b);
|
|
59
|
+
if (ai === -1 && bi === -1)
|
|
60
|
+
return a.localeCompare(b);
|
|
61
|
+
if (ai === -1)
|
|
62
|
+
return 1;
|
|
63
|
+
if (bi === -1)
|
|
64
|
+
return -1;
|
|
65
|
+
return ai - bi;
|
|
66
|
+
});
|
|
67
|
+
for (const category of categories) {
|
|
68
|
+
lines.push(`## ${displayCategory(category)}`, '');
|
|
69
|
+
for (const doc of grouped.get(category).sort((a, b) => (a.label || a.title).localeCompare(b.label || b.title))) {
|
|
70
|
+
lines.push(`- ${doc.label || doc.title} -> ${doc.path}`);
|
|
71
|
+
}
|
|
72
|
+
lines.push('');
|
|
50
73
|
}
|
|
51
|
-
lines.push('');
|
|
52
74
|
}
|
|
53
75
|
return lines.join('\n');
|
|
54
76
|
}
|
|
55
77
|
export function docIdFromPath(docPath) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return
|
|
78
|
+
const normalized = String(docPath || '').trim().replace(/^docs\//, '');
|
|
79
|
+
return normalized.replace(/\.md$/i, '') || 'doc';
|
|
80
|
+
}
|
|
81
|
+
export function normalizeCategory(value) {
|
|
82
|
+
return slugify(value || 'general');
|
|
83
|
+
}
|
|
84
|
+
export function deriveCategoryFromPath(docPath) {
|
|
85
|
+
const normalized = String(docPath || '').trim().replace(/^docs\//, '');
|
|
86
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
87
|
+
if (parts.length > 1)
|
|
88
|
+
return normalizeCategory(parts[0]);
|
|
89
|
+
return 'general';
|
|
90
|
+
}
|
|
91
|
+
export function displayCategory(category) {
|
|
92
|
+
const normalized = normalizeCategory(category);
|
|
93
|
+
const predefined = {
|
|
94
|
+
role: 'Role',
|
|
95
|
+
repos: 'Repos',
|
|
96
|
+
domain: 'Domain',
|
|
97
|
+
workflows: 'Workflows',
|
|
98
|
+
conventions: 'Conventions',
|
|
99
|
+
infra: 'Infra',
|
|
100
|
+
verification: 'Verification',
|
|
101
|
+
general: 'General',
|
|
102
|
+
};
|
|
103
|
+
return predefined[normalized] || normalized.split('-').map((part) => part[0]?.toUpperCase() + part.slice(1)).join(' ');
|
|
61
104
|
}
|
|
62
105
|
export function parseIndexEntries(indexContent) {
|
|
106
|
+
let currentCategory = 'general';
|
|
63
107
|
return String(indexContent || '')
|
|
64
108
|
.split(/\r?\n/)
|
|
65
109
|
.map((line) => line.trim())
|
|
66
|
-
.
|
|
67
|
-
|
|
110
|
+
.flatMap((line) => {
|
|
111
|
+
if (!line)
|
|
112
|
+
return [];
|
|
113
|
+
const heading = line.match(/^##\s+(.+)$/);
|
|
114
|
+
if (heading) {
|
|
115
|
+
currentCategory = normalizeCategory(heading[1] || 'general');
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
if (!/^-\s+.+\s+->\s+.+$/.test(line) || line.startsWith('- gc-branch:') || line.startsWith('- summary:')) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
68
121
|
const body = line.slice(2);
|
|
69
|
-
const [
|
|
70
|
-
|
|
122
|
+
const [label, path] = body.split('->').map((part) => part.trim());
|
|
123
|
+
const category = deriveCategoryFromPath(path) || currentCategory;
|
|
124
|
+
return [{ id: docIdFromPath(path), title: label, label, category, path }];
|
|
71
125
|
});
|
|
72
126
|
}
|
|
73
127
|
export function extractSummary(markdown) {
|
|
74
128
|
const match = String(markdown || '').match(/## Summary\s+([\s\S]*?)(?:\n## |$)/);
|
|
75
129
|
return match?.[1]?.trim() || '';
|
|
76
130
|
}
|
|
131
|
+
export function extractTitle(markdown) {
|
|
132
|
+
const match = String(markdown || '').match(/^#\s+(.+)$/m);
|
|
133
|
+
return match?.[1]?.trim() || '';
|
|
134
|
+
}
|
|
77
135
|
export function extractExcerpt(markdown, query) {
|
|
78
136
|
const content = String(markdown || '').replace(/\s+/g, ' ').trim();
|
|
79
137
|
if (!content)
|
package/dist/src/onboard.js
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
|
-
import { mkdir,
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { renderDocMarkdown } from './markdown.js';
|
|
1
|
+
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { renderDocMarkdown, slugify } from './markdown.js';
|
|
4
4
|
import { branchDocsDir, DEFAULT_BRANCH } from './paths.js';
|
|
5
5
|
import { ensureBranchExists, updateBranchMeta, writeIndexFromDocs } from './store.js';
|
|
6
|
+
function docRelativePath(doc) {
|
|
7
|
+
if (doc.slug?.includes('/'))
|
|
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) : '';
|
|
11
|
+
return category ? `${category}/${fileBase}.md` : `${fileBase}.md`;
|
|
12
|
+
}
|
|
6
13
|
export async function onboardBranch({ home, input, branch, }) {
|
|
7
14
|
const targetBranch = branch || input.branch || DEFAULT_BRANCH;
|
|
8
15
|
await ensureBranchExists(home, targetBranch);
|
|
16
|
+
await rm(branchDocsDir(home, targetBranch), { recursive: true, force: true });
|
|
9
17
|
await mkdir(branchDocsDir(home, targetBranch), { recursive: true });
|
|
10
|
-
const existing = await readdir(branchDocsDir(home, targetBranch)).catch(() => []);
|
|
11
|
-
await Promise.all(existing
|
|
12
|
-
.filter((file) => file.endsWith('.md'))
|
|
13
|
-
.map((file) => unlink(join(branchDocsDir(home, targetBranch), file))));
|
|
14
18
|
const written = [];
|
|
15
19
|
for (const doc of input.docs) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
.trim()
|
|
19
|
-
.toLowerCase()
|
|
20
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
21
|
-
.replace(/-+/g, '-')
|
|
22
|
-
.replace(/^-|-$/g, '') || 'doc'}.md`;
|
|
23
|
-
const fullPath = join(branchDocsDir(home, targetBranch), fileName);
|
|
20
|
+
const fullPath = join(branchDocsDir(home, targetBranch), docRelativePath(doc));
|
|
21
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
24
22
|
await writeFile(fullPath, renderDocMarkdown(doc), 'utf8');
|
|
25
23
|
written.push(fullPath);
|
|
26
24
|
}
|
|
@@ -23,7 +23,11 @@ export function onboardingProtocolLines() {
|
|
|
23
23
|
'Ask whether there are additional repositories for the current work type before moving on.',
|
|
24
24
|
'After repository coverage, ask for company/domain glossary terms and acronyms that should become durable context.',
|
|
25
25
|
'Then ask which verification commands should be treated as defaults for this kind of work.',
|
|
26
|
-
'Synthesize the interview into
|
|
26
|
+
'Synthesize the interview into an encyclopedia-style context set with many small docs instead of a few broad docs.',
|
|
27
|
+
'Prefer category directories such as `docs/role/`, `docs/repos/`, `docs/domain/`, `docs/workflows/`, `docs/conventions/`, and `docs/infra/` whenever that split fits the material.',
|
|
28
|
+
'Prefer one concept, one repository, one workflow, or one convention per file when possible.',
|
|
29
|
+
'Keep each doc summary-first so the top section gives the gist before deeper details.',
|
|
30
|
+
'Treat `index.md` as a human-readable dictionary-style table of contents grouped by category headings and `label -> path` entries.',
|
|
27
31
|
];
|
|
28
32
|
}
|
|
29
33
|
export function onboardingCompletionLines() {
|
package/dist/src/resolve.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { docIdFromPath, extractExcerpt, extractSummary, parseIndexEntries } from './markdown.js';
|
|
3
|
+
import { deriveCategoryFromPath, docIdFromPath, extractExcerpt, extractSummary, extractTitle, parseIndexEntries } from './markdown.js';
|
|
4
4
|
import { branchDir, branchIndexPath } from './paths.js';
|
|
5
5
|
// Common English stop words that are too short or generic to be meaningful query signals.
|
|
6
6
|
const STOP_WORDS = new Set([
|
|
@@ -49,7 +49,9 @@ async function readBranchDocs(home, branch) {
|
|
|
49
49
|
const raw = await readFile(fullPath, 'utf8');
|
|
50
50
|
return {
|
|
51
51
|
id: entry.id || docIdFromPath(entry.path),
|
|
52
|
-
title: entry.title,
|
|
52
|
+
title: extractTitle(raw) || entry.title,
|
|
53
|
+
label: entry.label,
|
|
54
|
+
category: entry.category || deriveCategoryFromPath(entry.path),
|
|
53
55
|
path: entry.path,
|
|
54
56
|
summary: extractSummary(raw),
|
|
55
57
|
content: raw,
|
|
@@ -69,6 +71,8 @@ export async function resolveContext({ home, branch, query, }) {
|
|
|
69
71
|
continue;
|
|
70
72
|
matches.push({
|
|
71
73
|
id: entry.id,
|
|
74
|
+
label: entry.label,
|
|
75
|
+
category: entry.category,
|
|
72
76
|
title: entry.title,
|
|
73
77
|
path: entry.path,
|
|
74
78
|
score,
|
|
@@ -105,6 +109,8 @@ export async function findRelatedDocs({ home, branch, id, limit = 5, }) {
|
|
|
105
109
|
return score > 0
|
|
106
110
|
? {
|
|
107
111
|
id: doc.id,
|
|
112
|
+
label: doc.label,
|
|
113
|
+
category: doc.category,
|
|
108
114
|
title: doc.title,
|
|
109
115
|
path: doc.path,
|
|
110
116
|
score,
|
package/dist/src/scaffold.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
+
import { gctreeHookJsonTargets, gctreeManagedMarkdownTargets, mergeGcTreeHooksJson, upsertManagedMarkdownBlock, } from './integration-files.js';
|
|
3
4
|
import { onboardingCompletionLines, onboardingProtocolLines } from './onboarding-protocol.js';
|
|
4
5
|
function renderCodexAgentsSnippet() {
|
|
5
6
|
return [
|
|
@@ -204,7 +205,7 @@ function renderClaudeUpdateCommand() {
|
|
|
204
205
|
'',
|
|
205
206
|
].join('\n');
|
|
206
207
|
}
|
|
207
|
-
function scaffoldFiles(host) {
|
|
208
|
+
export function scaffoldFiles(host) {
|
|
208
209
|
if (host === 'codex') {
|
|
209
210
|
return [
|
|
210
211
|
{ path: 'AGENTS.md', content: renderCodexAgentsSnippet() },
|
|
@@ -228,10 +229,31 @@ export async function scaffoldHostIntegration({ host, targetDir, force = false,
|
|
|
228
229
|
const files = scaffoldFiles(host);
|
|
229
230
|
const written = [];
|
|
230
231
|
const skippedExisting = [];
|
|
232
|
+
const markdownTargets = gctreeManagedMarkdownTargets(targetDir);
|
|
233
|
+
const hookTargets = gctreeHookJsonTargets(targetDir);
|
|
234
|
+
const isCodex = host === 'codex';
|
|
235
|
+
const managedMarkdownPath = isCodex ? markdownTargets.codex : markdownTargets.claude;
|
|
236
|
+
const managedMarkdownContent = isCodex ? renderCodexAgentsSnippet() : renderClaudeSnippet();
|
|
237
|
+
await upsertManagedMarkdownBlock({
|
|
238
|
+
filePath: managedMarkdownPath,
|
|
239
|
+
content: managedMarkdownContent,
|
|
240
|
+
marker: isCodex ? 'codex' : 'claude',
|
|
241
|
+
});
|
|
242
|
+
written.push(managedMarkdownPath);
|
|
243
|
+
const hookPath = isCodex ? hookTargets.codex : hookTargets.claude;
|
|
244
|
+
await mergeGcTreeHooksJson({
|
|
245
|
+
filePath: hookPath,
|
|
246
|
+
target: isCodex ? 'codex' : 'claude-code',
|
|
247
|
+
});
|
|
248
|
+
written.push(hookPath);
|
|
231
249
|
const targets = files.map((file) => ({
|
|
232
250
|
...file,
|
|
233
251
|
fullPath: join(targetDir, file.path),
|
|
234
|
-
}))
|
|
252
|
+
})).filter((target) => {
|
|
253
|
+
if (isCodex)
|
|
254
|
+
return target.path !== 'AGENTS.md' && target.path !== '.codex/hooks.json';
|
|
255
|
+
return target.path !== 'CLAUDE.md' && target.path !== '.claude/hooks/hooks.json';
|
|
256
|
+
});
|
|
235
257
|
for (const target of targets) {
|
|
236
258
|
let exists = false;
|
|
237
259
|
try {
|
package/dist/src/store.js
CHANGED
|
@@ -3,6 +3,21 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { branchDir, branchDocsDir, branchIndexPath, branchMetaPath, branchesRoot, DEFAULT_BRANCH, headPath, INDEX_WARNING_CHARS, } from './paths.js';
|
|
5
5
|
import { renderIndexMarkdown } from './markdown.js';
|
|
6
|
+
async function listDocRelativePaths(dir, prefix = '') {
|
|
7
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
8
|
+
const files = [];
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
11
|
+
const fullPath = join(dir, entry.name);
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
files.push(...(await listDocRelativePaths(fullPath, relative)));
|
|
14
|
+
}
|
|
15
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
16
|
+
files.push(relative);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return files.sort();
|
|
20
|
+
}
|
|
6
21
|
export async function ensureHome(home) {
|
|
7
22
|
await mkdir(branchesRoot(home), { recursive: true });
|
|
8
23
|
}
|
|
@@ -93,11 +108,14 @@ export async function checkoutBranch(home, branch, create = false) {
|
|
|
93
108
|
export async function writeIndexFromDocs(home, branch) {
|
|
94
109
|
const docsDir = branchDocsDir(home, branch);
|
|
95
110
|
await mkdir(docsDir, { recursive: true });
|
|
96
|
-
const files =
|
|
111
|
+
const files = await listDocRelativePaths(docsDir);
|
|
97
112
|
const docs = await Promise.all(files.map(async (file) => {
|
|
98
113
|
const raw = await readFile(join(docsDir, file), 'utf8');
|
|
99
114
|
const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() || file.replace(/\.md$/i, '').replace(/-/g, ' ');
|
|
100
|
-
|
|
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
|
+
return { title, label, category, path: `docs/${file}` };
|
|
101
119
|
}));
|
|
102
120
|
const meta = await readBranchMeta(home, branch);
|
|
103
121
|
const index = renderIndexMarkdown({ branch, branchSummary: meta.summary, docs });
|
|
@@ -105,7 +123,7 @@ export async function writeIndexFromDocs(home, branch) {
|
|
|
105
123
|
return { index_path: branchIndexPath(home, branch), doc_count: docs.length };
|
|
106
124
|
}
|
|
107
125
|
export async function isBranchContextEmpty(home, branch) {
|
|
108
|
-
const docs =
|
|
126
|
+
const docs = await listDocRelativePaths(branchDocsDir(home, branch));
|
|
109
127
|
return docs.length === 0;
|
|
110
128
|
}
|
|
111
129
|
export async function resetBranchContext(home, branch) {
|
|
@@ -122,7 +140,7 @@ export async function resetBranchContext(home, branch) {
|
|
|
122
140
|
}
|
|
123
141
|
export async function statusForBranch(home, branch) {
|
|
124
142
|
const indexRaw = await readFile(branchIndexPath(home, branch), 'utf8');
|
|
125
|
-
const docs =
|
|
143
|
+
const docs = await listDocRelativePaths(branchDocsDir(home, branch));
|
|
126
144
|
const warnings = [];
|
|
127
145
|
if (indexRaw.length > INDEX_WARNING_CHARS) {
|
|
128
146
|
warnings.push(`index.md is ${indexRaw.length} chars; keep it closer to an index than a knowledge dump.`);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { gctreeHookJsonTargets, gctreeManagedMarkdownTargets, pathExists, removeManagedMarkdownBlock, unmergeGcTreeHooksJson, } from './integration-files.js';
|
|
4
|
+
import { scaffoldFiles } from './scaffold.js';
|
|
5
|
+
async function pathHasEntries(path) {
|
|
6
|
+
const entries = await readdir(path).catch(() => []);
|
|
7
|
+
return entries.length > 0;
|
|
8
|
+
}
|
|
9
|
+
async function pruneEmptyParents(targetPath, stopDir) {
|
|
10
|
+
let current = dirname(targetPath);
|
|
11
|
+
while (current.startsWith(stopDir) && current !== stopDir) {
|
|
12
|
+
if (await pathHasEntries(current))
|
|
13
|
+
return;
|
|
14
|
+
await rm(current, { recursive: true, force: true });
|
|
15
|
+
current = dirname(current);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function hostsFor(mode) {
|
|
19
|
+
return mode === 'both' ? ['claude-code', 'codex'] : [mode];
|
|
20
|
+
}
|
|
21
|
+
export async function uninstallGcTree({ home, targetDir, host = 'both', keepHome = false, }) {
|
|
22
|
+
const removed = [];
|
|
23
|
+
const markdownTargets = gctreeManagedMarkdownTargets(targetDir);
|
|
24
|
+
const hookTargets = gctreeHookJsonTargets(targetDir);
|
|
25
|
+
for (const entry of hostsFor(host)) {
|
|
26
|
+
if (entry === 'codex') {
|
|
27
|
+
const markdownStatus = await removeManagedMarkdownBlock({
|
|
28
|
+
filePath: markdownTargets.codex,
|
|
29
|
+
marker: 'codex',
|
|
30
|
+
});
|
|
31
|
+
if (markdownStatus !== 'missing') {
|
|
32
|
+
removed.push(markdownTargets.codex);
|
|
33
|
+
await pruneEmptyParents(markdownTargets.codex, targetDir);
|
|
34
|
+
}
|
|
35
|
+
const hookStatus = await unmergeGcTreeHooksJson(hookTargets.codex);
|
|
36
|
+
if (hookStatus !== 'missing') {
|
|
37
|
+
removed.push(hookTargets.codex);
|
|
38
|
+
await pruneEmptyParents(hookTargets.codex, targetDir);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const markdownStatus = await removeManagedMarkdownBlock({
|
|
43
|
+
filePath: markdownTargets.claude,
|
|
44
|
+
marker: 'claude',
|
|
45
|
+
});
|
|
46
|
+
if (markdownStatus !== 'missing') {
|
|
47
|
+
removed.push(markdownTargets.claude);
|
|
48
|
+
await pruneEmptyParents(markdownTargets.claude, targetDir);
|
|
49
|
+
}
|
|
50
|
+
const hookStatus = await unmergeGcTreeHooksJson(hookTargets.claude);
|
|
51
|
+
if (hookStatus !== 'missing') {
|
|
52
|
+
removed.push(hookTargets.claude);
|
|
53
|
+
await pruneEmptyParents(hookTargets.claude, targetDir);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const extraTargets = scaffoldFiles(entry)
|
|
57
|
+
.map((file) => join(targetDir, file.path))
|
|
58
|
+
.filter((path) => entry === 'codex'
|
|
59
|
+
? path !== markdownTargets.codex && path !== hookTargets.codex
|
|
60
|
+
: path !== markdownTargets.claude && path !== hookTargets.claude);
|
|
61
|
+
for (const target of [...new Set(extraTargets)]) {
|
|
62
|
+
if (!(await pathExists(target)))
|
|
63
|
+
continue;
|
|
64
|
+
await rm(target, { recursive: true, force: true });
|
|
65
|
+
removed.push(target);
|
|
66
|
+
await pruneEmptyParents(target, targetDir);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let homeRemoved = false;
|
|
70
|
+
if (!keepHome && (await pathExists(home))) {
|
|
71
|
+
await rm(home, { recursive: true, force: true });
|
|
72
|
+
homeRemoved = true;
|
|
73
|
+
removed.push(home);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
target_dir: targetDir,
|
|
77
|
+
home,
|
|
78
|
+
host,
|
|
79
|
+
home_removed: homeRemoved,
|
|
80
|
+
removed,
|
|
81
|
+
};
|
|
82
|
+
}
|
package/dist/src/update.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
3
|
import { renderDocMarkdown, slugify } from './markdown.js';
|
|
4
4
|
import { branchDocsDir, DEFAULT_BRANCH } from './paths.js';
|
|
5
5
|
import { ensureBranchExists, updateBranchMeta, writeIndexFromDocs } from './store.js';
|
|
6
|
+
function docRelativePath(doc) {
|
|
7
|
+
if (doc.slug?.includes('/'))
|
|
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) : '';
|
|
11
|
+
return category ? `${category}/${fileBase}.md` : `${fileBase}.md`;
|
|
12
|
+
}
|
|
6
13
|
export async function updateBranchContext({ home, input, branch, }) {
|
|
7
14
|
const targetBranch = branch || input.branch || DEFAULT_BRANCH;
|
|
8
15
|
await ensureBranchExists(home, targetBranch);
|
|
9
16
|
await mkdir(branchDocsDir(home, targetBranch), { recursive: true });
|
|
10
17
|
const written = [];
|
|
11
18
|
for (const doc of input.docs) {
|
|
12
|
-
const
|
|
13
|
-
|
|
19
|
+
const fullPath = join(branchDocsDir(home, targetBranch), docRelativePath(doc));
|
|
20
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
14
21
|
await writeFile(fullPath, renderDocMarkdown(doc), 'utf8');
|
|
15
22
|
written.push(fullPath);
|
|
16
23
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@handsupmin/gc-tree",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
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,
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"skills",
|
|
10
10
|
"scaffolds",
|
|
11
11
|
"docs",
|
|
12
|
+
"ascii-logo.txt",
|
|
13
|
+
"ascii-tree.txt",
|
|
12
14
|
"README.md",
|
|
13
15
|
"LICENSE"
|
|
14
16
|
],
|
package/skills/onboard/SKILL.md
CHANGED
|
@@ -32,7 +32,10 @@ Use this when a user wants to create global context for a product, company, or w
|
|
|
32
32
|
- ask for multiple work types if needed, then multiple repos inside each work type if needed
|
|
33
33
|
- ask about glossary terms and default verification commands before you finish
|
|
34
34
|
- write compact source docs with a required `## Summary` section near the top
|
|
35
|
-
-
|
|
35
|
+
- prefer an encyclopedia-style context set with many small docs instead of a few broad docs
|
|
36
|
+
- prefer category directories like `docs/role/`, `docs/repos/`, `docs/domain/`, `docs/workflows/`, `docs/conventions/`, and `docs/infra/`
|
|
37
|
+
- prefer one concept, one repository, one workflow, or one convention per file when possible
|
|
38
|
+
- keep `index.md` as a human-readable dictionary-style TOC grouped by category headings and `label -> path` entries
|
|
36
39
|
- use onboarding only for an empty gc-branch
|
|
37
40
|
|
|
38
41
|
## Procedure
|
|
@@ -63,13 +66,15 @@ Use this when a user wants to create global context for a product, company, or w
|
|
|
63
66
|
17. After the user's first answer, proactively inspect relevant local repos, docs, paths, and workflows whenever the connection is strong enough to test your current frame.
|
|
64
67
|
18. Ask for company/domain glossary terms and acronyms that should become durable context.
|
|
65
68
|
19. Ask which verification commands should be treated as defaults for this gc-branch.
|
|
66
|
-
20.
|
|
67
|
-
21.
|
|
68
|
-
22.
|
|
69
|
-
23.
|
|
70
|
-
24.
|
|
71
|
-
25.
|
|
72
|
-
26.
|
|
73
|
-
27.
|
|
74
|
-
28.
|
|
75
|
-
29.
|
|
69
|
+
20. Structure the durable docs as a small encyclopedia: split by category directory, keep one concept/repo/workflow/convention per file when possible, and keep a short `## Summary` at the top of each doc.
|
|
70
|
+
21. Render `index.md` as a category-grouped dictionary-style table of contents with `label -> path` entries.
|
|
71
|
+
22. Launch the guided onboarding flow with `gctree onboard [--branch <name>]`.
|
|
72
|
+
23. Before you claim onboarding is complete, run `gctree verify-onboarding --branch <current-gc-branch>` and inspect the real gc-tree files.
|
|
73
|
+
24. Do not claim onboarding is complete unless verification returns `status: "complete"`.
|
|
74
|
+
25. After the onboarding docs are written, explicitly list which durable docs were saved.
|
|
75
|
+
26. Summarize what you now understand from the saved docs instead of ending at the filenames alone.
|
|
76
|
+
27. Ask whether that final summary matches the user's reality, and capture any corrections before you wrap up.
|
|
77
|
+
28. Ask whether anything else should be saved while the context is still fresh.
|
|
78
|
+
29. Do not finish onboarding while material related repos, workflows, or domain terms remain uninspected when recoverable local evidence is still available.
|
|
79
|
+
30. Only after the related repos, workflows, glossary, and default verification commands are either captured or explicitly unavailable should you wrap up, then remind the user that future changes belong in `gctree update-global-context`.
|
|
80
|
+
31. Keep the current gc-branch explicit while gathering context.
|