@logicseek/hello-cc 0.1.4 → 0.1.6
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/CHANGELOG.md +126 -0
- package/bin/hcc.mjs +2739 -6482
- package/docs/README.md +2 -0
- package/docs/README.zh-CN.md +3 -1
- package/docs/architecture-split-review.md +217 -0
- package/docs/architecture.md +385 -0
- package/docs/architecture.zh-CN.md +364 -0
- package/docs/commands.md +21 -9
- package/docs/commands.zh-CN.md +19 -9
- package/docs/layout-split-stack-audit.md +298 -0
- package/lib/automation.mjs +8 -0
- package/lib/cli-args.mjs +89 -0
- package/lib/cli-runtime.mjs +44 -0
- package/lib/coordination-state.mjs +218 -0
- package/lib/core/coordination/automation.mjs +222 -0
- package/lib/core/coordination/locks.mjs +46 -0
- package/lib/core/coordination/messages.mjs +116 -0
- package/lib/core/coordination/tasks.mjs +224 -0
- package/lib/core/coordination/teams.mjs +73 -0
- package/lib/core/coordination/timeline.mjs +187 -0
- package/lib/core/peers/bindings.mjs +83 -0
- package/lib/core/peers/format.mjs +13 -0
- package/lib/core/peers/identity.mjs +53 -0
- package/lib/core/peers/liveness.mjs +90 -0
- package/lib/core/peers/reconcile.mjs +238 -0
- package/lib/core/peers/session.mjs +14 -0
- package/lib/core/sessions/launch.mjs +89 -0
- package/lib/db/schema.mjs +324 -0
- package/lib/db/stores/peers.mjs +231 -0
- package/lib/db-schema.mjs +7 -0
- package/lib/errors.mjs +1 -0
- package/lib/format.mjs +39 -0
- package/lib/guidance.mjs +10 -2
- package/lib/handoff.mjs +26 -0
- package/lib/help.mjs +3 -0
- package/lib/integrations/hooks.mjs +171 -0
- package/lib/integrations/peers/identity.mjs +208 -0
- package/lib/integrations/peers/processes.mjs +151 -0
- package/lib/integrations/providers.mjs +282 -0
- package/lib/integrations/shims/script.mjs +384 -0
- package/lib/integrations/shims.mjs +106 -0
- package/lib/json-file.mjs +1 -0
- package/lib/locks.mjs +10 -0
- package/lib/messages.mjs +3 -0
- package/lib/package-meta.mjs +6 -24
- package/lib/peer-bindings.mjs +14 -0
- package/lib/peer-format.mjs +4 -0
- package/lib/peer-identity.mjs +15 -0
- package/lib/project-context.mjs +6 -0
- package/lib/project-registry.mjs +7 -0
- package/lib/provider-commands.mjs +13 -0
- package/lib/release/package-meta.mjs +31 -0
- package/lib/release/release-notes.mjs +40 -0
- package/lib/release-notes.mjs +6 -40
- package/lib/runtime/client.mjs +42 -0
- package/lib/runtime/paths.mjs +45 -0
- package/lib/runtime/project-context.mjs +32 -0
- package/lib/runtime/projects.mjs +76 -0
- package/lib/runtime/state.mjs +128 -0
- package/lib/runtime-client.mjs +3 -0
- package/lib/runtime-paths.mjs +11 -0
- package/lib/runtime-state.mjs +11 -0
- package/lib/session-launch.mjs +17 -0
- package/lib/setup.mjs +18 -765
- package/lib/shared/errors.mjs +7 -0
- package/lib/shared/json-file.mjs +14 -0
- package/lib/shell-path.mjs +69 -0
- package/lib/shim-script.mjs +1 -0
- package/lib/state-render.mjs +5 -0
- package/lib/task-cli.mjs +45 -0
- package/lib/task-liveness.mjs +8 -0
- package/lib/task-store.mjs +3 -0
- package/lib/team-planning.mjs +7 -0
- package/lib/terminal/tmux.mjs +372 -0
- package/lib/timeline.mjs +12 -0
- package/lib/tmux.mjs +18 -0
- package/lib/ui/help.mjs +419 -0
- package/lib/ui/state-render.mjs +61 -0
- package/lib/web/http.mjs +55 -0
- package/lib/web/peer-actions.mjs +270 -0
- package/lib/web/runtime.mjs +154 -0
- package/lib/web/ui-template.mjs +2055 -0
- package/lib/web-http.mjs +7 -0
- package/lib/web-peer-actions.mjs +3 -0
- package/lib/web-runtime.mjs +17 -0
- package/lib/web-ui-template.mjs +3 -0
- package/package.json +1 -1
- package/scripts/github-release.mjs +2 -2
- package/scripts/regression.mjs +2738 -22
- package/scripts/release-notes.mjs +2 -2
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { generateShim } from './shims/script.mjs';
|
|
6
|
+
|
|
7
|
+
export const SHIM_DIR = path.join(os.homedir(), '.hcc-shims');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Install shim scripts for `claude` and `codex` into SHIM_DIR.
|
|
11
|
+
* Returns { shimDir, installed: [paths], changed: [paths], skipped: [names] }.
|
|
12
|
+
*/
|
|
13
|
+
export function installShims(hccBin) {
|
|
14
|
+
fs.mkdirSync(SHIM_DIR, { recursive: true });
|
|
15
|
+
|
|
16
|
+
const tools = [
|
|
17
|
+
{ name: 'claude', kind: 'claude', resumeFlag: '--resume' },
|
|
18
|
+
{ name: 'codex', kind: 'codex', resumeSubcmd: 'resume' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const installed = [];
|
|
22
|
+
const changed = [];
|
|
23
|
+
const skipped = [];
|
|
24
|
+
|
|
25
|
+
for (const tool of tools) {
|
|
26
|
+
const realBin = findRealBinary(tool.name);
|
|
27
|
+
if (!realBin) {
|
|
28
|
+
skipped.push(`${tool.name} (not found on PATH)`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const shimPath = path.join(SHIM_DIR, tool.name);
|
|
32
|
+
const content = generateShim(hccBin, realBin, tool);
|
|
33
|
+
let previous = null;
|
|
34
|
+
try { previous = fs.readFileSync(shimPath, 'utf8'); } catch {}
|
|
35
|
+
if (previous !== content) {
|
|
36
|
+
fs.writeFileSync(shimPath, content, { mode: 0o755 });
|
|
37
|
+
changed.push(shimPath);
|
|
38
|
+
} else {
|
|
39
|
+
try { fs.chmodSync(shimPath, 0o755); } catch {}
|
|
40
|
+
}
|
|
41
|
+
installed.push(shimPath);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { shimDir: SHIM_DIR, installed, changed, skipped };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Remove shim scripts.
|
|
49
|
+
*/
|
|
50
|
+
export function uninstallShims() {
|
|
51
|
+
const removed = [];
|
|
52
|
+
for (const name of ['claude', 'codex']) {
|
|
53
|
+
const p = path.join(SHIM_DIR, name);
|
|
54
|
+
try { fs.unlinkSync(p); removed.push(p); } catch {}
|
|
55
|
+
}
|
|
56
|
+
try { fs.rmdirSync(SHIM_DIR); } catch {}
|
|
57
|
+
return removed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns true if at least one shim is installed and in an HCC shim dir.
|
|
62
|
+
*/
|
|
63
|
+
export function verifyShims() {
|
|
64
|
+
return ['claude', 'codex'].some(name =>
|
|
65
|
+
fsExists(path.join(SHIM_DIR, name))
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Find the real binary for `name`, skipping the SHIM_DIR.
|
|
71
|
+
* Checks PATH, then npm global bin, then common install locations.
|
|
72
|
+
*/
|
|
73
|
+
export function findRealBinary(name) {
|
|
74
|
+
// which -a lists all matches; pick first one outside SHIM_DIR
|
|
75
|
+
const r = spawnSync('which', ['-a', name], { encoding: 'utf8', timeout: 3000 });
|
|
76
|
+
if (r.status === 0) {
|
|
77
|
+
for (const p of r.stdout.trim().split('\n')) {
|
|
78
|
+
const resolved = p.trim();
|
|
79
|
+
if (resolved && !resolved.startsWith(SHIM_DIR)) return resolved;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Try npm global bin
|
|
84
|
+
const npmR = spawnSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 3000 });
|
|
85
|
+
if (npmR.status === 0) {
|
|
86
|
+
const npmBin = path.join(path.dirname(npmR.stdout.trim()), 'bin', name);
|
|
87
|
+
if (fsExists(npmBin)) return npmBin;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Common locations
|
|
91
|
+
for (const dir of [
|
|
92
|
+
'/usr/local/bin',
|
|
93
|
+
'/usr/bin',
|
|
94
|
+
path.join(os.homedir(), '.local', 'bin'),
|
|
95
|
+
path.join(os.homedir(), '.nvm', 'current', 'bin'),
|
|
96
|
+
]) {
|
|
97
|
+
const p = path.join(dir, name);
|
|
98
|
+
if (fsExists(p)) return p;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function fsExists(p) {
|
|
105
|
+
try { fs.accessSync(p); return true; } catch { return false; }
|
|
106
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { readJsonSafe, writeJsonSafe } from './shared/json-file.mjs';
|
package/lib/locks.mjs
ADDED
package/lib/messages.mjs
ADDED
package/lib/package-meta.mjs
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function readJson(file) {
|
|
10
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function readPackageJson(root) {
|
|
14
|
-
return readJson(path.join(root, 'package.json'));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function readPackageMeta(root) {
|
|
18
|
-
const pkg = readPackageJson(root);
|
|
19
|
-
return {
|
|
20
|
-
name: pkg.name,
|
|
21
|
-
version: pkg.version,
|
|
22
|
-
packageJson: pkg
|
|
23
|
-
};
|
|
24
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
packageRoot,
|
|
3
|
+
readJson,
|
|
4
|
+
readPackageJson,
|
|
5
|
+
readPackageMeta
|
|
6
|
+
} from './release/package-meta.mjs';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
bindingFromDetected,
|
|
3
|
+
bindingHasProviderSession,
|
|
4
|
+
bindingHasRuntime,
|
|
5
|
+
bindingProviderSessionValue,
|
|
6
|
+
comparePeerBindings,
|
|
7
|
+
mergePeerBinding,
|
|
8
|
+
mergeRuntimeBinding,
|
|
9
|
+
peerBindingRuntimeRank
|
|
10
|
+
} from './core/peers/bindings.mjs';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
createPeerBindingStore
|
|
14
|
+
} from './db/stores/peers.mjs';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
autoPeerBasis,
|
|
3
|
+
autoPeerId,
|
|
4
|
+
autoPeerKind,
|
|
5
|
+
autoPeerProviderSession,
|
|
6
|
+
autoPeerResumeId,
|
|
7
|
+
autoPeerSessionId,
|
|
8
|
+
currentPeer,
|
|
9
|
+
currentTtyName,
|
|
10
|
+
readAncestorCliInfo,
|
|
11
|
+
resolveCurrentPeer,
|
|
12
|
+
resumeIdFromArgs,
|
|
13
|
+
sanitizePeerPart,
|
|
14
|
+
shortHash
|
|
15
|
+
} from './integrations/peers/identity.mjs';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
bindingFromRun,
|
|
3
|
+
buildClaudeCommand,
|
|
4
|
+
buildCodexCommand,
|
|
5
|
+
buildPeerCommand,
|
|
6
|
+
defaultSessionCommand,
|
|
7
|
+
hasResumeOpts,
|
|
8
|
+
inferPeerKind,
|
|
9
|
+
parseClaudeCommandArgs,
|
|
10
|
+
parseCodexCommandArgs,
|
|
11
|
+
providerSessionParts,
|
|
12
|
+
providerSessionPeerId
|
|
13
|
+
} from './integrations/providers.mjs';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export function packageRoot(fromUrl = import.meta.url) {
|
|
6
|
+
let dir = path.dirname(fileURLToPath(fromUrl));
|
|
7
|
+
for (;;) {
|
|
8
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
|
|
9
|
+
const parent = path.dirname(dir);
|
|
10
|
+
if (parent === dir) break;
|
|
11
|
+
dir = parent;
|
|
12
|
+
}
|
|
13
|
+
return path.resolve(path.dirname(fileURLToPath(fromUrl)), '..', '..');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readJson(file) {
|
|
17
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function readPackageJson(root) {
|
|
21
|
+
return readJson(path.join(root, 'package.json'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function readPackageMeta(root) {
|
|
25
|
+
const pkg = readPackageJson(root);
|
|
26
|
+
return {
|
|
27
|
+
name: pkg.name,
|
|
28
|
+
version: pkg.version,
|
|
29
|
+
packageJson: pkg
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function normalizeVersion(version) {
|
|
2
|
+
return String(version || '').replace(/^v/, '');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function releaseSection(markdown, version) {
|
|
6
|
+
const lines = markdown.split(/\r?\n/);
|
|
7
|
+
const heading = `## ${version}`;
|
|
8
|
+
const start = lines.findIndex((line) => line.trim() === heading);
|
|
9
|
+
if (start < 0) return null;
|
|
10
|
+
|
|
11
|
+
let end = lines.length;
|
|
12
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
13
|
+
if (/^##\s+/.test(lines[i])) {
|
|
14
|
+
end = i;
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return lines.slice(start, end).join('\n').trimEnd();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateReleaseSection(section, version) {
|
|
22
|
+
const problems = [];
|
|
23
|
+
if (!section) {
|
|
24
|
+
problems.push(`CHANGELOG.md is missing section ## ${version}`);
|
|
25
|
+
return problems;
|
|
26
|
+
}
|
|
27
|
+
if (!section.includes('### Summary')) problems.push(`## ${version} is missing ### Summary`);
|
|
28
|
+
if (!section.includes('### Highlights')) problems.push(`## ${version} is missing ### Highlights`);
|
|
29
|
+
if (section.includes('TODO') || section.includes('TBD')) problems.push(`## ${version} still contains TODO/TBD text`);
|
|
30
|
+
const bulletCount = (section.match(/^- /gm) || []).length;
|
|
31
|
+
if (bulletCount < 2) problems.push(`## ${version} should include at least two release-note bullets`);
|
|
32
|
+
return problems;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function repoFromPackage(pkg) {
|
|
36
|
+
const repo = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
|
|
37
|
+
if (!repo) return null;
|
|
38
|
+
const match = repo.match(/github\.com[:/](.+?\/.+?)(?:\.git)?$/);
|
|
39
|
+
return match ? match[1] : null;
|
|
40
|
+
}
|
package/lib/release-notes.mjs
CHANGED
|
@@ -1,40 +1,6 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const heading = `## ${version}`;
|
|
8
|
-
const start = lines.findIndex((line) => line.trim() === heading);
|
|
9
|
-
if (start < 0) return null;
|
|
10
|
-
|
|
11
|
-
let end = lines.length;
|
|
12
|
-
for (let i = start + 1; i < lines.length; i += 1) {
|
|
13
|
-
if (/^##\s+/.test(lines[i])) {
|
|
14
|
-
end = i;
|
|
15
|
-
break;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return lines.slice(start, end).join('\n').trimEnd();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function validateReleaseSection(section, version) {
|
|
22
|
-
const problems = [];
|
|
23
|
-
if (!section) {
|
|
24
|
-
problems.push(`CHANGELOG.md is missing section ## ${version}`);
|
|
25
|
-
return problems;
|
|
26
|
-
}
|
|
27
|
-
if (!section.includes('### Summary')) problems.push(`## ${version} is missing ### Summary`);
|
|
28
|
-
if (!section.includes('### Highlights')) problems.push(`## ${version} is missing ### Highlights`);
|
|
29
|
-
if (section.includes('TODO') || section.includes('TBD')) problems.push(`## ${version} still contains TODO/TBD text`);
|
|
30
|
-
const bulletCount = (section.match(/^- /gm) || []).length;
|
|
31
|
-
if (bulletCount < 2) problems.push(`## ${version} should include at least two release-note bullets`);
|
|
32
|
-
return problems;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function repoFromPackage(pkg) {
|
|
36
|
-
const repo = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
|
|
37
|
-
if (!repo) return null;
|
|
38
|
-
const match = repo.match(/github\.com[:/](.+?\/.+?)(?:\.git)?$/);
|
|
39
|
-
return match ? match[1] : null;
|
|
40
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
normalizeVersion,
|
|
3
|
+
releaseSection,
|
|
4
|
+
repoFromPackage,
|
|
5
|
+
validateReleaseSection
|
|
6
|
+
} from './release/release-notes.mjs';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CliError } from '../shared/errors.mjs';
|
|
2
|
+
import { readRuntime } from './state.mjs';
|
|
3
|
+
import { runtimeApiUrl } from '../web/runtime.mjs';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CLI_NAME = 'hcc';
|
|
6
|
+
|
|
7
|
+
export async function runtimeRequest(ctx, method, route, body = null, runtime = null, opts = {}) {
|
|
8
|
+
const cliName = opts.cliName || DEFAULT_CLI_NAME;
|
|
9
|
+
const rt = runtime || readRuntime(ctx, { cliName });
|
|
10
|
+
const url = runtimeApiUrl(rt, route);
|
|
11
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
12
|
+
headers['X-HCC-Root'] = ctx.root;
|
|
13
|
+
headers['X-HCC-DB'] = ctx.dbPath;
|
|
14
|
+
if (rt.token) headers.Authorization = `Bearer ${rt.token}`;
|
|
15
|
+
let res;
|
|
16
|
+
try {
|
|
17
|
+
res = await fetch(url, {
|
|
18
|
+
method,
|
|
19
|
+
headers,
|
|
20
|
+
body: body === null ? undefined : JSON.stringify(body)
|
|
21
|
+
});
|
|
22
|
+
} catch (err) {
|
|
23
|
+
throw new CliError('RUNTIME_UNREACHABLE', `Runtime is not reachable at ${rt.base_url}. Start ${cliName} web again.`, {
|
|
24
|
+
runtime: rt.source || rt.base_url,
|
|
25
|
+
message: err.message
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
let json = null;
|
|
29
|
+
const text = await res.text();
|
|
30
|
+
if (text.trim()) {
|
|
31
|
+
try {
|
|
32
|
+
json = JSON.parse(text);
|
|
33
|
+
} catch {
|
|
34
|
+
throw new CliError('RUNTIME_BAD_RESPONSE', `Runtime returned non-JSON response from ${url.pathname}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
const error = json && json.error ? json.error : { code: 'RUNTIME_ERROR', message: `Runtime request failed: ${res.status}` };
|
|
39
|
+
throw new CliError(error.code || 'RUNTIME_ERROR', error.message || `Runtime request failed: ${res.status}`, error);
|
|
40
|
+
}
|
|
41
|
+
return json || {};
|
|
42
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function projectStateDir(root) {
|
|
5
|
+
return path.join(root, '.hello-cc');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function projectDbPath(root) {
|
|
9
|
+
return path.join(projectStateDir(root), 'mesh.db');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function runtimePath(ctx) {
|
|
13
|
+
return path.join(projectStateDir(ctx.root), 'runtime.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function webLogPath(ctx) {
|
|
17
|
+
return path.join(projectStateDir(ctx.root), 'web.log');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function globalStateDir() {
|
|
21
|
+
return path.join(os.homedir(), '.hello-cc');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function globalRuntimePath() {
|
|
25
|
+
return path.join(globalStateDir(), 'runtime.json');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function globalWebTokenPath() {
|
|
29
|
+
return path.join(globalStateDir(), 'web-token');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function projectRegistryPath() {
|
|
33
|
+
return path.join(globalStateDir(), 'projects.json');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function contextForProject(root, dbPath = null, base = {}) {
|
|
37
|
+
const resolvedRoot = path.resolve(root);
|
|
38
|
+
return {
|
|
39
|
+
cwd: base.cwd || resolvedRoot,
|
|
40
|
+
root: resolvedRoot,
|
|
41
|
+
dbPath: path.resolve(dbPath || projectDbPath(resolvedRoot)),
|
|
42
|
+
json: Boolean(base.json),
|
|
43
|
+
explicitRoot: true
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
export function runGit(args, cwd) {
|
|
7
|
+
const result = spawnSync('git', args, {
|
|
8
|
+
cwd,
|
|
9
|
+
encoding: 'utf8',
|
|
10
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
11
|
+
});
|
|
12
|
+
if (result.status !== 0) return null;
|
|
13
|
+
return result.stdout.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function hasHccRootSync(cwd) {
|
|
17
|
+
if (!cwd) return null;
|
|
18
|
+
const dir = path.resolve(cwd);
|
|
19
|
+
const hccDir = path.join(dir, '.hello-cc');
|
|
20
|
+
return fs.existsSync(path.join(hccDir, 'mesh.db')) ||
|
|
21
|
+
fs.existsSync(path.join(hccDir, 'config.json'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function detectRoot(cwd, explicitRoot) {
|
|
25
|
+
if (explicitRoot) return path.resolve(explicitRoot);
|
|
26
|
+
if (process.env.HCC_ROOT) return path.resolve(process.env.HCC_ROOT);
|
|
27
|
+
return path.resolve(cwd);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function detectBranch(cwd) {
|
|
31
|
+
return runGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd) || '';
|
|
32
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { projectDbPath, projectRegistryPath } from './paths.mjs';
|
|
4
|
+
|
|
5
|
+
function registryTimestamp(nowFn = () => Math.floor(Date.now() / 1000)) {
|
|
6
|
+
return nowFn();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeProjectEntry(project) {
|
|
10
|
+
if (!project?.root) return null;
|
|
11
|
+
const root = path.resolve(project.root);
|
|
12
|
+
return {
|
|
13
|
+
root,
|
|
14
|
+
db: path.resolve(project.db || projectDbPath(root)),
|
|
15
|
+
name: String(project.name || path.basename(root) || root),
|
|
16
|
+
last_seen_at: Number.parseInt(project.last_seen_at || '0', 10) || 0
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function projectRootExists(project) {
|
|
21
|
+
if (!project?.root) return false;
|
|
22
|
+
try {
|
|
23
|
+
return fs.existsSync(project.root) && fs.statSync(project.root).isDirectory();
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function projectRecord(ctx, nowFn) {
|
|
30
|
+
return {
|
|
31
|
+
root: ctx.root,
|
|
32
|
+
db: ctx.dbPath,
|
|
33
|
+
name: path.basename(ctx.root) || ctx.root,
|
|
34
|
+
last_seen_at: registryTimestamp(nowFn)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readProjectRegistry() {
|
|
39
|
+
const file = projectRegistryPath();
|
|
40
|
+
if (!fs.existsSync(file)) return [];
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
43
|
+
const rows = Array.isArray(parsed?.projects) ? parsed.projects : [];
|
|
44
|
+
return rows
|
|
45
|
+
.filter((p) => p && typeof p.root === 'string')
|
|
46
|
+
.map(normalizeProjectEntry)
|
|
47
|
+
.filter(projectRootExists);
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function writeProjectRegistry(projects) {
|
|
54
|
+
const file = projectRegistryPath();
|
|
55
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
56
|
+
const unique = new Map();
|
|
57
|
+
for (const project of projects) {
|
|
58
|
+
const row = normalizeProjectEntry(project);
|
|
59
|
+
if (!row) continue;
|
|
60
|
+
unique.set(row.root, row);
|
|
61
|
+
}
|
|
62
|
+
const rows = [...unique.values()].sort((a, b) => (b.last_seen_at || 0) - (a.last_seen_at || 0));
|
|
63
|
+
fs.writeFileSync(file, JSON.stringify({ projects: rows }, null, 2), { mode: 0o600 });
|
|
64
|
+
fs.chmodSync(file, 0o600);
|
|
65
|
+
return rows;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function registerProject(ctx) {
|
|
69
|
+
const rows = readProjectRegistry().filter((p) => path.resolve(p.root) !== ctx.root);
|
|
70
|
+
rows.unshift(projectRecord(ctx));
|
|
71
|
+
return writeProjectRegistry(rows);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function registerProjectActivity(ctx) {
|
|
75
|
+
try { registerProject(ctx); } catch {}
|
|
76
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { CliError } from '../shared/errors.mjs';
|
|
5
|
+
import {
|
|
6
|
+
globalRuntimePath,
|
|
7
|
+
runtimePath
|
|
8
|
+
} from './paths.mjs';
|
|
9
|
+
import { runtimeApiUrl } from '../web/runtime.mjs';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_PRODUCT_NAME = 'hello-cc';
|
|
12
|
+
const DEFAULT_CLI_NAME = 'hcc';
|
|
13
|
+
|
|
14
|
+
export function readGlobalRuntimeFile() {
|
|
15
|
+
const file = globalRuntimePath();
|
|
16
|
+
if (!fs.existsSync(file)) return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
try { fs.rmSync(file, { force: true }); } catch {}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function writeGlobalRuntime(runtime) {
|
|
26
|
+
const file = globalRuntimePath();
|
|
27
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
28
|
+
fs.writeFileSync(file, JSON.stringify(runtime, null, 2), { mode: 0o600 });
|
|
29
|
+
fs.chmodSync(file, 0o600);
|
|
30
|
+
return file;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function writeRuntime(ctx, runtime) {
|
|
34
|
+
const file = runtimePath(ctx);
|
|
35
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
36
|
+
fs.writeFileSync(file, JSON.stringify(runtime, null, 2), { mode: 0o600 });
|
|
37
|
+
fs.chmodSync(file, 0o600);
|
|
38
|
+
return file;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function readRuntime(ctx, opts = {}) {
|
|
42
|
+
if (process.env.HCC_RUNTIME_URL) {
|
|
43
|
+
return {
|
|
44
|
+
base_url: process.env.HCC_RUNTIME_URL,
|
|
45
|
+
token: process.env.HCC_RUNTIME_TOKEN || '',
|
|
46
|
+
source: 'env'
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const file = runtimePath(ctx);
|
|
50
|
+
if (fs.existsSync(file)) {
|
|
51
|
+
try {
|
|
52
|
+
const runtime = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
53
|
+
if (!runtime.base_url) throw new Error('missing base_url');
|
|
54
|
+
return { ...runtime, source: file };
|
|
55
|
+
} catch {
|
|
56
|
+
try { fs.rmSync(file, { force: true }); } catch {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const global = readGlobalRuntimeFile();
|
|
60
|
+
if (global?.base_url) {
|
|
61
|
+
return { ...global, source: globalRuntimePath(), global: true };
|
|
62
|
+
}
|
|
63
|
+
const productName = opts.productName || DEFAULT_PRODUCT_NAME;
|
|
64
|
+
const cliName = opts.cliName || DEFAULT_CLI_NAME;
|
|
65
|
+
throw new CliError('RUNTIME_NOT_RUNNING',
|
|
66
|
+
`No running ${productName} web runtime found. Start it with:\n ${cliName} web`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function readRuntimeFile(ctx) {
|
|
70
|
+
const file = runtimePath(ctx);
|
|
71
|
+
if (!fs.existsSync(file)) return null;
|
|
72
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function probeRuntime(runtime) {
|
|
76
|
+
if (!runtime?.base_url) return false;
|
|
77
|
+
const url = runtimeApiUrl(runtime, '/api/runtime');
|
|
78
|
+
const headers = {};
|
|
79
|
+
if (runtime.token) headers.Authorization = `Bearer ${runtime.token}`;
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(url, { headers });
|
|
82
|
+
return response.ok;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function readHealthyRuntime(ctx) {
|
|
89
|
+
try {
|
|
90
|
+
const runtime = readRuntimeFile(ctx);
|
|
91
|
+
if (runtime && await probeRuntime(runtime)) return runtime;
|
|
92
|
+
const global = readGlobalRuntimeFile();
|
|
93
|
+
if (global && await probeRuntime(global)) return global;
|
|
94
|
+
return null;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function readHealthyGlobalRuntime() {
|
|
101
|
+
try {
|
|
102
|
+
const runtime = readGlobalRuntimeFile();
|
|
103
|
+
if (!runtime) return null;
|
|
104
|
+
return await probeRuntime(runtime) ? runtime : null;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function clearRuntime(ctx, pid = process.pid) {
|
|
111
|
+
const file = runtimePath(ctx);
|
|
112
|
+
if (fs.existsSync(file)) {
|
|
113
|
+
try {
|
|
114
|
+
const runtime = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
115
|
+
if (!runtime.pid || runtime.pid === pid) fs.rmSync(file, { force: true });
|
|
116
|
+
} catch {
|
|
117
|
+
fs.rmSync(file, { force: true });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const globalFile = globalRuntimePath();
|
|
121
|
+
if (!fs.existsSync(globalFile)) return;
|
|
122
|
+
try {
|
|
123
|
+
const runtime = JSON.parse(fs.readFileSync(globalFile, 'utf8'));
|
|
124
|
+
if (!runtime.pid || runtime.pid === pid) fs.rmSync(globalFile, { force: true });
|
|
125
|
+
} catch {
|
|
126
|
+
fs.rmSync(globalFile, { force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|