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.
- package/package.json +1 -1
- 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.
|
|
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",
|
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,
|
|
@@ -522,7 +510,7 @@ export function applyProviderPrefix(
|
|
|
522
510
|
return updated;
|
|
523
511
|
}
|
|
524
512
|
|
|
525
|
-
/** Step
|
|
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
|
|
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
|
|
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
|
|
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
|
|
723
|
+
const result = await streamSpawn(["bun", "install", "--no-frozen-lockfile"], {
|
|
736
724
|
cwd: configDir,
|
|
737
|
-
|
|
738
|
-
|
|
725
|
+
label: "bun install (file: dep)",
|
|
726
|
+
nothrow: true,
|
|
739
727
|
});
|
|
740
|
-
await proc.exited;
|
|
741
728
|
|
|
742
|
-
if (
|
|
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
|
|
749
|
+
const linkResult = await streamSpawn(["bun", "link"], {
|
|
763
750
|
cwd: projectRoot,
|
|
764
|
-
|
|
765
|
-
|
|
751
|
+
label: "bun link",
|
|
752
|
+
nothrow: true,
|
|
766
753
|
});
|
|
767
|
-
await proc1.exited;
|
|
768
754
|
|
|
769
|
-
if (
|
|
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
|
|
761
|
+
const linkNdomo = await streamSpawn(["bun", "link", "ndomo"], {
|
|
776
762
|
cwd: configDir,
|
|
777
|
-
|
|
778
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
959
|
-
|
|
960
|
-
|
|
943
|
+
const result = await streamSpawn(["opencode", "plugin", "@tarquinen/opencode-dcp", "--global"], {
|
|
944
|
+
label: "opencode plugin dcp",
|
|
945
|
+
nothrow: true,
|
|
961
946
|
});
|
|
962
|
-
|
|
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:
|
|
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
|
|
1223
|
+
// Step 3: Copy skills
|
|
1243
1224
|
stepCopySkills(projectRoot, configDir, backupDir, flags.dryRun);
|
|
1244
1225
|
|
|
1245
|
-
// Step
|
|
1226
|
+
// Step 3.5: Apply preset
|
|
1246
1227
|
stepApplyPreset(configDir, configJson, flags.preset, flags.provider, flags.dryRun);
|
|
1247
1228
|
|
|
1248
|
-
// Step
|
|
1229
|
+
// Step 4: Copy config
|
|
1249
1230
|
stepCopyConfig(projectRoot, configDir, backupDir, flags.dryRun);
|
|
1250
1231
|
|
|
1251
|
-
// Step
|
|
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
|
|
1244
|
+
// Step 4.6: Install package
|
|
1264
1245
|
await stepInstallPackage(projectRoot, configDir, flags.dryRun);
|
|
1265
1246
|
|
|
1266
|
-
// Step
|
|
1247
|
+
// Step 4.7: Copy tools (npm distribution — no symlink)
|
|
1267
1248
|
stepCopyTools(projectRoot, configDir, flags.dryRun);
|
|
1268
1249
|
|
|
1269
|
-
// Step
|
|
1250
|
+
// Step 5: Inject preset
|
|
1270
1251
|
stepInjectPreset(configDir, flags.preset, flags.dryRun);
|
|
1271
1252
|
|
|
1272
|
-
// Step
|
|
1253
|
+
// Step 6: Optional DCP
|
|
1273
1254
|
if (flags.withDcp) {
|
|
1274
1255
|
await stepInstallDcp(flags.dryRun);
|
|
1275
1256
|
}
|