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.
@@ -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.0",
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
+ });
@@ -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 proc = Bun.spawn(["bun", "install", "--frozen-lockfile"], {
208
+ const frozen = await streamSpawn(["bun", "install", "--frozen-lockfile"], {
185
209
  cwd: projectRoot,
186
- stdout: "pipe",
187
- stderr: "pipe",
210
+ label: "bun install --frozen-lockfile",
211
+ nothrow: true,
188
212
  });
189
- await proc.exited;
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
- const proc2 = Bun.spawn(["bun", "install"], {
216
+ await streamSpawn(["bun", "install"], {
194
217
  cwd: projectRoot,
195
- stdout: "pipe",
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: Build TypeScript. */
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 5: Copy skills with timestamped backup. */
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
- * Parse YAML frontmatter from an agent .md file.
358
- * Returns { frontmatter: Record<string, string>, body: string, raw: string }.
359
- * Frontmatter is between the first two '---' lines.
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
- const content = readFileSync(filePath, "utf-8");
425
- const { frontmatter, body, startIdx, endIdx } = parseFrontmatter(content);
354
+ let content = readFileSync(filePath, "utf-8");
426
355
 
427
- if (startIdx === -1 || endIdx === -1) {
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
- // Apply model
435
- if (preset.model) {
436
- frontmatter["model"] = preset.model;
437
- changed = true;
438
- }
439
-
440
- // Apply temperature
441
- if (preset.temperature !== undefined) {
442
- frontmatter["temperature"] = String(preset.temperature);
443
- changed = true;
444
- }
445
-
446
- // Apply reasoningEffort (snake_case camelCase)
447
- if (preset.reasoning_effort) {
448
- frontmatter["reasoningEffort"] = preset.reasoning_effort;
449
- changed = true;
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
- // Serialize back preserving order: model, temperature, reasoningEffort, then others
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)}: model=${preset.model ?? "(keep)"}, temp=${preset.temperature ?? "(keep)"}, effort=${preset.reasoning_effort ?? "(keep)"}`);
395
+ info(`[dry-run] would update ${basename(filePath)}`);
472
396
  } else {
473
- writeFileSync(filePath, serializeFrontmatter(ordered, body));
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 5.5: Apply preset + optional provider prefix override. */
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 6: Copy config files ───────────────────────────────────────────────
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 6.5: Register plugins in opencode.json ─────────────────────────────
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 6.6: Install ndomo package (3 strategies) ──────────────────────────
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 proc = Bun.spawn(["bun", "install", "--no-frozen-lockfile"], {
658
+ const result = await streamSpawn(["bun", "install", "--no-frozen-lockfile"], {
736
659
  cwd: configDir,
737
- stdout: "pipe",
738
- stderr: "pipe",
660
+ label: "bun install (file: dep)",
661
+ nothrow: true,
739
662
  });
740
- await proc.exited;
741
663
 
742
- if (proc.exitCode === 0 && existsSync(nmNdomo) && !isSymlink(nmNdomo)) {
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 proc1 = Bun.spawn(["bun", "link"], {
684
+ const linkResult = await streamSpawn(["bun", "link"], {
763
685
  cwd: projectRoot,
764
- stdout: "pipe",
765
- stderr: "pipe",
686
+ label: "bun link",
687
+ nothrow: true,
766
688
  });
767
- await proc1.exited;
768
689
 
769
- if (proc1.exitCode !== 0) {
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 proc2 = Bun.spawn(["bun", "link", "ndomo"], {
696
+ const linkNdomo = await streamSpawn(["bun", "link", "ndomo"], {
776
697
  cwd: configDir,
777
- stdout: "pipe",
778
- stderr: "pipe",
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 (proc2.exitCode === 0 && existsSync(nmNdomo)) {
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 6.7: Copy custom tools ─────────────────────────────────────────────
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 7: Inject preset name into ndomo.json ──────────────────────────────
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 8: Optional DCP install ────────────────────────────────────────────
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 proc = Bun.spawn(["opencode", "plugin", "@tarquinen/opencode-dcp", "--global"], {
959
- stdout: "pipe",
960
- stderr: "pipe",
878
+ const result = await streamSpawn(["opencode", "plugin", "@tarquinen/opencode-dcp", "--global"], {
879
+ label: "opencode plugin dcp",
880
+ nothrow: true,
961
881
  });
962
- await proc.exited;
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: Build
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 5: Copy skills
1162
+ // Step 3: Copy skills
1243
1163
  stepCopySkills(projectRoot, configDir, backupDir, flags.dryRun);
1244
1164
 
1245
- // Step 5.5: Apply preset
1165
+ // Step 3.5: Apply preset
1246
1166
  stepApplyPreset(configDir, configJson, flags.preset, flags.provider, flags.dryRun);
1247
1167
 
1248
- // Step 6: Copy config
1168
+ // Step 4: Copy config
1249
1169
  stepCopyConfig(projectRoot, configDir, backupDir, flags.dryRun);
1250
1170
 
1251
- // Step 6.5: Register plugins
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 6.6: Install package
1183
+ // Step 4.6: Install package
1264
1184
  await stepInstallPackage(projectRoot, configDir, flags.dryRun);
1265
1185
 
1266
- // Step 6.7: Copy tools (npm distribution — no symlink)
1186
+ // Step 4.7: Copy tools (npm distribution — no symlink)
1267
1187
  stepCopyTools(projectRoot, configDir, flags.dryRun);
1268
1188
 
1269
- // Step 7: Inject preset
1189
+ // Step 5: Inject preset
1270
1190
  stepInjectPreset(configDir, flags.preset, flags.dryRun);
1271
1191
 
1272
- // Step 8: Optional DCP
1192
+ // Step 6: Optional DCP
1273
1193
  if (flags.withDcp) {
1274
1194
  await stepInstallDcp(flags.dryRun);
1275
1195
  }