ndomo 0.2.0 → 0.2.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/config/ndomo.config.json +60 -0
- package/package.json +1 -1
- package/src/cli/__tests__/install.test.ts +55 -0
- package/src/cli/install.ts +101 -181
package/config/ndomo.config.json
CHANGED
|
@@ -134,6 +134,36 @@
|
|
|
134
134
|
"model": "opencode-go/kimi-k2.7-code",
|
|
135
135
|
"temperature": 0.2,
|
|
136
136
|
"reasoning_effort": "high"
|
|
137
|
+
},
|
|
138
|
+
"craftsman": {
|
|
139
|
+
"model": "minimax/MiniMax-M3",
|
|
140
|
+
"temperature": 0.3,
|
|
141
|
+
"reasoning_effort": "high"
|
|
142
|
+
},
|
|
143
|
+
"warden": {
|
|
144
|
+
"model": "minimax/MiniMax-M3",
|
|
145
|
+
"temperature": 0.3,
|
|
146
|
+
"reasoning_effort": "high"
|
|
147
|
+
},
|
|
148
|
+
"ci-smith": {
|
|
149
|
+
"model": "opencode-go/deepseek-v4-flash",
|
|
150
|
+
"temperature": 0.1,
|
|
151
|
+
"reasoning_effort": "high"
|
|
152
|
+
},
|
|
153
|
+
"deploy-smith": {
|
|
154
|
+
"model": "opencode-go/deepseek-v4-flash",
|
|
155
|
+
"temperature": 0.1,
|
|
156
|
+
"reasoning_effort": "high"
|
|
157
|
+
},
|
|
158
|
+
"release-smith": {
|
|
159
|
+
"model": "opencode-go/deepseek-v4-flash",
|
|
160
|
+
"temperature": 0.1,
|
|
161
|
+
"reasoning_effort": "high"
|
|
162
|
+
},
|
|
163
|
+
"ops-scout": {
|
|
164
|
+
"model": "opencode-go/deepseek-v4-flash",
|
|
165
|
+
"temperature": 0.2,
|
|
166
|
+
"reasoning_effort": "medium"
|
|
137
167
|
}
|
|
138
168
|
},
|
|
139
169
|
"budget": {
|
|
@@ -216,6 +246,36 @@
|
|
|
216
246
|
"model": "opencode-go/deepseek-v4-flash",
|
|
217
247
|
"temperature": 0.2,
|
|
218
248
|
"reasoning_effort": "low"
|
|
249
|
+
},
|
|
250
|
+
"craftsman": {
|
|
251
|
+
"model": "opencode-go/deepseek-v4-flash",
|
|
252
|
+
"temperature": 0.3,
|
|
253
|
+
"reasoning_effort": "low"
|
|
254
|
+
},
|
|
255
|
+
"warden": {
|
|
256
|
+
"model": "opencode-go/deepseek-v4-flash",
|
|
257
|
+
"temperature": 0.3,
|
|
258
|
+
"reasoning_effort": "low"
|
|
259
|
+
},
|
|
260
|
+
"ci-smith": {
|
|
261
|
+
"model": "opencode-go/deepseek-v4-flash",
|
|
262
|
+
"temperature": 0.1,
|
|
263
|
+
"reasoning_effort": "low"
|
|
264
|
+
},
|
|
265
|
+
"deploy-smith": {
|
|
266
|
+
"model": "opencode-go/deepseek-v4-flash",
|
|
267
|
+
"temperature": 0.1,
|
|
268
|
+
"reasoning_effort": "low"
|
|
269
|
+
},
|
|
270
|
+
"release-smith": {
|
|
271
|
+
"model": "opencode-go/deepseek-v4-flash",
|
|
272
|
+
"temperature": 0.1,
|
|
273
|
+
"reasoning_effort": "low"
|
|
274
|
+
},
|
|
275
|
+
"ops-scout": {
|
|
276
|
+
"model": "opencode-go/deepseek-v4-flash",
|
|
277
|
+
"temperature": 0.2,
|
|
278
|
+
"reasoning_effort": "low"
|
|
219
279
|
}
|
|
220
280
|
}
|
|
221
281
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ndomo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "OpenCode multi-agent plugin. Taller de artesanos: foreman + 3 peer primaries (craftsman, warden, ranger) + specialists (scout, scribe, painter, smith, sage, guild, inspector, chronicler) + stack-smiths + ops (ci-smith, deploy-smith, release-smith, ops-scout). Caveman-native. opencode-mem integrated. DCP peer optional.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
writeHttpBlock,
|
|
26
26
|
applyPresetToFile,
|
|
27
27
|
applyProviderPrefix,
|
|
28
|
+
promptHttpEnable,
|
|
28
29
|
stepRegisterPlugins,
|
|
29
30
|
stepCopyAgents,
|
|
30
31
|
stepCopySkills,
|
|
@@ -731,3 +732,57 @@ describe("stepCopyTools", () => {
|
|
|
731
732
|
expect(copied).toBe(0);
|
|
732
733
|
});
|
|
733
734
|
});
|
|
735
|
+
|
|
736
|
+
// ─── Regex-based preset preserves nested YAML ──────────────────────────────
|
|
737
|
+
|
|
738
|
+
describe("applyPresetToFile — nested permission preservation", () => {
|
|
739
|
+
test("preserves nested permission structure byte-for-byte", () => {
|
|
740
|
+
const testFile = join(tmpDir, "test-agent.md");
|
|
741
|
+
const original = `---
|
|
742
|
+
description: Test Agent
|
|
743
|
+
mode: subagent
|
|
744
|
+
model: old/model
|
|
745
|
+
temperature: 0.5
|
|
746
|
+
permission:
|
|
747
|
+
edit: allow
|
|
748
|
+
write: ask
|
|
749
|
+
bash:
|
|
750
|
+
"*": ask
|
|
751
|
+
"ls *": allow
|
|
752
|
+
---
|
|
753
|
+
body content here
|
|
754
|
+
`;
|
|
755
|
+
writeFileSync(testFile, original);
|
|
756
|
+
const result = applyPresetToFile(
|
|
757
|
+
testFile,
|
|
758
|
+
{ model: "new/model", temperature: 0.3 } as PresetEntry,
|
|
759
|
+
false,
|
|
760
|
+
);
|
|
761
|
+
expect(result).toBe("updated");
|
|
762
|
+
const updated = readFileSync(testFile, "utf-8");
|
|
763
|
+
expect(updated).toContain("model: new/model");
|
|
764
|
+
expect(updated).toContain("temperature: 0.3");
|
|
765
|
+
// CRITICAL: permission block intact byte-for-byte
|
|
766
|
+
expect(updated).toContain("permission:");
|
|
767
|
+
expect(updated).toContain(" edit: allow");
|
|
768
|
+
expect(updated).toContain(" write: ask");
|
|
769
|
+
expect(updated).toContain(" bash:");
|
|
770
|
+
expect(updated).toContain(' "*": ask');
|
|
771
|
+
expect(updated).toContain(' "ls *": allow');
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// ─── Non-TTY promptHttpEnable ──────────────────────────────────────────────
|
|
776
|
+
|
|
777
|
+
describe("promptHttpEnable — non-TTY fallback", () => {
|
|
778
|
+
test("returns false immediately when stdin not TTY", async () => {
|
|
779
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
780
|
+
Object.defineProperty(process.stdin, "isTTY", { value: false, configurable: true });
|
|
781
|
+
try {
|
|
782
|
+
const result = await promptHttpEnable();
|
|
783
|
+
expect(result).toBe(false);
|
|
784
|
+
} finally {
|
|
785
|
+
Object.defineProperty(process.stdin, "isTTY", { value: originalIsTTY, configurable: true });
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
});
|
package/src/cli/install.ts
CHANGED
|
@@ -34,6 +34,30 @@ const die = (msg: string): never => {
|
|
|
34
34
|
process.exit(1);
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
/** Stream a child process, returning stdout/stderr. On non-zero exit, die() with truncated output. */
|
|
38
|
+
async function streamSpawn(
|
|
39
|
+
cmd: string[],
|
|
40
|
+
opts: { cwd?: string; label?: string; nothrow?: boolean } = {},
|
|
41
|
+
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
42
|
+
const spawnOpts: { cwd?: string; stdout: "pipe"; stderr: "pipe" } = {
|
|
43
|
+
stdout: "pipe",
|
|
44
|
+
stderr: "pipe",
|
|
45
|
+
};
|
|
46
|
+
if (opts.cwd !== undefined) spawnOpts.cwd = opts.cwd;
|
|
47
|
+
const proc = Bun.spawn(cmd, spawnOpts);
|
|
48
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
49
|
+
new Response(proc.stdout).text(),
|
|
50
|
+
new Response(proc.stderr).text(),
|
|
51
|
+
proc.exited,
|
|
52
|
+
]);
|
|
53
|
+
if (exitCode !== 0 && !opts.nothrow) {
|
|
54
|
+
const truncate = (s: string) => (s.length > 1024 ? s.slice(0, 1024) + "\n... [truncated]" : s);
|
|
55
|
+
const label = opts.label ?? cmd.join(" ");
|
|
56
|
+
die(`${label} failed (exit ${exitCode})\nstderr:\n${truncate(stderr)}\nstdout:\n${truncate(stdout)}`);
|
|
57
|
+
}
|
|
58
|
+
return { exitCode, stdout, stderr };
|
|
59
|
+
}
|
|
60
|
+
|
|
37
61
|
// ─── Path traversal protection ────────────────────────────────────────────────
|
|
38
62
|
const SAFE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
39
63
|
|
|
@@ -181,59 +205,23 @@ export async function stepInstallDeps(projectRoot: string, dryRun: boolean): Pro
|
|
|
181
205
|
info("[dry-run] would run: bun install --frozen-lockfile");
|
|
182
206
|
return;
|
|
183
207
|
}
|
|
184
|
-
const
|
|
208
|
+
const frozen = await streamSpawn(["bun", "install", "--frozen-lockfile"], {
|
|
185
209
|
cwd: projectRoot,
|
|
186
|
-
|
|
187
|
-
|
|
210
|
+
label: "bun install --frozen-lockfile",
|
|
211
|
+
nothrow: true,
|
|
188
212
|
});
|
|
189
|
-
|
|
190
|
-
if (proc.exitCode !== 0) {
|
|
213
|
+
if (frozen.exitCode !== 0) {
|
|
191
214
|
// Fallback to non-frozen
|
|
192
215
|
warn("frozen lockfile failed, retrying without --frozen-lockfile...");
|
|
193
|
-
|
|
216
|
+
await streamSpawn(["bun", "install"], {
|
|
194
217
|
cwd: projectRoot,
|
|
195
|
-
|
|
196
|
-
stderr: "pipe",
|
|
218
|
+
label: "bun install",
|
|
197
219
|
});
|
|
198
|
-
await proc2.exited;
|
|
199
|
-
if (proc2.exitCode !== 0) {
|
|
200
|
-
die("bun install failed");
|
|
201
|
-
}
|
|
202
220
|
}
|
|
203
221
|
ok("Dependencies installed");
|
|
204
222
|
}
|
|
205
223
|
|
|
206
|
-
/** Step 2:
|
|
207
|
-
export async function stepBuild(projectRoot: string, dryRun: boolean): Promise<void> {
|
|
208
|
-
info("Building TypeScript...");
|
|
209
|
-
if (dryRun) {
|
|
210
|
-
info("[dry-run] would run: bun run build (or tsc)");
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
// Check for build script in package.json
|
|
214
|
-
const pkgPath = join(projectRoot, "package.json");
|
|
215
|
-
let hasBuild = false;
|
|
216
|
-
try {
|
|
217
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
218
|
-
hasBuild = typeof pkg.scripts?.build === "string";
|
|
219
|
-
} catch {
|
|
220
|
-
// no package.json — try tsc
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const cmd = hasBuild ? ["bun", "run", "build"] : ["bun", "run", "--bun", "tsc"];
|
|
224
|
-
const proc = Bun.spawn(cmd, {
|
|
225
|
-
cwd: projectRoot,
|
|
226
|
-
stdout: "pipe",
|
|
227
|
-
stderr: "pipe",
|
|
228
|
-
});
|
|
229
|
-
await proc.exited;
|
|
230
|
-
if (proc.exitCode !== 0) {
|
|
231
|
-
die(`Build failed (exit ${proc.exitCode})`);
|
|
232
|
-
}
|
|
233
|
-
ok("Build complete");
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** Step 3+4: Copy agents with timestamped backup. */
|
|
224
|
+
/** Step 2: Copy agents with timestamped backup. */
|
|
237
225
|
export function stepCopyAgents(
|
|
238
226
|
projectRoot: string,
|
|
239
227
|
configDir: string,
|
|
@@ -289,7 +277,7 @@ export function stepCopyAgents(
|
|
|
289
277
|
return copied;
|
|
290
278
|
}
|
|
291
279
|
|
|
292
|
-
/** Step
|
|
280
|
+
/** Step 3: Copy skills with timestamped backup. */
|
|
293
281
|
export function stepCopySkills(
|
|
294
282
|
projectRoot: string,
|
|
295
283
|
configDir: string,
|
|
@@ -354,125 +342,60 @@ export function stepCopySkills(
|
|
|
354
342
|
// ─── Preset application ──────────────────────────────────────────────────────
|
|
355
343
|
|
|
356
344
|
/**
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*/
|
|
361
|
-
export function parseFrontmatter(content: string): {
|
|
362
|
-
frontmatter: Record<string, string>;
|
|
363
|
-
body: string;
|
|
364
|
-
startIdx: number;
|
|
365
|
-
endIdx: number;
|
|
366
|
-
} {
|
|
367
|
-
const lines = content.split("\n");
|
|
368
|
-
let startIdx = -1;
|
|
369
|
-
let endIdx = -1;
|
|
370
|
-
|
|
371
|
-
for (let i = 0; i < lines.length; i++) {
|
|
372
|
-
const line = lines[i];
|
|
373
|
-
if (line !== undefined && line.trim() === "---") {
|
|
374
|
-
if (startIdx === -1) {
|
|
375
|
-
startIdx = i;
|
|
376
|
-
} else {
|
|
377
|
-
endIdx = i;
|
|
378
|
-
break;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (startIdx === -1 || endIdx === -1) {
|
|
384
|
-
return { frontmatter: {}, body: content, startIdx: -1, endIdx: -1 };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const fm: Record<string, string> = {};
|
|
388
|
-
for (let i = startIdx + 1; i < endIdx; i++) {
|
|
389
|
-
const line = lines[i];
|
|
390
|
-
if (line === undefined) continue;
|
|
391
|
-
const colonIdx = line.indexOf(":");
|
|
392
|
-
if (colonIdx > 0) {
|
|
393
|
-
const key = line.slice(0, colonIdx).trim();
|
|
394
|
-
const value = line.slice(colonIdx + 1).trim();
|
|
395
|
-
fm[key] = value;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const body = lines.slice(endIdx + 1).join("\n");
|
|
400
|
-
return { frontmatter: fm, body, startIdx, endIdx };
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Serialize frontmatter + body back to a string.
|
|
405
|
-
*/
|
|
406
|
-
export function serializeFrontmatter(frontmatter: Record<string, string>, body: string): string {
|
|
407
|
-
const fmLines = Object.entries(frontmatter).map(([k, v]) => `${k}: ${v}`);
|
|
408
|
-
return `---\n${fmLines.join("\n")}\n---\n${body}`;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Apply preset values to an agent .md file's frontmatter.
|
|
413
|
-
* Handles reasoningEffort 3-tier insert fallback:
|
|
414
|
-
* 1. Update existing reasoningEffort line
|
|
415
|
-
* 2. Insert after temperature (if present)
|
|
416
|
-
* 3. Insert after model (if present)
|
|
417
|
-
* 4. Insert after opening ---
|
|
345
|
+
* Apply preset values to an agent .md file's frontmatter using regex-targeted
|
|
346
|
+
* line replacement. Preserves YAML structure 100% (including nested permission
|
|
347
|
+
* blocks) by only touching top-level (0-indent) key lines.
|
|
418
348
|
*/
|
|
419
349
|
export function applyPresetToFile(
|
|
420
350
|
filePath: string,
|
|
421
351
|
preset: PresetEntry,
|
|
422
352
|
dryRun: boolean,
|
|
423
353
|
): "updated" | "skipped" {
|
|
424
|
-
|
|
425
|
-
const { frontmatter, body, startIdx, endIdx } = parseFrontmatter(content);
|
|
354
|
+
let content = readFileSync(filePath, "utf-8");
|
|
426
355
|
|
|
427
|
-
|
|
356
|
+
// Find frontmatter bounds (first --- block at file start)
|
|
357
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
358
|
+
if (!fmMatch || !fmMatch[1] || !fmMatch[2]) {
|
|
428
359
|
warn(`No frontmatter found in ${basename(filePath)}, skipping`);
|
|
429
360
|
return "skipped";
|
|
430
361
|
}
|
|
431
|
-
|
|
362
|
+
const fmBody = fmMatch[1];
|
|
363
|
+
const body = fmMatch[2];
|
|
364
|
+
let newFmBody = fmBody;
|
|
432
365
|
let changed = false;
|
|
433
366
|
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
367
|
+
// Update or insert each preset field at top-level (0 indent)
|
|
368
|
+
const fields: Array<{ yamlName: string; value: string | undefined }> = [
|
|
369
|
+
{ yamlName: "model", value: preset.model },
|
|
370
|
+
{ yamlName: "temperature", value: preset.temperature !== undefined ? String(preset.temperature) : undefined },
|
|
371
|
+
{ yamlName: "reasoningEffort", value: preset.reasoning_effort },
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
for (const field of fields) {
|
|
375
|
+
if (field.value === undefined) continue;
|
|
376
|
+
// Match line at start-of-line (no indent), so we don't touch nested keys
|
|
377
|
+
const lineRegex = new RegExp(`^${field.yamlName}:.*$`, "m");
|
|
378
|
+
if (lineRegex.test(newFmBody)) {
|
|
379
|
+
newFmBody = newFmBody.replace(lineRegex, `${field.yamlName}: ${field.value}`);
|
|
380
|
+
changed = true;
|
|
381
|
+
} else {
|
|
382
|
+
// Append at end of frontmatter (top-level, 0 indent)
|
|
383
|
+
const sep = newFmBody.endsWith("\n") ? "" : "\n";
|
|
384
|
+
newFmBody = newFmBody + sep + `${field.yamlName}: ${field.value}\n`;
|
|
385
|
+
changed = true;
|
|
386
|
+
}
|
|
450
387
|
}
|
|
451
388
|
|
|
452
389
|
if (!changed) {
|
|
453
390
|
return "skipped";
|
|
454
391
|
}
|
|
455
392
|
|
|
456
|
-
|
|
457
|
-
const ordered: Record<string, string> = {};
|
|
458
|
-
const priority = ["model", "temperature", "reasoningEffort"];
|
|
459
|
-
for (const key of priority) {
|
|
460
|
-
if (frontmatter[key] !== undefined) {
|
|
461
|
-
ordered[key] = frontmatter[key];
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
for (const [key, value] of Object.entries(frontmatter)) {
|
|
465
|
-
if (!(key in ordered)) {
|
|
466
|
-
ordered[key] = value;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
393
|
+
const newContent = `---\n${newFmBody}---\n${body}`;
|
|
470
394
|
if (dryRun) {
|
|
471
|
-
info(`[dry-run] would update ${basename(filePath)}
|
|
395
|
+
info(`[dry-run] would update ${basename(filePath)}`);
|
|
472
396
|
} else {
|
|
473
|
-
writeFileSync(filePath,
|
|
397
|
+
writeFileSync(filePath, newContent);
|
|
474
398
|
}
|
|
475
|
-
|
|
476
399
|
return "updated";
|
|
477
400
|
}
|
|
478
401
|
|
|
@@ -522,7 +445,7 @@ export function applyProviderPrefix(
|
|
|
522
445
|
return updated;
|
|
523
446
|
}
|
|
524
447
|
|
|
525
|
-
/** Step
|
|
448
|
+
/** Step 3.5: Apply preset + optional provider prefix override. */
|
|
526
449
|
export function stepApplyPreset(
|
|
527
450
|
configDir: string,
|
|
528
451
|
configJson: NdomoConfig,
|
|
@@ -583,7 +506,7 @@ export function stepApplyPreset(
|
|
|
583
506
|
}
|
|
584
507
|
}
|
|
585
508
|
|
|
586
|
-
// ─── Step
|
|
509
|
+
// ─── Step 4: Copy config files ───────────────────────────────────────────────
|
|
587
510
|
export function stepCopyConfig(
|
|
588
511
|
projectRoot: string,
|
|
589
512
|
configDir: string,
|
|
@@ -632,7 +555,7 @@ export function stepCopyConfig(
|
|
|
632
555
|
}
|
|
633
556
|
}
|
|
634
557
|
|
|
635
|
-
// ─── Step
|
|
558
|
+
// ─── Step 4.5: Register plugins in opencode.json ─────────────────────────────
|
|
636
559
|
export function stepRegisterPlugins(
|
|
637
560
|
configDir: string,
|
|
638
561
|
configJson: NdomoConfig,
|
|
@@ -694,7 +617,7 @@ export function stepRegisterPlugins(
|
|
|
694
617
|
ok(`Registered ${allPlugins.length} ndomo plugin(s) in opencode.json`);
|
|
695
618
|
}
|
|
696
619
|
|
|
697
|
-
// ─── Step
|
|
620
|
+
// ─── Step 4.6: Install ndomo package (3 strategies) ──────────────────────────
|
|
698
621
|
|
|
699
622
|
function isSymlink(p: string): boolean {
|
|
700
623
|
try {
|
|
@@ -732,14 +655,13 @@ async function strategyFileDep(
|
|
|
732
655
|
writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
733
656
|
|
|
734
657
|
// Run bun install
|
|
735
|
-
const
|
|
658
|
+
const result = await streamSpawn(["bun", "install", "--no-frozen-lockfile"], {
|
|
736
659
|
cwd: configDir,
|
|
737
|
-
|
|
738
|
-
|
|
660
|
+
label: "bun install (file: dep)",
|
|
661
|
+
nothrow: true,
|
|
739
662
|
});
|
|
740
|
-
await proc.exited;
|
|
741
663
|
|
|
742
|
-
if (
|
|
664
|
+
if (result.exitCode === 0 && existsSync(nmNdomo) && !isSymlink(nmNdomo)) {
|
|
743
665
|
ok("ndomo installed via bun (file: dep) — real copy, no symlink");
|
|
744
666
|
return true;
|
|
745
667
|
}
|
|
@@ -759,28 +681,26 @@ async function strategyBunLink(projectRoot: string, configDir: string): Promise<
|
|
|
759
681
|
|
|
760
682
|
try {
|
|
761
683
|
// bun link in project root (registers package)
|
|
762
|
-
const
|
|
684
|
+
const linkResult = await streamSpawn(["bun", "link"], {
|
|
763
685
|
cwd: projectRoot,
|
|
764
|
-
|
|
765
|
-
|
|
686
|
+
label: "bun link",
|
|
687
|
+
nothrow: true,
|
|
766
688
|
});
|
|
767
|
-
await proc1.exited;
|
|
768
689
|
|
|
769
|
-
if (
|
|
690
|
+
if (linkResult.exitCode !== 0) {
|
|
770
691
|
warn("bun link in project root failed");
|
|
771
692
|
return false;
|
|
772
693
|
}
|
|
773
694
|
|
|
774
695
|
// bun link ndomo in config dir (links package)
|
|
775
|
-
const
|
|
696
|
+
const linkNdomo = await streamSpawn(["bun", "link", "ndomo"], {
|
|
776
697
|
cwd: configDir,
|
|
777
|
-
|
|
778
|
-
|
|
698
|
+
label: "bun link ndomo",
|
|
699
|
+
nothrow: true,
|
|
779
700
|
});
|
|
780
|
-
await proc2.exited;
|
|
781
701
|
|
|
782
702
|
const nmNdomo = join(configDir, "node_modules", "ndomo");
|
|
783
|
-
if (
|
|
703
|
+
if (linkNdomo.exitCode === 0 && existsSync(nmNdomo)) {
|
|
784
704
|
ok("ndomo linked via bun link (managed symlink)");
|
|
785
705
|
warn("bun link uses symlinks — run 'bun run dev:bust' if cache goes stale");
|
|
786
706
|
return true;
|
|
@@ -867,7 +787,7 @@ export async function stepInstallPackage(
|
|
|
867
787
|
}
|
|
868
788
|
}
|
|
869
789
|
|
|
870
|
-
// ─── Step
|
|
790
|
+
// ─── Step 4.7: Copy custom tools ─────────────────────────────────────────────
|
|
871
791
|
// npm distribution: tools live inside the installed ndomo package, so symlink
|
|
872
792
|
// dance (used in old repo-based install) is obsolete. Copy .ts files directly.
|
|
873
793
|
export function stepCopyTools(
|
|
@@ -924,7 +844,7 @@ export function stepCopyTools(
|
|
|
924
844
|
return copied;
|
|
925
845
|
}
|
|
926
846
|
|
|
927
|
-
// ─── Step
|
|
847
|
+
// ─── Step 5: Inject preset name into ndomo.json ──────────────────────────────
|
|
928
848
|
export function stepInjectPreset(configDir: string, preset: string, dryRun: boolean): void {
|
|
929
849
|
const ndomoJsonPath = join(configDir, "ndomo.json");
|
|
930
850
|
|
|
@@ -947,7 +867,7 @@ export function stepInjectPreset(configDir: string, preset: string, dryRun: bool
|
|
|
947
867
|
}
|
|
948
868
|
}
|
|
949
869
|
|
|
950
|
-
// ─── Step
|
|
870
|
+
// ─── Step 6: Optional DCP install ────────────────────────────────────────────
|
|
951
871
|
export async function stepInstallDcp(dryRun: boolean): Promise<void> {
|
|
952
872
|
info("Installing @tarquinen/opencode-dcp (AGPL-3.0)...");
|
|
953
873
|
if (dryRun) {
|
|
@@ -955,12 +875,11 @@ export async function stepInstallDcp(dryRun: boolean): Promise<void> {
|
|
|
955
875
|
return;
|
|
956
876
|
}
|
|
957
877
|
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
878
|
+
const result = await streamSpawn(["opencode", "plugin", "@tarquinen/opencode-dcp", "--global"], {
|
|
879
|
+
label: "opencode plugin dcp",
|
|
880
|
+
nothrow: true,
|
|
961
881
|
});
|
|
962
|
-
|
|
963
|
-
if (proc.exitCode === 0) {
|
|
882
|
+
if (result.exitCode === 0) {
|
|
964
883
|
ok("DCP plugin installed");
|
|
965
884
|
} else {
|
|
966
885
|
warn("DCP plugin install failed (non-fatal)");
|
|
@@ -1016,6 +935,10 @@ export function writeHttpBlock(projectRoot: string, httpConfig: HttpConfig, dryR
|
|
|
1016
935
|
* Returns true if user accepts, false otherwise.
|
|
1017
936
|
*/
|
|
1018
937
|
export async function promptHttpEnable(): Promise<boolean> {
|
|
938
|
+
if (!process.stdin.isTTY) {
|
|
939
|
+
info("Non-TTY stdin — skipping HTTP prompt (use --enable-http to enable)");
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
1019
942
|
console.log("");
|
|
1020
943
|
console.log("[?] Enable ndomo HTTP server? Allows programmatic plan/task control via API.");
|
|
1021
944
|
console.log(" Recommended for users integrating ndomo with other tools (port 4097, auth required).");
|
|
@@ -1231,24 +1154,21 @@ export async function runInstall(args: string[]): Promise<void> {
|
|
|
1231
1154
|
info("Skipping dependency installation (--skip-deps)");
|
|
1232
1155
|
}
|
|
1233
1156
|
|
|
1234
|
-
// Step 2:
|
|
1235
|
-
await stepBuild(projectRoot, flags.dryRun);
|
|
1236
|
-
|
|
1237
|
-
// Step 3+4: Copy agents
|
|
1157
|
+
// Step 2: Copy agents
|
|
1238
1158
|
mkdirSync(join(configDir, "agent"), { recursive: true });
|
|
1239
1159
|
mkdirSync(join(configDir, "skills"), { recursive: true });
|
|
1240
1160
|
stepCopyAgents(projectRoot, configDir, backupDir, flags.dryRun);
|
|
1241
1161
|
|
|
1242
|
-
// Step
|
|
1162
|
+
// Step 3: Copy skills
|
|
1243
1163
|
stepCopySkills(projectRoot, configDir, backupDir, flags.dryRun);
|
|
1244
1164
|
|
|
1245
|
-
// Step
|
|
1165
|
+
// Step 3.5: Apply preset
|
|
1246
1166
|
stepApplyPreset(configDir, configJson, flags.preset, flags.provider, flags.dryRun);
|
|
1247
1167
|
|
|
1248
|
-
// Step
|
|
1168
|
+
// Step 4: Copy config
|
|
1249
1169
|
stepCopyConfig(projectRoot, configDir, backupDir, flags.dryRun);
|
|
1250
1170
|
|
|
1251
|
-
// Step
|
|
1171
|
+
// Step 4.5: Register plugins
|
|
1252
1172
|
// Reload config from configDir (just copied)
|
|
1253
1173
|
let installedConfig: NdomoConfig = {};
|
|
1254
1174
|
const ndomoJsonPath = join(configDir, "ndomo.json");
|
|
@@ -1260,16 +1180,16 @@ export async function runInstall(args: string[]): Promise<void> {
|
|
|
1260
1180
|
}
|
|
1261
1181
|
stepRegisterPlugins(configDir, installedConfig, backupDir, flags.dryRun);
|
|
1262
1182
|
|
|
1263
|
-
// Step
|
|
1183
|
+
// Step 4.6: Install package
|
|
1264
1184
|
await stepInstallPackage(projectRoot, configDir, flags.dryRun);
|
|
1265
1185
|
|
|
1266
|
-
// Step
|
|
1186
|
+
// Step 4.7: Copy tools (npm distribution — no symlink)
|
|
1267
1187
|
stepCopyTools(projectRoot, configDir, flags.dryRun);
|
|
1268
1188
|
|
|
1269
|
-
// Step
|
|
1189
|
+
// Step 5: Inject preset
|
|
1270
1190
|
stepInjectPreset(configDir, flags.preset, flags.dryRun);
|
|
1271
1191
|
|
|
1272
|
-
// Step
|
|
1192
|
+
// Step 6: Optional DCP
|
|
1273
1193
|
if (flags.withDcp) {
|
|
1274
1194
|
await stepInstallDcp(flags.dryRun);
|
|
1275
1195
|
}
|