@indigoai-us/hq-cli 5.3.1 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/hq-auth-refresh.d.ts +13 -0
- package/dist/bin/hq-auth-refresh.d.ts.map +1 -0
- package/dist/bin/hq-auth-refresh.js +27 -0
- package/dist/bin/hq-auth-refresh.js.map +1 -0
- package/dist/commands/auth.d.ts +17 -13
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +83 -238
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/cloud.d.ts +3 -12
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +55 -212
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/login.d.ts +6 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +25 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +6 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +21 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/onboard.d.ts +23 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +142 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/commands/pkg-install.d.ts +17 -0
- package/dist/commands/pkg-install.d.ts.map +1 -0
- package/dist/commands/pkg-install.js +144 -0
- package/dist/commands/pkg-install.js.map +1 -0
- package/dist/commands/pkg-list.d.ts +8 -0
- package/dist/commands/pkg-list.d.ts.map +1 -0
- package/dist/commands/pkg-list.js +70 -0
- package/dist/commands/pkg-list.js.map +1 -0
- package/dist/commands/pkg-remove.d.ts +10 -0
- package/dist/commands/pkg-remove.d.ts.map +1 -0
- package/dist/commands/pkg-remove.js +53 -0
- package/dist/commands/pkg-remove.js.map +1 -0
- package/dist/commands/pkg-update.d.ts +9 -0
- package/dist/commands/pkg-update.d.ts.map +1 -0
- package/dist/commands/pkg-update.js +124 -0
- package/dist/commands/pkg-update.js.map +1 -0
- package/dist/commands/team-sync.d.ts +14 -0
- package/dist/commands/team-sync.d.ts.map +1 -0
- package/dist/commands/team-sync.js +422 -0
- package/dist/commands/team-sync.js.map +1 -0
- package/dist/commands/whoami.d.ts +6 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +29 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +33 -13
- package/dist/index.js.map +1 -1
- package/dist/strategies/merge.js +2 -2
- package/dist/strategies/merge.js.map +1 -1
- package/dist/utils/auth.d.ts +27 -0
- package/dist/utils/auth.d.ts.map +1 -0
- package/dist/utils/auth.js +155 -0
- package/dist/utils/auth.js.map +1 -0
- package/dist/utils/cognito-session.d.ts +35 -0
- package/dist/utils/cognito-session.d.ts.map +1 -0
- package/dist/utils/cognito-session.js +72 -0
- package/dist/utils/cognito-session.js.map +1 -0
- package/dist/utils/git.js +1 -1
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/hq-root.d.ts +10 -0
- package/dist/utils/hq-root.d.ts.map +1 -0
- package/dist/utils/hq-root.js +22 -0
- package/dist/utils/hq-root.js.map +1 -0
- package/dist/utils/integrity.d.ts +14 -0
- package/dist/utils/integrity.d.ts.map +1 -0
- package/dist/utils/integrity.js +40 -0
- package/dist/utils/integrity.js.map +1 -0
- package/dist/utils/manifest.d.ts.map +1 -1
- package/dist/utils/manifest.js +2 -5
- package/dist/utils/manifest.js.map +1 -1
- package/dist/utils/registry-client.d.ts +65 -0
- package/dist/utils/registry-client.d.ts.map +1 -0
- package/dist/utils/registry-client.js +102 -0
- package/dist/utils/registry-client.js.map +1 -0
- package/dist/utils/registry.d.ts +33 -0
- package/dist/utils/registry.d.ts.map +1 -0
- package/dist/utils/registry.js +58 -0
- package/dist/utils/registry.js.map +1 -0
- package/dist/utils/token-store.d.ts +28 -0
- package/dist/utils/token-store.d.ts.map +1 -0
- package/dist/utils/token-store.js +68 -0
- package/dist/utils/token-store.js.map +1 -0
- package/package.json +35 -45
- package/src/bin/hq-auth-refresh.ts +32 -0
- package/src/commands/add.ts +74 -0
- package/src/commands/auth.ts +153 -0
- package/src/commands/cloud.ts +125 -0
- package/src/commands/list.ts +66 -0
- package/src/commands/login.ts +29 -0
- package/src/commands/logout.ts +25 -0
- package/src/commands/pkg-install.ts +181 -0
- package/src/commands/pkg-list.ts +98 -0
- package/src/commands/pkg-remove.ts +71 -0
- package/src/commands/pkg-update.ts +189 -0
- package/src/commands/sync.ts +149 -0
- package/src/commands/team-sync.ts +629 -0
- package/src/commands/update.ts +71 -0
- package/src/commands/whoami.ts +38 -0
- package/src/index.ts +72 -0
- package/src/schemas/hq-package.schema.json +137 -0
- package/src/strategies/link.ts +62 -0
- package/src/strategies/merge.ts +142 -0
- package/src/types.ts +47 -0
- package/src/utils/auth.ts +193 -0
- package/src/utils/cognito-session.ts +94 -0
- package/src/utils/git.ts +74 -0
- package/src/utils/hq-root.ts +27 -0
- package/src/utils/integrity.ts +54 -0
- package/src/utils/manifest.ts +109 -0
- package/src/utils/registry-client.ts +204 -0
- package/src/utils/registry.ts +89 -0
- package/src/utils/token-store.ts +83 -0
- package/tsconfig.json +8 -0
- package/dist/commands/cloud-setup.d.ts +0 -19
- package/dist/commands/cloud-setup.d.ts.map +0 -1
- package/dist/commands/cloud-setup.js +0 -206
- package/dist/commands/cloud-setup.js.map +0 -1
- package/dist/commands/initial-upload.d.ts +0 -67
- package/dist/commands/initial-upload.d.ts.map +0 -1
- package/dist/commands/initial-upload.js +0 -205
- package/dist/commands/initial-upload.js.map +0 -1
- package/dist/sync-worker.d.ts +0 -11
- package/dist/sync-worker.d.ts.map +0 -1
- package/dist/sync-worker.js +0 -77
- package/dist/sync-worker.js.map +0 -1
- package/dist/utils/api-client.d.ts +0 -26
- package/dist/utils/api-client.d.ts.map +0 -1
- package/dist/utils/api-client.js +0 -87
- package/dist/utils/api-client.js.map +0 -1
- package/dist/utils/credentials.d.ts +0 -44
- package/dist/utils/credentials.d.ts.map +0 -1
- package/dist/utils/credentials.js +0 -101
- package/dist/utils/credentials.js.map +0 -1
- package/dist/utils/sync.d.ts +0 -125
- package/dist/utils/sync.d.ts.map +0 -1
- package/dist/utils/sync.js +0 -291
- package/dist/utils/sync.js.map +0 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Cognito session helpers for hq-cli commands.
|
|
3
|
+
*
|
|
4
|
+
* Consumed by `hq auth refresh` and the standalone `hq-auth-refresh` bin
|
|
5
|
+
* invoked by the deploy skill (.claude/skills/deploy/SKILL.md step 4).
|
|
6
|
+
*
|
|
7
|
+
* Defaults point at the shared hq-vault-dev Cognito pool. Override via env:
|
|
8
|
+
*
|
|
9
|
+
* AWS_REGION — e.g. us-east-1
|
|
10
|
+
* HQ_COGNITO_DOMAIN — Cognito User Pool domain prefix
|
|
11
|
+
* HQ_COGNITO_CLIENT_ID — App Client ID
|
|
12
|
+
* HQ_COGNITO_CALLBACK_PORT — Loopback OAuth callback port
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
loadCachedTokens,
|
|
17
|
+
isExpiring,
|
|
18
|
+
refreshTokens,
|
|
19
|
+
browserLogin,
|
|
20
|
+
type CognitoAuthConfig,
|
|
21
|
+
} from "@indigoai-us/hq-cloud";
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_COGNITO: CognitoAuthConfig = {
|
|
24
|
+
region: process.env.AWS_REGION ?? "us-east-1",
|
|
25
|
+
userPoolDomain: process.env.HQ_COGNITO_DOMAIN ?? "hq-vault-dev",
|
|
26
|
+
clientId:
|
|
27
|
+
process.env.HQ_COGNITO_CLIENT_ID ?? "4mmujmjq3srakdueg656b9m0mp",
|
|
28
|
+
port: process.env.HQ_COGNITO_CALLBACK_PORT
|
|
29
|
+
? Number(process.env.HQ_COGNITO_CALLBACK_PORT)
|
|
30
|
+
: 8765,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Return a non-expired Cognito access token, refreshing or browser-logging-in
|
|
35
|
+
* as needed. Cache lives at ~/.hq/cognito-tokens.json.
|
|
36
|
+
*
|
|
37
|
+
* Pass `interactive: false` from automated contexts where failing fast is
|
|
38
|
+
* better than opening a browser.
|
|
39
|
+
*/
|
|
40
|
+
export async function ensureCognitoToken(options: {
|
|
41
|
+
interactive?: boolean;
|
|
42
|
+
} = {}): Promise<string> {
|
|
43
|
+
const interactive = options.interactive ?? true;
|
|
44
|
+
const cached = loadCachedTokens();
|
|
45
|
+
|
|
46
|
+
if (cached && !isExpiring(cached, 120)) {
|
|
47
|
+
return cached.accessToken;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (cached) {
|
|
51
|
+
try {
|
|
52
|
+
const refreshed = await refreshTokens(
|
|
53
|
+
DEFAULT_COGNITO,
|
|
54
|
+
cached.refreshToken,
|
|
55
|
+
);
|
|
56
|
+
return refreshed.accessToken;
|
|
57
|
+
} catch {
|
|
58
|
+
// fall through to browser login
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!interactive) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"No valid HQ session and interactive login is disabled. Run `hq login` first.",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tokens = await browserLogin(DEFAULT_COGNITO);
|
|
69
|
+
return tokens.accessToken;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Refresh the cached Cognito session once and return the result. Used by
|
|
74
|
+
* `hq auth refresh` and the `hq-auth-refresh` bin. Never opens a browser —
|
|
75
|
+
* if no cached tokens exist or the refresh fails, throws.
|
|
76
|
+
*/
|
|
77
|
+
export async function refreshCachedSession(): Promise<{
|
|
78
|
+
refreshed: boolean;
|
|
79
|
+
reason?: string;
|
|
80
|
+
}> {
|
|
81
|
+
const cached = loadCachedTokens();
|
|
82
|
+
if (!cached) {
|
|
83
|
+
return { refreshed: false, reason: "no cached session" };
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await refreshTokens(DEFAULT_COGNITO, cached.refreshToken);
|
|
87
|
+
return { refreshed: true };
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return {
|
|
90
|
+
refreshed: false,
|
|
91
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
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, { 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,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HQ root detection — walks up from cwd looking for HQ markers (US-004)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Find the HQ root directory by walking up from cwd.
|
|
10
|
+
* Looks for CLAUDE.md or .claude/ directory as markers.
|
|
11
|
+
* Throws if not found.
|
|
12
|
+
*/
|
|
13
|
+
export function findHqRoot(): string {
|
|
14
|
+
let dir = process.cwd();
|
|
15
|
+
while (dir !== path.dirname(dir)) {
|
|
16
|
+
if (
|
|
17
|
+
fs.existsSync(path.join(dir, 'CLAUDE.md')) ||
|
|
18
|
+
fs.existsSync(path.join(dir, '.claude'))
|
|
19
|
+
) {
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
dir = path.dirname(dir);
|
|
23
|
+
}
|
|
24
|
+
throw new Error(
|
|
25
|
+
'Could not find HQ root. Run this command from within your HQ directory.'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integrity verification — SHA256 hash and RSA signature checks (US-005)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as crypto from 'crypto';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { findHqRoot } from './hq-root.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Verify a file's SHA256 hash matches the expected value.
|
|
12
|
+
*/
|
|
13
|
+
export async function verifySha256(
|
|
14
|
+
filePath: string,
|
|
15
|
+
expectedHash: string
|
|
16
|
+
): Promise<boolean> {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const hash = crypto.createHash('sha256');
|
|
19
|
+
const stream = fs.createReadStream(filePath);
|
|
20
|
+
|
|
21
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
22
|
+
stream.on('end', () => {
|
|
23
|
+
const computed = hash.digest('hex');
|
|
24
|
+
resolve(computed === expectedHash.toLowerCase());
|
|
25
|
+
});
|
|
26
|
+
stream.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Verify an RSA signature of a SHA256 hash using the registry public key.
|
|
32
|
+
* The public key is expected at packages/.keys/registry-public.pem.
|
|
33
|
+
* Returns false (does not throw) if the key file is missing.
|
|
34
|
+
*/
|
|
35
|
+
export function verifyRsaSignature(
|
|
36
|
+
sha256Hash: string,
|
|
37
|
+
signature: string,
|
|
38
|
+
publicKeyPath?: string
|
|
39
|
+
): boolean {
|
|
40
|
+
const keyPath =
|
|
41
|
+
publicKeyPath ??
|
|
42
|
+
path.resolve(findHqRoot(), 'packages', '.keys', 'registry-public.pem');
|
|
43
|
+
|
|
44
|
+
if (!fs.existsSync(keyPath)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const publicKey = fs.readFileSync(keyPath, 'utf-8');
|
|
49
|
+
const verifier = crypto.createVerify('SHA256');
|
|
50
|
+
verifier.update(sha256Hash);
|
|
51
|
+
verifier.end();
|
|
52
|
+
|
|
53
|
+
return verifier.verify(publicKey, Buffer.from(signature, 'base64'));
|
|
54
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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 (dir !== '/') {
|
|
13
|
+
if (fs.existsSync(path.join(dir, '.claude')) || fs.existsSync(path.join(dir, 'workers'))) {
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
dir = path.dirname(dir);
|
|
17
|
+
}
|
|
18
|
+
return process.cwd();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getManifestPath(hqRoot: string): string {
|
|
22
|
+
return path.join(hqRoot, MANIFEST_FILE);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getLockPath(hqRoot: string): string {
|
|
26
|
+
return path.join(hqRoot, LOCK_FILE);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getStatePath(hqRoot: string): string {
|
|
30
|
+
return path.join(hqRoot, STATE_FILE);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getModulesDir(hqRoot: string): string {
|
|
34
|
+
return path.join(hqRoot, 'modules');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readManifest(hqRoot: string): ModulesManifest | null {
|
|
38
|
+
const manifestPath = getManifestPath(hqRoot);
|
|
39
|
+
if (!fs.existsSync(manifestPath)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
43
|
+
return yaml.load(content) as ModulesManifest;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function writeManifest(hqRoot: string, manifest: ModulesManifest): void {
|
|
47
|
+
const manifestPath = getManifestPath(hqRoot);
|
|
48
|
+
const content = yaml.dump(manifest, { lineWidth: -1 });
|
|
49
|
+
fs.writeFileSync(manifestPath, content);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readLock(hqRoot: string): ModuleLock | null {
|
|
53
|
+
const lockPath = getLockPath(hqRoot);
|
|
54
|
+
if (!fs.existsSync(lockPath)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const content = fs.readFileSync(lockPath, 'utf-8');
|
|
58
|
+
return yaml.load(content) as ModuleLock;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function writeLock(hqRoot: string, lock: ModuleLock): void {
|
|
62
|
+
const lockPath = getLockPath(hqRoot);
|
|
63
|
+
const content = yaml.dump(lock, { lineWidth: -1 });
|
|
64
|
+
fs.writeFileSync(lockPath, content);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function readState(hqRoot: string): SyncState | null {
|
|
68
|
+
const statePath = getStatePath(hqRoot);
|
|
69
|
+
if (!fs.existsSync(statePath)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
73
|
+
return JSON.parse(content) as SyncState;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function writeState(hqRoot: string, state: SyncState): void {
|
|
77
|
+
const statePath = getStatePath(hqRoot);
|
|
78
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function addModule(hqRoot: string, module: ModuleDefinition): void {
|
|
82
|
+
let manifest = readManifest(hqRoot);
|
|
83
|
+
if (!manifest) {
|
|
84
|
+
manifest = { version: '1', modules: [] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check for duplicates
|
|
88
|
+
if (manifest.modules.some(m => m.name === module.name)) {
|
|
89
|
+
throw new Error(`Module "${module.name}" already exists`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
manifest.modules.push(module);
|
|
93
|
+
writeManifest(hqRoot, manifest);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parseRepoName(repoUrl: string): string {
|
|
97
|
+
// Extract repo name from URL
|
|
98
|
+
// https://github.com/user/repo.git -> repo
|
|
99
|
+
// git@github.com:user/repo.git -> repo
|
|
100
|
+
const match = repoUrl.match(/[\/:]([^\/]+?)(\.git)?$/);
|
|
101
|
+
if (!match) {
|
|
102
|
+
throw new Error(`Cannot parse repo name from: ${repoUrl}`);
|
|
103
|
+
}
|
|
104
|
+
return match[1];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function isValidRepoUrl(url: string): boolean {
|
|
108
|
+
return url.startsWith('https://') || url.startsWith('git@');
|
|
109
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry client — reads registry URL from packages/sources.yaml and provides
|
|
3
|
+
* API methods for the HQ package registry (US-004 base, US-005 full client).
|
|
4
|
+
*
|
|
5
|
+
* Auth tokens are NEVER written to stdout or logs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as yaml from 'js-yaml';
|
|
11
|
+
import { findHqRoot } from './hq-root.js';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Types
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
interface SourceEntry {
|
|
18
|
+
name: string;
|
|
19
|
+
url: string;
|
|
20
|
+
type: string;
|
|
21
|
+
auth: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SourcesFile {
|
|
25
|
+
sources: SourceEntry[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PackageMeta {
|
|
29
|
+
slug: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
tier: string;
|
|
33
|
+
latest_version: string;
|
|
34
|
+
author?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PackageListResponse {
|
|
38
|
+
packages: PackageMeta[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PackageResponse {
|
|
42
|
+
package: PackageMeta;
|
|
43
|
+
versions: { version: string; published_at: string }[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface EntitlementEntry {
|
|
47
|
+
slug: string;
|
|
48
|
+
tier: string;
|
|
49
|
+
granted_at: string;
|
|
50
|
+
expires_at?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface EntitlementListResponse {
|
|
54
|
+
entitlements: EntitlementEntry[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface EntitlementCheckResponse {
|
|
58
|
+
entitled: boolean;
|
|
59
|
+
tier?: string;
|
|
60
|
+
expires_at?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DownloadResponse {
|
|
64
|
+
url: string;
|
|
65
|
+
sha256: string;
|
|
66
|
+
signature?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// URL helper (unchanged from US-004)
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Read the registry URL from packages/sources.yaml in the user's HQ root.
|
|
75
|
+
* Returns the URL of the first source entry.
|
|
76
|
+
* Throws if sources.yaml is missing or has no sources.
|
|
77
|
+
*/
|
|
78
|
+
export function getRegistryUrl(): string {
|
|
79
|
+
const hqRoot = findHqRoot();
|
|
80
|
+
const sourcesPath = path.join(hqRoot, 'packages', 'sources.yaml');
|
|
81
|
+
|
|
82
|
+
if (!fs.existsSync(sourcesPath)) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`No packages/sources.yaml found at ${sourcesPath}. Is your HQ packages directory set up?`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const content = fs.readFileSync(sourcesPath, 'utf-8');
|
|
89
|
+
const parsed = yaml.load(content) as SourcesFile;
|
|
90
|
+
|
|
91
|
+
if (!parsed?.sources?.length) {
|
|
92
|
+
throw new Error('No sources defined in packages/sources.yaml');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return parsed.sources[0].url;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// RegistryClient
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
export class RegistryClient {
|
|
103
|
+
private baseUrl: string;
|
|
104
|
+
private authToken?: string;
|
|
105
|
+
|
|
106
|
+
constructor(baseUrl: string, authToken?: string) {
|
|
107
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
108
|
+
this.authToken = authToken;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---- helpers ----
|
|
112
|
+
|
|
113
|
+
private headers(auth: boolean): Record<string, string> {
|
|
114
|
+
const h: Record<string, string> = {
|
|
115
|
+
Accept: 'application/json',
|
|
116
|
+
};
|
|
117
|
+
if (auth && this.authToken) {
|
|
118
|
+
h['Authorization'] = `Bearer ${this.authToken}`;
|
|
119
|
+
}
|
|
120
|
+
return h;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async request<T>(
|
|
124
|
+
method: string,
|
|
125
|
+
urlPath: string,
|
|
126
|
+
opts: { auth?: boolean; body?: unknown; timeout?: number } = {}
|
|
127
|
+
): Promise<T> {
|
|
128
|
+
const auth = opts.auth ?? false;
|
|
129
|
+
const url = `${this.baseUrl}${urlPath}`;
|
|
130
|
+
|
|
131
|
+
const init: RequestInit = {
|
|
132
|
+
method,
|
|
133
|
+
headers: this.headers(auth),
|
|
134
|
+
signal: AbortSignal.timeout(opts.timeout ?? 30_000),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (opts.body) {
|
|
138
|
+
(init.headers as Record<string, string>)['Content-Type'] =
|
|
139
|
+
'application/json';
|
|
140
|
+
init.body = JSON.stringify(opts.body);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const res = await fetch(url, init);
|
|
144
|
+
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
const text = await res.text().catch(() => '');
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Registry API ${method} ${urlPath} failed (${res.status}): ${text}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (await res.json()) as T;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---- public (no auth) ----
|
|
156
|
+
|
|
157
|
+
async listPackages(opts?: {
|
|
158
|
+
tier?: string;
|
|
159
|
+
q?: string;
|
|
160
|
+
}): Promise<PackageListResponse> {
|
|
161
|
+
const params = new URLSearchParams();
|
|
162
|
+
if (opts?.tier) params.set('tier', opts.tier);
|
|
163
|
+
if (opts?.q) params.set('q', opts.q);
|
|
164
|
+
const qs = params.toString();
|
|
165
|
+
return this.request<PackageListResponse>(
|
|
166
|
+
'GET',
|
|
167
|
+
`/packages${qs ? `?${qs}` : ''}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async getPackage(slug: string): Promise<PackageResponse> {
|
|
172
|
+
return this.request<PackageResponse>('GET', `/packages/${encodeURIComponent(slug)}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---- auth required ----
|
|
176
|
+
|
|
177
|
+
async getMyEntitlements(): Promise<EntitlementListResponse> {
|
|
178
|
+
return this.request<EntitlementListResponse>('GET', '/entitlements', {
|
|
179
|
+
auth: true,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async checkEntitlement(slug: string): Promise<EntitlementCheckResponse> {
|
|
184
|
+
return this.request<EntitlementCheckResponse>(
|
|
185
|
+
'GET',
|
|
186
|
+
`/entitlements/${encodeURIComponent(slug)}`,
|
|
187
|
+
{ auth: true }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async getDownloadUrl(
|
|
192
|
+
slug: string,
|
|
193
|
+
version?: string
|
|
194
|
+
): Promise<DownloadResponse> {
|
|
195
|
+
const params = new URLSearchParams();
|
|
196
|
+
if (version) params.set('version', version);
|
|
197
|
+
const qs = params.toString();
|
|
198
|
+
return this.request<DownloadResponse>(
|
|
199
|
+
'GET',
|
|
200
|
+
`/packages/${encodeURIComponent(slug)}/download${qs ? `?${qs}` : ''}`,
|
|
201
|
+
{ auth: true }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry YAML helpers — read/write packages/registry.yaml (US-005)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as yaml from 'js-yaml';
|
|
8
|
+
|
|
9
|
+
export interface RegistryEntry {
|
|
10
|
+
name: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
version: string;
|
|
13
|
+
source: string;
|
|
14
|
+
license_key?: string;
|
|
15
|
+
scope?: string;
|
|
16
|
+
installed_at: string;
|
|
17
|
+
updated_at: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface RegistryFile {
|
|
21
|
+
packages: RegistryEntry[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function registryPath(hqRoot: string): string {
|
|
25
|
+
return path.resolve(hqRoot, 'packages', 'registry.yaml');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read all entries from packages/registry.yaml.
|
|
30
|
+
* Returns an empty array if the file does not exist.
|
|
31
|
+
*/
|
|
32
|
+
export function readRegistry(hqRoot: string): RegistryEntry[] {
|
|
33
|
+
const filePath = registryPath(hqRoot);
|
|
34
|
+
if (!fs.existsSync(filePath)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
38
|
+
const parsed = yaml.load(content) as RegistryFile | null;
|
|
39
|
+
return parsed?.packages ?? [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Write the full registry back to packages/registry.yaml.
|
|
44
|
+
*/
|
|
45
|
+
export function writeRegistry(
|
|
46
|
+
hqRoot: string,
|
|
47
|
+
entries: RegistryEntry[]
|
|
48
|
+
): void {
|
|
49
|
+
const filePath = registryPath(hqRoot);
|
|
50
|
+
const dir = path.dirname(filePath);
|
|
51
|
+
if (!fs.existsSync(dir)) {
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
const content = yaml.dump(
|
|
55
|
+
{ packages: entries },
|
|
56
|
+
{ lineWidth: 120, noRefs: true }
|
|
57
|
+
);
|
|
58
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Add or update an entry in packages/registry.yaml.
|
|
63
|
+
* If an entry with the same slug exists, it is replaced.
|
|
64
|
+
*/
|
|
65
|
+
export function addToRegistry(
|
|
66
|
+
hqRoot: string,
|
|
67
|
+
entry: RegistryEntry
|
|
68
|
+
): void {
|
|
69
|
+
const entries = readRegistry(hqRoot);
|
|
70
|
+
const idx = entries.findIndex((e) => e.slug === entry.slug);
|
|
71
|
+
if (idx >= 0) {
|
|
72
|
+
entries[idx] = entry;
|
|
73
|
+
} else {
|
|
74
|
+
entries.push(entry);
|
|
75
|
+
}
|
|
76
|
+
writeRegistry(hqRoot, entries);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Remove an entry by slug from packages/registry.yaml.
|
|
81
|
+
* No-op if the slug is not found.
|
|
82
|
+
*/
|
|
83
|
+
export function removeFromRegistry(
|
|
84
|
+
hqRoot: string,
|
|
85
|
+
slug: string
|
|
86
|
+
): void {
|
|
87
|
+
const entries = readRegistry(hqRoot).filter((e) => e.slug !== slug);
|
|
88
|
+
writeRegistry(hqRoot, entries);
|
|
89
|
+
}
|