jorgex-stack 1.9.1 → 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
4
4
  "description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI, OpenCode y Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,6 +16,32 @@ interface ScriptResult {
16
16
  scriptPath: string;
17
17
  }
18
18
 
19
+ const logToOpenCode = async (
20
+ client: unknown,
21
+ level: "warn" | "error",
22
+ message: string,
23
+ extra?: unknown,
24
+ ) => {
25
+ if (typeof client !== "object" || client === null) return;
26
+ const app = (client as { app?: unknown }).app;
27
+ if (typeof app !== "object" || app === null) return;
28
+ const log = (app as { log?: unknown }).log;
29
+ if (typeof log !== "function") return;
30
+
31
+ const safeExtra =
32
+ extra instanceof Error
33
+ ? { message: extra.message, stack: extra.stack }
34
+ : extra;
35
+
36
+ try {
37
+ await log.call(app, {
38
+ body: { service: "hooks", level, message, extra: safeExtra },
39
+ });
40
+ } catch {
41
+ // Logging must not break the OpenCode TUI hook.
42
+ }
43
+ };
44
+
19
45
  const isAbsolutePath = (value: string) =>
20
46
  /^[a-zA-Z]:[\\/]/.test(value) || value.startsWith("/");
21
47
 
@@ -43,6 +69,22 @@ const resolveProjectPath = (directory: string, target?: string) => {
43
69
  const isPlainObject = (value: unknown): value is Record<string, unknown> =>
44
70
  typeof value === "object" && value !== null && !Array.isArray(value);
45
71
 
72
+ const validateConfig = (config: Record<string, unknown>) => {
73
+ for (const field of ["setupScript", "docsReminderScript", "pathContains"]) {
74
+ if (field in config && typeof config[field] !== "string") {
75
+ return `Worktree config field "${field}" must be a string.`;
76
+ }
77
+ }
78
+
79
+ if (
80
+ "reminderLines" in config &&
81
+ (!Array.isArray(config.reminderLines) ||
82
+ !config.reminderLines.every((line) => typeof line === "string"))
83
+ ) {
84
+ return 'Worktree config field "reminderLines" must be an array of strings.';
85
+ }
86
+ };
87
+
46
88
  const getPayloadWorktreePath = (payload: unknown) => {
47
89
  if (!isPlainObject(payload)) return undefined;
48
90
 
@@ -142,15 +184,14 @@ const runScript = async (
142
184
  }
143
185
 
144
186
  if (!command) {
145
- await client.app.log({
146
- body: {
147
- service: "hooks",
148
- level: "warn",
149
- message: "Unsupported worktree hook script extension",
150
- extra: { script, scriptPath },
151
- },
152
- });
153
- return { exitCode: 0, stdout: "", stderr: "", script, scriptPath };
187
+ const stderr = `Unsupported worktree hook script extension for ${scriptPath}.`;
188
+ await logToOpenCode(
189
+ client,
190
+ "warn",
191
+ "Unsupported worktree hook script extension",
192
+ { script, scriptPath },
193
+ );
194
+ return { exitCode: 1, stdout: "", stderr, script, scriptPath };
154
195
  }
155
196
 
156
197
  try {
@@ -173,35 +214,26 @@ const runScript = async (
173
214
  ]);
174
215
 
175
216
  if (exitCode !== 0) {
176
- await client.app.log({
177
- body: {
178
- service: "hooks",
179
- level: "error",
180
- message: "Worktree hook script execution failed",
181
- extra: {
182
- script,
183
- scriptPath,
184
- exitCode,
185
- stderr: stderr.trim(),
186
- stdout: stdout.trim(),
187
- },
217
+ await logToOpenCode(
218
+ client,
219
+ "error",
220
+ "Worktree hook script execution failed",
221
+ {
222
+ script,
223
+ scriptPath,
224
+ exitCode,
225
+ stderr: stderr.trim(),
226
+ stdout: stdout.trim(),
188
227
  },
189
- });
228
+ );
190
229
  }
191
230
 
192
231
  return { exitCode, stdout, stderr, script, scriptPath };
193
232
  } catch (error) {
194
- await client.app.log({
195
- body: {
196
- service: "hooks",
197
- level: "error",
198
- message: "Worktree hook script execution failed",
199
- extra: {
200
- script,
201
- scriptPath,
202
- error: error instanceof Error ? error.message : String(error),
203
- },
204
- },
233
+ await logToOpenCode(client, "error", "Worktree hook script execution failed", {
234
+ script,
235
+ scriptPath,
236
+ error: error instanceof Error ? error.message : String(error),
205
237
  });
206
238
  return {
207
239
  exitCode: 1,
@@ -285,28 +317,67 @@ const getCommandCwd = (args: Record<string, unknown>, directory: string) => {
285
317
  const replaceToken = (value: string, token: string, replacement: string) =>
286
318
  value.split(token).join(replacement);
287
319
 
320
+ const isMissingFileError = (error: unknown) => {
321
+ const code =
322
+ typeof error === "object" && error !== null && "code" in error
323
+ ? (error as { code?: unknown }).code
324
+ : undefined;
325
+ return (
326
+ code === "ENOENT" ||
327
+ (error instanceof Error && /not found|no such file/i.test(error.message))
328
+ );
329
+ };
330
+
288
331
  export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
289
332
  let config: WorktreePluginConfig = {
290
- setupScript: "scripts/setup-worktree.ps1",
291
333
  pathContains: "worktrees/",
292
334
  reminderLines: [],
293
335
  };
336
+ let configError: string | undefined;
294
337
 
295
338
  try {
296
339
  const configPath = `${directory}/.opencode/worktree.json`;
297
- const content = await (globalThis as any).Bun.file(configPath).text();
298
- config = {
299
- ...config,
300
- ...JSON.parse(content),
301
- };
302
- } catch {
303
- // Optional project config
340
+ const configFile = (globalThis as any).Bun.file(configPath);
341
+ if (
342
+ typeof configFile.exists === "function" &&
343
+ !(await configFile.exists())
344
+ ) {
345
+ // Optional project config.
346
+ } else {
347
+ const parsed = JSON.parse(await configFile.text()) as unknown;
348
+ if (!isPlainObject(parsed)) {
349
+ configError = "Worktree config must be a JSON object.";
350
+ } else {
351
+ configError = validateConfig(parsed);
352
+ if (!configError) {
353
+ config = { ...config, ...(parsed as WorktreePluginConfig) };
354
+ }
355
+ }
356
+ }
357
+ } catch (error) {
358
+ if (!isMissingFileError(error)) {
359
+ const couldNotParse = error instanceof SyntaxError;
360
+ configError = couldNotParse
361
+ ? "Worktree config could not be parsed as JSON."
362
+ : "Worktree config could not be read.";
363
+ await logToOpenCode(
364
+ client,
365
+ "warn",
366
+ couldNotParse
367
+ ? "Worktree config could not be parsed"
368
+ : "Worktree config could not be read",
369
+ error,
370
+ );
371
+ }
304
372
  }
305
373
 
306
374
  return {
307
375
  "tool.execute.after": async (input: any, output: any) => {
308
376
  try {
309
377
  const tool = (input.tool || "").toLowerCase();
378
+ const args = input.args || {};
379
+ const command = args.command || "";
380
+
310
381
  const payload = {
311
382
  event: "tool.execute.after",
312
383
  directory,
@@ -315,6 +386,11 @@ export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
315
386
  };
316
387
 
317
388
  if (tool === "enterworktree") {
389
+ if (configError) {
390
+ appendToolOutput(output, [configError]);
391
+ return;
392
+ }
393
+
318
394
  const docsReminderScript = resolveProjectPath(
319
395
  directory,
320
396
  config.docsReminderScript,
@@ -337,8 +413,6 @@ export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
337
413
 
338
414
  if (tool !== "bash") return;
339
415
 
340
- const args = input.args || {};
341
- const command = args.command || "";
342
416
  const commandLower = command.toLowerCase();
343
417
  const pathContains = toSlashes(
344
418
  config.pathContains || "worktrees/",
@@ -366,12 +440,24 @@ export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
366
440
  `worktrees/${worktreeName}`,
367
441
  );
368
442
 
369
- if (!samePath(absoluteWorktreePath, expectedWorktreePath)) {
443
+ const isCanonicalPath = samePath(
444
+ absoluteWorktreePath,
445
+ expectedWorktreePath,
446
+ );
447
+ if (!isCanonicalPath) {
370
448
  appendToolOutput(output, [
371
449
  `Worktree path is not canonical: ${absoluteWorktreePath}`,
372
450
  `Use the project-local path instead: ${expectedWorktreePath}`,
373
451
  "Canonical rule: <project-root>/worktrees/<canonical-name> or <project-root>/worktrees/<canonical-name>-prNN.",
374
452
  ]);
453
+ }
454
+
455
+ if (configError) {
456
+ appendToolOutput(output, [configError]);
457
+ return;
458
+ }
459
+
460
+ if (!isCanonicalPath) {
375
461
  return;
376
462
  }
377
463
 
@@ -430,15 +516,16 @@ export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
430
516
  appendToolOutput(output, [banner]);
431
517
  }
432
518
  } catch (error) {
433
- await client.app.log({
434
- body: {
435
- service: "hooks",
436
- level: "error",
437
- message: "Worktree plugin execution failed",
438
- extra: {
439
- error: error instanceof Error ? error.message : String(error),
440
- },
441
- },
519
+ const details = truncateMessage(
520
+ error instanceof Error ? error.message : String(error),
521
+ );
522
+ appendToolOutput(output, [
523
+ "Worktree plugin could not process this worktree command.",
524
+ "Run `git rev-parse --show-toplevel` from the project root and retry.",
525
+ ...(details ? [`Details: ${details}`] : []),
526
+ ]);
527
+ await logToOpenCode(client, "error", "Worktree plugin execution failed", {
528
+ error: error instanceof Error ? error.message : String(error),
442
529
  });
443
530
  }
444
531
  },