javi-forge 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -3
- package/ci-local/hooks/pre-push +17 -13
- package/dist/commands/analyze.d.ts +1 -1
- package/dist/commands/analyze.js +15 -15
- package/dist/commands/atlassian-mcp.d.ts +42 -0
- package/dist/commands/atlassian-mcp.js +98 -0
- package/dist/commands/ci.d.ts +3 -3
- package/dist/commands/ci.js +185 -147
- package/dist/commands/crash-recovery.d.ts +34 -0
- package/dist/commands/crash-recovery.js +123 -0
- package/dist/commands/doctor.d.ts +2 -2
- package/dist/commands/doctor.js +113 -61
- package/dist/commands/harness-audit.d.ts +35 -0
- package/dist/commands/harness-audit.js +277 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +415 -118
- package/dist/commands/llmstxt.d.ts +1 -1
- package/dist/commands/llmstxt.js +36 -34
- package/dist/commands/parallel-batch.d.ts +42 -0
- package/dist/commands/parallel-batch.js +90 -0
- package/dist/commands/plugin.d.ts +26 -1
- package/dist/commands/plugin.js +138 -24
- package/dist/commands/secret-scanner.d.ts +30 -0
- package/dist/commands/secret-scanner.js +272 -0
- package/dist/commands/security-analysis.d.ts +74 -0
- package/dist/commands/security-analysis.js +487 -0
- package/dist/commands/security.d.ts +31 -0
- package/dist/commands/security.js +445 -0
- package/dist/commands/skill-scanner.d.ts +63 -0
- package/dist/commands/skill-scanner.js +383 -0
- package/dist/commands/skills.d.ts +139 -0
- package/dist/commands/skills.js +895 -0
- package/dist/commands/supply-chain.d.ts +23 -0
- package/dist/commands/supply-chain.js +126 -0
- package/dist/commands/tdd-pipeline.d.ts +17 -0
- package/dist/commands/tdd-pipeline.js +144 -0
- package/dist/commands/tdd.d.ts +21 -0
- package/dist/commands/tdd.js +120 -0
- package/dist/commands/team-presets.d.ts +53 -0
- package/dist/commands/team-presets.js +201 -0
- package/dist/commands/workflow.d.ts +23 -0
- package/dist/commands/workflow.js +114 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +208 -37
- package/dist/index.js +400 -54
- package/dist/lib/agent-skills.d.ts +73 -0
- package/dist/lib/agent-skills.js +260 -0
- package/dist/lib/auto-skill-install.d.ts +37 -0
- package/dist/lib/auto-skill-install.js +92 -0
- package/dist/lib/auto-wire.d.ts +20 -0
- package/dist/lib/auto-wire.js +240 -0
- package/dist/lib/claudemd.d.ts +20 -0
- package/dist/lib/claudemd.js +222 -0
- package/dist/lib/codex-export.d.ts +16 -0
- package/dist/lib/codex-export.js +109 -0
- package/dist/lib/common.d.ts +1 -1
- package/dist/lib/common.js +52 -44
- package/dist/lib/context.d.ts +27 -0
- package/dist/lib/context.js +204 -0
- package/dist/lib/docker.d.ts +1 -1
- package/dist/lib/docker.js +141 -112
- package/dist/lib/frontmatter.d.ts +1 -1
- package/dist/lib/frontmatter.js +29 -15
- package/dist/lib/plugin.d.ts +19 -1
- package/dist/lib/plugin.js +174 -47
- package/dist/lib/skill-publish.d.ts +40 -0
- package/dist/lib/skill-publish.js +146 -0
- package/dist/lib/stack-detector.d.ts +38 -0
- package/dist/lib/stack-detector.js +207 -0
- package/dist/lib/template.d.ts +16 -1
- package/dist/lib/template.js +46 -17
- package/dist/lib/workflow/discovery.d.ts +19 -0
- package/dist/lib/workflow/discovery.js +68 -0
- package/dist/lib/workflow/index.d.ts +5 -0
- package/dist/lib/workflow/index.js +5 -0
- package/dist/lib/workflow/parser.d.ts +16 -0
- package/dist/lib/workflow/parser.js +198 -0
- package/dist/lib/workflow/renderer.d.ts +9 -0
- package/dist/lib/workflow/renderer.js +152 -0
- package/dist/lib/workflow/validator.d.ts +10 -0
- package/dist/lib/workflow/validator.js +189 -0
- package/dist/tasks/index.d.ts +4 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/scaffold-tasks.d.ts +3 -0
- package/dist/tasks/scaffold-tasks.js +14 -0
- package/dist/tasks/task-id.d.ts +30 -0
- package/dist/tasks/task-id.js +55 -0
- package/dist/tasks/task-tracker.d.ts +15 -0
- package/dist/tasks/task-tracker.js +81 -0
- package/dist/types/index.d.ts +252 -5
- package/dist/types/index.js +11 -1
- package/dist/ui/AnalyzeUI.d.ts +1 -1
- package/dist/ui/AnalyzeUI.js +38 -39
- package/dist/ui/App.d.ts +5 -3
- package/dist/ui/App.js +92 -46
- package/dist/ui/AutoSkills.d.ts +9 -0
- package/dist/ui/AutoSkills.js +124 -0
- package/dist/ui/CI.d.ts +2 -2
- package/dist/ui/CI.js +24 -26
- package/dist/ui/CIContext.d.ts +1 -1
- package/dist/ui/CIContext.js +3 -2
- package/dist/ui/CISelector.d.ts +2 -2
- package/dist/ui/CISelector.js +23 -15
- package/dist/ui/Doctor.d.ts +1 -1
- package/dist/ui/Doctor.js +35 -29
- package/dist/ui/Header.d.ts +1 -1
- package/dist/ui/Header.js +14 -14
- package/dist/ui/HookProfileSelector.d.ts +9 -0
- package/dist/ui/HookProfileSelector.js +54 -0
- package/dist/ui/LlmsTxt.d.ts +1 -1
- package/dist/ui/LlmsTxt.js +31 -22
- package/dist/ui/MemorySelector.d.ts +2 -2
- package/dist/ui/MemorySelector.js +28 -16
- package/dist/ui/NameInput.d.ts +1 -1
- package/dist/ui/NameInput.js +21 -21
- package/dist/ui/OptionSelector.d.ts +8 -2
- package/dist/ui/OptionSelector.js +83 -26
- package/dist/ui/Plugin.d.ts +4 -3
- package/dist/ui/Plugin.js +89 -29
- package/dist/ui/Progress.d.ts +3 -3
- package/dist/ui/Progress.js +23 -22
- package/dist/ui/Skills.d.ts +11 -0
- package/dist/ui/Skills.js +148 -0
- package/dist/ui/StackSelector.d.ts +2 -2
- package/dist/ui/StackSelector.js +26 -16
- package/dist/ui/Summary.d.ts +3 -3
- package/dist/ui/Summary.js +60 -50
- package/dist/ui/Welcome.d.ts +1 -1
- package/dist/ui/Welcome.js +15 -16
- package/dist/ui/theme.d.ts +1 -1
- package/dist/ui/theme.js +6 -6
- package/package.json +9 -6
- package/templates/common/atlassian/mcp-atlassian-snippet.json +16 -0
- package/templates/common/repoforge/mcp-repoforge-snippet.json +11 -0
- package/templates/common/repoforge/repoforge.yaml +34 -0
- package/templates/github/deploy-docker-zero-downtime.yml +140 -0
- package/templates/github/repoforge-graph.yml +45 -0
- package/templates/gitlab/deploy-docker-zero-downtime.yml +57 -0
- package/templates/local-ai/.env.example +17 -0
- package/templates/local-ai/docker-compose.yml +95 -0
- package/templates/security-hooks/claude-settings-security.json +30 -0
- package/templates/security-hooks/commit-msg-signing +29 -0
- package/templates/security-hooks/pre-commit-permissions +74 -0
- package/templates/security-hooks/pre-commit-secrets +74 -0
- package/templates/security-hooks/pre-push-branch-protection +62 -0
- package/templates/security-hooks/pre-push-deps +83 -0
- package/templates/security-hooks/pre-push-signing +67 -0
- package/templates/woodpecker/deploy-docker-zero-downtime.yml +50 -0
- package/templates/workflows/ci-pipeline.dot +15 -0
- package/templates/workflows/feature-flow.dot +21 -0
- package/templates/workflows/release.dot +16 -0
- package/dist/__integration__/helpers.d.ts +0 -20
- package/dist/__integration__/helpers.d.ts.map +0 -1
- package/dist/__integration__/helpers.js +0 -31
- package/dist/__integration__/helpers.js.map +0 -1
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/ci.d.ts.map +0 -1
- package/dist/commands/ci.js.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/llmstxt.d.ts.map +0 -1
- package/dist/commands/llmstxt.js.map +0 -1
- package/dist/commands/plugin.d.ts.map +0 -1
- package/dist/commands/plugin.js.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.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/docker.d.ts.map +0 -1
- package/dist/lib/docker.js.map +0 -1
- package/dist/lib/frontmatter.d.ts.map +0 -1
- package/dist/lib/frontmatter.js.map +0 -1
- package/dist/lib/plugin.d.ts.map +0 -1
- package/dist/lib/plugin.js.map +0 -1
- package/dist/lib/template.d.ts.map +0 -1
- package/dist/lib/template.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/CI.d.ts.map +0 -1
- package/dist/ui/CI.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/LlmsTxt.d.ts.map +0 -1
- package/dist/ui/LlmsTxt.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/Plugin.d.ts.map +0 -1
- package/dist/ui/Plugin.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/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/dist/lib/plugin.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { InstalledPlugin, PluginRegistryEntry, PluginSyncResult, PluginValidationResult } from "../types/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* Validate a plugin directory structure and manifest.
|
|
4
4
|
*/
|
|
@@ -31,6 +31,24 @@ export declare function listInstalledPlugins(): Promise<InstalledPlugin[]>;
|
|
|
31
31
|
* Fetch the remote plugin registry and optionally filter by query.
|
|
32
32
|
*/
|
|
33
33
|
export declare function searchRegistry(query?: string): Promise<PluginRegistryEntry[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Detect installed plugins in a project's .javi-forge/plugins/ directory.
|
|
36
|
+
* Returns an array of plugin names (sorted alphabetically).
|
|
37
|
+
*/
|
|
38
|
+
export declare function detectProjectPlugins(projectDir: string): Promise<string[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Detect installed plugins with full metadata (including manifest).
|
|
41
|
+
* Used by auto-wiring to read plugin capabilities.
|
|
42
|
+
*/
|
|
43
|
+
export declare function detectProjectPluginsFull(projectDir: string): Promise<InstalledPlugin[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Sync detected plugins into the project manifest and auto-wire
|
|
46
|
+
* their capabilities into CLAUDE.md and .claude/settings.json.
|
|
47
|
+
* Returns a report of added, removed, unchanged, wired, and unwired plugins.
|
|
48
|
+
*/
|
|
49
|
+
export declare function syncPlugins(projectDir: string, options?: {
|
|
50
|
+
dryRun?: boolean;
|
|
51
|
+
}): Promise<PluginSyncResult>;
|
|
34
52
|
/**
|
|
35
53
|
* Normalize a GitHub source to a git clone URL.
|
|
36
54
|
* Accepts: "org/repo", "https://github.com/org/repo", "github.com/org/repo"
|
package/dist/lib/plugin.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import { promisify } from
|
|
5
|
-
import {
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
import { PLUGIN_ASSET_DIRS, PLUGIN_MANIFEST_FILE, PLUGIN_REGISTRY_URL, PLUGINS_DIR, } from "../constants.js";
|
|
6
|
+
import { generateAgentSkillsManifest } from "./agent-skills.js";
|
|
7
|
+
import { autoWirePlugins } from "./auto-wire.js";
|
|
6
8
|
const execFileAsync = promisify(execFile);
|
|
7
9
|
const KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
8
10
|
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
@@ -14,49 +16,60 @@ export async function validatePlugin(pluginDir) {
|
|
|
14
16
|
const errors = [];
|
|
15
17
|
// Check plugin.json exists
|
|
16
18
|
const manifestPath = path.join(pluginDir, PLUGIN_MANIFEST_FILE);
|
|
17
|
-
if (!await fs.pathExists(manifestPath)) {
|
|
19
|
+
if (!(await fs.pathExists(manifestPath))) {
|
|
18
20
|
return {
|
|
19
21
|
valid: false,
|
|
20
|
-
errors: [
|
|
22
|
+
errors: [
|
|
23
|
+
{ path: PLUGIN_MANIFEST_FILE, message: "plugin.json not found" },
|
|
24
|
+
],
|
|
21
25
|
manifest: null,
|
|
22
26
|
};
|
|
23
27
|
}
|
|
24
28
|
// Parse manifest
|
|
25
29
|
let manifest;
|
|
26
30
|
try {
|
|
27
|
-
manifest = await fs.readJson(manifestPath);
|
|
31
|
+
manifest = (await fs.readJson(manifestPath));
|
|
28
32
|
}
|
|
29
33
|
catch {
|
|
30
34
|
return {
|
|
31
35
|
valid: false,
|
|
32
|
-
errors: [{ path: PLUGIN_MANIFEST_FILE, message:
|
|
36
|
+
errors: [{ path: PLUGIN_MANIFEST_FILE, message: "invalid JSON" }],
|
|
33
37
|
manifest: null,
|
|
34
38
|
};
|
|
35
39
|
}
|
|
36
40
|
// Validate required fields
|
|
37
|
-
if (typeof manifest.name !==
|
|
38
|
-
errors.push({ path:
|
|
41
|
+
if (typeof manifest.name !== "string" || !manifest.name) {
|
|
42
|
+
errors.push({ path: "name", message: "name is required" });
|
|
39
43
|
}
|
|
40
44
|
else if (!KEBAB_RE.test(manifest.name)) {
|
|
41
|
-
errors.push({ path:
|
|
45
|
+
errors.push({ path: "name", message: "name must be kebab-case" });
|
|
42
46
|
}
|
|
43
47
|
else if (manifest.name.length < 2 || manifest.name.length > 60) {
|
|
44
|
-
errors.push({ path:
|
|
48
|
+
errors.push({ path: "name", message: "name must be 2-60 characters" });
|
|
45
49
|
}
|
|
46
|
-
if (typeof manifest.version !==
|
|
47
|
-
errors.push({ path:
|
|
50
|
+
if (typeof manifest.version !== "string" || !manifest.version) {
|
|
51
|
+
errors.push({ path: "version", message: "version is required" });
|
|
48
52
|
}
|
|
49
53
|
else if (!SEMVER_RE.test(manifest.version)) {
|
|
50
|
-
errors.push({
|
|
54
|
+
errors.push({
|
|
55
|
+
path: "version",
|
|
56
|
+
message: "version must be semver (e.g. 1.0.0)",
|
|
57
|
+
});
|
|
51
58
|
}
|
|
52
|
-
if (typeof manifest.description !==
|
|
53
|
-
errors.push({ path:
|
|
59
|
+
if (typeof manifest.description !== "string" || !manifest.description) {
|
|
60
|
+
errors.push({ path: "description", message: "description is required" });
|
|
54
61
|
}
|
|
55
62
|
else if (manifest.description.length < 10) {
|
|
56
|
-
errors.push({
|
|
63
|
+
errors.push({
|
|
64
|
+
path: "description",
|
|
65
|
+
message: "description must be at least 10 characters",
|
|
66
|
+
});
|
|
57
67
|
}
|
|
58
68
|
else if (manifest.description.length > 200) {
|
|
59
|
-
errors.push({
|
|
69
|
+
errors.push({
|
|
70
|
+
path: "description",
|
|
71
|
+
message: "description must be at most 200 characters",
|
|
72
|
+
});
|
|
60
73
|
}
|
|
61
74
|
// Validate asset directories actually exist when declared
|
|
62
75
|
for (const assetType of PLUGIN_ASSET_DIRS) {
|
|
@@ -64,26 +77,36 @@ export async function validatePlugin(pluginDir) {
|
|
|
64
77
|
if (!Array.isArray(declared) || declared.length === 0)
|
|
65
78
|
continue;
|
|
66
79
|
const assetDir = path.join(pluginDir, assetType);
|
|
67
|
-
if (!await fs.pathExists(assetDir)) {
|
|
68
|
-
errors.push({
|
|
80
|
+
if (!(await fs.pathExists(assetDir))) {
|
|
81
|
+
errors.push({
|
|
82
|
+
path: assetType,
|
|
83
|
+
message: `declared ${assetType}/ directory not found`,
|
|
84
|
+
});
|
|
69
85
|
continue;
|
|
70
86
|
}
|
|
71
87
|
// Check each declared asset exists
|
|
72
88
|
for (const entry of declared) {
|
|
73
89
|
const entryPath = path.join(assetDir, entry);
|
|
74
|
-
if (!await fs.pathExists(entryPath)) {
|
|
75
|
-
errors.push({
|
|
90
|
+
if (!(await fs.pathExists(entryPath))) {
|
|
91
|
+
errors.push({
|
|
92
|
+
path: `${assetType}/${entry}`,
|
|
93
|
+
message: `declared entry not found`,
|
|
94
|
+
});
|
|
76
95
|
}
|
|
77
96
|
}
|
|
78
97
|
}
|
|
79
98
|
// Validate tags
|
|
80
99
|
if (manifest.tags && !Array.isArray(manifest.tags)) {
|
|
81
|
-
errors.push({ path:
|
|
100
|
+
errors.push({ path: "tags", message: "tags must be an array" });
|
|
82
101
|
}
|
|
83
102
|
else if (manifest.tags && manifest.tags.length > 10) {
|
|
84
|
-
errors.push({ path:
|
|
103
|
+
errors.push({ path: "tags", message: "max 10 tags allowed" });
|
|
85
104
|
}
|
|
86
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
valid: errors.length === 0,
|
|
107
|
+
errors,
|
|
108
|
+
manifest: errors.length === 0 ? manifest : manifest,
|
|
109
|
+
};
|
|
87
110
|
}
|
|
88
111
|
// ── Installation ────────────────────────────────────────────────────────────
|
|
89
112
|
/**
|
|
@@ -95,23 +118,36 @@ export async function installPlugin(source, options = {}) {
|
|
|
95
118
|
// Normalize source to a git URL
|
|
96
119
|
const gitUrl = normalizeGitUrl(source);
|
|
97
120
|
if (!gitUrl) {
|
|
98
|
-
return {
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
error: `invalid source: ${source}. Use org/repo or a GitHub URL`,
|
|
124
|
+
};
|
|
99
125
|
}
|
|
100
126
|
// Clone to temp
|
|
101
|
-
const tmpDir = path.join(PLUGINS_DIR,
|
|
127
|
+
const tmpDir = path.join(PLUGINS_DIR, ".tmp", `install-${Date.now()}`);
|
|
102
128
|
try {
|
|
103
129
|
if (!dryRun) {
|
|
104
130
|
await fs.ensureDir(tmpDir);
|
|
105
|
-
await execFileAsync(
|
|
131
|
+
await execFileAsync("git", ["clone", "--depth", "1", gitUrl, tmpDir], {
|
|
106
132
|
timeout: 60_000,
|
|
107
133
|
});
|
|
108
134
|
}
|
|
109
135
|
// Validate
|
|
110
136
|
const validation = dryRun
|
|
111
|
-
? {
|
|
137
|
+
? {
|
|
138
|
+
valid: true,
|
|
139
|
+
errors: [],
|
|
140
|
+
manifest: {
|
|
141
|
+
name: source.split("/").pop() ?? "unknown",
|
|
142
|
+
version: "0.0.0",
|
|
143
|
+
description: "dry-run placeholder",
|
|
144
|
+
},
|
|
145
|
+
}
|
|
112
146
|
: await validatePlugin(tmpDir);
|
|
113
147
|
if (!validation.valid || !validation.manifest) {
|
|
114
|
-
const msgs = validation.errors
|
|
148
|
+
const msgs = validation.errors
|
|
149
|
+
.map((e) => ` ${e.path}: ${e.message}`)
|
|
150
|
+
.join("\n");
|
|
115
151
|
return { success: false, error: `validation failed:\n${msgs}` };
|
|
116
152
|
}
|
|
117
153
|
const pluginName = validation.manifest.name;
|
|
@@ -130,7 +166,9 @@ export async function installPlugin(source, options = {}) {
|
|
|
130
166
|
source,
|
|
131
167
|
manifest: validation.manifest,
|
|
132
168
|
};
|
|
133
|
-
await fs.writeJson(path.join(destDir,
|
|
169
|
+
await fs.writeJson(path.join(destDir, ".installed.json"), installedPlugin, { spaces: 2 });
|
|
170
|
+
// Generate Agent Skills spec manifest for cross-agent compatibility
|
|
171
|
+
await generateAgentSkillsManifest(destDir, source).catch(() => { });
|
|
134
172
|
}
|
|
135
173
|
return { success: true, name: pluginName };
|
|
136
174
|
}
|
|
@@ -140,7 +178,7 @@ export async function installPlugin(source, options = {}) {
|
|
|
140
178
|
}
|
|
141
179
|
finally {
|
|
142
180
|
// Clean up tmp if it still exists (error path)
|
|
143
|
-
if (!dryRun && await fs.pathExists(tmpDir)) {
|
|
181
|
+
if (!dryRun && (await fs.pathExists(tmpDir))) {
|
|
144
182
|
await fs.remove(tmpDir).catch(() => { });
|
|
145
183
|
}
|
|
146
184
|
}
|
|
@@ -150,7 +188,7 @@ export async function installPlugin(source, options = {}) {
|
|
|
150
188
|
*/
|
|
151
189
|
export async function removePlugin(name, options = {}) {
|
|
152
190
|
const pluginDir = path.join(PLUGINS_DIR, name);
|
|
153
|
-
if (!await fs.pathExists(pluginDir)) {
|
|
191
|
+
if (!(await fs.pathExists(pluginDir))) {
|
|
154
192
|
return { success: false, error: `plugin "${name}" is not installed` };
|
|
155
193
|
}
|
|
156
194
|
if (!options.dryRun) {
|
|
@@ -163,20 +201,22 @@ export async function removePlugin(name, options = {}) {
|
|
|
163
201
|
* List all installed plugins.
|
|
164
202
|
*/
|
|
165
203
|
export async function listInstalledPlugins() {
|
|
166
|
-
if (!await fs.pathExists(PLUGINS_DIR))
|
|
204
|
+
if (!(await fs.pathExists(PLUGINS_DIR)))
|
|
167
205
|
return [];
|
|
168
206
|
const entries = await fs.readdir(PLUGINS_DIR);
|
|
169
207
|
const plugins = [];
|
|
170
208
|
for (const entry of entries) {
|
|
171
|
-
if (entry.startsWith(
|
|
209
|
+
if (entry.startsWith("."))
|
|
172
210
|
continue;
|
|
173
|
-
const metaPath = path.join(PLUGINS_DIR, entry,
|
|
211
|
+
const metaPath = path.join(PLUGINS_DIR, entry, ".installed.json");
|
|
174
212
|
if (await fs.pathExists(metaPath)) {
|
|
175
213
|
try {
|
|
176
|
-
const meta = await fs.readJson(metaPath);
|
|
214
|
+
const meta = (await fs.readJson(metaPath));
|
|
177
215
|
plugins.push(meta);
|
|
178
216
|
}
|
|
179
|
-
catch {
|
|
217
|
+
catch {
|
|
218
|
+
/* skip corrupt entries */
|
|
219
|
+
}
|
|
180
220
|
}
|
|
181
221
|
}
|
|
182
222
|
return plugins;
|
|
@@ -190,13 +230,13 @@ export async function searchRegistry(query) {
|
|
|
190
230
|
if (!response.ok) {
|
|
191
231
|
return [];
|
|
192
232
|
}
|
|
193
|
-
const registry = await response.json();
|
|
233
|
+
const registry = (await response.json());
|
|
194
234
|
let plugins = registry.plugins ?? [];
|
|
195
235
|
if (query) {
|
|
196
236
|
const q = query.toLowerCase();
|
|
197
|
-
plugins = plugins.filter(p => p.id.toLowerCase().includes(q) ||
|
|
237
|
+
plugins = plugins.filter((p) => p.id.toLowerCase().includes(q) ||
|
|
198
238
|
p.description.toLowerCase().includes(q) ||
|
|
199
|
-
p.tags.some(t => t.toLowerCase().includes(q)));
|
|
239
|
+
p.tags.some((t) => t.toLowerCase().includes(q)));
|
|
200
240
|
}
|
|
201
241
|
return plugins;
|
|
202
242
|
}
|
|
@@ -204,6 +244,93 @@ export async function searchRegistry(query) {
|
|
|
204
244
|
return [];
|
|
205
245
|
}
|
|
206
246
|
}
|
|
247
|
+
// ── Sync ───────────────────────────────────────────────────────────────
|
|
248
|
+
/**
|
|
249
|
+
* Detect installed plugins in a project's .javi-forge/plugins/ directory.
|
|
250
|
+
* Returns an array of plugin names (sorted alphabetically).
|
|
251
|
+
*/
|
|
252
|
+
export async function detectProjectPlugins(projectDir) {
|
|
253
|
+
const full = await detectProjectPluginsFull(projectDir);
|
|
254
|
+
return full.map((p) => p.name).sort();
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Detect installed plugins with full metadata (including manifest).
|
|
258
|
+
* Used by auto-wiring to read plugin capabilities.
|
|
259
|
+
*/
|
|
260
|
+
export async function detectProjectPluginsFull(projectDir) {
|
|
261
|
+
const pluginsDir = path.join(projectDir, ".javi-forge", "plugins");
|
|
262
|
+
if (!(await fs.pathExists(pluginsDir)))
|
|
263
|
+
return [];
|
|
264
|
+
const entries = await fs.readdir(pluginsDir);
|
|
265
|
+
const plugins = [];
|
|
266
|
+
for (const entry of entries) {
|
|
267
|
+
if (entry.startsWith("."))
|
|
268
|
+
continue;
|
|
269
|
+
const metaPath = path.join(pluginsDir, entry, ".installed.json");
|
|
270
|
+
if (await fs.pathExists(metaPath)) {
|
|
271
|
+
try {
|
|
272
|
+
const meta = (await fs.readJson(metaPath));
|
|
273
|
+
if (meta.name) {
|
|
274
|
+
plugins.push(meta);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
/* skip corrupt entries */
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return plugins.sort((a, b) => a.name.localeCompare(b.name));
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Sync detected plugins into the project manifest and auto-wire
|
|
286
|
+
* their capabilities into CLAUDE.md and .claude/settings.json.
|
|
287
|
+
* Returns a report of added, removed, unchanged, wired, and unwired plugins.
|
|
288
|
+
*/
|
|
289
|
+
export async function syncPlugins(projectDir, options = {}) {
|
|
290
|
+
const { dryRun = false } = options;
|
|
291
|
+
const detectedFull = await detectProjectPluginsFull(projectDir);
|
|
292
|
+
const detected = detectedFull.map((p) => p.name).sort();
|
|
293
|
+
const manifestPath = path.join(projectDir, ".javi-forge", "manifest.json");
|
|
294
|
+
let manifest;
|
|
295
|
+
if (await fs.pathExists(manifestPath)) {
|
|
296
|
+
manifest = (await fs.readJson(manifestPath));
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
// No manifest yet — treat current plugins as empty
|
|
300
|
+
manifest = {
|
|
301
|
+
version: "0.1.0",
|
|
302
|
+
projectName: path.basename(projectDir),
|
|
303
|
+
stack: "node",
|
|
304
|
+
ciProvider: "github",
|
|
305
|
+
memory: "none",
|
|
306
|
+
createdAt: new Date().toISOString(),
|
|
307
|
+
updatedAt: new Date().toISOString(),
|
|
308
|
+
modules: [],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const previous = new Set(manifest.plugins ?? []);
|
|
312
|
+
const current = new Set(detected);
|
|
313
|
+
const added = detected.filter((p) => !previous.has(p));
|
|
314
|
+
const removed = [...previous].filter((p) => !current.has(p));
|
|
315
|
+
const unchanged = detected.filter((p) => previous.has(p));
|
|
316
|
+
if (!dryRun && (added.length > 0 || removed.length > 0)) {
|
|
317
|
+
manifest.plugins = detected;
|
|
318
|
+
manifest.updatedAt = new Date().toISOString();
|
|
319
|
+
await fs.ensureDir(path.dirname(manifestPath));
|
|
320
|
+
await fs.writeJson(manifestPath, manifest, { spaces: 2 });
|
|
321
|
+
}
|
|
322
|
+
// ── Auto-wire plugin capabilities ──────────────────────────────────
|
|
323
|
+
const wireResult = await autoWirePlugins(projectDir, detectedFull, {
|
|
324
|
+
dryRun,
|
|
325
|
+
});
|
|
326
|
+
return {
|
|
327
|
+
added,
|
|
328
|
+
removed,
|
|
329
|
+
unchanged,
|
|
330
|
+
wired: wireResult.wired,
|
|
331
|
+
unwired: wireResult.unwired,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
207
334
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
208
335
|
/**
|
|
209
336
|
* Normalize a GitHub source to a git clone URL.
|
|
@@ -211,15 +338,15 @@ export async function searchRegistry(query) {
|
|
|
211
338
|
*/
|
|
212
339
|
export function normalizeGitUrl(source) {
|
|
213
340
|
// Already a full URL
|
|
214
|
-
if (source.startsWith(
|
|
215
|
-
return source.endsWith(
|
|
341
|
+
if (source.startsWith("https://github.com/")) {
|
|
342
|
+
return source.endsWith(".git") ? source : `${source}.git`;
|
|
216
343
|
}
|
|
217
344
|
// github.com/org/repo
|
|
218
|
-
if (source.startsWith(
|
|
345
|
+
if (source.startsWith("github.com/")) {
|
|
219
346
|
return `https://${source}.git`;
|
|
220
347
|
}
|
|
221
348
|
// org/repo shorthand
|
|
222
|
-
const parts = source.split(
|
|
349
|
+
const parts = source.split("/");
|
|
223
350
|
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
224
351
|
return `https://github.com/${parts[0]}/${parts[1]}.git`;
|
|
225
352
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PluginManifest } from "../types/index.js";
|
|
2
|
+
export interface SkillPublishResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
/** Path to the generated plugin.json */
|
|
5
|
+
pluginJsonPath?: string;
|
|
6
|
+
/** The generated manifest */
|
|
7
|
+
manifest?: PluginManifest;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SkillPublishOptions {
|
|
11
|
+
/** Path to the skill directory (contains SKILL.md) */
|
|
12
|
+
skillDir: string;
|
|
13
|
+
/** Author name (optional, falls back to frontmatter or git user) */
|
|
14
|
+
author?: string;
|
|
15
|
+
/** Repository URL (optional) */
|
|
16
|
+
repository?: string;
|
|
17
|
+
/** Tags for marketplace discovery */
|
|
18
|
+
tags?: string[];
|
|
19
|
+
/** If true, skip writing files */
|
|
20
|
+
dryRun?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Package a skill directory for marketplace distribution.
|
|
24
|
+
*
|
|
25
|
+
* Reads the SKILL.md, extracts metadata from frontmatter, and generates
|
|
26
|
+
* a plugin.json compatible with both `javi-forge plugin install` and
|
|
27
|
+
* `claude plugin install` (Anthropic's plugin format).
|
|
28
|
+
*
|
|
29
|
+
* Expected input structure:
|
|
30
|
+
* skill-name/
|
|
31
|
+
* SKILL.md
|
|
32
|
+
* (optional other files)
|
|
33
|
+
*
|
|
34
|
+
* Output:
|
|
35
|
+
* skill-name/
|
|
36
|
+
* SKILL.md
|
|
37
|
+
* plugin.json ← generated
|
|
38
|
+
*/
|
|
39
|
+
export declare function publishSkill(options: SkillPublishOptions): Promise<SkillPublishResult>;
|
|
40
|
+
//# sourceMappingURL=skill-publish.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { PLUGIN_MANIFEST_FILE } from "../constants.js";
|
|
4
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
5
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* Package a skill directory for marketplace distribution.
|
|
8
|
+
*
|
|
9
|
+
* Reads the SKILL.md, extracts metadata from frontmatter, and generates
|
|
10
|
+
* a plugin.json compatible with both `javi-forge plugin install` and
|
|
11
|
+
* `claude plugin install` (Anthropic's plugin format).
|
|
12
|
+
*
|
|
13
|
+
* Expected input structure:
|
|
14
|
+
* skill-name/
|
|
15
|
+
* SKILL.md
|
|
16
|
+
* (optional other files)
|
|
17
|
+
*
|
|
18
|
+
* Output:
|
|
19
|
+
* skill-name/
|
|
20
|
+
* SKILL.md
|
|
21
|
+
* plugin.json ← generated
|
|
22
|
+
*/
|
|
23
|
+
export async function publishSkill(options) {
|
|
24
|
+
const { skillDir, author, repository, tags = [], dryRun = false } = options;
|
|
25
|
+
// Validate skill directory
|
|
26
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
27
|
+
if (!(await fs.pathExists(skillMdPath))) {
|
|
28
|
+
return { success: false, error: `SKILL.md not found in ${skillDir}` };
|
|
29
|
+
}
|
|
30
|
+
// Read and parse SKILL.md
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = await fs.readFile(skillMdPath, "utf-8");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return { success: false, error: "Failed to read SKILL.md" };
|
|
37
|
+
}
|
|
38
|
+
const parsed = parseFrontmatter(raw);
|
|
39
|
+
const frontmatter = parsed?.data ?? {};
|
|
40
|
+
// Extract metadata
|
|
41
|
+
const name = frontmatter["name"] ?? path.basename(skillDir);
|
|
42
|
+
const version = frontmatter["version"] ?? "1.0.0";
|
|
43
|
+
const description = frontmatter["description"] ?? `${name} AI skill`;
|
|
44
|
+
// Validate name format (kebab-case)
|
|
45
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
error: `Skill name "${name}" must be kebab-case (e.g., "react-19", "tailwind-4")`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// Validate description length
|
|
52
|
+
if (description.length < 10) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: "Description must be at least 10 characters",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Build plugin.json manifest
|
|
59
|
+
const manifest = {
|
|
60
|
+
name,
|
|
61
|
+
version,
|
|
62
|
+
description: description.length > 200
|
|
63
|
+
? description.slice(0, 197) + "..."
|
|
64
|
+
: description,
|
|
65
|
+
skills: [name],
|
|
66
|
+
tags: tags.length > 0 ? tags : extractTagsFromDescription(description),
|
|
67
|
+
...(author ? { author } : {}),
|
|
68
|
+
...(repository ? { repository } : {}),
|
|
69
|
+
};
|
|
70
|
+
if (!dryRun) {
|
|
71
|
+
const pluginJsonPath = path.join(skillDir, PLUGIN_MANIFEST_FILE);
|
|
72
|
+
await fs.writeJson(pluginJsonPath, manifest, { spaces: 2 });
|
|
73
|
+
// Also create a skills/ subdirectory structure for plugin compatibility
|
|
74
|
+
const skillsSubdir = path.join(skillDir, "skills", name);
|
|
75
|
+
if (!(await fs.pathExists(skillsSubdir))) {
|
|
76
|
+
await fs.ensureDir(skillsSubdir);
|
|
77
|
+
// Symlink or copy SKILL.md into skills/name/
|
|
78
|
+
const targetSkillMd = path.join(skillsSubdir, "SKILL.md");
|
|
79
|
+
if (!(await fs.pathExists(targetSkillMd))) {
|
|
80
|
+
await fs.copy(skillMdPath, targetSkillMd);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { success: true, pluginJsonPath, manifest };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
success: true,
|
|
87
|
+
pluginJsonPath: path.join(skillDir, PLUGIN_MANIFEST_FILE),
|
|
88
|
+
manifest,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Extract reasonable tags from a skill description.
|
|
94
|
+
* Looks for common tech keywords.
|
|
95
|
+
*/
|
|
96
|
+
function extractTagsFromDescription(description) {
|
|
97
|
+
const keywords = [
|
|
98
|
+
"react",
|
|
99
|
+
"next",
|
|
100
|
+
"nextjs",
|
|
101
|
+
"angular",
|
|
102
|
+
"vue",
|
|
103
|
+
"svelte",
|
|
104
|
+
"typescript",
|
|
105
|
+
"javascript",
|
|
106
|
+
"python",
|
|
107
|
+
"go",
|
|
108
|
+
"rust",
|
|
109
|
+
"java",
|
|
110
|
+
"elixir",
|
|
111
|
+
"tailwind",
|
|
112
|
+
"css",
|
|
113
|
+
"styling",
|
|
114
|
+
"testing",
|
|
115
|
+
"test",
|
|
116
|
+
"e2e",
|
|
117
|
+
"unit",
|
|
118
|
+
"api",
|
|
119
|
+
"rest",
|
|
120
|
+
"graphql",
|
|
121
|
+
"ai",
|
|
122
|
+
"llm",
|
|
123
|
+
"ml",
|
|
124
|
+
"agent",
|
|
125
|
+
"security",
|
|
126
|
+
"auth",
|
|
127
|
+
"database",
|
|
128
|
+
"sql",
|
|
129
|
+
"nosql",
|
|
130
|
+
"docker",
|
|
131
|
+
"kubernetes",
|
|
132
|
+
"devops",
|
|
133
|
+
"ci",
|
|
134
|
+
"cd",
|
|
135
|
+
"state",
|
|
136
|
+
"management",
|
|
137
|
+
"store",
|
|
138
|
+
"validation",
|
|
139
|
+
"schema",
|
|
140
|
+
"zod",
|
|
141
|
+
];
|
|
142
|
+
const lower = description.toLowerCase();
|
|
143
|
+
const found = keywords.filter((kw) => lower.includes(kw));
|
|
144
|
+
return found.slice(0, 10); // Max 10 tags per plugin spec
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=skill-publish.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Stack } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Maps detected project signals to recommended AI skills.
|
|
4
|
+
* Each key is a detection signal (file/dependency/pattern),
|
|
5
|
+
* and the value is the list of skills it implies.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SIGNAL_SKILL_MAP: Record<string, string[]>;
|
|
8
|
+
/**
|
|
9
|
+
* Docker-related files that indicate a containerized project.
|
|
10
|
+
* Used by the init command to suggest Docker zero-downtime deploy scaffolding.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DOCKER_FILES: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Detect whether the project uses Docker (Dockerfile or compose file present).
|
|
15
|
+
*/
|
|
16
|
+
export declare function detectDockerPresence(projectDir: string): Promise<boolean>;
|
|
17
|
+
export interface StackDetectionResult {
|
|
18
|
+
/** Primary stack type (node, python, etc.) */
|
|
19
|
+
stack: Stack | null;
|
|
20
|
+
/** All detected signals with their source */
|
|
21
|
+
signals: DetectedSignal[];
|
|
22
|
+
/** De-duplicated list of recommended skill names */
|
|
23
|
+
recommendedSkills: string[];
|
|
24
|
+
}
|
|
25
|
+
export interface DetectedSignal {
|
|
26
|
+
/** What was detected (e.g., "react", "tailwindcss", "tsconfig.json") */
|
|
27
|
+
signal: string;
|
|
28
|
+
/** Where it was found (e.g., "package.json dependencies", "file exists") */
|
|
29
|
+
source: string;
|
|
30
|
+
/** Skills this signal maps to */
|
|
31
|
+
skills: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Scan a project directory and detect its tech stack + recommended skills.
|
|
35
|
+
* Reads package.json deps, pyproject.toml, and well-known config files.
|
|
36
|
+
*/
|
|
37
|
+
export declare function detectProjectStack(projectDir: string): Promise<StackDetectionResult>;
|
|
38
|
+
//# sourceMappingURL=stack-detector.d.ts.map
|