goalbuddy 0.2.21 → 0.3.0

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 (51) hide show
  1. package/CONTRIBUTING.md +14 -5
  2. package/README.md +68 -55
  3. package/goalbuddy/SKILL.md +44 -14
  4. package/goalbuddy/agents/README.md +15 -8
  5. package/goalbuddy/extend/github-projects/README.md +105 -0
  6. package/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +63 -0
  7. package/goalbuddy/extend/github-projects/extension.yaml +43 -0
  8. package/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +728 -0
  9. package/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +362 -0
  10. package/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +193 -0
  11. package/goalbuddy/extend/github-projects/test/github-projects.test.mjs +267 -0
  12. package/goalbuddy/extend/local-goal-board/README.md +75 -0
  13. package/goalbuddy/extend/local-goal-board/assets/goalbuddy-mark.png +0 -0
  14. package/goalbuddy/extend/local-goal-board/examples/sample-goal/notes/T001-scout.md +3 -0
  15. package/goalbuddy/extend/local-goal-board/examples/sample-goal/state.yaml +124 -0
  16. package/goalbuddy/extend/local-goal-board/extension.yaml +37 -0
  17. package/goalbuddy/extend/local-goal-board/scripts/lib/goal-board.mjs +1225 -0
  18. package/goalbuddy/extend/local-goal-board/scripts/local-goal-board.mjs +258 -0
  19. package/goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs +146 -0
  20. package/goalbuddy/scripts/check-goal-state.mjs +24 -9
  21. package/goalbuddy/templates/state.yaml +18 -3
  22. package/internal/assets/goalbuddy-live-board.jpg +0 -0
  23. package/internal/cli/goal-maker.mjs +424 -31
  24. package/internal/cli/postinstall.mjs +3 -3
  25. package/package.json +7 -2
  26. package/plugins/goalbuddy/.claude-plugin/plugin.json +24 -0
  27. package/plugins/goalbuddy/.codex-plugin/plugin.json +5 -4
  28. package/plugins/goalbuddy/README.md +23 -13
  29. package/plugins/goalbuddy/agents/goal-judge.md +27 -0
  30. package/plugins/goalbuddy/agents/goal-scout.md +24 -0
  31. package/plugins/goalbuddy/agents/goal-worker.md +26 -0
  32. package/plugins/goalbuddy/commands/goal-prep.md +12 -0
  33. package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +44 -14
  34. package/plugins/goalbuddy/skills/goalbuddy/agents/README.md +15 -8
  35. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/README.md +105 -0
  36. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +63 -0
  37. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/extension.yaml +43 -0
  38. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +728 -0
  39. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +362 -0
  40. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +193 -0
  41. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/test/github-projects.test.mjs +267 -0
  42. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/README.md +75 -0
  43. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/assets/goalbuddy-mark.png +0 -0
  44. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/sample-goal/notes/T001-scout.md +3 -0
  45. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/sample-goal/state.yaml +124 -0
  46. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/extension.yaml +37 -0
  47. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/scripts/lib/goal-board.mjs +1225 -0
  48. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/scripts/local-goal-board.mjs +258 -0
  49. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs +146 -0
  50. package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +24 -9
  51. package/plugins/goalbuddy/skills/goalbuddy/templates/state.yaml +18 -3
@@ -25,23 +25,33 @@ const canonicalSkillDirectory = "goalbuddy";
25
25
  const legacyCliName = "goal-maker";
26
26
  const legacySkillName = "goal-maker";
27
27
  const skillSource = join(packageRoot, canonicalSkillDirectory);
28
+ const claudePluginSource = join(packageRoot, "plugins", "goalbuddy");
28
29
  const packageInfo = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
29
30
  const defaultCodexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
31
+ const defaultClaudeHome = process.env.CLAUDE_HOME || join(homedir(), ".claude");
30
32
  const defaultCatalogUrl = "https://raw.githubusercontent.com/tolibear/goalbuddy/main/extend/catalog.json";
31
33
  const requiredAgentFiles = [
32
34
  "goal_judge.toml",
33
35
  "goal_scout.toml",
34
36
  "goal_worker.toml",
35
37
  ];
38
+ const requiredClaudeAgentFiles = [
39
+ "goal-scout.md",
40
+ "goal-judge.md",
41
+ "goal-worker.md",
42
+ ];
43
+ const bundledCoreExtensionIds = new Set(["github-projects", "local-goal-board"]);
36
44
  const optionsWithValues = new Set([
37
45
  "--catalog",
38
46
  "--catalog-url",
47
+ "--claude-home",
39
48
  "--codex-home",
40
49
  "--goal",
41
50
  "--host",
42
51
  "--kind",
43
52
  "--port",
44
53
  "--source",
54
+ "--target",
45
55
  ]);
46
56
 
47
57
  const args = process.argv.slice(2);
@@ -61,17 +71,37 @@ async function main() {
61
71
  maybePrintLegacyNotice();
62
72
  switch (command) {
63
73
  case "default":
64
- installPlugin();
74
+ if (installTargetMode() === "all") {
75
+ await installEverywhere();
76
+ } else if (installTargetMode() === "codex") {
77
+ installPlugin();
78
+ } else {
79
+ await installClaudeAll();
80
+ }
65
81
  break;
66
82
  case "install":
67
83
  case "update":
68
- await installAll();
84
+ if (installTargetMode() === "all") {
85
+ await installEverywhere();
86
+ } else if (installTargetMode() === "codex") {
87
+ await installAll();
88
+ } else {
89
+ await installClaudeAll();
90
+ }
69
91
  break;
70
92
  case "agents":
71
- installAgents();
93
+ if (targetMode() === "codex") {
94
+ installAgents();
95
+ } else {
96
+ installClaudeAgents();
97
+ }
72
98
  break;
73
99
  case "doctor":
74
- doctor();
100
+ if (targetMode() === "codex") {
101
+ doctor();
102
+ } else {
103
+ doctorClaude();
104
+ }
75
105
  break;
76
106
  case "check-update":
77
107
  case "update-check":
@@ -145,15 +175,15 @@ function positionalArgs() {
145
175
  }
146
176
 
147
177
  function usage() {
148
- console.log(`Codex ${canonicalProductName}
178
+ console.log(`${canonicalProductName} for Claude Code and Codex
149
179
 
150
180
  Usage:
151
- ${canonicalCliName} [--codex-home <path>] [--json]
181
+ ${canonicalCliName} [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--json]
152
182
  ${canonicalCliName} plugin install [--source <marketplace-source>] [--codex-home <path>] [--json]
153
- ${canonicalCliName} install [--codex-home <path>] [--force] [--json]
154
- ${canonicalCliName} update [--codex-home <path>] [--json]
155
- ${canonicalCliName} agents [--codex-home <path>] [--force]
156
- ${canonicalCliName} doctor [--codex-home <path>] [--goal-ready]
183
+ ${canonicalCliName} install [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--force] [--json]
184
+ ${canonicalCliName} update [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--json]
185
+ ${canonicalCliName} agents [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--force]
186
+ ${canonicalCliName} doctor [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--goal-ready]
157
187
  ${canonicalCliName} check-update [--json]
158
188
  ${canonicalCliName} extend [--catalog-url <url-or-path>] [--kind <kind>] [--json]
159
189
  ${canonicalCliName} extend <id> [--catalog-url <url-or-path>] [--json]
@@ -162,17 +192,19 @@ Usage:
162
192
  ${canonicalCliName} extend doctor [<id>] [--codex-home <path>] [--json]
163
193
  ${canonicalCliName} board <docs/goals/slug> [--catalog-url <url-or-path>] [--host <host>] [--port <port>] [--once] [--json]
164
194
 
165
- Default:
166
- ${canonicalCliName} Installs and enables the native Codex plugin.
195
+ Targets: by default, install/update prepares both Codex (~/.codex) and Claude Code (~/.claude). Use --target codex or --target claude to limit the command.
167
196
 
168
- Skill-only fallback:
169
- ${canonicalCliName} install Installs the legacy skill payload and bundled agent definitions.
197
+ Default:
198
+ ${canonicalCliName} Installs and enables Codex, then installs Claude Code skill + agents + /goal-prep command.
199
+ ${canonicalCliName} --target claude Installs ${canonicalProductName} for Claude Code (skill + agents + /goal-prep command).
200
+ ${canonicalCliName} --target codex Installs and enables the native Codex plugin.
170
201
 
171
202
  Compatibility:
172
203
  ${legacyCliName} remains a temporary alias and prints the new npx command for human-facing use.
173
204
 
174
205
  Environment:
175
206
  CODEX_HOME Overrides the default ~/.codex target.
207
+ CLAUDE_HOME Overrides the default ~/.claude target (and selects Claude Code unless --target codex is set).
176
208
  GOALBUDDY_EXTEND_CATALOG_URL Overrides the default GitHub-hosted extension catalog.
177
209
  GOAL_MAKER_EXTEND_CATALOG_URL Legacy fallback for the extension catalog.
178
210
  `);
@@ -182,6 +214,272 @@ function codexHome() {
182
214
  return resolve(optionValue("--codex-home") || defaultCodexHome);
183
215
  }
184
216
 
217
+ function claudeHome() {
218
+ return resolve(optionValue("--claude-home") || defaultClaudeHome);
219
+ }
220
+
221
+ function targetMode() {
222
+ const value = (optionValue("--target") || "").toLowerCase();
223
+ if (value === "codex" || value === "claude") return value;
224
+ // Explicit --claude-home or CLAUDE_HOME implies Claude target unless --target codex is set.
225
+ if (optionValue("--claude-home") || process.env.CLAUDE_HOME) return "claude";
226
+ return "codex";
227
+ }
228
+
229
+ function installTargetMode() {
230
+ const value = (optionValue("--target") || "").toLowerCase();
231
+ if (value === "codex" || value === "claude") return value;
232
+
233
+ const hasCodexHomeOption = Boolean(optionValue("--codex-home"));
234
+ const hasClaudeHomeOption = Boolean(optionValue("--claude-home"));
235
+ if (hasCodexHomeOption && !hasClaudeHomeOption) return "codex";
236
+ if (hasClaudeHomeOption && !hasCodexHomeOption) return "claude";
237
+ if (process.env.CLAUDE_HOME && !hasCodexHomeOption) return "claude";
238
+ return "all";
239
+ }
240
+
241
+ function claudeSkillRoot() {
242
+ return join(claudeHome(), "skills", canonicalSkillDirectory);
243
+ }
244
+
245
+ function claudeAgentsRoot() {
246
+ return join(claudeHome(), "agents");
247
+ }
248
+
249
+ function claudeCommandsRoot() {
250
+ return join(claudeHome(), "commands");
251
+ }
252
+
253
+ function installClaudeSkill({ quiet = false } = {}) {
254
+ const target = claudeSkillRoot();
255
+ if (!existsSync(skillSource)) {
256
+ console.error(`Skill payload not found: ${skillSource}`);
257
+ process.exit(1);
258
+ }
259
+
260
+ const previousMetadata = readInstallMetadata(target);
261
+ const previousFingerprint = existsSync(target) ? directoryFingerprint(target, { exclude: installFingerprintExcludes() }) : "";
262
+ const preservedExtensions = preserveInstalledExtensions([target], { tempRoot: claudeHome() });
263
+ const extensionTempPath = preservedExtensions.tempPath;
264
+ const preservedExtensionIds = preservedExtensions.ids;
265
+
266
+ mkdirSync(dirname(target), { recursive: true });
267
+ rmSync(target, { recursive: true, force: true });
268
+ cpSync(skillSource, target, { recursive: true });
269
+ restoreInstalledExtensions(target, extensionTempPath);
270
+ writeInstallMetadata(target, previousMetadata);
271
+ cleanupPreservedExtensions([extensionTempPath]);
272
+
273
+ const currentFingerprint = directoryFingerprint(target, { exclude: installFingerprintExcludes() });
274
+ const status = previousFingerprint
275
+ ? previousFingerprint === currentFingerprint ? "unchanged" : "updated"
276
+ : "installed";
277
+ if (!quiet) console.log(`Installed Claude Code ${canonicalProductName} skill to ${target}`);
278
+
279
+ return {
280
+ status,
281
+ path: target,
282
+ previous_version: previousMetadata?.package_version || "",
283
+ current_version: packageInfo.version,
284
+ preserved_extensions: preservedExtensionIds,
285
+ };
286
+ }
287
+
288
+ function installClaudeAgents({ quiet = false } = {}) {
289
+ const source = join(claudePluginSource, "agents");
290
+ const target = claudeAgentsRoot();
291
+ const force = hasFlag("--force") || command === "update" || command === "install" || command === "default";
292
+ mkdirSync(target, { recursive: true });
293
+
294
+ const results = [];
295
+ if (!existsSync(source)) return results;
296
+ for (const file of readdirSync(source)) {
297
+ if (!file.endsWith(".md")) continue;
298
+ const dest = join(target, file);
299
+ const sourceHash = sha256(readFileSync(join(source, file)));
300
+ const previousHash = existsSync(dest) ? sha256(readFileSync(dest)) : "";
301
+ if (existsSync(dest) && !force) {
302
+ if (!quiet) console.log(`skip existing ${dest} (use --force to overwrite)`);
303
+ results.push({ file, status: "skipped", path: dest });
304
+ continue;
305
+ }
306
+ cpSync(join(source, file), dest);
307
+ const status = previousHash ? previousHash === sourceHash ? "unchanged" : "updated" : "installed";
308
+ if (!quiet) console.log(`installed ${dest}`);
309
+ results.push({ file, status, path: dest });
310
+ }
311
+ return results;
312
+ }
313
+
314
+ function installClaudeCommands({ quiet = false } = {}) {
315
+ const source = join(claudePluginSource, "commands");
316
+ const target = claudeCommandsRoot();
317
+ const force = hasFlag("--force") || command === "update" || command === "install" || command === "default";
318
+ mkdirSync(target, { recursive: true });
319
+
320
+ const results = [];
321
+ if (!existsSync(source)) return results;
322
+ for (const file of readdirSync(source)) {
323
+ if (!file.endsWith(".md")) continue;
324
+ const dest = join(target, file);
325
+ if (existsSync(dest) && !force) {
326
+ if (!quiet) console.log(`skip existing ${dest} (use --force to overwrite)`);
327
+ results.push({ file, status: "skipped", path: dest });
328
+ continue;
329
+ }
330
+ const sourceHash = sha256(readFileSync(join(source, file)));
331
+ const previousHash = existsSync(dest) ? sha256(readFileSync(dest)) : "";
332
+ cpSync(join(source, file), dest);
333
+ const status = previousHash ? previousHash === sourceHash ? "unchanged" : "updated" : "installed";
334
+ if (!quiet) console.log(`installed ${dest}`);
335
+ results.push({ file, status, path: dest });
336
+ }
337
+ return results;
338
+ }
339
+
340
+ async function buildClaudeInstallReport() {
341
+ const quiet = true;
342
+ const report = {
343
+ command,
344
+ target: "claude",
345
+ package: {
346
+ name: packageInfo.name,
347
+ current_version: packageInfo.version,
348
+ },
349
+ claude_home: claudeHome(),
350
+ skill: installClaudeSkill({ quiet }),
351
+ agents: installClaudeAgents({ quiet }),
352
+ commands: installClaudeCommands({ quiet }),
353
+ extensions: await extensionDiscoverySummary(),
354
+ warnings: [],
355
+ };
356
+
357
+ report.package.previous_version = report.skill.previous_version;
358
+ return report;
359
+ }
360
+
361
+ async function installClaudeAll() {
362
+ const report = await buildClaudeInstallReport();
363
+
364
+ if (hasFlag("--json")) {
365
+ printJson(report);
366
+ } else {
367
+ printClaudeInstallReport(report);
368
+ }
369
+ }
370
+
371
+ async function installEverywhere() {
372
+ const report = {
373
+ command,
374
+ package: {
375
+ name: packageInfo.name,
376
+ current_version: packageInfo.version,
377
+ },
378
+ codex: null,
379
+ claude: null,
380
+ errors: [],
381
+ };
382
+
383
+ try {
384
+ report.codex = installPlugin({ quiet: true });
385
+ } catch (error) {
386
+ report.errors.push({ target: "codex", error: error.message });
387
+ report.codex = { target: "codex", ok: false, error: error.message };
388
+ }
389
+
390
+ try {
391
+ report.claude = await buildClaudeInstallReport();
392
+ } catch (error) {
393
+ report.errors.push({ target: "claude", error: error.message });
394
+ report.claude = { target: "claude", ok: false, error: error.message };
395
+ }
396
+
397
+ report.ok = report.errors.length === 0;
398
+
399
+ if (hasFlag("--json")) {
400
+ printJson(report);
401
+ } else {
402
+ printEverywhereInstallReport(report);
403
+ }
404
+
405
+ if (!report.ok) process.exit(1);
406
+ }
407
+
408
+ function doctorClaude() {
409
+ const skillPath = join(claudeSkillRoot(), "SKILL.md");
410
+ const agentsPath = claudeAgentsRoot();
411
+ const commandsPath = claudeCommandsRoot();
412
+ const installed = existsSync(skillPath);
413
+ const agents = existsSync(agentsPath)
414
+ ? readdirSync(agentsPath).filter((file) => file.startsWith("goal-") && file.endsWith(".md"))
415
+ : [];
416
+ const missingAgents = requiredClaudeAgentFiles.filter((file) => !agents.includes(file));
417
+ const staleAgents = requiredClaudeAgentFiles.filter((file) => {
418
+ const installedAgent = join(agentsPath, file);
419
+ const bundledAgent = join(claudePluginSource, "agents", file);
420
+ if (!existsSync(installedAgent) || !existsSync(bundledAgent)) return false;
421
+ return sha256(readFileSync(installedAgent)) !== sha256(readFileSync(bundledAgent));
422
+ });
423
+ const commands = existsSync(commandsPath)
424
+ ? readdirSync(commandsPath).filter((file) => file === "goal-prep.md")
425
+ : [];
426
+
427
+ console.log(JSON.stringify({
428
+ target: "claude",
429
+ claude_home: claudeHome(),
430
+ skill_installed: installed,
431
+ skill_path: skillPath,
432
+ installed_agents: agents,
433
+ missing_agents: missingAgents,
434
+ stale_agents: staleAgents,
435
+ installed_commands: commands,
436
+ }, null, 2));
437
+
438
+ const installOk = installed && missingAgents.length === 0 && staleAgents.length === 0;
439
+ process.exit(installOk ? 0 : 1);
440
+ }
441
+
442
+ function printClaudeInstallReport(report) {
443
+ const verb = report.command === "update" ? "Updated" : "Installed";
444
+ const previous = report.package.previous_version && report.package.previous_version !== report.package.current_version
445
+ ? ` ${report.package.previous_version} -> ${report.package.current_version}`
446
+ : ` ${report.package.current_version}`;
447
+ console.log("");
448
+ console.log(`${verb} ${canonicalProductName} for Claude Code${previous}`);
449
+ console.log("");
450
+ console.log(`Skill: ${report.skill.status} at ${report.skill.path}`);
451
+ console.log(`Agents: ${summarizeStatuses(report.agents)}`);
452
+ console.log(`Commands: ${summarizeStatuses(report.commands)}`);
453
+ if (report.skill.preserved_extensions.length) {
454
+ console.log(`Preserved extensions: ${report.skill.preserved_extensions.join(", ")}`);
455
+ }
456
+
457
+ if (report.extensions?.error) {
458
+ console.log("");
459
+ console.log(`Extensions: unavailable (${report.extensions.error})`);
460
+ } else if (report.extensions) {
461
+ console.log("");
462
+ console.log(`Extensions: ${report.extensions.available_count} available from ${report.extensions.catalog_url}`);
463
+ if (report.extensions.recommended?.length) {
464
+ console.log("");
465
+ console.log("Recommended:");
466
+ for (const extension of report.extensions.recommended.slice(0, 3)) {
467
+ console.log(` ${extension.name || extension.id}`);
468
+ if (extension.summary) console.log(` ${extension.summary}`);
469
+ console.log(` Details: npx ${extension.next_command}`);
470
+ }
471
+ }
472
+ }
473
+
474
+ console.log("");
475
+ console.log("Next:");
476
+ console.log(` Restart Claude Code, then run: /goal-prep`);
477
+ console.log(` Or invoke the skill: ${canonicalSkillName}`);
478
+ console.log("");
479
+ console.log("Also available for Codex:");
480
+ console.log(` npx ${canonicalCliName} --target codex`);
481
+ }
482
+
185
483
  function installSkill({ force = true, quiet = false } = {}) {
186
484
  const target = installedSkillRoot();
187
485
  const legacyTarget = legacyInstalledSkillRoot();
@@ -192,7 +490,7 @@ function installSkill({ force = true, quiet = false } = {}) {
192
490
 
193
491
  const previousMetadata = readInstallMetadata(target) || readInstallMetadata(legacyTarget);
194
492
  const previousFingerprint = existsSync(target) ? directoryFingerprint(target, { exclude: installFingerprintExcludes() }) : "";
195
- const preservedExtensions = preserveInstalledExtensions([target, legacyTarget]);
493
+ const preservedExtensions = preserveInstalledExtensions([target, legacyTarget], { tempRoot: codexHome() });
196
494
  const extensionTempPath = preservedExtensions.tempPath;
197
495
  const preservedExtensionIds = preservedExtensions.ids;
198
496
 
@@ -416,7 +714,7 @@ Default source:
416
714
  `);
417
715
  }
418
716
 
419
- function installPlugin() {
717
+ function installPlugin({ quiet = false } = {}) {
420
718
  const source = optionValue("--source") || "tolibear/goalbuddy";
421
719
  const pluginSource = join(packageRoot, "plugins", pluginName);
422
720
  const pluginManifestPath = join(pluginSource, ".codex-plugin", "plugin.json");
@@ -426,42 +724,55 @@ function installPlugin() {
426
724
 
427
725
  const pluginManifest = JSON.parse(readFileSync(pluginManifestPath, "utf8"));
428
726
  const pluginCachePath = pluginCacheRoot(pluginManifest.version);
727
+ const pluginSkillPath = join(pluginCachePath, "skills", canonicalSkillDirectory);
429
728
  const marketplace = runCodex(["plugin", "marketplace", "add", source]);
430
729
  if (!marketplace.ok) {
431
730
  throw new Error(`Failed to add Codex plugin marketplace: ${firstLine(marketplace.stderr || marketplace.stdout)}`);
432
731
  }
433
732
 
733
+ const existingPluginSkillPath = installedPluginSkillRoot();
734
+ const preservedExtensions = preserveInstalledExtensions([existingPluginSkillPath], { tempRoot: dirname(pluginCachePath) });
434
735
  mkdirSync(dirname(pluginCachePath), { recursive: true });
435
736
  rmSync(pluginCachePath, { recursive: true, force: true });
436
737
  cpSync(pluginSource, pluginCachePath, { recursive: true });
738
+ restoreInstalledExtensions(pluginSkillPath, preservedExtensions.tempPath);
739
+ cleanupPreservedExtensions([preservedExtensions.tempPath]);
437
740
  const configPath = enablePluginConfig();
438
741
 
439
742
  const report = {
440
743
  installed: true,
744
+ target: "codex",
441
745
  plugin: `${pluginName}@${pluginName}`,
442
746
  version: pluginManifest.version,
443
747
  codex_home: codexHome(),
444
748
  marketplace_source: source,
445
749
  cache_path: pluginCachePath,
446
750
  config_path: configPath,
751
+ preserved_extensions: preservedExtensions.ids,
447
752
  };
448
753
 
449
- if (hasFlag("--json")) {
754
+ if (hasFlag("--json") && !quiet) {
450
755
  printJson(report);
451
- return;
756
+ return report;
452
757
  }
453
758
 
759
+ if (quiet) return report;
760
+
454
761
  console.log(`Installed ${canonicalProductName} Codex plugin ${pluginManifest.version}`);
455
762
  console.log(`Marketplace: ${source}`);
456
763
  console.log(`Cache: ${pluginCachePath}`);
457
764
  console.log(`Config: ${configPath}`);
765
+ if (report.preserved_extensions.length) {
766
+ console.log(`Preserved extensions: ${report.preserved_extensions.join(", ")}`);
767
+ }
458
768
  console.log("");
459
769
  console.log("Restart Codex, then use:");
460
770
  console.log(` $${canonicalSkillName}`);
461
771
  console.log("");
462
- console.log("Optional extensions:");
463
- console.log(` npx ${canonicalCliName} extend`);
464
- console.log(` npx ${canonicalCliName} extend install --all`);
772
+ console.log("Bundled visual boards:");
773
+ console.log(` npx ${canonicalCliName} board docs/goals/<slug>`);
774
+ console.log(` npx ${canonicalCliName} extend github-projects`);
775
+ return report;
465
776
  }
466
777
 
467
778
  function pluginCacheRoot(version) {
@@ -527,9 +838,12 @@ function codexGoalRuntimeStatus() {
527
838
  }
528
839
 
529
840
  function runCodex(args) {
530
- const result = spawnSync("codex", args, {
841
+ const env = { ...process.env, CODEX_HOME: codexHome() };
842
+ const command = codexSpawnCommand(args, env);
843
+ const result = spawnSync(command.file, command.args, {
531
844
  encoding: "utf8",
532
- env: { ...process.env, CODEX_HOME: codexHome() },
845
+ env,
846
+ shell: command.shell || false,
533
847
  });
534
848
  return {
535
849
  ok: result.status === 0,
@@ -539,6 +853,38 @@ function runCodex(args) {
539
853
  };
540
854
  }
541
855
 
856
+ function codexSpawnCommand(args, env) {
857
+ if (process.platform !== "win32") return { file: "codex", args };
858
+
859
+ const command = resolveWindowsCommand("codex", env);
860
+ if (!command) return { file: "codex", args };
861
+ if (/\.(?:cmd|bat)$/i.test(command)) {
862
+ const commandLine = [quoteWindowsCommandArg(command), ...args.map(quoteWindowsCommandArg)].join(" ");
863
+ return {
864
+ file: commandLine,
865
+ args: [],
866
+ shell: true,
867
+ };
868
+ }
869
+ return { file: command, args };
870
+ }
871
+
872
+ function resolveWindowsCommand(name, env) {
873
+ const systemWhere = env.SystemRoot ? join(env.SystemRoot, "System32", "where.exe") : "";
874
+ const whereCommand = systemWhere && existsSync(systemWhere) ? systemWhere : "where.exe";
875
+ const where = spawnSync(whereCommand, [name], { encoding: "utf8", env });
876
+ if (where.status !== 0) return "";
877
+ const candidates = where.stdout
878
+ .split(/\r?\n/)
879
+ .map((line) => line.trim())
880
+ .filter(Boolean);
881
+ return candidates.find((candidate) => /\.(?:exe|cmd|bat)$/i.test(candidate)) || "";
882
+ }
883
+
884
+ function quoteWindowsCommandArg(value) {
885
+ return `"${String(value).replace(/(["^&|<>()%])/g, "^$1")}"`;
886
+ }
887
+
542
888
  function parseGoalFeature(output) {
543
889
  const line = output.split(/\r?\n/).find((candidate) => candidate.trim().startsWith("goals"));
544
890
  if (!line) return { enabled: false, stage: "" };
@@ -788,6 +1134,11 @@ async function extendInstall() {
788
1134
  async function extendInstallAll(catalog) {
789
1135
  const results = [];
790
1136
  for (const extension of catalog.extensions) {
1137
+ if (existsSync(extensionTarget(extension.id)) && !hasFlag("--force")) {
1138
+ validateCatalogExtension(extension);
1139
+ results.push({ extension, target: extensionTarget(extension.id), plan: installPlan(catalog, extension, extensionTarget(extension.id)), skipped: true });
1140
+ continue;
1141
+ }
791
1142
  results.push(await installCatalogExtension(catalog, extension));
792
1143
  }
793
1144
 
@@ -815,11 +1166,13 @@ async function extendInstallAll(catalog) {
815
1166
  printJson({
816
1167
  installed: true,
817
1168
  count: results.length,
818
- extensions: results.map(({ extension, target }) => ({ id: extension.id, target })),
1169
+ extensions: results.map(({ extension, target, skipped }) => ({ id: extension.id, target, skipped: Boolean(skipped) })),
819
1170
  });
820
1171
  } else {
821
- console.log(`Installed ${results.length} extensions`);
822
- for (const { extension, target } of results) console.log(` ${extension.id} -> ${target}`);
1172
+ const installedCount = results.filter((result) => !result.skipped).length;
1173
+ const skippedCount = results.length - installedCount;
1174
+ console.log(`Installed ${installedCount} extensions${skippedCount ? `, skipped ${skippedCount} already installed` : ""}`);
1175
+ for (const { extension, target, skipped } of results) console.log(` ${extension.id} -> ${target}${skipped ? " (already installed)" : ""}`);
823
1176
  }
824
1177
  }
825
1178
 
@@ -1058,15 +1411,18 @@ function listFiles(root, { exclude = new Set(), prefix = "" } = {}) {
1058
1411
  return files;
1059
1412
  }
1060
1413
 
1061
- function preserveInstalledExtensions(targets) {
1414
+ function preserveInstalledExtensions(targets, { tempRoot = "" } = {}) {
1062
1415
  const ids = [];
1063
- const tempPath = join(codexHome(), `.goalbuddy-preserved-extend-${process.pid}-${Date.now()}`);
1416
+ const firstTarget = targets.find(Boolean) || codexHome();
1417
+ const tempPath = join(tempRoot || dirname(dirname(firstTarget)), `.goalbuddy-preserved-extend-${process.pid}-${Date.now()}`);
1064
1418
  let hasExtensions = false;
1065
1419
  for (const target of targets) {
1420
+ if (!target) continue;
1066
1421
  const source = join(target, "extend");
1067
1422
  if (!existsSync(source)) continue;
1068
1423
  mkdirSync(tempPath, { recursive: true });
1069
1424
  for (const entry of readdirSync(source, { withFileTypes: true })) {
1425
+ if (bundledCoreExtensionIds.has(entry.name)) continue;
1070
1426
  const from = join(source, entry.name);
1071
1427
  const to = join(tempPath, entry.name);
1072
1428
  cpSync(from, to, { recursive: true, force: true });
@@ -1080,9 +1436,11 @@ function preserveInstalledExtensions(targets) {
1080
1436
 
1081
1437
  function restoreInstalledExtensions(target, tempPath) {
1082
1438
  if (!tempPath) return;
1083
- rmSync(join(target, "extend"), { recursive: true, force: true });
1084
- mkdirSync(target, { recursive: true });
1085
- cpSync(tempPath, join(target, "extend"), { recursive: true });
1439
+ const destinationRoot = join(target, "extend");
1440
+ mkdirSync(destinationRoot, { recursive: true });
1441
+ for (const entry of readdirSync(tempPath, { withFileTypes: true })) {
1442
+ cpSync(join(tempPath, entry.name), join(destinationRoot, entry.name), { recursive: true, force: true });
1443
+ }
1086
1444
  }
1087
1445
 
1088
1446
  function cleanupPreservedExtensions(paths) {
@@ -1215,6 +1573,41 @@ function printInstallReport(report) {
1215
1573
  console.log(` ${legacyCliName} remains a temporary compatibility alias.`);
1216
1574
  }
1217
1575
 
1576
+ function printEverywhereInstallReport(report) {
1577
+ const verb = report.command === "update" ? "Updated" : "Installed";
1578
+ console.log("");
1579
+ console.log(`${verb} ${canonicalProductName} for Codex and Claude Code ${report.package.current_version}`);
1580
+ console.log("");
1581
+
1582
+ if (report.codex?.ok === false) {
1583
+ console.log(`Codex: not completed (${report.codex.error})`);
1584
+ } else if (report.codex) {
1585
+ console.log(`Codex: plugin ${report.codex.version} enabled at ${report.codex.cache_path}`);
1586
+ if (report.codex.preserved_extensions?.length) {
1587
+ console.log(`Codex preserved extensions: ${report.codex.preserved_extensions.join(", ")}`);
1588
+ }
1589
+ }
1590
+
1591
+ if (report.claude?.ok === false) {
1592
+ console.log(`Claude Code: not completed (${report.claude.error})`);
1593
+ } else if (report.claude) {
1594
+ console.log(`Claude Code: skill ${report.claude.skill.status} at ${report.claude.skill.path}`);
1595
+ console.log(`Claude Code agents: ${summarizeStatuses(report.claude.agents)}`);
1596
+ console.log(`Claude Code commands: ${summarizeStatuses(report.claude.commands)}`);
1597
+ }
1598
+
1599
+ if (report.errors.length) {
1600
+ console.log("");
1601
+ console.log("One or more targets need attention:");
1602
+ for (const error of report.errors) console.log(` ${error.target}: ${error.error}`);
1603
+ }
1604
+
1605
+ console.log("");
1606
+ console.log("Next:");
1607
+ console.log(` Restart Codex, then use: $${canonicalSkillName}`);
1608
+ console.log(" Restart Claude Code, then run: /goal-prep");
1609
+ }
1610
+
1218
1611
  function summarizeStatuses(items) {
1219
1612
  const counts = items.reduce((memo, item) => {
1220
1613
  memo[item.status] = (memo[item.status] || 0) + 1;
@@ -12,7 +12,7 @@ if (!globalInstall || process.env.GOALBUDDY_SKIP_POSTINSTALL) {
12
12
  process.exit(0);
13
13
  }
14
14
 
15
- const result = spawnSync(process.execPath, [cliPath, "plugin", "install"], {
15
+ const result = spawnSync(process.execPath, [cliPath], {
16
16
  encoding: "utf8",
17
17
  env: process.env,
18
18
  stdio: "inherit",
@@ -23,7 +23,7 @@ if (result.status === 0) {
23
23
  }
24
24
 
25
25
  console.error("");
26
- console.error("GoalBuddy installed globally, but Codex plugin setup did not complete.");
27
- console.error("Run this after Codex is available:");
26
+ console.error("GoalBuddy installed globally, but setup did not complete for every target.");
27
+ console.error("Run this after Codex and Claude Code are available:");
28
28
  console.error(" goalbuddy");
29
29
  process.exit(0);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "goalbuddy",
3
- "version": "0.2.21",
4
- "description": "Turn open-ended Codex goals into a GoalBuddy Scout/Judge/Worker board with receipts, verification, and optional extensions.",
3
+ "version": "0.3.0",
4
+ "description": "A /goal operating system for Codex and Claude Code: Scout/Judge/Worker boards with visual board surfaces, receipts, and verification.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "goalbuddy": "internal/cli/goal-maker.mjs",
@@ -18,6 +18,7 @@
18
18
  "goalbuddy/SKILL.md",
19
19
  "goalbuddy/agents",
20
20
  "goalbuddy/scripts",
21
+ "goalbuddy/extend",
21
22
  "goalbuddy/templates"
22
23
  ],
23
24
  "scripts": {
@@ -36,6 +37,10 @@
36
37
  "codex",
37
38
  "codex-skill",
38
39
  "openai-codex",
40
+ "claude-code",
41
+ "claude-code-plugin",
42
+ "claude-code-skill",
43
+ "anthropic",
39
44
  "goalbuddy",
40
45
  "goal",
41
46
  "task-board",