create-harness-vibe-coding 0.8.18 → 0.8.19
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 +13 -0
- package/README.md +1 -0
- package/package.json +5 -2
- package/src/index.js +383 -267
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
- package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
- package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
- package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
- package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
- package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
- package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
- package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
- package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
- package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
- package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
- package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
- package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
- package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
- package/src/ui/dist/index.html +18 -0
- package/src/ui/index.html +17 -0
- package/src/ui/package.json +33 -0
- package/src/ui/pnpm-lock.yaml +1582 -0
- package/src/ui/pnpm-workspace.yaml +2 -0
- package/src/ui/src/App.tsx +86 -0
- package/src/ui/src/api.ts +93 -0
- package/src/ui/src/components/AgentsRoute.tsx +503 -0
- package/src/ui/src/components/Footer.tsx +69 -0
- package/src/ui/src/components/Header.tsx +175 -0
- package/src/ui/src/components/LoadingView.tsx +22 -0
- package/src/ui/src/components/RolesRoute.tsx +169 -0
- package/src/ui/src/components/SettingsRoute.tsx +166 -0
- package/src/ui/src/components/TaskList.tsx +388 -0
- package/src/ui/src/components/TerminalDrawer.tsx +611 -0
- package/src/ui/src/components/WorkflowRoute.tsx +670 -0
- package/src/ui/src/hooks/useReducedMotion.ts +17 -0
- package/src/ui/src/hooks/useServerConnection.ts +114 -0
- package/src/ui/src/index.css +281 -0
- package/src/ui/src/main.tsx +11 -0
- package/src/ui/src/types.ts +108 -0
- package/src/ui/tsconfig.json +21 -0
- package/src/ui/vite.config.ts +19 -0
- package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
- package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
- package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
- package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
- package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
- package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
- package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
- package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
- package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
- package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
- package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
- package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
- package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
- package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
- package/src/wf-ui-server/a2a-store.mjs +296 -0
- package/src/wf-ui-server/peer-capsule.mjs +213 -0
- package/src/wf-ui-server/pty-adapter.mjs +155 -0
- package/src/wf-ui-server/runtime-config.mjs +153 -0
- package/src/wf-ui-server/runtime-detector.mjs +374 -0
- package/src/wf-ui-server/security.mjs +67 -0
- package/src/wf-ui-server/server.mjs +849 -0
- package/src/wf-ui-server/session-registry.mjs +204 -0
- package/src/wf-ui-server/settings.mjs +100 -0
- package/src/wf-ui-server/task-parser.mjs +133 -0
- package/src/wf-ui-server/terminal-store.mjs +225 -0
- package/src/wf-ui-server/token.mjs +41 -0
- package/src/wf-ui-server/ws-events.mjs +354 -0
- package/src/wf-ui-server/ws-terminal.mjs +473 -0
- package/templates/common/.claude/commands/wf-help.md +1 -0
- package/templates/common/.claude/commands/wf-task-list.md +2 -0
- package/templates/common/.claude/commands/wf-ui.md +26 -0
- package/templates/common/.claude/commands/wf-update.md +54 -9
- package/templates/common/.claude/rules/ecc/common.md +1 -1
- package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
- package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
- package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
- package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
- package/templates/common/.harness-version +63 -39
- package/templates/common/.opencode/commands/wf-help.md +1 -0
- package/templates/common/.opencode/commands/wf-task-list.md +2 -0
- package/templates/common/.opencode/commands/wf-ui.md +26 -0
- package/templates/common/.opencode/commands/wf-update.md +54 -9
- package/templates/common/CLAUDE.md +1 -1
- package/templates/common/Harness/MEMORY.md +2 -0
- package/templates/common/Harness/README.md +6 -4
- package/templates/common/Harness/a2a/role-graph.json +79 -0
- package/templates/common/Harness/a2a/runtime-registry.json +5 -0
- package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
- package/templates/common/Harness/ownership.manifest.json +57 -2
- package/templates/common/Harness/scripts/README.md +134 -0
- package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
- package/templates/common/Harness/scripts/context-budget.mjs +1 -1
- package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
- package/templates/common/Harness/scripts/task-state.mjs +556 -23
- package/templates/common/Harness/scripts/validate-harness.mjs +229 -19
- package/templates/common/Harness/scripts/wf-remove.mjs +8 -3
- package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
- package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
- package/templates/common/Harness/settings.json +35 -0
- package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +7 -2
- package/templates/common/Harness/specs/runtime/command-surface.json +14 -0
- package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
- package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
- package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
- package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
- package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
- package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
- package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
- package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
- package/templates/common/Harness/tasks/_template/NOTES.md +0 -3
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const MAX_LIMIT = 2000;
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const [command, ...rest] = argv;
|
|
10
|
+
const flags = {};
|
|
11
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
12
|
+
const arg = rest[i];
|
|
13
|
+
if (!arg.startsWith('--')) continue;
|
|
14
|
+
const key = arg.slice(2);
|
|
15
|
+
const value = rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[++i] : 'true';
|
|
16
|
+
flags[key] = value;
|
|
17
|
+
}
|
|
18
|
+
return { command, flags };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validId(id) {
|
|
22
|
+
return typeof id === 'string' && /^[a-zA-Z0-9_-]+$/.test(id);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readJson(filePath, fallback = null) {
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
28
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readJsonl(filePath) {
|
|
35
|
+
try {
|
|
36
|
+
if (!fs.existsSync(filePath)) return [];
|
|
37
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
38
|
+
.split(/\r?\n/)
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.map((line) => JSON.parse(line));
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function appendJsonl(filePath, row) {
|
|
47
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
48
|
+
fs.appendFileSync(filePath, `${JSON.stringify(row)}\n`, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function allSessionDirs(projectRoot) {
|
|
52
|
+
const tasksRoot = path.join(projectRoot, 'Harness', 'tasks');
|
|
53
|
+
let tasks = [];
|
|
54
|
+
try { tasks = fs.readdirSync(tasksRoot, { withFileTypes: true }); } catch { return []; }
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const task of tasks) {
|
|
57
|
+
if (!task.isDirectory() || task.name.startsWith('_')) continue;
|
|
58
|
+
const sessionsRoot = path.join(tasksRoot, task.name, 'sessions');
|
|
59
|
+
let sessions = [];
|
|
60
|
+
try { sessions = fs.readdirSync(sessionsRoot, { withFileTypes: true }); } catch { continue; }
|
|
61
|
+
for (const session of sessions) {
|
|
62
|
+
if (session.isDirectory()) out.push({ taskId: task.name, sessionId: session.name, dir: path.join(sessionsRoot, session.name) });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findSession(projectRoot, sessionId) {
|
|
69
|
+
if (!validId(sessionId)) return null;
|
|
70
|
+
return allSessionDirs(projectRoot).find((session) => session.sessionId === sessionId) || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function globToRegex(pattern) {
|
|
74
|
+
const escaped = String(pattern || '**/*')
|
|
75
|
+
.replace(/\\/g, '/')
|
|
76
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
77
|
+
.replace(/\*\*/g, '::DOUBLE_STAR::')
|
|
78
|
+
.replace(/\*/g, '[^/]*')
|
|
79
|
+
.replace(/::DOUBLE_STAR::/g, '.*');
|
|
80
|
+
return new RegExp(`^${escaped}$`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function print(data) {
|
|
84
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function listSessions(projectRoot) {
|
|
88
|
+
return allSessionDirs(projectRoot)
|
|
89
|
+
.map(({ dir }) => readJson(path.join(dir, 'STATE.json')))
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readRange(projectRoot, flags) {
|
|
95
|
+
const found = findSession(projectRoot, flags.session);
|
|
96
|
+
if (!found) return { sessionId: flags.session, entries: [] };
|
|
97
|
+
let entries = readJsonl(path.join(found.dir, 'terminal.jsonl'));
|
|
98
|
+
const from = Number(flags.from);
|
|
99
|
+
const to = Number(flags.to);
|
|
100
|
+
const tail = Number(flags.tail);
|
|
101
|
+
if (Number.isFinite(from)) entries = entries.filter((entry) => entry.seq >= from);
|
|
102
|
+
if (Number.isFinite(to)) entries = entries.filter((entry) => entry.seq <= to);
|
|
103
|
+
if (Number.isFinite(tail) && tail > 0) entries = entries.slice(-Math.min(tail, MAX_LIMIT));
|
|
104
|
+
else entries = entries.slice(0, MAX_LIMIT);
|
|
105
|
+
return { taskId: found.taskId, sessionId: found.sessionId, entries };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function globEvents(projectRoot, flags) {
|
|
109
|
+
const regex = globToRegex(flags.pattern || flags.glob || '**/*');
|
|
110
|
+
const limit = Math.min(Math.max(Number(flags.limit) || 200, 1), MAX_LIMIT);
|
|
111
|
+
const sinceMs = flags.since ? new Date(flags.since).getTime() : null;
|
|
112
|
+
const rows = [];
|
|
113
|
+
for (const session of allSessionDirs(projectRoot)) {
|
|
114
|
+
for (const filename of ['events.jsonl', 'terminal.jsonl', 'input-requests.jsonl']) {
|
|
115
|
+
const rel = `Harness/tasks/${session.taskId}/sessions/${session.sessionId}/${filename}`;
|
|
116
|
+
if (!regex.test(rel)) continue;
|
|
117
|
+
for (const row of readJsonl(path.join(session.dir, filename))) {
|
|
118
|
+
const tsMs = row.ts ? new Date(row.ts).getTime() : 0;
|
|
119
|
+
if (sinceMs && tsMs < sinceMs) continue;
|
|
120
|
+
rows.push({ source: rel, ...row });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return rows.sort((a, b) => String(a.ts || '').localeCompare(String(b.ts || ''))).slice(-limit);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function postInput(urlText, token, sessionId, text) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const base = new URL(urlText);
|
|
130
|
+
const body = JSON.stringify({ data: text });
|
|
131
|
+
const req = http.request({
|
|
132
|
+
hostname: base.hostname,
|
|
133
|
+
port: base.port,
|
|
134
|
+
path: `/api/sessions/${encodeURIComponent(sessionId)}/input`,
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
'Content-Type': 'application/json',
|
|
138
|
+
'Content-Length': Buffer.byteLength(body),
|
|
139
|
+
Authorization: `Bearer ${token}`,
|
|
140
|
+
},
|
|
141
|
+
}, (res) => {
|
|
142
|
+
let data = '';
|
|
143
|
+
res.on('data', chunk => { data += chunk; });
|
|
144
|
+
res.on('end', () => {
|
|
145
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
|
|
146
|
+
catch { resolve({ status: res.statusCode, body: data }); }
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
req.on('error', reject);
|
|
150
|
+
req.write(body);
|
|
151
|
+
req.end();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function sendInput(projectRoot, flags) {
|
|
156
|
+
const sessionId = flags.session;
|
|
157
|
+
const text = flags.text || '';
|
|
158
|
+
const url = flags.url || process.env.WF_UI_URL;
|
|
159
|
+
const token = flags.token || process.env.WF_UI_TOKEN;
|
|
160
|
+
if (url && token) return postInput(url, token, sessionId, text);
|
|
161
|
+
|
|
162
|
+
const found = findSession(projectRoot, sessionId);
|
|
163
|
+
if (!found) throw new Error(`Session not found: ${sessionId}`);
|
|
164
|
+
const state = readJson(path.join(found.dir, 'STATE.json'));
|
|
165
|
+
if (!state?.attachMode) throw new Error('Input rejected: session is in watch mode. Enable attach mode first.');
|
|
166
|
+
const row = {
|
|
167
|
+
ts: new Date().toISOString(),
|
|
168
|
+
type: 'terminal.input.requested',
|
|
169
|
+
taskId: found.taskId,
|
|
170
|
+
sessionId,
|
|
171
|
+
data: text,
|
|
172
|
+
};
|
|
173
|
+
appendJsonl(path.join(found.dir, 'input-requests.jsonl'), row);
|
|
174
|
+
return { queued: true, ...row };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function main() {
|
|
178
|
+
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
179
|
+
const projectRoot = path.resolve(flags.project || process.cwd());
|
|
180
|
+
if (command === 'list-sessions') return print(listSessions(projectRoot));
|
|
181
|
+
if (command === 'read-range') return print(readRange(projectRoot, flags));
|
|
182
|
+
if (command === 'tail') return print(readRange(projectRoot, { ...flags, tail: flags.lines || flags.tail || 100 }));
|
|
183
|
+
if (command === 'glob-events') return print(globEvents(projectRoot, flags));
|
|
184
|
+
if (command === 'send-input') return print(await sendInput(projectRoot, flags));
|
|
185
|
+
throw new Error('Usage: a2a-terminal.mjs list-sessions|read-range|tail|glob-events|send-input [--project .]');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
main().catch((err) => {
|
|
189
|
+
process.stderr.write(`${err.message}\n`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
});
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
|
|
10
|
+
function readFlagValue(flagName) {
|
|
11
|
+
const idx = args.indexOf(flagName);
|
|
12
|
+
if (idx === -1) return null;
|
|
13
|
+
const value = args[idx + 1];
|
|
14
|
+
return value && !value.startsWith('--') ? value : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ROOT = path.resolve(process.env.WF_ROOT || readFlagValue('--root') || path.resolve(__dirname, '..', '..'));
|
|
18
|
+
const VERSION_FILE = path.join(ROOT, 'Harness', '.harness-version');
|
|
19
|
+
const APPLY = args.includes('--apply');
|
|
20
|
+
const JSON_OUT = args.includes('--json');
|
|
21
|
+
const DRY_RUN = args.includes('--dry-run') || (!APPLY && !args.includes('--apply'));
|
|
22
|
+
|
|
23
|
+
const HARNESS_MARKERS = [
|
|
24
|
+
/\bcreate-harness-vibe-coding\b/i,
|
|
25
|
+
/\bproject harness\b/i,
|
|
26
|
+
/\bHarness\/(?:specs|WF|MEMORY|tasks|scripts|subagents|dispatch|context-loading|lifecycle|SETUP)\b/,
|
|
27
|
+
/\bWF-(?:MAX|AUTO|KERNEL|STATE)\b/,
|
|
28
|
+
/^harness:\s*(?:wf-agent|wf-framework|create-harness-vibe-coding)\b/im,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function normalizePath(value) {
|
|
32
|
+
return String(value || '').replace(/\\/g, '/');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sha256(content) {
|
|
36
|
+
return 'sha256-' + createHash('sha256').update(content).digest('hex');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readNormalized(filePath) {
|
|
40
|
+
return fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sha256File(filePath) {
|
|
44
|
+
if (!fs.existsSync(filePath)) return null;
|
|
45
|
+
return sha256(readNormalized(filePath));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function containsPath(parent, child) {
|
|
49
|
+
const resolvedParent = path.resolve(parent);
|
|
50
|
+
const resolvedChild = path.resolve(child);
|
|
51
|
+
const rel = path.relative(resolvedParent, resolvedChild);
|
|
52
|
+
return rel === '' || (rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveUnder(base, rel) {
|
|
56
|
+
if (!rel || typeof rel !== 'string' || path.isAbsolute(rel)) return null;
|
|
57
|
+
const parts = normalizePath(rel).split('/').filter(Boolean);
|
|
58
|
+
if (parts.some(part => part === '..')) return null;
|
|
59
|
+
const resolved = path.resolve(base, ...parts);
|
|
60
|
+
return containsPath(base, resolved) ? resolved : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sourceDestForHostFile(host, file) {
|
|
64
|
+
const normalized = normalizePath(file);
|
|
65
|
+
if (host === 'claude') {
|
|
66
|
+
if (normalized === 'settings.json') return '.claude/settings.json';
|
|
67
|
+
if (/^(commands|skills|agents|rules)\//.test(normalized)) return `.claude/${normalized}`;
|
|
68
|
+
}
|
|
69
|
+
if (host === 'codex') {
|
|
70
|
+
if (normalized === 'config.toml') return '.codex/config.toml';
|
|
71
|
+
if (normalized === 'hooks.json') return '.codex/hooks.json';
|
|
72
|
+
if (normalized.startsWith('skills/')) return `.agents/${normalized}`;
|
|
73
|
+
}
|
|
74
|
+
if (host === 'opencode') {
|
|
75
|
+
if (normalized === 'opencode.json') return 'opencode.json';
|
|
76
|
+
if (/^(commands|agents|plugins)\//.test(normalized)) return `.opencode/${normalized}`;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasHarnessMarker(filePath) {
|
|
82
|
+
try {
|
|
83
|
+
const body = fs.readFileSync(filePath, 'utf8');
|
|
84
|
+
return HARNESS_MARKERS.some(marker => marker.test(body));
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readVersion() {
|
|
91
|
+
if (!fs.existsSync(VERSION_FILE)) {
|
|
92
|
+
throw new Error(`Harness/.harness-version not found under ${ROOT}`);
|
|
93
|
+
}
|
|
94
|
+
return JSON.parse(fs.readFileSync(VERSION_FILE, 'utf8'));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hostTargets(version) {
|
|
98
|
+
const hostGlobal = version?.hostGlobal;
|
|
99
|
+
if (!hostGlobal || hostGlobal.copyMode !== 'copy' || !hostGlobal.targets || typeof hostGlobal.targets !== 'object') {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return hostGlobal.targets;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function summarize(plan) {
|
|
106
|
+
return {
|
|
107
|
+
created: plan.created.length,
|
|
108
|
+
updated: plan.updated.length,
|
|
109
|
+
conflict: plan.conflict.length,
|
|
110
|
+
skipped: plan.skipped.length,
|
|
111
|
+
errors: plan.errors.length,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createPlan(version) {
|
|
116
|
+
const targets = hostTargets(version);
|
|
117
|
+
const plan = {
|
|
118
|
+
root: normalizePath(ROOT),
|
|
119
|
+
installScope: version?.installScope || 'project',
|
|
120
|
+
created: [],
|
|
121
|
+
updated: [],
|
|
122
|
+
conflict: [],
|
|
123
|
+
skipped: [],
|
|
124
|
+
errors: [],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (version?.installScope !== 'global') {
|
|
128
|
+
plan.skipped.push({ reason: 'not a global install' });
|
|
129
|
+
return plan;
|
|
130
|
+
}
|
|
131
|
+
if (!targets) {
|
|
132
|
+
plan.errors.push({ reason: 'Harness/.harness-version missing hostGlobal copy targets' });
|
|
133
|
+
return plan;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const [host, target] of Object.entries(targets)) {
|
|
137
|
+
const hostRoot = target && typeof target.root === 'string'
|
|
138
|
+
? path.resolve(target.root)
|
|
139
|
+
: null;
|
|
140
|
+
const files = Array.isArray(target?.files) ? target.files : [];
|
|
141
|
+
if (!hostRoot) {
|
|
142
|
+
plan.errors.push({ host, reason: 'missing host root' });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const sourceDest = sourceDestForHostFile(host, file);
|
|
147
|
+
const sourcePath = sourceDest ? resolveUnder(ROOT, sourceDest) : null;
|
|
148
|
+
const targetPath = resolveUnder(hostRoot, file);
|
|
149
|
+
if (!sourceDest || !sourcePath) {
|
|
150
|
+
plan.errors.push({ host, file, reason: 'unknown host-global source mapping' });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (!targetPath) {
|
|
154
|
+
plan.errors.push({ host, file, source: sourceDest, reason: 'invalid host-global target path' });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!fs.existsSync(sourcePath)) {
|
|
158
|
+
plan.errors.push({ host, file, source: sourceDest, reason: 'missing runtime source file' });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (fs.existsSync(targetPath)) {
|
|
162
|
+
const stat = fs.lstatSync(targetPath);
|
|
163
|
+
if (stat.isSymbolicLink()) {
|
|
164
|
+
plan.errors.push({ host, file, source: sourceDest, reason: 'target is symlink' });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (!stat.isFile()) {
|
|
168
|
+
plan.errors.push({ host, file, source: sourceDest, reason: 'target is not a regular file' });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const sourceHash = sha256File(sourcePath);
|
|
174
|
+
const targetHash = sha256File(targetPath);
|
|
175
|
+
const entry = { host, file, source: sourceDest, targetRoot: normalizePath(hostRoot), sourceHash, targetHash };
|
|
176
|
+
if (!targetHash) {
|
|
177
|
+
plan.created.push({ ...entry, reason: 'missing host-global copy' });
|
|
178
|
+
} else if (targetHash === sourceHash) {
|
|
179
|
+
plan.skipped.push({ ...entry, reason: 'already current' });
|
|
180
|
+
} else if (hasHarnessMarker(targetPath)) {
|
|
181
|
+
plan.updated.push({ ...entry, reason: 'stale Harness-marked host-global copy' });
|
|
182
|
+
} else {
|
|
183
|
+
plan.conflict.push({ ...entry, reason: 'existing host-global file lacks Harness ownership marker' });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return plan;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function applyPlan(plan) {
|
|
192
|
+
const written = [];
|
|
193
|
+
for (const entry of [...plan.created, ...plan.updated]) {
|
|
194
|
+
const sourcePath = resolveUnder(ROOT, entry.source);
|
|
195
|
+
const targetPath = resolveUnder(entry.targetRoot, entry.file);
|
|
196
|
+
if (!sourcePath || !targetPath) {
|
|
197
|
+
plan.errors.push({ ...entry, reason: 'path resolution failed during apply' });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (fs.existsSync(targetPath) && fs.lstatSync(targetPath).isSymbolicLink()) {
|
|
201
|
+
plan.errors.push({ ...entry, reason: 'target became symlink during apply' });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const content = readNormalized(sourcePath);
|
|
205
|
+
if (sha256(content) !== entry.sourceHash) {
|
|
206
|
+
plan.errors.push({ ...entry, reason: 'runtime source changed during apply' });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
210
|
+
fs.writeFileSync(targetPath, content, 'utf8');
|
|
211
|
+
written.push({ host: entry.host, file: entry.file, source: entry.source });
|
|
212
|
+
}
|
|
213
|
+
return written;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function printResult(result) {
|
|
217
|
+
if (JSON_OUT) {
|
|
218
|
+
console.log(JSON.stringify(result, null, 2));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (result.status === 'not-global') {
|
|
222
|
+
console.log('Host-global sync skipped: installScope is not global.');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
console.log(`Host-global sync ${result.status}: ${result.summary.created} created, ${result.summary.updated} updated, ${result.summary.conflict} conflict, ${result.summary.errors} error.`);
|
|
226
|
+
if (result.plan?.conflict?.length) {
|
|
227
|
+
for (const entry of result.plan.conflict) {
|
|
228
|
+
console.log(` conflict ${entry.host}:${entry.file} (${entry.reason})`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (result.plan?.errors?.length) {
|
|
232
|
+
for (const entry of result.plan.errors) {
|
|
233
|
+
console.log(` error ${entry.host || 'host'}:${entry.file || ''} ${entry.reason}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function main() {
|
|
239
|
+
let version;
|
|
240
|
+
try {
|
|
241
|
+
version = readVersion();
|
|
242
|
+
} catch (err) {
|
|
243
|
+
const result = { status: 'error', root: normalizePath(ROOT), message: err.message };
|
|
244
|
+
printResult(result);
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const plan = createPlan(version);
|
|
250
|
+
const summary = summarize(plan);
|
|
251
|
+
let written = [];
|
|
252
|
+
if (APPLY && summary.conflict === 0 && summary.errors === 0) {
|
|
253
|
+
written = applyPlan(plan);
|
|
254
|
+
}
|
|
255
|
+
const afterErrors = plan.errors.length;
|
|
256
|
+
const status = version?.installScope !== 'global'
|
|
257
|
+
? 'not-global'
|
|
258
|
+
: afterErrors > 0
|
|
259
|
+
? 'error'
|
|
260
|
+
: summary.conflict > 0
|
|
261
|
+
? 'conflict'
|
|
262
|
+
: summary.created + summary.updated > 0
|
|
263
|
+
? (APPLY ? 'synced' : 'needs-sync')
|
|
264
|
+
: 'ok';
|
|
265
|
+
const result = {
|
|
266
|
+
status,
|
|
267
|
+
dryRun: DRY_RUN,
|
|
268
|
+
applied: APPLY && status === 'synced',
|
|
269
|
+
root: normalizePath(ROOT),
|
|
270
|
+
summary: summarize(plan),
|
|
271
|
+
written,
|
|
272
|
+
plan,
|
|
273
|
+
};
|
|
274
|
+
printResult(result);
|
|
275
|
+
if (!['ok', 'synced', 'not-global'].includes(status)) process.exitCode = 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
main();
|