@punal_manalan/blender-project-harness 0.0.0-stage → 1.0.2
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/.kilo/agent/blender-grader.md +197 -0
- package/.kilo/agent/blender-worker.md +127 -0
- package/.kilo/blender-project-harness/config.example.json +71 -0
- package/.kilo/blender-project-harness/tools/harness/EXPECTED_FLOW.md +656 -0
- package/.kilo/blender-project-harness/tools/harness/blender-operations.mjs +882 -0
- package/.kilo/blender-project-harness/tools/harness/blender-operations.test.mjs +550 -0
- package/.kilo/blender-project-harness/tools/harness/blender.mjs +301 -0
- package/.kilo/blender-project-harness/tools/harness/blender.test.mjs +87 -0
- package/.kilo/blender-project-harness/tools/harness/cli.mjs +127 -0
- package/.kilo/blender-project-harness/tools/harness/documents.mjs +201 -0
- package/.kilo/blender-project-harness/tools/harness/download.mjs +94 -0
- package/.kilo/blender-project-harness/tools/harness/download.test.mjs +63 -0
- package/.kilo/blender-project-harness/tools/harness/engine.mjs +1866 -0
- package/.kilo/blender-project-harness/tools/harness/engine.test.mjs +2219 -0
- package/.kilo/blender-project-harness/tools/harness/intake.mjs +571 -0
- package/.kilo/blender-project-harness/tools/harness/intake.test.mjs +181 -0
- package/.kilo/blender-project-harness/tools/harness/package-lock.json +1227 -0
- package/.kilo/blender-project-harness/tools/harness/package.json +15 -0
- package/.kilo/blender-project-harness/tools/harness/plugin.mjs +550 -0
- package/.kilo/blender-project-harness/tools/harness/plugin.test.mjs +526 -0
- package/.kilo/blender-project-harness/tools/harness/proxy.mjs +222 -0
- package/.kilo/blender-project-harness/tools/harness/proxy.test.mjs +217 -0
- package/.kilo/blender-project-harness/tools/harness/scaffold.mjs +83 -0
- package/.kilo/blender-project-harness/tools/harness/scaffold.test.mjs +74 -0
- package/.kilo/blender-project-harness/tools/harness/server.mjs +24 -0
- package/.kilo/blender-project-harness/tools/harness/shell-policy.mjs +196 -0
- package/.kilo/blender-project-harness/tools/harness/shell-policy.test.mjs +68 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/ASSETS.md +29 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/DECISIONS.md +8 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/Goals/01-project-brief.md +71 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/Goals/README.md +17 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/INTAKE.md +8 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/README.md +57 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/REQUIREMENTS.md +64 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/REVIEW.md +29 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/References/README.md +18 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/Reviews/README.md +48 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/TASKSTRACKER.md +10 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/Tasks/README.md +4 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/Documents/WORKFLOW.md +36 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/README.md +130 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/README.md +31 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/audio/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/external/README.md +43 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/external/SOURCE_TEMPLATE.md +18 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/hdri/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/models/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/assets/textures/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/backups/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/backups/README.md +5 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/exports/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/exports/README.md +11 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/renders/README.md +7 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/renders/final/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/renders/wip/.gitkeep +0 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/scenes/README.md +10 -0
- package/.kilo/blender-project-harness/tools/harness/templates/project/scripts/README.md +16 -0
- package/.kilo/blender-project-harness/tools/harness/test-fixture.mjs +91 -0
- package/.kilo/blender-project-harness/tools/harness/workspace.mjs +30 -0
- package/.kilo/blender-project-harness/tools/harness/workspace.test.mjs +31 -0
- package/.kilo/blender-project-harness/tools/test_blender_tools.py +427 -0
- package/.kilo/plugins/blender-workflow.js +3 -0
- package/.kilo/skills/blender-assets/SKILL.md +99 -0
- package/.kilo/skills/blender-mcp/SKILL.md +147 -0
- package/.kilo/skills/blender-mcp/references/README.md +22 -0
- package/.kilo/skills/blender-mcp/references/animation.md +83 -0
- package/.kilo/skills/blender-mcp/references/bpy.md +97 -0
- package/.kilo/skills/blender-mcp/references/level-design.md +53 -0
- package/.kilo/skills/blender-mcp/references/materials.md +80 -0
- package/.kilo/skills/blender-mcp/references/retopology.md +104 -0
- package/.kilo/skills/blender-mcp/references/rigging.md +123 -0
- package/.kilo/skills/blender-mcp/references/scene.md +72 -0
- package/.kilo/skills/blender-project-goal/SKILL.md +115 -0
- package/.kilo/skills/blender-project-grading/SKILL.md +184 -0
- package/.kilo/skills/blender-project-readme/SKILL.md +46 -0
- package/.kilo/skills/blender-project-requirements/SKILL.md +84 -0
- package/.kilo/skills/blender-project-scaffold/SKILL.md +59 -0
- package/.kilo/skills/blender-project-workflow/SKILL.md +96 -0
- package/.kilo/skills/blender-subagents/SKILL.md +204 -0
- package/.kilo/skills/blender-workflow-harness/SKILL.md +187 -0
- package/AGENTS.md +234 -0
- package/Blender/README.md +30 -0
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +368 -2
- package/THIRD_PARTY_NOTICES.md +51 -0
- package/bin/blender-project-harness.mjs +4 -0
- package/installer/cli.mjs +116 -0
- package/installer/install.mjs +325 -0
- package/installer/install.test.mjs +134 -0
- package/package.json +58 -6
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { applyEdits, modify, parse, printParseErrorCode } from 'jsonc-parser';
|
|
6
|
+
|
|
7
|
+
export const PACKAGE_NAME = '@punal_manalan/blender-project-harness';
|
|
8
|
+
export const INSTALL_RELATIVE = '.kilo/blender-project-harness';
|
|
9
|
+
export const BACKUP_RELATIVE = '.temp/blender-project-harness-backups';
|
|
10
|
+
export const SERVER_RELATIVE = `${INSTALL_RELATIVE}/tools/harness/server.mjs`;
|
|
11
|
+
|
|
12
|
+
const FORMAT = { formattingOptions: { insertSpaces: true, tabSize: 2 } };
|
|
13
|
+
const GITIGNORE_HEADER = '# Blender project harness';
|
|
14
|
+
const GITIGNORE_LINES = ['/.kilo/blender-project-harness/config.json', '.temp/'];
|
|
15
|
+
|
|
16
|
+
function exists(file) {
|
|
17
|
+
try { fs.lstatSync(file); return true; } catch { return false; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readJson(file) {
|
|
21
|
+
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeJson(file, value) {
|
|
25
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function copyFile(source, destination) {
|
|
29
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
30
|
+
fs.copyFileSync(source, destination);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function copyTree(source, destination) {
|
|
34
|
+
const base = path.resolve(source);
|
|
35
|
+
const skip = new Set(['node_modules', '__pycache__', '.pytest_cache']);
|
|
36
|
+
fs.cpSync(source, destination, {
|
|
37
|
+
recursive: true,
|
|
38
|
+
filter: (candidate) => {
|
|
39
|
+
const relative = path.relative(base, path.resolve(candidate));
|
|
40
|
+
return !relative.split(path.sep).some((part) => skip.has(part));
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function movePath(source, destination) {
|
|
46
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
47
|
+
try {
|
|
48
|
+
fs.renameSync(source, destination);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.code !== 'EXDEV') throw error;
|
|
51
|
+
copyTree(source, destination);
|
|
52
|
+
fs.rmSync(source, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function stamp() {
|
|
57
|
+
return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function uniqueBackup(root, version) {
|
|
61
|
+
const parent = path.join(root, BACKUP_RELATIVE);
|
|
62
|
+
const base = `${version || 'unknown'}-${stamp()}`;
|
|
63
|
+
let candidate = path.join(parent, base);
|
|
64
|
+
let index = 2;
|
|
65
|
+
while (exists(candidate)) candidate = path.join(parent, `${base}-${index++}`);
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function installedVersion(installDir) {
|
|
70
|
+
const versionFile = path.join(installDir, 'VERSION');
|
|
71
|
+
if (exists(versionFile)) return fs.readFileSync(versionFile, 'utf8').trim() || null;
|
|
72
|
+
const stateFile = path.join(installDir, 'install.json');
|
|
73
|
+
if (!exists(stateFile)) return null;
|
|
74
|
+
try { return readJson(stateFile).version ?? null; } catch { return null; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readState(installDir) {
|
|
78
|
+
const file = path.join(installDir, 'install.json');
|
|
79
|
+
if (!exists(file)) return null;
|
|
80
|
+
try { return readJson(file); } catch { return null; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function chooseConfig({ installDir, packageRoot, resetConfig }) {
|
|
84
|
+
if (!resetConfig) {
|
|
85
|
+
const current = path.join(installDir, 'config.json');
|
|
86
|
+
if (exists(current)) {
|
|
87
|
+
try {
|
|
88
|
+
JSON.parse(fs.readFileSync(current, 'utf8').replace(/^\uFEFF/, ''));
|
|
89
|
+
return current;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new Error(`invalid config ${current}: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const example = path.join(packageRoot, '.kilo', 'blender-project-harness', 'config.example.json');
|
|
96
|
+
if (!exists(example)) throw new Error(`package config example missing: ${example}`);
|
|
97
|
+
return example;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function runNpmCi(harnessDir) {
|
|
101
|
+
const args = ['ci', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'];
|
|
102
|
+
const result = spawnSync('npm', args, {
|
|
103
|
+
cwd: harnessDir,
|
|
104
|
+
env: process.env,
|
|
105
|
+
shell: process.platform === 'win32',
|
|
106
|
+
stdio: 'inherit',
|
|
107
|
+
windowsHide: false,
|
|
108
|
+
});
|
|
109
|
+
if (result.error) throw new Error(`npm ci failed to start: ${result.error.message}`);
|
|
110
|
+
if (result.status !== 0) throw new Error(`npm ci failed in ${harnessDir} with exit ${result.status}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function edit(text, pointer, value) {
|
|
114
|
+
const edits = modify(text, pointer, value, FORMAT);
|
|
115
|
+
return edits.length ? applyEdits(text, edits) : text;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function prepareKiloConfig(root) {
|
|
119
|
+
const file = path.join(root, '.kilo', 'kilo.jsonc');
|
|
120
|
+
let text = exists(file) ? fs.readFileSync(file, 'utf8') : '{\n "$schema": "https://app.kilo.ai/config.json",\n "mcp": {}\n}\n';
|
|
121
|
+
const errors = [];
|
|
122
|
+
parse(text, errors);
|
|
123
|
+
if (errors.length) {
|
|
124
|
+
throw new Error(`invalid .kilo/kilo.jsonc: ${printParseErrorCode(errors[0].error)} at offset ${errors[0].offset}`);
|
|
125
|
+
}
|
|
126
|
+
text = edit(text, ['mcp', 'blender', 'type'], 'local');
|
|
127
|
+
text = edit(text, ['mcp', 'blender', 'command'], ['node', SERVER_RELATIVE]);
|
|
128
|
+
text = edit(text, ['mcp', 'blender', 'enabled'], true);
|
|
129
|
+
const parsed = parse(text, []);
|
|
130
|
+
if (!parsed?.mcp?.blender?.environment?.BLENDER_PORT) {
|
|
131
|
+
text = edit(text, ['mcp', 'blender', 'environment', 'BLENDER_PORT'], '9877');
|
|
132
|
+
}
|
|
133
|
+
return text.endsWith('\n') ? text : `${text}\n`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function preparePlugin() {
|
|
137
|
+
return [
|
|
138
|
+
"import { server } from '../blender-project-harness/tools/harness/plugin.mjs';",
|
|
139
|
+
'',
|
|
140
|
+
"export default { id: 'blender-workflow-harness', server };",
|
|
141
|
+
'',
|
|
142
|
+
].join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function prepareGitignore(root) {
|
|
146
|
+
const file = path.join(root, '.gitignore');
|
|
147
|
+
const text = exists(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
148
|
+
if (text.includes(GITIGNORE_HEADER)) return text;
|
|
149
|
+
const lines = [GITIGNORE_HEADER, ...GITIGNORE_LINES];
|
|
150
|
+
const separator = text && !text.endsWith('\n') ? '\n\n' : text ? '\n' : '';
|
|
151
|
+
return `${text}${separator}${lines.join('\n')}\n`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function copyMissingFile(source, destination) {
|
|
155
|
+
if (exists(destination)) return false;
|
|
156
|
+
copyFile(source, destination);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function copyMissingTree(source, destination) {
|
|
161
|
+
if (!exists(source)) return [];
|
|
162
|
+
const copied = [];
|
|
163
|
+
const entries = fs.readdirSync(source, { withFileTypes: true });
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
const from = path.join(source, entry.name);
|
|
166
|
+
const to = path.join(destination, entry.name);
|
|
167
|
+
if (entry.isDirectory()) {
|
|
168
|
+
if (!exists(to)) {
|
|
169
|
+
copyTree(from, to);
|
|
170
|
+
copied.push(path.relative(source, from));
|
|
171
|
+
} else {
|
|
172
|
+
copied.push(...copyMissingTree(from, to).map((name) => path.join(entry.name, name)));
|
|
173
|
+
}
|
|
174
|
+
} else if (entry.isFile() && !exists(to)) {
|
|
175
|
+
copyFile(from, to);
|
|
176
|
+
copied.push(entry.name);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return copied;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function backupWorkspaceFile(root, relative, version) {
|
|
183
|
+
const source = path.join(root, relative);
|
|
184
|
+
if (!exists(source)) return null;
|
|
185
|
+
const backup = path.join(root, BACKUP_RELATIVE, `${version || 'unknown'}-${stamp()}-workspace`, relative);
|
|
186
|
+
copyFile(source, backup);
|
|
187
|
+
return path.relative(root, backup).replaceAll('\\', '/');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function installSurfaces({ root, packageRoot, version, backupRoot }) {
|
|
191
|
+
const warnings = [];
|
|
192
|
+
const actions = [];
|
|
193
|
+
const written = [];
|
|
194
|
+
if (copyMissingFile(path.join(packageRoot, 'AGENTS.md'), path.join(root, 'AGENTS.md'))) actions.push('AGENTS.md');
|
|
195
|
+
if (copyMissingFile(path.join(packageRoot, 'README.md'), path.join(root, 'README.md'))) actions.push('README.md');
|
|
196
|
+
const skills = copyMissingTree(path.join(packageRoot, '.kilo', 'skills'), path.join(root, '.kilo', 'skills'));
|
|
197
|
+
if (skills.length) actions.push(`${skills.length} skill file(s)`);
|
|
198
|
+
const agents = copyMissingTree(path.join(packageRoot, '.kilo', 'agent'), path.join(root, '.kilo', 'agent'));
|
|
199
|
+
if (agents.length) actions.push(`${agents.length} agent file(s)`);
|
|
200
|
+
if (copyMissingFile(path.join(packageRoot, 'Blender', 'README.md'), path.join(root, 'Blender', 'README.md'))) actions.push('Blender/README.md');
|
|
201
|
+
|
|
202
|
+
const plugin = path.join(root, '.kilo', 'plugins', 'blender-workflow.js');
|
|
203
|
+
const pluginText = preparePlugin();
|
|
204
|
+
if (exists(plugin) && fs.readFileSync(plugin, 'utf8') !== pluginText) {
|
|
205
|
+
const backup = backupWorkspaceFile(root, '.kilo/plugins/blender-workflow.js', version);
|
|
206
|
+
if (backup) warnings.push(`previous plugin backed up to ${backup}`);
|
|
207
|
+
}
|
|
208
|
+
copyFile(path.join(packageRoot, '.kilo', 'plugins', 'blender-workflow.js'), plugin);
|
|
209
|
+
fs.writeFileSync(plugin, pluginText);
|
|
210
|
+
written.push('.kilo/plugins/blender-workflow.js');
|
|
211
|
+
|
|
212
|
+
const kiloFile = path.join(root, '.kilo', 'kilo.jsonc');
|
|
213
|
+
const kiloText = prepareKiloConfig(root);
|
|
214
|
+
const oldKilo = exists(kiloFile) ? fs.readFileSync(kiloFile, 'utf8') : null;
|
|
215
|
+
if (oldKilo !== kiloText) {
|
|
216
|
+
fs.mkdirSync(path.dirname(kiloFile), { recursive: true });
|
|
217
|
+
fs.writeFileSync(kiloFile, kiloText);
|
|
218
|
+
written.push('.kilo/kilo.jsonc');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const gitignoreFile = path.join(root, '.gitignore');
|
|
222
|
+
const gitignoreText = prepareGitignore(root);
|
|
223
|
+
if (!exists(gitignoreFile) || fs.readFileSync(gitignoreFile, 'utf8') !== gitignoreText) {
|
|
224
|
+
fs.writeFileSync(gitignoreFile, gitignoreText);
|
|
225
|
+
written.push('.gitignore');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (backupRoot && exists(backupRoot)) warnings.push(`previous install backed up to ${path.relative(root, backupRoot).replaceAll('\\', '/')}`);
|
|
229
|
+
return { actions, warnings, written };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function install({ root, packageRoot, version, skipDeps = false, resetConfig = false, force = false, dryRun = false }) {
|
|
233
|
+
root = path.resolve(root);
|
|
234
|
+
packageRoot = path.resolve(packageRoot);
|
|
235
|
+
if (!version) throw new Error('package version is missing');
|
|
236
|
+
const installDir = path.join(root, INSTALL_RELATIVE);
|
|
237
|
+
const previousVersion = exists(installDir) ? installedVersion(installDir) : null;
|
|
238
|
+
const current = previousVersion === version;
|
|
239
|
+
if (current && !force) {
|
|
240
|
+
return { ok: true, status: 'current', root, installDir, version, previousVersion, backup: null };
|
|
241
|
+
}
|
|
242
|
+
const backup = exists(installDir) ? uniqueBackup(root, previousVersion) : null;
|
|
243
|
+
const configSource = chooseConfig({ installDir, packageRoot, resetConfig });
|
|
244
|
+
const dependencies = skipDeps ? 'skipped' : 'install';
|
|
245
|
+
if (dryRun) {
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
dryRun: true,
|
|
249
|
+
status: current ? 'current' : previousVersion ? 'update' : 'install',
|
|
250
|
+
root,
|
|
251
|
+
installDir,
|
|
252
|
+
version,
|
|
253
|
+
previousVersion,
|
|
254
|
+
backup: backup ? path.relative(root, backup).replaceAll('\\', '/') : null,
|
|
255
|
+
config: path.relative(root, configSource).replaceAll('\\', '/'),
|
|
256
|
+
dependencies,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const stage = path.join(root, '.kilo', `.blender-project-harness-${crypto.randomUUID().replaceAll('-', '')}`);
|
|
261
|
+
let moved = false;
|
|
262
|
+
const dependencyState = skipDeps ? 'skipped' : 'installed';
|
|
263
|
+
try {
|
|
264
|
+
fs.mkdirSync(stage, { recursive: true });
|
|
265
|
+
const source = path.join(packageRoot, '.kilo', 'blender-project-harness');
|
|
266
|
+
copyTree(path.join(source, 'tools'), path.join(stage, 'tools'));
|
|
267
|
+
copyFile(path.join(source, 'config.example.json'), path.join(stage, 'config.example.json'));
|
|
268
|
+
copyFile(configSource, path.join(stage, 'config.json'));
|
|
269
|
+
copyFile(path.join(packageRoot, 'AGENTS.md'), path.join(stage, 'AGENTS.md'));
|
|
270
|
+
fs.writeFileSync(path.join(stage, 'VERSION'), `${version}\n`);
|
|
271
|
+
if (!skipDeps) runNpmCi(path.join(stage, 'tools', 'harness'));
|
|
272
|
+
writeJson(path.join(stage, 'install.json'), {
|
|
273
|
+
schemaVersion: 1,
|
|
274
|
+
package: PACKAGE_NAME,
|
|
275
|
+
version,
|
|
276
|
+
installedAt: new Date().toISOString(),
|
|
277
|
+
dependencies: dependencyState,
|
|
278
|
+
node: process.version,
|
|
279
|
+
server: SERVER_RELATIVE,
|
|
280
|
+
agents: 'AGENTS.md',
|
|
281
|
+
backup: backup ? path.relative(root, backup).replaceAll('\\', '/') : null,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (backup) {
|
|
285
|
+
movePath(installDir, backup);
|
|
286
|
+
moved = true;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
movePath(stage, installDir);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (moved) movePath(backup, installDir);
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const surfaces = installSurfaces({ root, packageRoot, version, backupRoot: backup });
|
|
300
|
+
return {
|
|
301
|
+
ok: true,
|
|
302
|
+
status: previousVersion ? 'updated' : 'installed',
|
|
303
|
+
root,
|
|
304
|
+
installDir,
|
|
305
|
+
version,
|
|
306
|
+
previousVersion,
|
|
307
|
+
backup: backup ? path.relative(root, backup).replaceAll('\\', '/') : null,
|
|
308
|
+
dependencies: dependencyState,
|
|
309
|
+
...surfaces,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function status({ root }) {
|
|
314
|
+
root = path.resolve(root);
|
|
315
|
+
const installDir = path.join(root, INSTALL_RELATIVE);
|
|
316
|
+
const state = readState(installDir);
|
|
317
|
+
return {
|
|
318
|
+
installed: Boolean(state),
|
|
319
|
+
installDir,
|
|
320
|
+
version: state?.version ?? installedVersion(installDir),
|
|
321
|
+
installedAt: state?.installedAt ?? null,
|
|
322
|
+
backup: state?.backup ?? null,
|
|
323
|
+
server: state?.server ?? SERVER_RELATIVE,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import { install, status } from './install.mjs';
|
|
9
|
+
|
|
10
|
+
function makePackage(t) {
|
|
11
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'blender-harness-package-'));
|
|
12
|
+
t.after(() => fs.rmSync(temp, { recursive: true, force: true }));
|
|
13
|
+
const packageRoot = path.join(temp, 'package');
|
|
14
|
+
const write = (name, text) => {
|
|
15
|
+
const file = path.join(packageRoot, name);
|
|
16
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
17
|
+
fs.writeFileSync(file, text);
|
|
18
|
+
return file;
|
|
19
|
+
};
|
|
20
|
+
write('.kilo/blender-project-harness/tools/harness/server.mjs', '// server\n');
|
|
21
|
+
write('.kilo/blender-project-harness/tools/harness/package.json', '{ "type": "module" }\n');
|
|
22
|
+
write('.kilo/blender-project-harness/tools/harness/package-lock.json', '{}\n');
|
|
23
|
+
write('.kilo/blender-project-harness/config.example.json', JSON.stringify({ blender_subagents: { graders: { enabled: true } } }) + '\n');
|
|
24
|
+
write('AGENTS.md', '# Agent contract\n');
|
|
25
|
+
write('README.md', '# Package readme\n');
|
|
26
|
+
write('.kilo/skills/test-skill/SKILL.md', '# Skill\n');
|
|
27
|
+
write('.kilo/agent/blender-worker.md', '---\nsteps: 10\n---\n');
|
|
28
|
+
write('.kilo/plugins/blender-workflow.js', "import { server } from '../../.kilo/blender-project-harness/tools/harness/plugin.mjs';\n");
|
|
29
|
+
write('Blender/README.md', '# Projects\n');
|
|
30
|
+
return { temp, packageRoot };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeWorkspace(t) {
|
|
34
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'blender-harness-workspace-'));
|
|
35
|
+
t.after(() => fs.rmSync(temp, { recursive: true, force: true }));
|
|
36
|
+
const root = path.join(temp, 'workspace');
|
|
37
|
+
fs.mkdirSync(path.join(root, '.kilo'), { recursive: true });
|
|
38
|
+
fs.writeFileSync(path.join(root, '.kilo', 'kilo.jsonc'), [
|
|
39
|
+
'{',
|
|
40
|
+
' // keep this comment',
|
|
41
|
+
' "mcp": {',
|
|
42
|
+
' "blender": {',
|
|
43
|
+
' "type": "local",',
|
|
44
|
+
' "command": ["node", "tools/harness/server.mjs"],',
|
|
45
|
+
' "enabled": true,',
|
|
46
|
+
' "environment": { "BLENDER_PORT": "9877" }',
|
|
47
|
+
' }',
|
|
48
|
+
' }',
|
|
49
|
+
'}',
|
|
50
|
+
'',
|
|
51
|
+
].join('\n'));
|
|
52
|
+
return { temp, root };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test('installs the harness, config, and workspace surfaces', (t) => {
|
|
56
|
+
const { packageRoot } = makePackage(t);
|
|
57
|
+
const { root } = makeWorkspace(t);
|
|
58
|
+
const result = install({ root, packageRoot, version: '1.0.0', skipDeps: true });
|
|
59
|
+
assert.equal(result.status, 'installed');
|
|
60
|
+
assert.equal(result.dependencies, 'skipped');
|
|
61
|
+
assert.equal(fs.readFileSync(path.join(root, '.kilo', 'blender-project-harness', 'VERSION'), 'utf8').trim(), '1.0.0');
|
|
62
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(root, '.kilo', 'blender-project-harness', 'config.json'), 'utf8')).blender_subagents.graders.enabled, true);
|
|
63
|
+
assert.equal(fs.existsSync(path.join(root, '.kilo', 'blender-project-harness', 'tools', 'harness', 'server.mjs')), true);
|
|
64
|
+
assert.equal(fs.readFileSync(path.join(root, '.kilo', 'blender-project-harness', 'AGENTS.md'), 'utf8'), '# Agent contract\n');
|
|
65
|
+
assert.match(fs.readFileSync(path.join(root, '.kilo', 'kilo.jsonc'), 'utf8'), /keep this comment/);
|
|
66
|
+
assert.match(fs.readFileSync(path.join(root, '.kilo', 'kilo.jsonc'), 'utf8'), /\.kilo\/blender-project-harness\/tools\/harness\/server\.mjs/);
|
|
67
|
+
assert.match(fs.readFileSync(path.join(root, '.kilo', 'plugins', 'blender-workflow.js'), 'utf8'), /\.\.\/blender-project-harness\/tools\/harness\/plugin\.mjs/);
|
|
68
|
+
assert.equal(fs.existsSync(path.join(root, 'AGENTS.md')), true);
|
|
69
|
+
assert.equal(fs.existsSync(path.join(root, '.kilo', 'skills', 'test-skill', 'SKILL.md')), true);
|
|
70
|
+
assert.match(fs.readFileSync(path.join(root, '.gitignore'), 'utf8'), /\.kilo\/blender-project-harness\/config\.json/);
|
|
71
|
+
assert.equal(status({ root }).installed, true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('updates to a new version, backs up the old tree, and preserves config', (t) => {
|
|
75
|
+
const { packageRoot } = makePackage(t);
|
|
76
|
+
const { root } = makeWorkspace(t);
|
|
77
|
+
const config = path.join(root, '.kilo', 'blender-project-harness', 'config.json');
|
|
78
|
+
fs.mkdirSync(path.dirname(config), { recursive: true });
|
|
79
|
+
fs.writeFileSync(config, JSON.stringify({ custom: true }) + '\n');
|
|
80
|
+
install({ root, packageRoot, version: '1.0.0', skipDeps: true });
|
|
81
|
+
fs.writeFileSync(path.join(packageRoot, 'AGENTS.md'), '# Agent contract v2\n');
|
|
82
|
+
const updated = install({ root, packageRoot, version: '2.0.0', skipDeps: true });
|
|
83
|
+
assert.equal(updated.status, 'updated');
|
|
84
|
+
assert.ok(updated.backup);
|
|
85
|
+
assert.equal(fs.readFileSync(path.join(root, '.kilo', 'blender-project-harness', 'VERSION'), 'utf8').trim(), '2.0.0');
|
|
86
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(root, '.kilo', 'blender-project-harness', 'config.json'), 'utf8')).custom, true);
|
|
87
|
+
assert.equal(fs.readFileSync(path.join(root, '.kilo', 'blender-project-harness', 'AGENTS.md'), 'utf8'), '# Agent contract v2\n');
|
|
88
|
+
assert.equal(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8'), '# Agent contract\n');
|
|
89
|
+
assert.equal(fs.readFileSync(path.join(root, updated.backup, 'VERSION'), 'utf8').trim(), '1.0.0');
|
|
90
|
+
assert.equal(fs.readFileSync(path.join(root, updated.backup, 'AGENTS.md'), 'utf8'), '# Agent contract\n');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('same version is idempotent unless forced', (t) => {
|
|
94
|
+
const { packageRoot } = makePackage(t);
|
|
95
|
+
const { root } = makeWorkspace(t);
|
|
96
|
+
install({ root, packageRoot, version: '1.0.0', skipDeps: true });
|
|
97
|
+
const current = install({ root, packageRoot, version: '1.0.0', skipDeps: true });
|
|
98
|
+
assert.equal(current.status, 'current');
|
|
99
|
+
const forced = install({ root, packageRoot, version: '1.0.0', skipDeps: true, force: true });
|
|
100
|
+
assert.equal(forced.status, 'updated');
|
|
101
|
+
assert.ok(forced.backup);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('dry-run reports the plan without writing', (t) => {
|
|
105
|
+
const { packageRoot } = makePackage(t);
|
|
106
|
+
const { root } = makeWorkspace(t);
|
|
107
|
+
const result = install({ root, packageRoot, version: '1.0.0', skipDeps: true, dryRun: true });
|
|
108
|
+
assert.equal(result.dryRun, true);
|
|
109
|
+
assert.equal(result.status, 'install');
|
|
110
|
+
assert.equal(fs.existsSync(path.join(root, '.kilo', 'blender-project-harness')), false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('reset-config uses the package example', (t) => {
|
|
114
|
+
const { packageRoot } = makePackage(t);
|
|
115
|
+
const { root } = makeWorkspace(t);
|
|
116
|
+
install({ root, packageRoot, version: '1.0.0', skipDeps: true });
|
|
117
|
+
install({ root, packageRoot, version: '2.0.0', skipDeps: true, resetConfig: true });
|
|
118
|
+
const config = JSON.parse(fs.readFileSync(path.join(root, '.kilo', 'blender-project-harness', 'config.json'), 'utf8'));
|
|
119
|
+
assert.equal(config.custom, undefined);
|
|
120
|
+
assert.equal(config.blender_subagents.graders.enabled, true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('CLI installs, reports status, and prints JSON', (t) => {
|
|
124
|
+
const { root } = makeWorkspace(t);
|
|
125
|
+
const bin = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'blender-project-harness.mjs');
|
|
126
|
+
const installResult = spawnSync(process.execPath, [bin, 'install', '--root', root, '--skip-deps', '--json'], { encoding: 'utf8' });
|
|
127
|
+
assert.equal(installResult.status, 0, installResult.stderr);
|
|
128
|
+
const installed = JSON.parse(installResult.stdout);
|
|
129
|
+
assert.equal(installed.status, 'installed');
|
|
130
|
+
assert.equal(fs.existsSync(path.join(root, '.kilo', 'blender-project-harness', 'VERSION')), true);
|
|
131
|
+
const statusResult = spawnSync(process.execPath, [bin, 'status', '--root', root, '--json'], { encoding: 'utf8' });
|
|
132
|
+
assert.equal(statusResult.status, 0, statusResult.stderr);
|
|
133
|
+
assert.equal(JSON.parse(statusResult.stdout).installed, true);
|
|
134
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@punal_manalan/blender-project-harness",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@punal_manalan/blender-project-harness",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Install and update the guarded Blender project harness for Kilo workspaces.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/punal100/blender-project-harness.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/punal100/blender-project-harness#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/punal100/blender-project-harness/issues"
|
|
12
|
+
},
|
|
13
|
+
"author": "Punal Manalan",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"blender-project-harness": "bin/blender-project-harness.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"AGENTS.md",
|
|
20
|
+
"Blender/README.md",
|
|
21
|
+
"CHANGELOG.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"README.md",
|
|
24
|
+
"THIRD_PARTY_NOTICES.md",
|
|
25
|
+
"bin/",
|
|
26
|
+
"installer/",
|
|
27
|
+
".kilo/blender-project-harness/config.example.json",
|
|
28
|
+
".kilo/blender-project-harness/tools/",
|
|
29
|
+
"!.kilo/blender-project-harness/tools/**/node_modules/",
|
|
30
|
+
"!.kilo/blender-project-harness/tools/**/__pycache__/",
|
|
31
|
+
"!.kilo/blender-project-harness/tools/**/.pytest_cache/",
|
|
32
|
+
".kilo/agent/",
|
|
33
|
+
".kilo/plugins/",
|
|
34
|
+
".kilo/skills/"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "node --test installer/*.test.mjs .kilo/blender-project-harness/tools/harness/*.test.mjs",
|
|
41
|
+
"test:installer": "node --test installer/*.test.mjs",
|
|
42
|
+
"pack:check": "npm pack --dry-run"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"jsonc-parser": "3.3.1"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"blender",
|
|
49
|
+
"kilo",
|
|
50
|
+
"mcp",
|
|
51
|
+
"workflow",
|
|
52
|
+
"harness"
|
|
53
|
+
],
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|