@indigoai-us/hq-cli 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/credentials.test.d.ts +5 -0
- package/dist/__tests__/credentials.test.d.ts.map +1 -0
- package/dist/__tests__/credentials.test.js +169 -0
- package/dist/__tests__/credentials.test.js.map +1 -0
- package/dist/commands/add.d.ts +6 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +60 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/auth.d.ts +17 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +269 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/cloud-setup.d.ts +19 -0
- package/dist/commands/cloud-setup.d.ts.map +1 -0
- package/dist/commands/cloud-setup.js +206 -0
- package/dist/commands/cloud-setup.js.map +1 -0
- package/dist/commands/cloud.d.ts +16 -0
- package/dist/commands/cloud.d.ts.map +1 -0
- package/dist/commands/cloud.js +263 -0
- package/dist/commands/cloud.js.map +1 -0
- package/dist/commands/initial-upload.d.ts +67 -0
- package/dist/commands/initial-upload.d.ts.map +1 -0
- package/dist/commands/initial-upload.js +205 -0
- package/dist/commands/initial-upload.js.map +1 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +55 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/sync.d.ts +6 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +104 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +60 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/strategies/link.d.ts +7 -0
- package/dist/strategies/link.d.ts.map +1 -0
- package/dist/strategies/link.js +51 -0
- package/dist/strategies/link.js.map +1 -0
- package/dist/strategies/merge.d.ts +7 -0
- package/dist/strategies/merge.d.ts.map +1 -0
- package/dist/strategies/merge.js +110 -0
- package/dist/strategies/merge.js.map +1 -0
- package/dist/sync-worker.d.ts +11 -0
- package/dist/sync-worker.d.ts.map +1 -0
- package/dist/sync-worker.js +77 -0
- package/dist/sync-worker.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/api-client.d.ts +26 -0
- package/dist/utils/api-client.d.ts.map +1 -0
- package/dist/utils/api-client.js +87 -0
- package/dist/utils/api-client.js.map +1 -0
- package/dist/utils/credentials.d.ts +44 -0
- package/dist/utils/credentials.d.ts.map +1 -0
- package/dist/utils/credentials.js +101 -0
- package/dist/utils/credentials.js.map +1 -0
- package/dist/utils/git.d.ts +13 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +70 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/manifest.d.ts +16 -0
- package/dist/utils/manifest.d.ts.map +1 -0
- package/dist/utils/manifest.js +95 -0
- package/dist/utils/manifest.js.map +1 -0
- package/dist/utils/sync.d.ts +125 -0
- package/dist/utils/sync.d.ts.map +1 -0
- package/dist/utils/sync.js +291 -0
- package/dist/utils/sync.js.map +1 -0
- package/package.json +36 -0
- package/src/__tests__/cloud-setup.test.ts +117 -0
- package/src/__tests__/credentials.test.ts +203 -0
- package/src/__tests__/initial-upload.test.ts +414 -0
- package/src/__tests__/sync.test.ts +627 -0
- package/src/commands/add.ts +74 -0
- package/src/commands/auth.ts +303 -0
- package/src/commands/cloud-setup.ts +251 -0
- package/src/commands/cloud.ts +300 -0
- package/src/commands/initial-upload.ts +263 -0
- package/src/commands/list.ts +66 -0
- package/src/commands/sync.ts +149 -0
- package/src/commands/update.ts +71 -0
- package/src/hq-cloud.d.ts +19 -0
- package/src/index.ts +46 -0
- package/src/strategies/link.ts +62 -0
- package/src/strategies/merge.ts +142 -0
- package/src/sync-worker.ts +82 -0
- package/src/types.ts +47 -0
- package/src/utils/api-client.ts +111 -0
- package/src/utils/credentials.ts +124 -0
- package/src/utils/git.ts +74 -0
- package/src/utils/manifest.ts +111 -0
- package/src/utils/sync.ts +381 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API client for hq-cloud.
|
|
3
|
+
* Reads stored credentials and attaches Authorization header to all requests.
|
|
4
|
+
*
|
|
5
|
+
* Base URL resolution order:
|
|
6
|
+
* 1. HQ_CLOUD_API_URL environment variable
|
|
7
|
+
* 2. ~/.hq/config.json "apiUrl" field
|
|
8
|
+
* 3. Default production URL
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import { readCredentials } from './credentials.js';
|
|
15
|
+
|
|
16
|
+
/** Default API base URL (production) */
|
|
17
|
+
const DEFAULT_API_URL = 'https://api.hq.indigoai.com';
|
|
18
|
+
|
|
19
|
+
/** Path to optional config file */
|
|
20
|
+
const CONFIG_PATH = path.join(os.homedir(), '.hq', 'config.json');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the hq-cloud API base URL.
|
|
24
|
+
*/
|
|
25
|
+
export function getApiUrl(): string {
|
|
26
|
+
// 1. Environment variable takes precedence
|
|
27
|
+
if (process.env['HQ_CLOUD_API_URL']) {
|
|
28
|
+
return process.env['HQ_CLOUD_API_URL'].replace(/\/+$/, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Config file
|
|
32
|
+
try {
|
|
33
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
34
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
35
|
+
const config = JSON.parse(raw) as { apiUrl?: string };
|
|
36
|
+
if (config.apiUrl) {
|
|
37
|
+
return config.apiUrl.replace(/\/+$/, '');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Ignore config read errors
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Default
|
|
45
|
+
return DEFAULT_API_URL;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Standard response shape from the API */
|
|
49
|
+
export interface ApiResponse<T = unknown> {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
status: number;
|
|
52
|
+
data?: T;
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Make an authenticated request to the hq-cloud API.
|
|
58
|
+
* Throws if not logged in. Returns parsed JSON response.
|
|
59
|
+
*/
|
|
60
|
+
export async function apiRequest<T = unknown>(
|
|
61
|
+
method: string,
|
|
62
|
+
urlPath: string,
|
|
63
|
+
body?: unknown,
|
|
64
|
+
): Promise<ApiResponse<T>> {
|
|
65
|
+
const creds = readCredentials();
|
|
66
|
+
if (!creds) {
|
|
67
|
+
throw new Error('Not logged in. Run "hq auth login" first.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const baseUrl = getApiUrl();
|
|
71
|
+
const url = `${baseUrl}${urlPath.startsWith('/') ? urlPath : '/' + urlPath}`;
|
|
72
|
+
|
|
73
|
+
const headers: Record<string, string> = {
|
|
74
|
+
'Authorization': `Bearer ${creds.token}`,
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const fetchOptions: RequestInit = {
|
|
79
|
+
method,
|
|
80
|
+
headers,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (body !== undefined && method !== 'GET') {
|
|
84
|
+
fetchOptions.body = JSON.stringify(body);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const response = await fetch(url, fetchOptions);
|
|
88
|
+
|
|
89
|
+
let data: T | undefined;
|
|
90
|
+
try {
|
|
91
|
+
data = await response.json() as T;
|
|
92
|
+
} catch {
|
|
93
|
+
// Response may not be JSON
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
status: response.status,
|
|
100
|
+
error: (data as Record<string, string> | undefined)?.message
|
|
101
|
+
?? (data as Record<string, string> | undefined)?.error
|
|
102
|
+
?? `HTTP ${response.status}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
ok: true,
|
|
108
|
+
status: response.status,
|
|
109
|
+
data,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential storage for HQ CLI authentication.
|
|
3
|
+
* Stores Clerk auth tokens in ~/.hq/credentials.json.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
|
|
10
|
+
/** Stored credential shape */
|
|
11
|
+
export interface HqCredentials {
|
|
12
|
+
/** Clerk session token (JWT) */
|
|
13
|
+
token: string;
|
|
14
|
+
/** Clerk user ID */
|
|
15
|
+
userId: string;
|
|
16
|
+
/** User's email (for display) */
|
|
17
|
+
email?: string;
|
|
18
|
+
/** When the token was stored (ISO string) */
|
|
19
|
+
storedAt: string;
|
|
20
|
+
/** When the token expires (ISO string, if known) */
|
|
21
|
+
expiresAt?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Override for the config directory base path.
|
|
26
|
+
* Set via HQ_CONFIG_HOME env var or _setConfigHome (for testing).
|
|
27
|
+
* When null, defaults to os.homedir().
|
|
28
|
+
*/
|
|
29
|
+
let configHomeOverride: string | null = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Set the base directory for config files. Intended for testing only.
|
|
33
|
+
*/
|
|
34
|
+
export function _setConfigHome(dir: string | null): void {
|
|
35
|
+
configHomeOverride = dir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the ~/.hq config directory path.
|
|
40
|
+
* Respects HQ_CONFIG_HOME env var, _setConfigHome override, or defaults to ~/.hq.
|
|
41
|
+
*/
|
|
42
|
+
function getConfigDir(): string {
|
|
43
|
+
const base = configHomeOverride
|
|
44
|
+
?? process.env['HQ_CONFIG_HOME']
|
|
45
|
+
?? os.homedir();
|
|
46
|
+
return path.join(base, '.hq');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the credentials file path.
|
|
51
|
+
*/
|
|
52
|
+
function getCredentialsFilePath(): string {
|
|
53
|
+
return path.join(getConfigDir(), 'credentials.json');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Ensure the config directory exists with restricted permissions.
|
|
58
|
+
*/
|
|
59
|
+
function ensureConfigDir(): void {
|
|
60
|
+
const dir = getConfigDir();
|
|
61
|
+
if (!fs.existsSync(dir)) {
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read stored credentials. Returns null if not logged in or file is missing/corrupt.
|
|
68
|
+
*/
|
|
69
|
+
export function readCredentials(): HqCredentials | null {
|
|
70
|
+
try {
|
|
71
|
+
const credPath = getCredentialsFilePath();
|
|
72
|
+
if (!fs.existsSync(credPath)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const raw = fs.readFileSync(credPath, 'utf-8');
|
|
76
|
+
const creds = JSON.parse(raw) as HqCredentials;
|
|
77
|
+
if (!creds.token || !creds.userId) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return creds;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Write credentials to disk. Creates ~/.hq if needed.
|
|
88
|
+
* File permissions are set to owner-only (0o600).
|
|
89
|
+
*/
|
|
90
|
+
export function writeCredentials(creds: HqCredentials): void {
|
|
91
|
+
ensureConfigDir();
|
|
92
|
+
const content = JSON.stringify(creds, null, 2);
|
|
93
|
+
fs.writeFileSync(getCredentialsFilePath(), content, { mode: 0o600 });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Clear stored credentials (logout).
|
|
98
|
+
* Returns true if credentials were removed, false if none existed.
|
|
99
|
+
*/
|
|
100
|
+
export function clearCredentials(): boolean {
|
|
101
|
+
const credPath = getCredentialsFilePath();
|
|
102
|
+
if (!fs.existsSync(credPath)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
fs.unlinkSync(credPath);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get the credentials file path (for display/debugging).
|
|
111
|
+
*/
|
|
112
|
+
export function getCredentialsPath(): string {
|
|
113
|
+
return getCredentialsFilePath();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if credentials are expired (if expiresAt is set).
|
|
118
|
+
*/
|
|
119
|
+
export function isExpired(creds: HqCredentials): boolean {
|
|
120
|
+
if (!creds.expiresAt) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return new Date(creds.expiresAt) <= new Date();
|
|
124
|
+
}
|
package/src/utils/git.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { simpleGit, type SimpleGit } from 'simple-git';
|
|
4
|
+
|
|
5
|
+
export async function cloneRepo(repoUrl: string, targetDir: string, branch?: string): Promise<void> {
|
|
6
|
+
const git = simpleGit();
|
|
7
|
+
const options = branch ? ['--branch', branch] : [];
|
|
8
|
+
await git.clone(repoUrl, targetDir, options);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function fetchRepo(repoDir: string): Promise<void> {
|
|
12
|
+
const git = simpleGit(repoDir);
|
|
13
|
+
await git.fetch(['--all']);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function pullRepo(repoDir: string): Promise<void> {
|
|
17
|
+
const git = simpleGit(repoDir);
|
|
18
|
+
await git.pull();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getCurrentCommit(repoDir: string): Promise<string> {
|
|
22
|
+
const git = simpleGit(repoDir);
|
|
23
|
+
const log = await git.log({ maxCount: 1 });
|
|
24
|
+
return log.latest?.hash ?? '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function checkoutCommit(repoDir: string, commitSha: string): Promise<void> {
|
|
28
|
+
const git = simpleGit(repoDir);
|
|
29
|
+
await git.checkout(commitSha);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function isRepo(dir: string): Promise<boolean> {
|
|
33
|
+
if (!fs.existsSync(dir)) return false;
|
|
34
|
+
try {
|
|
35
|
+
const git = simpleGit(dir);
|
|
36
|
+
return await git.checkIsRepo();
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function getRemoteUrl(repoDir: string): Promise<string | null> {
|
|
43
|
+
try {
|
|
44
|
+
const git = simpleGit(repoDir);
|
|
45
|
+
const remotes = await git.getRemotes(true);
|
|
46
|
+
const origin = remotes.find(r => r.name === 'origin');
|
|
47
|
+
return origin?.refs?.fetch ?? null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function isBehindRemote(repoDir: string): Promise<{ behind: boolean; commits: number }> {
|
|
54
|
+
try {
|
|
55
|
+
const git = simpleGit(repoDir);
|
|
56
|
+
await git.fetch();
|
|
57
|
+
const status = await git.status();
|
|
58
|
+
return { behind: status.behind > 0, commits: status.behind };
|
|
59
|
+
} catch {
|
|
60
|
+
return { behind: false, commits: 0 };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function ensureGitignore(hqRoot: string, entry: string): void {
|
|
65
|
+
const gitignorePath = path.join(hqRoot, '.gitignore');
|
|
66
|
+
let content = '';
|
|
67
|
+
if (fs.existsSync(gitignorePath)) {
|
|
68
|
+
content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
if (!content.includes(entry)) {
|
|
71
|
+
content = content.trimEnd() + '\n' + entry + '\n';
|
|
72
|
+
fs.writeFileSync(gitignorePath, content);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as yaml from 'js-yaml';
|
|
4
|
+
import type { ModulesManifest, ModuleDefinition, ModuleLock, SyncState } from '../types.js';
|
|
5
|
+
|
|
6
|
+
const MANIFEST_FILE = 'modules.yaml';
|
|
7
|
+
const LOCK_FILE = 'modules.lock';
|
|
8
|
+
const STATE_FILE = '.hq-sync-state.json';
|
|
9
|
+
|
|
10
|
+
export function findHqRoot(): string {
|
|
11
|
+
let dir = process.cwd();
|
|
12
|
+
while (true) {
|
|
13
|
+
if (fs.existsSync(path.join(dir, '.claude')) || fs.existsSync(path.join(dir, 'workers'))) {
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
const parent = path.dirname(dir);
|
|
17
|
+
if (parent === dir) break; // Reached filesystem root (works on Windows and Unix)
|
|
18
|
+
dir = parent;
|
|
19
|
+
}
|
|
20
|
+
return process.cwd();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getManifestPath(hqRoot: string): string {
|
|
24
|
+
return path.join(hqRoot, MANIFEST_FILE);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getLockPath(hqRoot: string): string {
|
|
28
|
+
return path.join(hqRoot, LOCK_FILE);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getStatePath(hqRoot: string): string {
|
|
32
|
+
return path.join(hqRoot, STATE_FILE);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getModulesDir(hqRoot: string): string {
|
|
36
|
+
return path.join(hqRoot, 'modules');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readManifest(hqRoot: string): ModulesManifest | null {
|
|
40
|
+
const manifestPath = getManifestPath(hqRoot);
|
|
41
|
+
if (!fs.existsSync(manifestPath)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
45
|
+
return yaml.load(content) as ModulesManifest;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function writeManifest(hqRoot: string, manifest: ModulesManifest): void {
|
|
49
|
+
const manifestPath = getManifestPath(hqRoot);
|
|
50
|
+
const content = yaml.dump(manifest, { lineWidth: -1 });
|
|
51
|
+
fs.writeFileSync(manifestPath, content);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function readLock(hqRoot: string): ModuleLock | null {
|
|
55
|
+
const lockPath = getLockPath(hqRoot);
|
|
56
|
+
if (!fs.existsSync(lockPath)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const content = fs.readFileSync(lockPath, 'utf-8');
|
|
60
|
+
return yaml.load(content) as ModuleLock;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function writeLock(hqRoot: string, lock: ModuleLock): void {
|
|
64
|
+
const lockPath = getLockPath(hqRoot);
|
|
65
|
+
const content = yaml.dump(lock, { lineWidth: -1 });
|
|
66
|
+
fs.writeFileSync(lockPath, content);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function readState(hqRoot: string): SyncState | null {
|
|
70
|
+
const statePath = getStatePath(hqRoot);
|
|
71
|
+
if (!fs.existsSync(statePath)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
75
|
+
return JSON.parse(content) as SyncState;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function writeState(hqRoot: string, state: SyncState): void {
|
|
79
|
+
const statePath = getStatePath(hqRoot);
|
|
80
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function addModule(hqRoot: string, module: ModuleDefinition): void {
|
|
84
|
+
let manifest = readManifest(hqRoot);
|
|
85
|
+
if (!manifest) {
|
|
86
|
+
manifest = { version: '1', modules: [] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check for duplicates
|
|
90
|
+
if (manifest.modules.some(m => m.name === module.name)) {
|
|
91
|
+
throw new Error(`Module "${module.name}" already exists`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
manifest.modules.push(module);
|
|
95
|
+
writeManifest(hqRoot, manifest);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function parseRepoName(repoUrl: string): string {
|
|
99
|
+
// Extract repo name from URL
|
|
100
|
+
// https://github.com/user/repo.git -> repo
|
|
101
|
+
// git@github.com:user/repo.git -> repo
|
|
102
|
+
const match = repoUrl.match(/[\/:]([^\/]+?)(\.git)?$/);
|
|
103
|
+
if (!match) {
|
|
104
|
+
throw new Error(`Cannot parse repo name from: ${repoUrl}`);
|
|
105
|
+
}
|
|
106
|
+
return match[1];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isValidRepoUrl(url: string): boolean {
|
|
110
|
+
return url.startsWith('https://') || url.startsWith('git@');
|
|
111
|
+
}
|