ndomo 0.2.0 → 0.2.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli/install.ts +64 -83
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ndomo",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",
@@ -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,
@@ -522,7 +510,7 @@ export function applyProviderPrefix(
522
510
  return updated;
523
511
  }
524
512
 
525
- /** Step 5.5: Apply preset + optional provider prefix override. */
513
+ /** Step 3.5: Apply preset + optional provider prefix override. */
526
514
  export function stepApplyPreset(
527
515
  configDir: string,
528
516
  configJson: NdomoConfig,
@@ -583,7 +571,7 @@ export function stepApplyPreset(
583
571
  }
584
572
  }
585
573
 
586
- // ─── Step 6: Copy config files ───────────────────────────────────────────────
574
+ // ─── Step 4: Copy config files ───────────────────────────────────────────────
587
575
  export function stepCopyConfig(
588
576
  projectRoot: string,
589
577
  configDir: string,
@@ -632,7 +620,7 @@ export function stepCopyConfig(
632
620
  }
633
621
  }
634
622
 
635
- // ─── Step 6.5: Register plugins in opencode.json ─────────────────────────────
623
+ // ─── Step 4.5: Register plugins in opencode.json ─────────────────────────────
636
624
  export function stepRegisterPlugins(
637
625
  configDir: string,
638
626
  configJson: NdomoConfig,
@@ -694,7 +682,7 @@ export function stepRegisterPlugins(
694
682
  ok(`Registered ${allPlugins.length} ndomo plugin(s) in opencode.json`);
695
683
  }
696
684
 
697
- // ─── Step 6.6: Install ndomo package (3 strategies) ──────────────────────────
685
+ // ─── Step 4.6: Install ndomo package (3 strategies) ──────────────────────────
698
686
 
699
687
  function isSymlink(p: string): boolean {
700
688
  try {
@@ -732,14 +720,13 @@ async function strategyFileDep(
732
720
  writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + "\n");
733
721
 
734
722
  // Run bun install
735
- const proc = Bun.spawn(["bun", "install", "--no-frozen-lockfile"], {
723
+ const result = await streamSpawn(["bun", "install", "--no-frozen-lockfile"], {
736
724
  cwd: configDir,
737
- stdout: "pipe",
738
- stderr: "pipe",
725
+ label: "bun install (file: dep)",
726
+ nothrow: true,
739
727
  });
740
- await proc.exited;
741
728
 
742
- if (proc.exitCode === 0 && existsSync(nmNdomo) && !isSymlink(nmNdomo)) {
729
+ if (result.exitCode === 0 && existsSync(nmNdomo) && !isSymlink(nmNdomo)) {
743
730
  ok("ndomo installed via bun (file: dep) — real copy, no symlink");
744
731
  return true;
745
732
  }
@@ -759,28 +746,26 @@ async function strategyBunLink(projectRoot: string, configDir: string): Promise<
759
746
 
760
747
  try {
761
748
  // bun link in project root (registers package)
762
- const proc1 = Bun.spawn(["bun", "link"], {
749
+ const linkResult = await streamSpawn(["bun", "link"], {
763
750
  cwd: projectRoot,
764
- stdout: "pipe",
765
- stderr: "pipe",
751
+ label: "bun link",
752
+ nothrow: true,
766
753
  });
767
- await proc1.exited;
768
754
 
769
- if (proc1.exitCode !== 0) {
755
+ if (linkResult.exitCode !== 0) {
770
756
  warn("bun link in project root failed");
771
757
  return false;
772
758
  }
773
759
 
774
760
  // bun link ndomo in config dir (links package)
775
- const proc2 = Bun.spawn(["bun", "link", "ndomo"], {
761
+ const linkNdomo = await streamSpawn(["bun", "link", "ndomo"], {
776
762
  cwd: configDir,
777
- stdout: "pipe",
778
- stderr: "pipe",
763
+ label: "bun link ndomo",
764
+ nothrow: true,
779
765
  });
780
- await proc2.exited;
781
766
 
782
767
  const nmNdomo = join(configDir, "node_modules", "ndomo");
783
- if (proc2.exitCode === 0 && existsSync(nmNdomo)) {
768
+ if (linkNdomo.exitCode === 0 && existsSync(nmNdomo)) {
784
769
  ok("ndomo linked via bun link (managed symlink)");
785
770
  warn("bun link uses symlinks — run 'bun run dev:bust' if cache goes stale");
786
771
  return true;
@@ -867,7 +852,7 @@ export async function stepInstallPackage(
867
852
  }
868
853
  }
869
854
 
870
- // ─── Step 6.7: Copy custom tools ─────────────────────────────────────────────
855
+ // ─── Step 4.7: Copy custom tools ─────────────────────────────────────────────
871
856
  // npm distribution: tools live inside the installed ndomo package, so symlink
872
857
  // dance (used in old repo-based install) is obsolete. Copy .ts files directly.
873
858
  export function stepCopyTools(
@@ -924,7 +909,7 @@ export function stepCopyTools(
924
909
  return copied;
925
910
  }
926
911
 
927
- // ─── Step 7: Inject preset name into ndomo.json ──────────────────────────────
912
+ // ─── Step 5: Inject preset name into ndomo.json ──────────────────────────────
928
913
  export function stepInjectPreset(configDir: string, preset: string, dryRun: boolean): void {
929
914
  const ndomoJsonPath = join(configDir, "ndomo.json");
930
915
 
@@ -947,7 +932,7 @@ export function stepInjectPreset(configDir: string, preset: string, dryRun: bool
947
932
  }
948
933
  }
949
934
 
950
- // ─── Step 8: Optional DCP install ────────────────────────────────────────────
935
+ // ─── Step 6: Optional DCP install ────────────────────────────────────────────
951
936
  export async function stepInstallDcp(dryRun: boolean): Promise<void> {
952
937
  info("Installing @tarquinen/opencode-dcp (AGPL-3.0)...");
953
938
  if (dryRun) {
@@ -955,12 +940,11 @@ export async function stepInstallDcp(dryRun: boolean): Promise<void> {
955
940
  return;
956
941
  }
957
942
 
958
- const proc = Bun.spawn(["opencode", "plugin", "@tarquinen/opencode-dcp", "--global"], {
959
- stdout: "pipe",
960
- stderr: "pipe",
943
+ const result = await streamSpawn(["opencode", "plugin", "@tarquinen/opencode-dcp", "--global"], {
944
+ label: "opencode plugin dcp",
945
+ nothrow: true,
961
946
  });
962
- await proc.exited;
963
- if (proc.exitCode === 0) {
947
+ if (result.exitCode === 0) {
964
948
  ok("DCP plugin installed");
965
949
  } else {
966
950
  warn("DCP plugin install failed (non-fatal)");
@@ -1231,24 +1215,21 @@ export async function runInstall(args: string[]): Promise<void> {
1231
1215
  info("Skipping dependency installation (--skip-deps)");
1232
1216
  }
1233
1217
 
1234
- // Step 2: Build
1235
- await stepBuild(projectRoot, flags.dryRun);
1236
-
1237
- // Step 3+4: Copy agents
1218
+ // Step 2: Copy agents
1238
1219
  mkdirSync(join(configDir, "agent"), { recursive: true });
1239
1220
  mkdirSync(join(configDir, "skills"), { recursive: true });
1240
1221
  stepCopyAgents(projectRoot, configDir, backupDir, flags.dryRun);
1241
1222
 
1242
- // Step 5: Copy skills
1223
+ // Step 3: Copy skills
1243
1224
  stepCopySkills(projectRoot, configDir, backupDir, flags.dryRun);
1244
1225
 
1245
- // Step 5.5: Apply preset
1226
+ // Step 3.5: Apply preset
1246
1227
  stepApplyPreset(configDir, configJson, flags.preset, flags.provider, flags.dryRun);
1247
1228
 
1248
- // Step 6: Copy config
1229
+ // Step 4: Copy config
1249
1230
  stepCopyConfig(projectRoot, configDir, backupDir, flags.dryRun);
1250
1231
 
1251
- // Step 6.5: Register plugins
1232
+ // Step 4.5: Register plugins
1252
1233
  // Reload config from configDir (just copied)
1253
1234
  let installedConfig: NdomoConfig = {};
1254
1235
  const ndomoJsonPath = join(configDir, "ndomo.json");
@@ -1260,16 +1241,16 @@ export async function runInstall(args: string[]): Promise<void> {
1260
1241
  }
1261
1242
  stepRegisterPlugins(configDir, installedConfig, backupDir, flags.dryRun);
1262
1243
 
1263
- // Step 6.6: Install package
1244
+ // Step 4.6: Install package
1264
1245
  await stepInstallPackage(projectRoot, configDir, flags.dryRun);
1265
1246
 
1266
- // Step 6.7: Copy tools (npm distribution — no symlink)
1247
+ // Step 4.7: Copy tools (npm distribution — no symlink)
1267
1248
  stepCopyTools(projectRoot, configDir, flags.dryRun);
1268
1249
 
1269
- // Step 7: Inject preset
1250
+ // Step 5: Inject preset
1270
1251
  stepInjectPreset(configDir, flags.preset, flags.dryRun);
1271
1252
 
1272
- // Step 8: Optional DCP
1253
+ // Step 6: Optional DCP
1273
1254
  if (flags.withDcp) {
1274
1255
  await stepInstallDcp(flags.dryRun);
1275
1256
  }