contextgit 0.0.10 → 0.0.12
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/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +32 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.d.ts +2 -0
- package/dist/commands/doctor.test.d.ts.map +1 -0
- package/dist/commands/doctor.test.js +29 -0
- package/dist/commands/doctor.test.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +36 -121
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +13 -9
- package/dist/commands/pull.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +6 -8
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/set-remote.d.ts +2 -1
- package/dist/commands/set-remote.d.ts.map +1 -1
- package/dist/commands/set-remote.js +32 -10
- package/dist/commands/set-remote.js.map +1 -1
- package/dist/commands/set-remote.test.d.ts +2 -0
- package/dist/commands/set-remote.test.d.ts.map +1 -0
- package/dist/commands/set-remote.test.js +37 -0
- package/dist/commands/set-remote.test.js.map +1 -0
- package/dist/lib/client-config.d.ts +1 -18
- package/dist/lib/client-config.d.ts.map +1 -1
- package/dist/lib/client-config.js +1 -73
- package/dist/lib/client-config.js.map +1 -1
- package/dist/lib/client-config.test.js +1 -96
- package/dist/lib/client-config.test.js.map +1 -1
- package/dist/lib/init-helpers.d.ts +24 -0
- package/dist/lib/init-helpers.d.ts.map +1 -0
- package/dist/lib/init-helpers.js +185 -0
- package/dist/lib/init-helpers.js.map +1 -0
- package/dist/lib/init-helpers.test.d.ts +2 -0
- package/dist/lib/init-helpers.test.d.ts.map +1 -0
- package/dist/lib/init-helpers.test.js +73 -0
- package/dist/lib/init-helpers.test.js.map +1 -0
- package/dist/lib/remote-store.d.ts +4 -0
- package/dist/lib/remote-store.d.ts.map +1 -0
- package/dist/lib/remote-store.js +28 -0
- package/dist/lib/remote-store.js.map +1 -0
- package/dist/lib/remote-store.test.d.ts +2 -0
- package/dist/lib/remote-store.test.d.ts.map +1 -0
- package/dist/lib/remote-store.test.js +40 -0
- package/dist/lib/remote-store.test.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { loadConfig, saveConfig } from '../config.js';
|
|
3
|
+
vi.mock('../config.js', () => ({
|
|
4
|
+
loadConfig: vi.fn(() => ({ projectId: 'p1', project: 'test', remote: undefined, supabaseUrl: undefined })),
|
|
5
|
+
saveConfig: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
// We test the dispatch logic directly since oclif commands are hard to unit-test.
|
|
8
|
+
// The key behaviors: 'supabase' keyword writes supabaseUrl; anything else writes remote.
|
|
9
|
+
describe('set-remote dispatch', () => {
|
|
10
|
+
beforeEach(() => { vi.clearAllMocks(); });
|
|
11
|
+
it('writes supabaseUrl when first arg is "supabase"', () => {
|
|
12
|
+
// Simulate the dispatch logic from the command
|
|
13
|
+
const typeOrUrl = 'supabase';
|
|
14
|
+
const url = 'https://xyz.supabase.co';
|
|
15
|
+
const config = loadConfig();
|
|
16
|
+
if (typeOrUrl === 'supabase') {
|
|
17
|
+
saveConfig({ ...config, supabaseUrl: url });
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
saveConfig({ ...config, remote: typeOrUrl });
|
|
21
|
+
}
|
|
22
|
+
expect(saveConfig).toHaveBeenCalledWith(expect.objectContaining({ supabaseUrl: 'https://xyz.supabase.co' }));
|
|
23
|
+
});
|
|
24
|
+
it('writes remote (HTTP) when first arg is a URL', () => {
|
|
25
|
+
const typeOrUrl = 'https://api.example.com';
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
if (typeOrUrl === 'supabase') {
|
|
28
|
+
saveConfig({ ...config, supabaseUrl: 'irrelevant' });
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
saveConfig({ ...config, remote: typeOrUrl });
|
|
32
|
+
}
|
|
33
|
+
expect(saveConfig).toHaveBeenCalledWith(expect.objectContaining({ remote: 'https://api.example.com' }));
|
|
34
|
+
expect(saveConfig).not.toHaveBeenCalledWith(expect.objectContaining({ supabaseUrl: expect.anything() }));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
//# sourceMappingURL=set-remote.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set-remote.test.js","sourceRoot":"","sources":["../../src/commands/set-remote.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAC7D,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAErD,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7B,UAAU,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC,CAAC;IAC1G,UAAU,EAAE,EAAE,CAAC,EAAE,EAAE;CACpB,CAAC,CAAC,CAAA;AAEH,kFAAkF;AAClF,yFAAyF;AAEzF,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,aAAa,EAAE,CAAA,CAAC,CAAC,CAAC,CAAA;IAExC,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,+CAA+C;QAC/C,MAAM,SAAS,GAAG,UAAU,CAAA;QAC5B,MAAM,GAAG,GAAG,yBAAyB,CAAA;QACrC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAC3B,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;YAC7B,UAAU,CAAC,EAAE,GAAG,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAA;QAC7C,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAA;QAC9C,CAAC;QACD,MAAM,CAAC,UAAU,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE,WAAW,EAAE,yBAAyB,EAAE,CAAC,CAAC,CAAA;IAC9G,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,SAAS,GAAW,yBAAyB,CAAA;QACnD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAC3B,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;YAC7B,UAAU,CAAC,EAAE,GAAG,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC,CAAA;QACtD,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAA;QAC9C,CAAC;QACD,MAAM,CAAC,UAAU,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,yBAAyB,EAAE,CAAC,CAAC,CAAA;QACvG,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAA;IAC1G,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -1,19 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export interface DetectedClient {
|
|
3
|
-
type: ClientType;
|
|
4
|
-
path: string;
|
|
5
|
-
}
|
|
6
|
-
export interface InjectionResult {
|
|
7
|
-
status: 'injected' | 'already-present' | 'skipped' | 'error';
|
|
8
|
-
reason?: string;
|
|
9
|
-
}
|
|
10
|
-
/** Return all MCP clients whose config file exists on disk. */
|
|
11
|
-
export declare function detectClients(home?: string): DetectedClient[];
|
|
12
|
-
/** Check whether a contextgit entry already exists under mcpServers. */
|
|
13
|
-
export declare function isAlreadyInjected(config: Record<string, unknown>): boolean;
|
|
14
|
-
/**
|
|
15
|
-
* Inject the contextgit MCP server entry into the given client config file.
|
|
16
|
-
* Uses an atomic write (temp file + rename) so the original is never corrupted.
|
|
17
|
-
*/
|
|
18
|
-
export declare function injectMcpServer(configPath: string, _clientType: ClientType, systemPrompt: string): InjectionResult;
|
|
1
|
+
export {};
|
|
19
2
|
//# sourceMappingURL=client-config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-config.d.ts","sourceRoot":"","sources":["../../src/lib/client-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client-config.d.ts","sourceRoot":"","sources":["../../src/lib/client-config.ts"],"names":[],"mappings":""}
|
|
@@ -1,74 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { homedir } from 'os';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
const MCP_ENTRY = {
|
|
5
|
-
command: 'npx',
|
|
6
|
-
args: ['-y', '@contextgit/mcp'],
|
|
7
|
-
};
|
|
8
|
-
/** Resolve known config paths for each client type. */
|
|
9
|
-
function clientPaths(home) {
|
|
10
|
-
const appData = process.env['APPDATA'] ?? '';
|
|
11
|
-
return {
|
|
12
|
-
'claude-code': join(home, '.claude.json'),
|
|
13
|
-
'cursor': join(home, '.cursor', 'mcp.json'),
|
|
14
|
-
'claude-desktop': process.platform === 'win32'
|
|
15
|
-
? join(appData, 'Claude', 'claude_desktop_config.json')
|
|
16
|
-
: join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
/** Return all MCP clients whose config file exists on disk. */
|
|
20
|
-
export function detectClients(home = homedir()) {
|
|
21
|
-
const paths = clientPaths(home);
|
|
22
|
-
return Object.keys(paths)
|
|
23
|
-
.filter(type => existsSync(paths[type]))
|
|
24
|
-
.map(type => ({ type, path: paths[type] }));
|
|
25
|
-
}
|
|
26
|
-
/** Check whether a contextgit entry already exists under mcpServers. */
|
|
27
|
-
export function isAlreadyInjected(config) {
|
|
28
|
-
const servers = config['mcpServers'] ?? config['globalShortcuts']?.['mcpServers'];
|
|
29
|
-
if (!servers || typeof servers !== 'object')
|
|
30
|
-
return false;
|
|
31
|
-
return 'contextgit' in servers;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Inject the contextgit MCP server entry into the given client config file.
|
|
35
|
-
* Uses an atomic write (temp file + rename) so the original is never corrupted.
|
|
36
|
-
*/
|
|
37
|
-
export function injectMcpServer(configPath, _clientType, systemPrompt) {
|
|
38
|
-
// Read existing content — create empty object if file doesn't exist
|
|
39
|
-
let raw = '{}';
|
|
40
|
-
if (existsSync(configPath)) {
|
|
41
|
-
raw = readFileSync(configPath, 'utf8');
|
|
42
|
-
}
|
|
43
|
-
let config;
|
|
44
|
-
try {
|
|
45
|
-
config = JSON.parse(raw);
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return {
|
|
49
|
-
status: 'error',
|
|
50
|
-
reason: 'existing config is not valid JSON — skipped to avoid data loss',
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
if (isAlreadyInjected(config)) {
|
|
54
|
-
return { status: 'already-present' };
|
|
55
|
-
}
|
|
56
|
-
// Resolve the mcpServers object — handle Claude Desktop's globalShortcuts wrapper
|
|
57
|
-
let target;
|
|
58
|
-
if (config['globalShortcuts'] &&
|
|
59
|
-
typeof config['globalShortcuts'] === 'object' &&
|
|
60
|
-
'mcpServers' in config['globalShortcuts']) {
|
|
61
|
-
target = config['globalShortcuts']['mcpServers'];
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
if (!config['mcpServers'])
|
|
65
|
-
config['mcpServers'] = {};
|
|
66
|
-
target = config['mcpServers'];
|
|
67
|
-
}
|
|
68
|
-
target['contextgit'] = { ...MCP_ENTRY, systemPrompt };
|
|
69
|
-
const tmpPath = configPath + '.contextgit-tmp';
|
|
70
|
-
writeFileSync(tmpPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
71
|
-
renameSync(tmpPath, configPath);
|
|
72
|
-
return { status: 'injected' };
|
|
73
|
-
}
|
|
1
|
+
export {};
|
|
74
2
|
//# sourceMappingURL=client-config.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-config.js","sourceRoot":"","sources":["../../src/lib/client-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client-config.js","sourceRoot":"","sources":["../../src/lib/client-config.ts"],"names":[],"mappings":""}
|
|
@@ -1,97 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'fs';
|
|
3
|
-
import { tmpdir } from 'os';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { detectClients, injectMcpServer, isAlreadyInjected } from './client-config.js';
|
|
6
|
-
const SYSTEM_PROMPT = 'test system prompt';
|
|
7
|
-
let tmpDir;
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
tmpDir = mkdtempSync(join(tmpdir(), 'contextgit-test-'));
|
|
10
|
-
});
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
13
|
-
});
|
|
14
|
-
// 1. detectClients returns empty array when no config files exist
|
|
15
|
-
it('detectClients returns empty array when no config files exist', () => {
|
|
16
|
-
const result = detectClients(tmpDir);
|
|
17
|
-
expect(result).toEqual([]);
|
|
18
|
-
});
|
|
19
|
-
// 2. detectClients returns Claude Code entry when ~/.claude.json exists
|
|
20
|
-
it('detectClients returns claude-code when .claude.json exists', () => {
|
|
21
|
-
writeFileSync(join(tmpDir, '.claude.json'), '{}');
|
|
22
|
-
const result = detectClients(tmpDir);
|
|
23
|
-
expect(result).toHaveLength(1);
|
|
24
|
-
expect(result[0].type).toBe('claude-code');
|
|
25
|
-
expect(result[0].path).toBe(join(tmpDir, '.claude.json'));
|
|
26
|
-
});
|
|
27
|
-
// 3. injectMcpServer writes correct JSON structure to a new empty config file
|
|
28
|
-
it('injectMcpServer writes correct JSON to a new empty config file', () => {
|
|
29
|
-
const configPath = join(tmpDir, '.claude.json');
|
|
30
|
-
// file does not exist yet
|
|
31
|
-
const result = injectMcpServer(configPath, 'claude-code', SYSTEM_PROMPT);
|
|
32
|
-
expect(result.status).toBe('injected');
|
|
33
|
-
const written = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
34
|
-
const servers = written['mcpServers'];
|
|
35
|
-
expect(servers).toBeDefined();
|
|
36
|
-
const entry = servers['contextgit'];
|
|
37
|
-
expect(entry['command']).toBe('npx');
|
|
38
|
-
expect(entry['args']).toEqual(['contextgit', 'mcp']);
|
|
39
|
-
expect(entry['systemPrompt']).toBe(SYSTEM_PROMPT);
|
|
40
|
-
});
|
|
41
|
-
// 4. injectMcpServer merges into existing config without touching other keys
|
|
42
|
-
it('injectMcpServer merges without touching other keys', () => {
|
|
43
|
-
const configPath = join(tmpDir, '.claude.json');
|
|
44
|
-
writeFileSync(configPath, JSON.stringify({
|
|
45
|
-
someOtherKey: 'keep-me',
|
|
46
|
-
mcpServers: { 'other-server': { command: 'npx', args: ['other'] } },
|
|
47
|
-
}));
|
|
48
|
-
const result = injectMcpServer(configPath, 'claude-code', SYSTEM_PROMPT);
|
|
49
|
-
expect(result.status).toBe('injected');
|
|
50
|
-
const written = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
51
|
-
expect(written['someOtherKey']).toBe('keep-me');
|
|
52
|
-
const servers = written['mcpServers'];
|
|
53
|
-
expect(servers['other-server']).toBeDefined();
|
|
54
|
-
expect(servers['contextgit']).toBeDefined();
|
|
55
|
-
});
|
|
56
|
-
// 5. injectMcpServer returns already-present if contextgit key exists
|
|
57
|
-
it('injectMcpServer returns already-present if contextgit exists', () => {
|
|
58
|
-
const configPath = join(tmpDir, '.claude.json');
|
|
59
|
-
writeFileSync(configPath, JSON.stringify({
|
|
60
|
-
mcpServers: { contextgit: { command: 'npx', args: ['contextgit', 'mcp'] } },
|
|
61
|
-
}));
|
|
62
|
-
const result = injectMcpServer(configPath, 'claude-code', SYSTEM_PROMPT);
|
|
63
|
-
expect(result.status).toBe('already-present');
|
|
64
|
-
});
|
|
65
|
-
// 6. injectMcpServer returns error and does not write if existing file is invalid JSON
|
|
66
|
-
it('injectMcpServer returns error and does not write if file is invalid JSON', () => {
|
|
67
|
-
const configPath = join(tmpDir, '.claude.json');
|
|
68
|
-
writeFileSync(configPath, 'NOT JSON {{{');
|
|
69
|
-
const originalContent = readFileSync(configPath, 'utf8');
|
|
70
|
-
const result = injectMcpServer(configPath, 'claude-code', SYSTEM_PROMPT);
|
|
71
|
-
expect(result.status).toBe('error');
|
|
72
|
-
expect(result.reason).toContain('not valid JSON');
|
|
73
|
-
// File must not have been overwritten
|
|
74
|
-
expect(readFileSync(configPath, 'utf8')).toBe(originalContent);
|
|
75
|
-
});
|
|
76
|
-
// 7. injectMcpServer uses atomic write (temp file + rename)
|
|
77
|
-
it('injectMcpServer cleans up temp file after write', () => {
|
|
78
|
-
const configPath = join(tmpDir, '.claude.json');
|
|
79
|
-
injectMcpServer(configPath, 'claude-code', SYSTEM_PROMPT);
|
|
80
|
-
// Temp file should not exist after successful rename
|
|
81
|
-
expect(existsSync(configPath + '.contextgit-tmp')).toBe(false);
|
|
82
|
-
// Final file should exist
|
|
83
|
-
expect(existsSync(configPath)).toBe(true);
|
|
84
|
-
});
|
|
85
|
-
// 8. isAlreadyInjected returns true when contextgit is present under mcpServers
|
|
86
|
-
describe('isAlreadyInjected', () => {
|
|
87
|
-
it('returns true when contextgit is under mcpServers', () => {
|
|
88
|
-
expect(isAlreadyInjected({ mcpServers: { contextgit: {} } })).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
it('returns false when contextgit is absent', () => {
|
|
91
|
-
expect(isAlreadyInjected({ mcpServers: { other: {} } })).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
it('returns false when mcpServers is missing', () => {
|
|
94
|
-
expect(isAlreadyInjected({})).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
1
|
+
export {};
|
|
97
2
|
//# sourceMappingURL=client-config.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-config.test.js","sourceRoot":"","sources":["../../src/lib/client-config.test.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client-config.test.js","sourceRoot":"","sources":["../../src/lib/client-config.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const CLAUDE_MD_SENTINEL_START = "<!-- contextgit:start -->";
|
|
2
|
+
export declare const CLAUDE_MD_SENTINEL_END = "<!-- contextgit:end -->";
|
|
3
|
+
export declare const CLAUDE_MD_FRAGMENT = "\n<!-- contextgit:start -->\n## ContextGit Memory\n\nThis project uses ContextGit for persistent AI memory across sessions.\n\n## Session Start (do this every time)\n\nCall `project_memory_load` immediately.\nDo not ask questions first. Read the snapshot, then start working.\nStart the next specific task from the snapshot \u2014 not an entire feature or milestone.\nOne task per session unless it is trivially small.\n\n## After EVERY completed task\n\nDo not wait to be asked. Every git commit = immediate context commit.\nDo not proceed to the next task until both are done.\n\n1. `git add . && git commit -m \"feat/fix: <what was done>\"`\n2. Call `project_memory_save` immediately after with:\n - One-line summary of what was done\n - What was decided and why\n - What was built (files changed, approach taken)\n - Open questions\n - Git branch and commit hash\n - The next concrete task\n\nThese two always go together. Never git commit without a context commit.\n\n## Session End (do this every time)\n\nBefore stopping work for any reason, call `project_memory_save` with:\n- What was built this session\n- Key decisions and why\n- Open threads and blockers\n- The first concrete task for the next session\n\nDo not end a session without a context commit. The next session starts blind without it.\n\n## Before risky exploration\n\nCall `project_memory_branch` to create an isolated context workspace before trying anything uncertain.\n\n## Before starting a task (multi-agent)\n\nCall `project_task_claim` to prevent other agents from duplicating your work.\n\n## When scope changes mid-session\n\nWrite a `project_memory_save` with replan: prefix BEFORE building new scope:\n`project_memory_save \"replan: <what changed and why>\"`\nThen build the new scope. Then write a normal context commit when done.\n<!-- contextgit:end -->\n";
|
|
4
|
+
/**
|
|
5
|
+
* Write the ContextGit CLAUDE.md fragment into the given directory.
|
|
6
|
+
* Idempotent: skips if sentinel already present.
|
|
7
|
+
* @returns 'written' | 'already-present' | 'error'
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeClaude(projectDir: string): {
|
|
10
|
+
status: 'written' | 'already-present' | 'error';
|
|
11
|
+
reason?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const CONTEXT_COMMIT_SKILL = "---\nname: context-commit\ndescription: \"MANDATORY: Save project memory after completing ANY work. Use this skill after EVERY completed task and EVERY git commit \u2014 finished implementing a feature, resolved a bug, made a decision, closed a thread, completed a review, or ending a session. Every git commit must be followed immediately by a context commit. Do not wait to be asked. Do not proceed to the next task until both git commit and context commit are done. Triggers: git commit made, task complete, 'that's working', 'let's move on', 'I'm done', session ending. Skipping this means the next session starts completely blind.\"\n---\n\n# ContextGit \u2014 Context Commit Discipline\n\n## RULE: Every git commit = immediate context commit\n\nDo not wait to be asked. Do not batch. Do not skip.\nAfter every git commit, call `project_memory_save` immediately.\nDo not proceed to the next task until both git commit and context commit are done.\nThese two always go together. Never git commit without a context commit.\n\n## When to commit context\n\nCall `project_memory_save` (MCP tool) after:\n- Every git commit (mandatory \u2014 these always go together)\n- Completing a feature or task (before moving to the next)\n- Making an architectural decision or choosing between options\n- Resolving a bug, blocker, or open question\n- Closing a thread\n- Before ending the session for any reason\n\n## What makes a good commit message\n\nA context commit message is a future-you briefing. Write it so the next agent session (or the next developer) can pick up exactly where this one left off.\n\nStructure:\n```\n<one-line summary of what was done>\n\nWhat was decided: <the decision and why>\nWhat was built: <files changed, approach taken>\nOpen questions: <anything unresolved>\nNext task: <the first concrete thing the next session should do>\nGit: <branch> | <commit hash if available>\n```\n\n## How to call it\n\nUse the `project_memory_save` MCP tool (alias: `context_commit`). Pass the full message as the `message` argument.\n\nDo not skip this step when the work feels small. Small decisions compound. The next session starts blind without them.\n";
|
|
14
|
+
export declare const CONTEXT_BRANCH_SKILL = "---\nname: context-branch\ndescription: \"Create an isolated context branch before risky or experimental work. Use this skill when the agent is about to explore something uncertain, experimental, or potentially breaking \u2014 trying an approach that might not work, refactoring something risky, exploring an architectural alternative, or doing anything that should be isolatable and reversible. Triggers on: 'let me try', 'what if we', 'I want to explore', 'let's experiment', 'alternative approach', or any phrasing that signals exploration rather than execution.\"\n---\n\n# ContextGit \u2014 Context Branch Discipline\n\n## When to branch context\n\nCall `project_memory_branch` (MCP tool) before:\n- Trying an approach you're not sure will work\n- Refactoring something that touches many files\n- Exploring an architectural alternative to the current plan\n- Doing anything you'd want to be able to roll back semantically (not just via git)\n\n## Why this matters\n\nA context branch creates an isolated snapshot workspace. If the exploration fails, you can return to the main branch context without polluting the session history with dead-end decisions.\n\nIt's cheap. It takes one tool call. The cost of not doing it is re-explaining to the next session why you abandoned the approach you just spent an hour on.\n\n## How to call it\n\nUse the `project_memory_branch` MCP tool (alias: `context_branch`). Pass a short descriptive name:\n\n```\nproject_memory_branch name=\"explore-queue-based-concurrency\"\n```\n\nWhen the exploration concludes:\n- If it worked: `project_memory_save` your findings and merge back\n- If it failed: `project_memory_save` a brief note (\"explored X, abandoned because Y\") and switch back to main branch\n\nThe failure note is as valuable as the success note. The next session needs to know not to try the same dead end.\n";
|
|
15
|
+
/**
|
|
16
|
+
* Write the context-commit and context-branch skills into <projectDir>/.claude/skills/.
|
|
17
|
+
* Overwrites if already present (these files are managed by contextgit).
|
|
18
|
+
* @returns 'written' | 'error'
|
|
19
|
+
*/
|
|
20
|
+
export declare function writeSkills(projectDir: string): {
|
|
21
|
+
status: 'written' | 'error';
|
|
22
|
+
reason?: string;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=init-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/init-helpers.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,wBAAwB,8BAA8B,CAAA;AACnE,eAAO,MAAM,sBAAsB,4BAA4B,CAAA;AAE/D,eAAO,MAAM,kBAAkB,m0DAqD9B,CAAA;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,GACjB;IAAE,MAAM,EAAE,SAAS,GAAG,iBAAiB,GAAG,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAgBtE;AAID,eAAO,MAAM,oBAAoB,wnEA4ChC,CAAA;AAED,eAAO,MAAM,oBAAoB,y0DAkChC,CAAA;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,GACjB;IAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAYlD"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// init-helpers.ts — testable helpers for contextgit init
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
// ── CLAUDE.md ─────────────────────────────────────────────────────────────────
|
|
5
|
+
export const CLAUDE_MD_SENTINEL_START = '<!-- contextgit:start -->';
|
|
6
|
+
export const CLAUDE_MD_SENTINEL_END = '<!-- contextgit:end -->';
|
|
7
|
+
export const CLAUDE_MD_FRAGMENT = `
|
|
8
|
+
<!-- contextgit:start -->
|
|
9
|
+
## ContextGit Memory
|
|
10
|
+
|
|
11
|
+
This project uses ContextGit for persistent AI memory across sessions.
|
|
12
|
+
|
|
13
|
+
## Session Start (do this every time)
|
|
14
|
+
|
|
15
|
+
Call \`project_memory_load\` immediately.
|
|
16
|
+
Do not ask questions first. Read the snapshot, then start working.
|
|
17
|
+
Start the next specific task from the snapshot — not an entire feature or milestone.
|
|
18
|
+
One task per session unless it is trivially small.
|
|
19
|
+
|
|
20
|
+
## After EVERY completed task
|
|
21
|
+
|
|
22
|
+
Do not wait to be asked. Every git commit = immediate context commit.
|
|
23
|
+
Do not proceed to the next task until both are done.
|
|
24
|
+
|
|
25
|
+
1. \`git add . && git commit -m "feat/fix: <what was done>"\`
|
|
26
|
+
2. Call \`project_memory_save\` immediately after with:
|
|
27
|
+
- One-line summary of what was done
|
|
28
|
+
- What was decided and why
|
|
29
|
+
- What was built (files changed, approach taken)
|
|
30
|
+
- Open questions
|
|
31
|
+
- Git branch and commit hash
|
|
32
|
+
- The next concrete task
|
|
33
|
+
|
|
34
|
+
These two always go together. Never git commit without a context commit.
|
|
35
|
+
|
|
36
|
+
## Session End (do this every time)
|
|
37
|
+
|
|
38
|
+
Before stopping work for any reason, call \`project_memory_save\` with:
|
|
39
|
+
- What was built this session
|
|
40
|
+
- Key decisions and why
|
|
41
|
+
- Open threads and blockers
|
|
42
|
+
- The first concrete task for the next session
|
|
43
|
+
|
|
44
|
+
Do not end a session without a context commit. The next session starts blind without it.
|
|
45
|
+
|
|
46
|
+
## Before risky exploration
|
|
47
|
+
|
|
48
|
+
Call \`project_memory_branch\` to create an isolated context workspace before trying anything uncertain.
|
|
49
|
+
|
|
50
|
+
## Before starting a task (multi-agent)
|
|
51
|
+
|
|
52
|
+
Call \`project_task_claim\` to prevent other agents from duplicating your work.
|
|
53
|
+
|
|
54
|
+
## When scope changes mid-session
|
|
55
|
+
|
|
56
|
+
Write a \`project_memory_save\` with replan: prefix BEFORE building new scope:
|
|
57
|
+
\`project_memory_save "replan: <what changed and why>"\`
|
|
58
|
+
Then build the new scope. Then write a normal context commit when done.
|
|
59
|
+
<!-- contextgit:end -->
|
|
60
|
+
`;
|
|
61
|
+
/**
|
|
62
|
+
* Write the ContextGit CLAUDE.md fragment into the given directory.
|
|
63
|
+
* Idempotent: skips if sentinel already present.
|
|
64
|
+
* @returns 'written' | 'already-present' | 'error'
|
|
65
|
+
*/
|
|
66
|
+
export function writeClaude(projectDir) {
|
|
67
|
+
const claudePath = join(projectDir, 'CLAUDE.md');
|
|
68
|
+
try {
|
|
69
|
+
if (existsSync(claudePath)) {
|
|
70
|
+
const content = readFileSync(claudePath, 'utf8');
|
|
71
|
+
if (content.includes(CLAUDE_MD_SENTINEL_START)) {
|
|
72
|
+
return { status: 'already-present' };
|
|
73
|
+
}
|
|
74
|
+
writeFileSync(claudePath, content + CLAUDE_MD_FRAGMENT);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
writeFileSync(claudePath, CLAUDE_MD_FRAGMENT.trimStart());
|
|
78
|
+
}
|
|
79
|
+
return { status: 'written' };
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return { status: 'error', reason: String(err) };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// ── Skills ────────────────────────────────────────────────────────────────────
|
|
86
|
+
export const CONTEXT_COMMIT_SKILL = `---
|
|
87
|
+
name: context-commit
|
|
88
|
+
description: "MANDATORY: Save project memory after completing ANY work. Use this skill after EVERY completed task and EVERY git commit — finished implementing a feature, resolved a bug, made a decision, closed a thread, completed a review, or ending a session. Every git commit must be followed immediately by a context commit. Do not wait to be asked. Do not proceed to the next task until both git commit and context commit are done. Triggers: git commit made, task complete, 'that's working', 'let's move on', 'I'm done', session ending. Skipping this means the next session starts completely blind."
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
# ContextGit — Context Commit Discipline
|
|
92
|
+
|
|
93
|
+
## RULE: Every git commit = immediate context commit
|
|
94
|
+
|
|
95
|
+
Do not wait to be asked. Do not batch. Do not skip.
|
|
96
|
+
After every git commit, call \`project_memory_save\` immediately.
|
|
97
|
+
Do not proceed to the next task until both git commit and context commit are done.
|
|
98
|
+
These two always go together. Never git commit without a context commit.
|
|
99
|
+
|
|
100
|
+
## When to commit context
|
|
101
|
+
|
|
102
|
+
Call \`project_memory_save\` (MCP tool) after:
|
|
103
|
+
- Every git commit (mandatory — these always go together)
|
|
104
|
+
- Completing a feature or task (before moving to the next)
|
|
105
|
+
- Making an architectural decision or choosing between options
|
|
106
|
+
- Resolving a bug, blocker, or open question
|
|
107
|
+
- Closing a thread
|
|
108
|
+
- Before ending the session for any reason
|
|
109
|
+
|
|
110
|
+
## What makes a good commit message
|
|
111
|
+
|
|
112
|
+
A context commit message is a future-you briefing. Write it so the next agent session (or the next developer) can pick up exactly where this one left off.
|
|
113
|
+
|
|
114
|
+
Structure:
|
|
115
|
+
\`\`\`
|
|
116
|
+
<one-line summary of what was done>
|
|
117
|
+
|
|
118
|
+
What was decided: <the decision and why>
|
|
119
|
+
What was built: <files changed, approach taken>
|
|
120
|
+
Open questions: <anything unresolved>
|
|
121
|
+
Next task: <the first concrete thing the next session should do>
|
|
122
|
+
Git: <branch> | <commit hash if available>
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
## How to call it
|
|
126
|
+
|
|
127
|
+
Use the \`project_memory_save\` MCP tool (alias: \`context_commit\`). Pass the full message as the \`message\` argument.
|
|
128
|
+
|
|
129
|
+
Do not skip this step when the work feels small. Small decisions compound. The next session starts blind without them.
|
|
130
|
+
`;
|
|
131
|
+
export const CONTEXT_BRANCH_SKILL = `---
|
|
132
|
+
name: context-branch
|
|
133
|
+
description: "Create an isolated context branch before risky or experimental work. Use this skill when the agent is about to explore something uncertain, experimental, or potentially breaking — trying an approach that might not work, refactoring something risky, exploring an architectural alternative, or doing anything that should be isolatable and reversible. Triggers on: 'let me try', 'what if we', 'I want to explore', 'let's experiment', 'alternative approach', or any phrasing that signals exploration rather than execution."
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
# ContextGit — Context Branch Discipline
|
|
137
|
+
|
|
138
|
+
## When to branch context
|
|
139
|
+
|
|
140
|
+
Call \`project_memory_branch\` (MCP tool) before:
|
|
141
|
+
- Trying an approach you're not sure will work
|
|
142
|
+
- Refactoring something that touches many files
|
|
143
|
+
- Exploring an architectural alternative to the current plan
|
|
144
|
+
- Doing anything you'd want to be able to roll back semantically (not just via git)
|
|
145
|
+
|
|
146
|
+
## Why this matters
|
|
147
|
+
|
|
148
|
+
A context branch creates an isolated snapshot workspace. If the exploration fails, you can return to the main branch context without polluting the session history with dead-end decisions.
|
|
149
|
+
|
|
150
|
+
It's cheap. It takes one tool call. The cost of not doing it is re-explaining to the next session why you abandoned the approach you just spent an hour on.
|
|
151
|
+
|
|
152
|
+
## How to call it
|
|
153
|
+
|
|
154
|
+
Use the \`project_memory_branch\` MCP tool (alias: \`context_branch\`). Pass a short descriptive name:
|
|
155
|
+
|
|
156
|
+
\`\`\`
|
|
157
|
+
project_memory_branch name="explore-queue-based-concurrency"
|
|
158
|
+
\`\`\`
|
|
159
|
+
|
|
160
|
+
When the exploration concludes:
|
|
161
|
+
- If it worked: \`project_memory_save\` your findings and merge back
|
|
162
|
+
- If it failed: \`project_memory_save\` a brief note ("explored X, abandoned because Y") and switch back to main branch
|
|
163
|
+
|
|
164
|
+
The failure note is as valuable as the success note. The next session needs to know not to try the same dead end.
|
|
165
|
+
`;
|
|
166
|
+
/**
|
|
167
|
+
* Write the context-commit and context-branch skills into <projectDir>/.claude/skills/.
|
|
168
|
+
* Overwrites if already present (these files are managed by contextgit).
|
|
169
|
+
* @returns 'written' | 'error'
|
|
170
|
+
*/
|
|
171
|
+
export function writeSkills(projectDir) {
|
|
172
|
+
try {
|
|
173
|
+
const commitDir = join(projectDir, '.claude', 'skills', 'context-commit');
|
|
174
|
+
const branchDir = join(projectDir, '.claude', 'skills', 'context-branch');
|
|
175
|
+
mkdirSync(commitDir, { recursive: true });
|
|
176
|
+
mkdirSync(branchDir, { recursive: true });
|
|
177
|
+
writeFileSync(join(commitDir, 'SKILL.md'), CONTEXT_COMMIT_SKILL);
|
|
178
|
+
writeFileSync(join(branchDir, 'SKILL.md'), CONTEXT_BRANCH_SKILL);
|
|
179
|
+
return { status: 'written' };
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
return { status: 'error', reason: String(err) };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=init-helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init-helpers.js","sourceRoot":"","sources":["../../src/lib/init-helpers.ts"],"names":[],"mappings":"AAAA,yDAAyD;AAEzD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,IAAI,CAAA;AACvE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B,iFAAiF;AAEjF,MAAM,CAAC,MAAM,wBAAwB,GAAG,2BAA2B,CAAA;AACnE,MAAM,CAAC,MAAM,sBAAsB,GAAG,yBAAyB,CAAA;AAE/D,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqDjC,CAAA;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,UAAkB;IAElB,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAA;IAChD,IAAI,CAAC;QACH,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAG,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;YAChD,IAAI,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAC,EAAE,CAAC;gBAC/C,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAA;YACtC,CAAC;YACD,aAAa,CAAC,UAAU,EAAE,OAAO,GAAG,kBAAkB,CAAC,CAAA;QACzD,CAAC;aAAM,CAAC;YACN,aAAa,CAAC,UAAU,EAAE,kBAAkB,CAAC,SAAS,EAAE,CAAC,CAAA;QAC3D,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAA;IACjD,CAAC;AACH,CAAC;AAED,iFAAiF;AAEjF,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4CnC,CAAA;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCnC,CAAA;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,UAAkB;IAElB,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,gBAAgB,CAAC,CAAA;QACzE,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,gBAAgB,CAAC,CAAA;QACzE,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACzC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACzC,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,oBAAoB,CAAC,CAAA;QAChE,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,oBAAoB,CAAC,CAAA;QAChE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAA;IACjD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init-helpers.test.d.ts","sourceRoot":"","sources":["../../src/lib/init-helpers.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { writeClaude, writeSkills, CLAUDE_MD_SENTINEL_START, CLAUDE_MD_SENTINEL_END, } from './init-helpers.js';
|
|
6
|
+
let tmpDir;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'contextgit-init-test-'));
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
// ── writeClaude ───────────────────────────────────────────────────────────────
|
|
14
|
+
describe('writeClaude', () => {
|
|
15
|
+
it('creates CLAUDE.md with sentinel when file does not exist', () => {
|
|
16
|
+
const result = writeClaude(tmpDir);
|
|
17
|
+
expect(result.status).toBe('written');
|
|
18
|
+
const content = readFileSync(join(tmpDir, 'CLAUDE.md'), 'utf8');
|
|
19
|
+
expect(content).toContain(CLAUDE_MD_SENTINEL_START);
|
|
20
|
+
expect(content).toContain(CLAUDE_MD_SENTINEL_END);
|
|
21
|
+
expect(content).toContain('project_memory_load');
|
|
22
|
+
});
|
|
23
|
+
it('appends sentinel to existing CLAUDE.md', () => {
|
|
24
|
+
const claudePath = join(tmpDir, 'CLAUDE.md');
|
|
25
|
+
writeFileSync(claudePath, '# My Project\n\nExisting content.\n');
|
|
26
|
+
const result = writeClaude(tmpDir);
|
|
27
|
+
expect(result.status).toBe('written');
|
|
28
|
+
const content = readFileSync(claudePath, 'utf8');
|
|
29
|
+
expect(content).toContain('# My Project');
|
|
30
|
+
expect(content).toContain('Existing content.');
|
|
31
|
+
expect(content).toContain(CLAUDE_MD_SENTINEL_START);
|
|
32
|
+
});
|
|
33
|
+
it('is idempotent — skips if sentinel already present', () => {
|
|
34
|
+
const claudePath = join(tmpDir, 'CLAUDE.md');
|
|
35
|
+
writeClaude(tmpDir); // first call
|
|
36
|
+
const afterFirst = readFileSync(claudePath, 'utf8');
|
|
37
|
+
writeClaude(tmpDir); // second call
|
|
38
|
+
const afterSecond = readFileSync(claudePath, 'utf8');
|
|
39
|
+
expect(afterSecond).toBe(afterFirst); // no change
|
|
40
|
+
// sentinel appears exactly once
|
|
41
|
+
const occurrences = afterSecond.split(CLAUDE_MD_SENTINEL_START).length - 1;
|
|
42
|
+
expect(occurrences).toBe(1);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
// ── writeSkills ───────────────────────────────────────────────────────────────
|
|
46
|
+
describe('writeSkills', () => {
|
|
47
|
+
it('creates both skill files under .claude/skills/', () => {
|
|
48
|
+
const result = writeSkills(tmpDir);
|
|
49
|
+
expect(result.status).toBe('written');
|
|
50
|
+
const commitSkill = join(tmpDir, '.claude', 'skills', 'context-commit', 'SKILL.md');
|
|
51
|
+
const branchSkill = join(tmpDir, '.claude', 'skills', 'context-branch', 'SKILL.md');
|
|
52
|
+
expect(existsSync(commitSkill)).toBe(true);
|
|
53
|
+
expect(existsSync(branchSkill)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it('context-commit skill contains correct name frontmatter', () => {
|
|
56
|
+
writeSkills(tmpDir);
|
|
57
|
+
const content = readFileSync(join(tmpDir, '.claude', 'skills', 'context-commit', 'SKILL.md'), 'utf8');
|
|
58
|
+
expect(content).toContain('name: context-commit');
|
|
59
|
+
expect(content).toContain('project_memory_save');
|
|
60
|
+
});
|
|
61
|
+
it('context-branch skill contains correct name frontmatter', () => {
|
|
62
|
+
writeSkills(tmpDir);
|
|
63
|
+
const content = readFileSync(join(tmpDir, '.claude', 'skills', 'context-branch', 'SKILL.md'), 'utf8');
|
|
64
|
+
expect(content).toContain('name: context-branch');
|
|
65
|
+
expect(content).toContain('project_memory_branch');
|
|
66
|
+
});
|
|
67
|
+
it('overwrites existing skill files on repeated calls', () => {
|
|
68
|
+
writeSkills(tmpDir); // first call
|
|
69
|
+
const result = writeSkills(tmpDir); // second call
|
|
70
|
+
expect(result.status).toBe('written');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
//# sourceMappingURL=init-helpers.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init-helpers.test.js","sourceRoot":"","sources":["../../src/lib/init-helpers.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-store.d.ts","sourceRoot":"","sources":["../../src/lib/remote-store.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,gBAAgB,EACxB,UAAU,CAAC,EAAE,MAAM,GAClB,YAAY,CAuBd"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// remote-store.ts — resolve the correct remote ContextStore for push/pull.
|
|
2
|
+
//
|
|
3
|
+
// Priority:
|
|
4
|
+
// 1. --remote flag (always HTTP RemoteStore, bypasses everything)
|
|
5
|
+
// 2. config.supabaseUrl → SupabaseStore (requires SUPABASE_SERVICE_KEY env)
|
|
6
|
+
// 3. config.remote → RemoteStore (HTTP)
|
|
7
|
+
// 4. Neither → error
|
|
8
|
+
import { RemoteStore, SupabaseStore } from '@contextgit/store';
|
|
9
|
+
export function resolveRemoteStore(config, remoteFlag) {
|
|
10
|
+
// --remote flag always means HTTP RemoteStore, regardless of other config
|
|
11
|
+
if (remoteFlag)
|
|
12
|
+
return new RemoteStore(remoteFlag);
|
|
13
|
+
// Supabase takes precedence over HTTP remote when both configured
|
|
14
|
+
if (config.supabaseUrl) {
|
|
15
|
+
const key = process.env['SUPABASE_SERVICE_KEY'];
|
|
16
|
+
if (!key) {
|
|
17
|
+
throw new Error('SUPABASE_SERVICE_KEY env var is required when supabaseUrl is configured.\n' +
|
|
18
|
+
'Set it in your shell or Claude Code env config.');
|
|
19
|
+
}
|
|
20
|
+
return new SupabaseStore(config.supabaseUrl, key);
|
|
21
|
+
}
|
|
22
|
+
if (config.remote)
|
|
23
|
+
return new RemoteStore(config.remote);
|
|
24
|
+
throw new Error('No remote configured.\n' +
|
|
25
|
+
'Run: contextgit set-remote supabase <url> (Supabase)\n' +
|
|
26
|
+
' or: contextgit set-remote <url> (self-hosted API)');
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=remote-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-store.js","sourceRoot":"","sources":["../../src/lib/remote-store.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,EAAE;AACF,YAAY;AACZ,oEAAoE;AACpE,8EAA8E;AAC9E,0CAA0C;AAC1C,uBAAuB;AAEvB,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAI9D,MAAM,UAAU,kBAAkB,CAChC,MAAwB,EACxB,UAAmB;IAEnB,0EAA0E;IAC1E,IAAI,UAAU;QAAE,OAAO,IAAI,WAAW,CAAC,UAAU,CAAC,CAAA;IAElD,kEAAkE;IAClE,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAA;QAC/C,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACb,4EAA4E;gBAC5E,iDAAiD,CAClD,CAAA;QACH,CAAC;QACD,OAAO,IAAI,aAAa,CAAC,MAAM,CAAC,WAAW,EAAE,GAAG,CAAC,CAAA;IACnD,CAAC;IAED,IAAI,MAAM,CAAC,MAAM;QAAE,OAAO,IAAI,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAExD,MAAM,IAAI,KAAK,CACb,yBAAyB;QACzB,yDAAyD;QACzD,8DAA8D,CAC/D,CAAA;AACH,CAAC"}
|