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.
- package/.releaserc +2 -1
- package/README.md +143 -31
- package/ai-config/commands/workflows/diagnose.md +70 -0
- package/ai-config/commands/workflows/discover.md +86 -0
- package/dist/commands/doctor.js +24 -1
- package/dist/commands/init.js +48 -1
- package/dist/commands/llmstxt.d.ts +9 -0
- package/dist/commands/llmstxt.js +93 -0
- package/dist/commands/llmstxt.test.d.ts +2 -0
- package/dist/commands/plugin.d.ts +24 -0
- package/dist/commands/plugin.js +78 -0
- package/dist/commands/plugin.test.d.ts +2 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +8 -0
- package/dist/index.js +33 -4
- package/dist/lib/plugin.d.ts +39 -0
- package/dist/lib/plugin.js +228 -0
- package/dist/lib/plugin.test.d.ts +2 -0
- package/dist/types/index.d.ts +42 -0
- package/dist/ui/App.d.ts +2 -1
- package/dist/ui/App.js +2 -1
- package/dist/ui/LlmsTxt.d.ts +8 -0
- package/dist/ui/LlmsTxt.js +48 -0
- package/dist/ui/Plugin.d.ts +9 -0
- package/dist/ui/Plugin.js +96 -0
- package/modules/obsidian-brain/README.md +32 -0
- package/modules/obsidian-brain/core/templates/braindump.md +15 -0
- package/modules/obsidian-brain/core/templates/consolidation.md +42 -0
- package/modules/obsidian-brain/core/templates/daily-note.md +18 -0
- package/modules/obsidian-brain/core/templates/resource-capture.md +14 -0
- package/modules/obsidian-brain/developer/templates/adr.md +40 -0
- package/modules/obsidian-brain/developer/templates/coding-session.md +24 -0
- package/modules/obsidian-brain/developer/templates/debug-journal.md +22 -0
- package/modules/obsidian-brain/developer/templates/sdd-feedback.md +27 -0
- package/modules/obsidian-brain/developer/templates/tech-debt.md +20 -0
- package/modules/obsidian-brain/pm-lead/templates/daily-brief.md +25 -0
- package/modules/obsidian-brain/pm-lead/templates/meeting-notes.md +24 -0
- package/modules/obsidian-brain/pm-lead/templates/risk-registry.md +12 -0
- package/modules/obsidian-brain/pm-lead/templates/sprint-review.md +27 -0
- package/modules/obsidian-brain/pm-lead/templates/stakeholder-update.md +24 -0
- package/modules/obsidian-brain/pm-lead/templates/team-intelligence.md +19 -0
- package/modules/obsidian-brain/pm-lead/templates/weekly-brief.md +29 -0
- package/package.json +1 -1
- package/schemas/plugin.schema.json +62 -0
- package/ai-config/skills/docs/api-documentation/SKILL.md +0 -293
- package/ai-config/skills/docs/docs-spring/SKILL.md +0 -377
- package/ai-config/skills/docs/mustache-templates/SKILL.md +0 -190
- package/ai-config/skills/docs/technical-docs/SKILL.md +0 -447
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/analyze.test.d.ts.map +0 -1
- package/dist/commands/analyze.test.js +0 -145
- package/dist/commands/analyze.test.js.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/doctor.test.d.ts.map +0 -1
- package/dist/commands/doctor.test.js +0 -200
- package/dist/commands/doctor.test.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/init.test.d.ts.map +0 -1
- package/dist/commands/init.test.js +0 -271
- package/dist/commands/init.test.js.map +0 -1
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/sync.js.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/e2e/aggressive.e2e.test.d.ts.map +0 -1
- package/dist/e2e/aggressive.e2e.test.js +0 -350
- package/dist/e2e/aggressive.e2e.test.js.map +0 -1
- package/dist/e2e/commands.e2e.test.d.ts.map +0 -1
- package/dist/e2e/commands.e2e.test.js +0 -213
- package/dist/e2e/commands.e2e.test.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/common.d.ts.map +0 -1
- package/dist/lib/common.js.map +0 -1
- package/dist/lib/common.test.d.ts.map +0 -1
- package/dist/lib/common.test.js +0 -316
- package/dist/lib/common.test.js.map +0 -1
- package/dist/lib/frontmatter.d.ts.map +0 -1
- package/dist/lib/frontmatter.js.map +0 -1
- package/dist/lib/frontmatter.test.d.ts.map +0 -1
- package/dist/lib/frontmatter.test.js +0 -257
- package/dist/lib/frontmatter.test.js.map +0 -1
- package/dist/lib/template.d.ts.map +0 -1
- package/dist/lib/template.js.map +0 -1
- package/dist/lib/template.test.d.ts.map +0 -1
- package/dist/lib/template.test.js +0 -201
- package/dist/lib/template.test.js.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/ui/AnalyzeUI.d.ts.map +0 -1
- package/dist/ui/AnalyzeUI.js.map +0 -1
- package/dist/ui/App.d.ts.map +0 -1
- package/dist/ui/App.js.map +0 -1
- package/dist/ui/CIContext.d.ts.map +0 -1
- package/dist/ui/CIContext.js.map +0 -1
- package/dist/ui/CISelector.d.ts.map +0 -1
- package/dist/ui/CISelector.js.map +0 -1
- package/dist/ui/Doctor.d.ts.map +0 -1
- package/dist/ui/Doctor.js.map +0 -1
- package/dist/ui/Header.d.ts.map +0 -1
- package/dist/ui/Header.js.map +0 -1
- package/dist/ui/MemorySelector.d.ts.map +0 -1
- package/dist/ui/MemorySelector.js.map +0 -1
- package/dist/ui/NameInput.d.ts.map +0 -1
- package/dist/ui/NameInput.js.map +0 -1
- package/dist/ui/OptionSelector.d.ts.map +0 -1
- package/dist/ui/OptionSelector.js.map +0 -1
- package/dist/ui/Progress.d.ts.map +0 -1
- package/dist/ui/Progress.js.map +0 -1
- package/dist/ui/StackSelector.d.ts.map +0 -1
- package/dist/ui/StackSelector.js.map +0 -1
- package/dist/ui/Summary.d.ts.map +0 -1
- package/dist/ui/Summary.js.map +0 -1
- package/dist/ui/SyncUI.d.ts.map +0 -1
- package/dist/ui/SyncUI.js.map +0 -1
- package/dist/ui/Welcome.d.ts.map +0 -1
- package/dist/ui/Welcome.js.map +0 -1
- package/dist/ui/theme.d.ts.map +0 -1
- package/dist/ui/theme.js.map +0 -1
- package/modules/obsidian-brain/.obsidian/plugins/dataview/data.json +0 -25
- package/modules/obsidian-brain/.obsidian/plugins/obsidian-kanban/data.json +0 -29
- package/modules/obsidian-brain/.obsidian/plugins/templater-obsidian/data.json +0 -18
- package/src/commands/analyze.test.ts +0 -145
- package/src/commands/analyze.ts +0 -69
- package/src/commands/doctor.test.ts +0 -208
- package/src/commands/doctor.ts +0 -163
- package/src/commands/init.test.ts +0 -298
- package/src/commands/init.ts +0 -285
- package/src/constants.ts +0 -69
- package/src/e2e/aggressive.e2e.test.ts +0 -557
- package/src/e2e/commands.e2e.test.ts +0 -298
- package/src/index.tsx +0 -106
- package/src/lib/common.test.ts +0 -318
- package/src/lib/common.ts +0 -127
- package/src/lib/frontmatter.test.ts +0 -291
- package/src/lib/frontmatter.ts +0 -77
- package/src/lib/template.test.ts +0 -226
- package/src/lib/template.ts +0 -99
- package/src/types/index.ts +0 -53
- package/src/ui/AnalyzeUI.tsx +0 -133
- package/src/ui/App.tsx +0 -175
- package/src/ui/CIContext.tsx +0 -25
- package/src/ui/CISelector.tsx +0 -72
- package/src/ui/Doctor.tsx +0 -122
- package/src/ui/Header.tsx +0 -48
- package/src/ui/MemorySelector.tsx +0 -73
- package/src/ui/NameInput.tsx +0 -82
- package/src/ui/OptionSelector.tsx +0 -100
- package/src/ui/Progress.tsx +0 -88
- package/src/ui/StackSelector.tsx +0 -101
- package/src/ui/Summary.tsx +0 -134
- package/src/ui/Welcome.tsx +0 -54
- package/src/ui/theme.ts +0 -10
- package/stryker.config.json +0 -19
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -16
package/dist/constants.d.ts
CHANGED
|
@@ -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
|
|
15
|
-
analyze
|
|
16
|
-
doctor
|
|
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
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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,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 |
|