javi-forge 0.1.0 → 1.0.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 (159) hide show
  1. package/.releaserc +2 -1
  2. package/README.md +143 -31
  3. package/ai-config/commands/workflows/diagnose.md +70 -0
  4. package/ai-config/commands/workflows/discover.md +86 -0
  5. package/dist/commands/doctor.js +24 -1
  6. package/dist/commands/init.js +48 -1
  7. package/dist/commands/llmstxt.d.ts +9 -0
  8. package/dist/commands/llmstxt.js +93 -0
  9. package/dist/commands/llmstxt.test.d.ts +2 -0
  10. package/dist/commands/plugin.d.ts +24 -0
  11. package/dist/commands/plugin.js +78 -0
  12. package/dist/commands/plugin.test.d.ts +2 -0
  13. package/dist/constants.d.ts +8 -0
  14. package/dist/constants.js +8 -0
  15. package/dist/index.js +33 -4
  16. package/dist/lib/plugin.d.ts +39 -0
  17. package/dist/lib/plugin.js +228 -0
  18. package/dist/lib/plugin.test.d.ts +2 -0
  19. package/dist/types/index.d.ts +42 -0
  20. package/dist/ui/App.d.ts +2 -1
  21. package/dist/ui/App.js +2 -1
  22. package/dist/ui/LlmsTxt.d.ts +8 -0
  23. package/dist/ui/LlmsTxt.js +48 -0
  24. package/dist/ui/Plugin.d.ts +9 -0
  25. package/dist/ui/Plugin.js +96 -0
  26. package/modules/obsidian-brain/README.md +32 -0
  27. package/modules/obsidian-brain/core/templates/braindump.md +15 -0
  28. package/modules/obsidian-brain/core/templates/consolidation.md +42 -0
  29. package/modules/obsidian-brain/core/templates/daily-note.md +18 -0
  30. package/modules/obsidian-brain/core/templates/resource-capture.md +14 -0
  31. package/modules/obsidian-brain/developer/templates/adr.md +40 -0
  32. package/modules/obsidian-brain/developer/templates/coding-session.md +24 -0
  33. package/modules/obsidian-brain/developer/templates/debug-journal.md +22 -0
  34. package/modules/obsidian-brain/developer/templates/sdd-feedback.md +27 -0
  35. package/modules/obsidian-brain/developer/templates/tech-debt.md +20 -0
  36. package/modules/obsidian-brain/pm-lead/templates/daily-brief.md +25 -0
  37. package/modules/obsidian-brain/pm-lead/templates/meeting-notes.md +24 -0
  38. package/modules/obsidian-brain/pm-lead/templates/risk-registry.md +12 -0
  39. package/modules/obsidian-brain/pm-lead/templates/sprint-review.md +27 -0
  40. package/modules/obsidian-brain/pm-lead/templates/stakeholder-update.md +24 -0
  41. package/modules/obsidian-brain/pm-lead/templates/team-intelligence.md +19 -0
  42. package/modules/obsidian-brain/pm-lead/templates/weekly-brief.md +29 -0
  43. package/package.json +1 -1
  44. package/schemas/plugin.schema.json +62 -0
  45. package/ai-config/skills/docs/api-documentation/SKILL.md +0 -293
  46. package/ai-config/skills/docs/docs-spring/SKILL.md +0 -377
  47. package/ai-config/skills/docs/mustache-templates/SKILL.md +0 -190
  48. package/ai-config/skills/docs/technical-docs/SKILL.md +0 -447
  49. package/dist/commands/analyze.d.ts.map +0 -1
  50. package/dist/commands/analyze.js.map +0 -1
  51. package/dist/commands/analyze.test.d.ts.map +0 -1
  52. package/dist/commands/analyze.test.js +0 -145
  53. package/dist/commands/analyze.test.js.map +0 -1
  54. package/dist/commands/doctor.d.ts.map +0 -1
  55. package/dist/commands/doctor.js.map +0 -1
  56. package/dist/commands/doctor.test.d.ts.map +0 -1
  57. package/dist/commands/doctor.test.js +0 -200
  58. package/dist/commands/doctor.test.js.map +0 -1
  59. package/dist/commands/init.d.ts.map +0 -1
  60. package/dist/commands/init.js.map +0 -1
  61. package/dist/commands/init.test.d.ts.map +0 -1
  62. package/dist/commands/init.test.js +0 -271
  63. package/dist/commands/init.test.js.map +0 -1
  64. package/dist/commands/sync.d.ts.map +0 -1
  65. package/dist/commands/sync.js.map +0 -1
  66. package/dist/constants.d.ts.map +0 -1
  67. package/dist/constants.js.map +0 -1
  68. package/dist/e2e/aggressive.e2e.test.d.ts.map +0 -1
  69. package/dist/e2e/aggressive.e2e.test.js +0 -350
  70. package/dist/e2e/aggressive.e2e.test.js.map +0 -1
  71. package/dist/e2e/commands.e2e.test.d.ts.map +0 -1
  72. package/dist/e2e/commands.e2e.test.js +0 -213
  73. package/dist/e2e/commands.e2e.test.js.map +0 -1
  74. package/dist/index.d.ts.map +0 -1
  75. package/dist/index.js.map +0 -1
  76. package/dist/lib/common.d.ts.map +0 -1
  77. package/dist/lib/common.js.map +0 -1
  78. package/dist/lib/common.test.d.ts.map +0 -1
  79. package/dist/lib/common.test.js +0 -316
  80. package/dist/lib/common.test.js.map +0 -1
  81. package/dist/lib/frontmatter.d.ts.map +0 -1
  82. package/dist/lib/frontmatter.js.map +0 -1
  83. package/dist/lib/frontmatter.test.d.ts.map +0 -1
  84. package/dist/lib/frontmatter.test.js +0 -257
  85. package/dist/lib/frontmatter.test.js.map +0 -1
  86. package/dist/lib/template.d.ts.map +0 -1
  87. package/dist/lib/template.js.map +0 -1
  88. package/dist/lib/template.test.d.ts.map +0 -1
  89. package/dist/lib/template.test.js +0 -201
  90. package/dist/lib/template.test.js.map +0 -1
  91. package/dist/types/index.d.ts.map +0 -1
  92. package/dist/types/index.js.map +0 -1
  93. package/dist/ui/AnalyzeUI.d.ts.map +0 -1
  94. package/dist/ui/AnalyzeUI.js.map +0 -1
  95. package/dist/ui/App.d.ts.map +0 -1
  96. package/dist/ui/App.js.map +0 -1
  97. package/dist/ui/CIContext.d.ts.map +0 -1
  98. package/dist/ui/CIContext.js.map +0 -1
  99. package/dist/ui/CISelector.d.ts.map +0 -1
  100. package/dist/ui/CISelector.js.map +0 -1
  101. package/dist/ui/Doctor.d.ts.map +0 -1
  102. package/dist/ui/Doctor.js.map +0 -1
  103. package/dist/ui/Header.d.ts.map +0 -1
  104. package/dist/ui/Header.js.map +0 -1
  105. package/dist/ui/MemorySelector.d.ts.map +0 -1
  106. package/dist/ui/MemorySelector.js.map +0 -1
  107. package/dist/ui/NameInput.d.ts.map +0 -1
  108. package/dist/ui/NameInput.js.map +0 -1
  109. package/dist/ui/OptionSelector.d.ts.map +0 -1
  110. package/dist/ui/OptionSelector.js.map +0 -1
  111. package/dist/ui/Progress.d.ts.map +0 -1
  112. package/dist/ui/Progress.js.map +0 -1
  113. package/dist/ui/StackSelector.d.ts.map +0 -1
  114. package/dist/ui/StackSelector.js.map +0 -1
  115. package/dist/ui/Summary.d.ts.map +0 -1
  116. package/dist/ui/Summary.js.map +0 -1
  117. package/dist/ui/SyncUI.d.ts.map +0 -1
  118. package/dist/ui/SyncUI.js.map +0 -1
  119. package/dist/ui/Welcome.d.ts.map +0 -1
  120. package/dist/ui/Welcome.js.map +0 -1
  121. package/dist/ui/theme.d.ts.map +0 -1
  122. package/dist/ui/theme.js.map +0 -1
  123. package/modules/obsidian-brain/.obsidian/plugins/dataview/data.json +0 -25
  124. package/modules/obsidian-brain/.obsidian/plugins/obsidian-kanban/data.json +0 -29
  125. package/modules/obsidian-brain/.obsidian/plugins/templater-obsidian/data.json +0 -18
  126. package/src/commands/analyze.test.ts +0 -145
  127. package/src/commands/analyze.ts +0 -69
  128. package/src/commands/doctor.test.ts +0 -208
  129. package/src/commands/doctor.ts +0 -163
  130. package/src/commands/init.test.ts +0 -298
  131. package/src/commands/init.ts +0 -285
  132. package/src/constants.ts +0 -69
  133. package/src/e2e/aggressive.e2e.test.ts +0 -557
  134. package/src/e2e/commands.e2e.test.ts +0 -298
  135. package/src/index.tsx +0 -106
  136. package/src/lib/common.test.ts +0 -318
  137. package/src/lib/common.ts +0 -127
  138. package/src/lib/frontmatter.test.ts +0 -291
  139. package/src/lib/frontmatter.ts +0 -77
  140. package/src/lib/template.test.ts +0 -226
  141. package/src/lib/template.ts +0 -99
  142. package/src/types/index.ts +0 -53
  143. package/src/ui/AnalyzeUI.tsx +0 -133
  144. package/src/ui/App.tsx +0 -175
  145. package/src/ui/CIContext.tsx +0 -25
  146. package/src/ui/CISelector.tsx +0 -72
  147. package/src/ui/Doctor.tsx +0 -122
  148. package/src/ui/Header.tsx +0 -48
  149. package/src/ui/MemorySelector.tsx +0 -73
  150. package/src/ui/NameInput.tsx +0 -82
  151. package/src/ui/OptionSelector.tsx +0 -100
  152. package/src/ui/Progress.tsx +0 -88
  153. package/src/ui/StackSelector.tsx +0 -101
  154. package/src/ui/Summary.tsx +0 -134
  155. package/src/ui/Welcome.tsx +0 -54
  156. package/src/ui/theme.ts +0 -10
  157. package/stryker.config.json +0 -19
  158. package/tsconfig.json +0 -19
  159. package/vitest.config.ts +0 -16
@@ -14,6 +14,14 @@ export declare const SCHEMAS_DIR: string;
14
14
  export declare const CI_LOCAL_DIR: string;
15
15
  /** Dependabot fragment directory */
16
16
  export declare const DEPENDABOT_FRAGMENTS_DIR: string;
17
+ /** Plugins directory (installed plugins) */
18
+ export declare const PLUGINS_DIR: string;
19
+ /** Plugin registry URL */
20
+ export declare const PLUGIN_REGISTRY_URL = "https://raw.githubusercontent.com/JNZader/javi-forge-registry/main/registry.json";
21
+ /** Plugin manifest filename */
22
+ export declare const PLUGIN_MANIFEST_FILE = "plugin.json";
23
+ /** Valid plugin asset directories */
24
+ export declare const PLUGIN_ASSET_DIRS: readonly ["skills", "commands", "hooks", "agents"];
17
25
  /** Stack-to-dependabot fragment mapping */
18
26
  export declare const STACK_DEPENDABOT_MAP: Record<string, string[]>;
19
27
  /** Stack-to-CI template filename mapping */
package/dist/constants.js CHANGED
@@ -17,6 +17,14 @@ export const SCHEMAS_DIR = path.join(FORGE_ROOT, 'schemas');
17
17
  export const CI_LOCAL_DIR = path.join(FORGE_ROOT, 'ci-local');
18
18
  /** Dependabot fragment directory */
19
19
  export const DEPENDABOT_FRAGMENTS_DIR = path.join(TEMPLATES_DIR, 'common', 'dependabot');
20
+ /** Plugins directory (installed plugins) */
21
+ export const PLUGINS_DIR = path.join(FORGE_ROOT, 'plugins');
22
+ /** Plugin registry URL */
23
+ export const PLUGIN_REGISTRY_URL = 'https://raw.githubusercontent.com/JNZader/javi-forge-registry/main/registry.json';
24
+ /** Plugin manifest filename */
25
+ export const PLUGIN_MANIFEST_FILE = 'plugin.json';
26
+ /** Valid plugin asset directories */
27
+ export const PLUGIN_ASSET_DIRS = ['skills', 'commands', 'hooks', 'agents'];
20
28
  /** Stack-to-dependabot fragment mapping */
21
29
  export const STACK_DEPENDABOT_MAP = {
22
30
  'node': ['npm'],
package/dist/index.js CHANGED
@@ -5,15 +5,23 @@ import meow from 'meow';
5
5
  import App from './ui/App.js';
6
6
  import Doctor from './ui/Doctor.js';
7
7
  import AnalyzeUI from './ui/AnalyzeUI.js';
8
+ import Plugin from './ui/Plugin.js';
9
+ import LlmsTxt from './ui/LlmsTxt.js';
8
10
  import { CIProvider as CIContextProvider } from './ui/CIContext.js';
9
11
  const cli = meow(`
10
12
  Usage
11
13
  $ javi-forge [command] [options]
12
14
 
13
15
  Commands
14
- init Bootstrap a new project (default)
15
- analyze Run repoforge skills analysis
16
- doctor Show health report
16
+ init Bootstrap a new project (default)
17
+ analyze Run repoforge skills analysis
18
+ doctor Show health report
19
+ plugin add Install a plugin from GitHub (org/repo)
20
+ plugin remove Remove an installed plugin
21
+ plugin list List installed plugins
22
+ plugin search Search the plugin registry
23
+ plugin validate Validate a local plugin directory
24
+ llms-txt Generate AI-friendly llms.txt for current project
17
25
 
18
26
  Options
19
27
  --dry-run Preview changes without writing files
@@ -22,6 +30,7 @@ const cli = meow(`
22
30
  --memory Memory module (engram, obsidian-brain, memory-simple, none)
23
31
  --project-name Project name (skips name prompt)
24
32
  --ghagga Enable GHAGGA review system
33
+ --mock Enable mock-first mode (no real API keys needed)
25
34
  --batch Non-interactive mode (auto-proceed, no keyboard input)
26
35
  --version Show version
27
36
  --help Show this help
@@ -34,6 +43,11 @@ const cli = meow(`
34
43
  $ javi-forge analyze
35
44
  $ javi-forge analyze --dry-run
36
45
  $ javi-forge doctor
46
+ $ javi-forge plugin add mapbox/agent-skills
47
+ $ javi-forge plugin list
48
+ $ javi-forge plugin search ai
49
+ $ javi-forge plugin validate ./my-plugin
50
+ $ javi-forge plugin remove my-plugin
37
51
  `, {
38
52
  importMeta: import.meta,
39
53
  flags: {
@@ -43,6 +57,7 @@ const cli = meow(`
43
57
  memory: { type: 'string', default: '' },
44
58
  projectName: { type: 'string', default: '' },
45
59
  ghagga: { type: 'boolean', default: false },
60
+ mock: { type: 'boolean', default: false },
46
61
  batch: { type: 'boolean', default: false },
47
62
  }
48
63
  });
@@ -62,6 +77,20 @@ switch (subcommand) {
62
77
  React.createElement(AnalyzeUI, { dryRun: cli.flags.dryRun })));
63
78
  break;
64
79
  }
80
+ case 'llms-txt': {
81
+ render(React.createElement(CIContextProvider, { isCI: isCI },
82
+ React.createElement(LlmsTxt, { projectDir: process.cwd(), dryRun: cli.flags.dryRun })));
83
+ break;
84
+ }
85
+ case 'plugin': {
86
+ const pluginAction = cli.input[1];
87
+ const VALID_PLUGIN_ACTIONS = ['add', 'remove', 'list', 'search', 'validate'];
88
+ const action = pluginAction && VALID_PLUGIN_ACTIONS.includes(pluginAction) ? pluginAction : 'list';
89
+ const target = cli.input[2];
90
+ render(React.createElement(CIContextProvider, { isCI: isCI },
91
+ React.createElement(Plugin, { action: action, target: target, dryRun: cli.flags.dryRun })));
92
+ break;
93
+ }
65
94
  case 'init':
66
95
  default: {
67
96
  const presetStack = VALID_STACKS.includes(cli.flags.stack)
@@ -75,7 +104,7 @@ switch (subcommand) {
75
104
  : undefined;
76
105
  const presetName = cli.flags.projectName || undefined;
77
106
  render(React.createElement(CIContextProvider, { isCI: isCI },
78
- React.createElement(App, { dryRun: cli.flags.dryRun, presetStack: presetStack, presetCI: presetCI, presetMemory: presetMemory, presetName: presetName, presetGhagga: cli.flags.ghagga })));
107
+ React.createElement(App, { dryRun: cli.flags.dryRun, presetStack: presetStack, presetCI: presetCI, presetMemory: presetMemory, presetName: presetName, presetGhagga: cli.flags.ghagga, presetMock: cli.flags.mock ?? false })));
79
108
  break;
80
109
  }
81
110
  }
@@ -0,0 +1,39 @@
1
+ import type { PluginValidationResult, InstalledPlugin, PluginRegistryEntry } from '../types/index.js';
2
+ /**
3
+ * Validate a plugin directory structure and manifest.
4
+ */
5
+ export declare function validatePlugin(pluginDir: string): Promise<PluginValidationResult>;
6
+ /**
7
+ * Install a plugin from a GitHub repository.
8
+ * Clones the repo to a temp dir, validates, then copies to plugins dir.
9
+ */
10
+ export declare function installPlugin(source: string, options?: {
11
+ dryRun?: boolean;
12
+ }): Promise<{
13
+ success: boolean;
14
+ name?: string;
15
+ error?: string;
16
+ }>;
17
+ /**
18
+ * Remove an installed plugin by name.
19
+ */
20
+ export declare function removePlugin(name: string, options?: {
21
+ dryRun?: boolean;
22
+ }): Promise<{
23
+ success: boolean;
24
+ error?: string;
25
+ }>;
26
+ /**
27
+ * List all installed plugins.
28
+ */
29
+ export declare function listInstalledPlugins(): Promise<InstalledPlugin[]>;
30
+ /**
31
+ * Fetch the remote plugin registry and optionally filter by query.
32
+ */
33
+ export declare function searchRegistry(query?: string): Promise<PluginRegistryEntry[]>;
34
+ /**
35
+ * Normalize a GitHub source to a git clone URL.
36
+ * Accepts: "org/repo", "https://github.com/org/repo", "github.com/org/repo"
37
+ */
38
+ export declare function normalizeGitUrl(source: string): string | null;
39
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1,228 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { execFile } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import { PLUGINS_DIR, PLUGIN_MANIFEST_FILE, PLUGIN_ASSET_DIRS, PLUGIN_REGISTRY_URL } from '../constants.js';
6
+ const execFileAsync = promisify(execFile);
7
+ const KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
8
+ const SEMVER_RE = /^\d+\.\d+\.\d+$/;
9
+ // ── Validation ──────────────────────────────────────────────────────────────
10
+ /**
11
+ * Validate a plugin directory structure and manifest.
12
+ */
13
+ export async function validatePlugin(pluginDir) {
14
+ const errors = [];
15
+ // Check plugin.json exists
16
+ const manifestPath = path.join(pluginDir, PLUGIN_MANIFEST_FILE);
17
+ if (!await fs.pathExists(manifestPath)) {
18
+ return {
19
+ valid: false,
20
+ errors: [{ path: PLUGIN_MANIFEST_FILE, message: 'plugin.json not found' }],
21
+ manifest: null,
22
+ };
23
+ }
24
+ // Parse manifest
25
+ let manifest;
26
+ try {
27
+ manifest = await fs.readJson(manifestPath);
28
+ }
29
+ catch {
30
+ return {
31
+ valid: false,
32
+ errors: [{ path: PLUGIN_MANIFEST_FILE, message: 'invalid JSON' }],
33
+ manifest: null,
34
+ };
35
+ }
36
+ // Validate required fields
37
+ if (typeof manifest.name !== 'string' || !manifest.name) {
38
+ errors.push({ path: 'name', message: 'name is required' });
39
+ }
40
+ else if (!KEBAB_RE.test(manifest.name)) {
41
+ errors.push({ path: 'name', message: 'name must be kebab-case' });
42
+ }
43
+ else if (manifest.name.length < 2 || manifest.name.length > 60) {
44
+ errors.push({ path: 'name', message: 'name must be 2-60 characters' });
45
+ }
46
+ if (typeof manifest.version !== 'string' || !manifest.version) {
47
+ errors.push({ path: 'version', message: 'version is required' });
48
+ }
49
+ else if (!SEMVER_RE.test(manifest.version)) {
50
+ errors.push({ path: 'version', message: 'version must be semver (e.g. 1.0.0)' });
51
+ }
52
+ if (typeof manifest.description !== 'string' || !manifest.description) {
53
+ errors.push({ path: 'description', message: 'description is required' });
54
+ }
55
+ else if (manifest.description.length < 10) {
56
+ errors.push({ path: 'description', message: 'description must be at least 10 characters' });
57
+ }
58
+ else if (manifest.description.length > 200) {
59
+ errors.push({ path: 'description', message: 'description must be at most 200 characters' });
60
+ }
61
+ // Validate asset directories actually exist when declared
62
+ for (const assetType of PLUGIN_ASSET_DIRS) {
63
+ const declared = manifest[assetType];
64
+ if (!Array.isArray(declared) || declared.length === 0)
65
+ continue;
66
+ const assetDir = path.join(pluginDir, assetType);
67
+ if (!await fs.pathExists(assetDir)) {
68
+ errors.push({ path: assetType, message: `declared ${assetType}/ directory not found` });
69
+ continue;
70
+ }
71
+ // Check each declared asset exists
72
+ for (const entry of declared) {
73
+ const entryPath = path.join(assetDir, entry);
74
+ if (!await fs.pathExists(entryPath)) {
75
+ errors.push({ path: `${assetType}/${entry}`, message: `declared entry not found` });
76
+ }
77
+ }
78
+ }
79
+ // Validate tags
80
+ if (manifest.tags && !Array.isArray(manifest.tags)) {
81
+ errors.push({ path: 'tags', message: 'tags must be an array' });
82
+ }
83
+ else if (manifest.tags && manifest.tags.length > 10) {
84
+ errors.push({ path: 'tags', message: 'max 10 tags allowed' });
85
+ }
86
+ return { valid: errors.length === 0, errors, manifest: errors.length === 0 ? manifest : manifest };
87
+ }
88
+ // ── Installation ────────────────────────────────────────────────────────────
89
+ /**
90
+ * Install a plugin from a GitHub repository.
91
+ * Clones the repo to a temp dir, validates, then copies to plugins dir.
92
+ */
93
+ export async function installPlugin(source, options = {}) {
94
+ const { dryRun = false } = options;
95
+ // Normalize source to a git URL
96
+ const gitUrl = normalizeGitUrl(source);
97
+ if (!gitUrl) {
98
+ return { success: false, error: `invalid source: ${source}. Use org/repo or a GitHub URL` };
99
+ }
100
+ // Clone to temp
101
+ const tmpDir = path.join(PLUGINS_DIR, '.tmp', `install-${Date.now()}`);
102
+ try {
103
+ if (!dryRun) {
104
+ await fs.ensureDir(tmpDir);
105
+ await execFileAsync('git', ['clone', '--depth', '1', gitUrl, tmpDir], {
106
+ timeout: 60_000,
107
+ });
108
+ }
109
+ // Validate
110
+ const validation = dryRun
111
+ ? { valid: true, errors: [], manifest: { name: source.split('/').pop() ?? 'unknown', version: '0.0.0', description: 'dry-run placeholder' } }
112
+ : await validatePlugin(tmpDir);
113
+ if (!validation.valid || !validation.manifest) {
114
+ const msgs = validation.errors.map(e => ` ${e.path}: ${e.message}`).join('\n');
115
+ return { success: false, error: `validation failed:\n${msgs}` };
116
+ }
117
+ const pluginName = validation.manifest.name;
118
+ const destDir = path.join(PLUGINS_DIR, pluginName);
119
+ if (!dryRun) {
120
+ // Remove existing version if present
121
+ if (await fs.pathExists(destDir)) {
122
+ await fs.remove(destDir);
123
+ }
124
+ await fs.move(tmpDir, destDir);
125
+ // Write install metadata
126
+ const installedPlugin = {
127
+ name: pluginName,
128
+ version: validation.manifest.version,
129
+ installedAt: new Date().toISOString(),
130
+ source,
131
+ manifest: validation.manifest,
132
+ };
133
+ await fs.writeJson(path.join(destDir, '.installed.json'), installedPlugin, { spaces: 2 });
134
+ }
135
+ return { success: true, name: pluginName };
136
+ }
137
+ catch (e) {
138
+ const msg = e instanceof Error ? e.message : String(e);
139
+ return { success: false, error: msg };
140
+ }
141
+ finally {
142
+ // Clean up tmp if it still exists (error path)
143
+ if (!dryRun && await fs.pathExists(tmpDir)) {
144
+ await fs.remove(tmpDir).catch(() => { });
145
+ }
146
+ }
147
+ }
148
+ /**
149
+ * Remove an installed plugin by name.
150
+ */
151
+ export async function removePlugin(name, options = {}) {
152
+ const pluginDir = path.join(PLUGINS_DIR, name);
153
+ if (!await fs.pathExists(pluginDir)) {
154
+ return { success: false, error: `plugin "${name}" is not installed` };
155
+ }
156
+ if (!options.dryRun) {
157
+ await fs.remove(pluginDir);
158
+ }
159
+ return { success: true };
160
+ }
161
+ // ── Listing & Search ────────────────────────────────────────────────────────
162
+ /**
163
+ * List all installed plugins.
164
+ */
165
+ export async function listInstalledPlugins() {
166
+ if (!await fs.pathExists(PLUGINS_DIR))
167
+ return [];
168
+ const entries = await fs.readdir(PLUGINS_DIR);
169
+ const plugins = [];
170
+ for (const entry of entries) {
171
+ if (entry.startsWith('.'))
172
+ continue;
173
+ const metaPath = path.join(PLUGINS_DIR, entry, '.installed.json');
174
+ if (await fs.pathExists(metaPath)) {
175
+ try {
176
+ const meta = await fs.readJson(metaPath);
177
+ plugins.push(meta);
178
+ }
179
+ catch { /* skip corrupt entries */ }
180
+ }
181
+ }
182
+ return plugins;
183
+ }
184
+ /**
185
+ * Fetch the remote plugin registry and optionally filter by query.
186
+ */
187
+ export async function searchRegistry(query) {
188
+ try {
189
+ const response = await fetch(PLUGIN_REGISTRY_URL);
190
+ if (!response.ok) {
191
+ return [];
192
+ }
193
+ const registry = await response.json();
194
+ let plugins = registry.plugins ?? [];
195
+ if (query) {
196
+ const q = query.toLowerCase();
197
+ plugins = plugins.filter(p => p.id.toLowerCase().includes(q) ||
198
+ p.description.toLowerCase().includes(q) ||
199
+ p.tags.some(t => t.toLowerCase().includes(q)));
200
+ }
201
+ return plugins;
202
+ }
203
+ catch {
204
+ return [];
205
+ }
206
+ }
207
+ // ── Helpers ─────────────────────────────────────────────────────────────────
208
+ /**
209
+ * Normalize a GitHub source to a git clone URL.
210
+ * Accepts: "org/repo", "https://github.com/org/repo", "github.com/org/repo"
211
+ */
212
+ export function normalizeGitUrl(source) {
213
+ // Already a full URL
214
+ if (source.startsWith('https://github.com/')) {
215
+ return source.endsWith('.git') ? source : `${source}.git`;
216
+ }
217
+ // github.com/org/repo
218
+ if (source.startsWith('github.com/')) {
219
+ return `https://${source}.git`;
220
+ }
221
+ // org/repo shorthand
222
+ const parts = source.split('/');
223
+ if (parts.length === 2 && parts[0] && parts[1]) {
224
+ return `https://github.com/${parts[0]}/${parts[1]}.git`;
225
+ }
226
+ return null;
227
+ }
228
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=plugin.test.d.ts.map
@@ -10,6 +10,7 @@ export interface InitOptions {
10
10
  aiSync: boolean;
11
11
  sdd: boolean;
12
12
  ghagga: boolean;
13
+ mock: boolean;
13
14
  dryRun: boolean;
14
15
  }
15
16
  export interface InitStep {
@@ -45,4 +46,45 @@ export interface DoctorSection {
45
46
  export interface DoctorResult {
46
47
  sections: DoctorSection[];
47
48
  }
49
+ export interface PluginManifest {
50
+ name: string;
51
+ version: string;
52
+ description: string;
53
+ author?: string;
54
+ repository?: string;
55
+ skills?: string[];
56
+ commands?: string[];
57
+ hooks?: string[];
58
+ agents?: string[];
59
+ tags?: string[];
60
+ }
61
+ export interface PluginRegistryEntry {
62
+ id: string;
63
+ repository: string;
64
+ description: string;
65
+ tags: string[];
66
+ stars?: number;
67
+ updatedAt?: string;
68
+ }
69
+ export interface PluginRegistry {
70
+ version: string;
71
+ updatedAt: string;
72
+ plugins: PluginRegistryEntry[];
73
+ }
74
+ export interface PluginValidationError {
75
+ path: string;
76
+ message: string;
77
+ }
78
+ export interface PluginValidationResult {
79
+ valid: boolean;
80
+ errors: PluginValidationError[];
81
+ manifest: PluginManifest | null;
82
+ }
83
+ export interface InstalledPlugin {
84
+ name: string;
85
+ version: string;
86
+ installedAt: string;
87
+ source: string;
88
+ manifest: PluginManifest;
89
+ }
48
90
  //# sourceMappingURL=index.d.ts.map
package/dist/ui/App.d.ts CHANGED
@@ -7,7 +7,8 @@ interface AppProps {
7
7
  presetMemory?: MemoryOption;
8
8
  presetName?: string;
9
9
  presetGhagga?: boolean;
10
+ presetMock?: boolean;
10
11
  }
11
- export default function App({ dryRun, presetStack, presetCI, presetMemory, presetName, presetGhagga, }: AppProps): React.JSX.Element;
12
+ export default function App({ dryRun, presetStack, presetCI, presetMemory, presetName, presetGhagga, presetMock, }: AppProps): React.JSX.Element;
12
13
  export {};
13
14
  //# sourceMappingURL=App.d.ts.map
package/dist/ui/App.js CHANGED
@@ -11,7 +11,7 @@ import OptionSelector from './OptionSelector.js';
11
11
  import Progress from './Progress.js';
12
12
  import Summary from './Summary.js';
13
13
  import { initProject } from '../commands/init.js';
14
- export default function App({ dryRun = false, presetStack, presetCI, presetMemory, presetName, presetGhagga = false, }) {
14
+ export default function App({ dryRun = false, presetStack, presetCI, presetMemory, presetName, presetGhagga = false, presetMock = false, }) {
15
15
  const [stage, setStage] = useState('welcome');
16
16
  const [projectName, setProjectName] = useState(presetName ?? '');
17
17
  const [projectDir, setProjectDir] = useState(presetName ? path.resolve(process.cwd(), presetName) : '');
@@ -54,6 +54,7 @@ export default function App({ dryRun = false, presetStack, presetCI, presetMemor
54
54
  aiSync: opts.aiSync,
55
55
  sdd: opts.sdd,
56
56
  ghagga: opts.ghagga,
57
+ mock: presetMock,
57
58
  dryRun,
58
59
  }, (step) => setSteps(prev => {
59
60
  const idx = prev.findIndex(s => s.id === step.id);
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ interface Props {
3
+ projectDir: string;
4
+ dryRun: boolean;
5
+ }
6
+ export default function LlmsTxt({ projectDir, dryRun }: Props): React.JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=LlmsTxt.d.ts.map
@@ -0,0 +1,48 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import { generateLlmsTxt } from '../commands/llmstxt.js';
5
+ import { theme } from './theme.js';
6
+ const STATUS_ICON = {
7
+ done: '\u2713', error: '\u2717', skipped: '\u2013',
8
+ };
9
+ export default function LlmsTxt({ projectDir, dryRun }) {
10
+ const [steps, setSteps] = useState([]);
11
+ const [done, setDone] = useState(false);
12
+ const onStep = (step) => {
13
+ setSteps(prev => {
14
+ const idx = prev.findIndex(s => s.id === step.id);
15
+ if (idx >= 0) {
16
+ const n = [...prev];
17
+ n[idx] = step;
18
+ return n;
19
+ }
20
+ return [...prev, step];
21
+ });
22
+ };
23
+ useEffect(() => {
24
+ generateLlmsTxt(projectDir, dryRun, onStep)
25
+ .catch(e => onStep({ id: 'fatal', label: 'Error', status: 'error', detail: String(e) }))
26
+ .finally(() => setDone(true));
27
+ }, [projectDir, dryRun]);
28
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
29
+ React.createElement(Box, { marginBottom: 1 },
30
+ React.createElement(Text, { bold: true, color: theme.primary }, "javi-forge"),
31
+ React.createElement(Text, null, " llms-txt"),
32
+ dryRun && React.createElement(Text, { color: theme.warning }, " (dry-run)")),
33
+ steps.map(s => (React.createElement(Box, { key: s.id, marginLeft: 2 }, s.status === 'running'
34
+ ? React.createElement(Text, { color: theme.warning },
35
+ React.createElement(Spinner, { type: "dots" }),
36
+ " ",
37
+ s.label)
38
+ : React.createElement(Text, { color: s.status === 'done' ? theme.success : s.status === 'error' ? theme.error : theme.muted },
39
+ STATUS_ICON[s.status],
40
+ " ",
41
+ s.label,
42
+ s.detail ? React.createElement(Text, { color: theme.muted, dimColor: true },
43
+ " ",
44
+ s.detail) : null)))),
45
+ done && React.createElement(Box, { marginTop: 1 },
46
+ React.createElement(Text, { color: theme.muted }, "Done."))));
47
+ }
48
+ //# sourceMappingURL=LlmsTxt.js.map
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface PluginProps {
3
+ action: 'add' | 'remove' | 'list' | 'search' | 'validate';
4
+ target?: string;
5
+ dryRun: boolean;
6
+ }
7
+ export default function Plugin({ action, target, dryRun }: PluginProps): React.JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=Plugin.d.ts.map
@@ -0,0 +1,96 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import { runPluginAdd, runPluginRemove, runPluginList, runPluginSearch, runPluginValidate } from '../commands/plugin.js';
5
+ import { theme } from './theme.js';
6
+ const STATUS_ICON = {
7
+ pending: '\u25cb',
8
+ done: '\u2713',
9
+ error: '\u2717',
10
+ skipped: '\u2013',
11
+ };
12
+ const STATUS_COLOR = {
13
+ pending: theme.muted,
14
+ running: theme.warning,
15
+ done: theme.success,
16
+ error: theme.error,
17
+ skipped: theme.muted,
18
+ };
19
+ export default function Plugin({ action, target, dryRun }) {
20
+ const [steps, setSteps] = useState([]);
21
+ const [done, setDone] = useState(false);
22
+ const onStep = (step) => {
23
+ setSteps(prev => {
24
+ const idx = prev.findIndex(s => s.id === step.id);
25
+ if (idx >= 0) {
26
+ const next = [...prev];
27
+ next[idx] = step;
28
+ return next;
29
+ }
30
+ return [...prev, step];
31
+ });
32
+ };
33
+ useEffect(() => {
34
+ const run = async () => {
35
+ try {
36
+ switch (action) {
37
+ case 'add':
38
+ if (!target) {
39
+ onStep({ id: 'err', label: 'Error', status: 'error', detail: 'source required: javi-forge plugin add <org/repo>' });
40
+ break;
41
+ }
42
+ await runPluginAdd(target, dryRun, onStep);
43
+ break;
44
+ case 'remove':
45
+ if (!target) {
46
+ onStep({ id: 'err', label: 'Error', status: 'error', detail: 'name required: javi-forge plugin remove <name>' });
47
+ break;
48
+ }
49
+ await runPluginRemove(target, dryRun, onStep);
50
+ break;
51
+ case 'list':
52
+ await runPluginList(onStep);
53
+ break;
54
+ case 'search':
55
+ await runPluginSearch(target, onStep);
56
+ break;
57
+ case 'validate':
58
+ if (!target) {
59
+ onStep({ id: 'err', label: 'Error', status: 'error', detail: 'path required: javi-forge plugin validate <dir>' });
60
+ break;
61
+ }
62
+ await runPluginValidate(target, onStep);
63
+ break;
64
+ }
65
+ }
66
+ catch (e) {
67
+ onStep({ id: 'fatal', label: 'Fatal error', status: 'error', detail: String(e) });
68
+ }
69
+ setDone(true);
70
+ };
71
+ run();
72
+ }, [action, target, dryRun]);
73
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
74
+ React.createElement(Box, { marginBottom: 1 },
75
+ React.createElement(Text, { bold: true, color: theme.primary }, "javi-forge"),
76
+ React.createElement(Text, null,
77
+ " plugin ",
78
+ action),
79
+ dryRun && React.createElement(Text, { color: theme.warning }, " (dry-run)")),
80
+ steps.map(step => (React.createElement(Box, { key: step.id, marginLeft: 2 }, step.status === 'running' ? (React.createElement(Text, { color: theme.warning },
81
+ React.createElement(Spinner, { type: "dots" }),
82
+ ' ',
83
+ step.label,
84
+ step.detail ? React.createElement(Text, { color: theme.muted, dimColor: true },
85
+ " ",
86
+ step.detail) : null)) : (React.createElement(Text, { color: STATUS_COLOR[step.status] },
87
+ STATUS_ICON[step.status],
88
+ " ",
89
+ step.label,
90
+ step.detail ? React.createElement(Text, { color: theme.muted, dimColor: true },
91
+ " ",
92
+ step.detail) : null))))),
93
+ done && (React.createElement(Box, { marginTop: 1 },
94
+ React.createElement(Text, { color: theme.muted }, "Done.")))));
95
+ }
96
+ //# sourceMappingURL=Plugin.js.map
@@ -266,3 +266,35 @@ Si ya usas vibekanban:
266
266
  3. Agregar frontmatter YAML a archivos existentes (CONTEXT, DECISIONS, etc.)
267
267
  4. Agregar inline fields Dataview a ADRs y blockers existentes
268
268
  5. Actualizar `.gitignore` con el snippet de Obsidian
269
+
270
+ ## Role-Based Templates
271
+
272
+ Obsidian Brain includes role-based templates organized by persona, sourced from vault/Javi.Dots:
273
+
274
+ ### Core Templates (`core/templates/`)
275
+ | Template | Purpose |
276
+ |----------|---------|
277
+ | `daily-note.md` | Daily focus, notes, reflections |
278
+ | `braindump.md` | Quick unstructured idea capture |
279
+ | `consolidation.md` | Weekly knowledge consolidation |
280
+ | `resource-capture.md` | Annotated resource bookmarking |
281
+
282
+ ### Developer Templates (`developer/templates/`)
283
+ | Template | Purpose |
284
+ |----------|---------|
285
+ | `coding-session.md` | Goal, work log, blockers, decisions, next steps |
286
+ | `adr.md` | Architecture Decision Record with decision map |
287
+ | `debug-journal.md` | Structured debugging sessions |
288
+ | `sdd-feedback.md` | Spec-Driven Development feedback |
289
+ | `tech-debt.md` | Technical debt tracking |
290
+
291
+ ### PM/Lead Templates (`pm-lead/templates/`)
292
+ | Template | Purpose |
293
+ |----------|---------|
294
+ | `daily-brief.md` | Daily standup summary |
295
+ | `weekly-brief.md` | Weekly progress report |
296
+ | `sprint-review.md` | Sprint retrospective |
297
+ | `meeting-notes.md` | Structured meeting notes |
298
+ | `risk-registry.md` | Risk tracking and mitigation |
299
+ | `stakeholder-update.md` | Stakeholder communication |
300
+ | `team-intelligence.md` | Team performance patterns |