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.
Files changed (217) hide show
  1. package/README.md +191 -3
  2. package/ci-local/hooks/pre-push +17 -13
  3. package/dist/commands/analyze.d.ts +1 -1
  4. package/dist/commands/analyze.js +15 -15
  5. package/dist/commands/atlassian-mcp.d.ts +42 -0
  6. package/dist/commands/atlassian-mcp.js +98 -0
  7. package/dist/commands/ci.d.ts +3 -3
  8. package/dist/commands/ci.js +185 -147
  9. package/dist/commands/crash-recovery.d.ts +34 -0
  10. package/dist/commands/crash-recovery.js +123 -0
  11. package/dist/commands/doctor.d.ts +2 -2
  12. package/dist/commands/doctor.js +113 -61
  13. package/dist/commands/harness-audit.d.ts +35 -0
  14. package/dist/commands/harness-audit.js +277 -0
  15. package/dist/commands/init.d.ts +1 -1
  16. package/dist/commands/init.js +415 -118
  17. package/dist/commands/llmstxt.d.ts +1 -1
  18. package/dist/commands/llmstxt.js +36 -34
  19. package/dist/commands/parallel-batch.d.ts +42 -0
  20. package/dist/commands/parallel-batch.js +90 -0
  21. package/dist/commands/plugin.d.ts +26 -1
  22. package/dist/commands/plugin.js +138 -24
  23. package/dist/commands/secret-scanner.d.ts +30 -0
  24. package/dist/commands/secret-scanner.js +272 -0
  25. package/dist/commands/security-analysis.d.ts +74 -0
  26. package/dist/commands/security-analysis.js +487 -0
  27. package/dist/commands/security.d.ts +31 -0
  28. package/dist/commands/security.js +445 -0
  29. package/dist/commands/skill-scanner.d.ts +63 -0
  30. package/dist/commands/skill-scanner.js +383 -0
  31. package/dist/commands/skills.d.ts +139 -0
  32. package/dist/commands/skills.js +895 -0
  33. package/dist/commands/supply-chain.d.ts +23 -0
  34. package/dist/commands/supply-chain.js +126 -0
  35. package/dist/commands/tdd-pipeline.d.ts +17 -0
  36. package/dist/commands/tdd-pipeline.js +144 -0
  37. package/dist/commands/tdd.d.ts +21 -0
  38. package/dist/commands/tdd.js +120 -0
  39. package/dist/commands/team-presets.d.ts +53 -0
  40. package/dist/commands/team-presets.js +201 -0
  41. package/dist/commands/workflow.d.ts +23 -0
  42. package/dist/commands/workflow.js +114 -0
  43. package/dist/constants.d.ts +21 -0
  44. package/dist/constants.js +208 -37
  45. package/dist/index.js +400 -54
  46. package/dist/lib/agent-skills.d.ts +73 -0
  47. package/dist/lib/agent-skills.js +260 -0
  48. package/dist/lib/auto-skill-install.d.ts +37 -0
  49. package/dist/lib/auto-skill-install.js +92 -0
  50. package/dist/lib/auto-wire.d.ts +20 -0
  51. package/dist/lib/auto-wire.js +240 -0
  52. package/dist/lib/claudemd.d.ts +20 -0
  53. package/dist/lib/claudemd.js +222 -0
  54. package/dist/lib/codex-export.d.ts +16 -0
  55. package/dist/lib/codex-export.js +109 -0
  56. package/dist/lib/common.d.ts +1 -1
  57. package/dist/lib/common.js +52 -44
  58. package/dist/lib/context.d.ts +27 -0
  59. package/dist/lib/context.js +204 -0
  60. package/dist/lib/docker.d.ts +1 -1
  61. package/dist/lib/docker.js +141 -112
  62. package/dist/lib/frontmatter.d.ts +1 -1
  63. package/dist/lib/frontmatter.js +29 -15
  64. package/dist/lib/plugin.d.ts +19 -1
  65. package/dist/lib/plugin.js +174 -47
  66. package/dist/lib/skill-publish.d.ts +40 -0
  67. package/dist/lib/skill-publish.js +146 -0
  68. package/dist/lib/stack-detector.d.ts +38 -0
  69. package/dist/lib/stack-detector.js +207 -0
  70. package/dist/lib/template.d.ts +16 -1
  71. package/dist/lib/template.js +46 -17
  72. package/dist/lib/workflow/discovery.d.ts +19 -0
  73. package/dist/lib/workflow/discovery.js +68 -0
  74. package/dist/lib/workflow/index.d.ts +5 -0
  75. package/dist/lib/workflow/index.js +5 -0
  76. package/dist/lib/workflow/parser.d.ts +16 -0
  77. package/dist/lib/workflow/parser.js +198 -0
  78. package/dist/lib/workflow/renderer.d.ts +9 -0
  79. package/dist/lib/workflow/renderer.js +152 -0
  80. package/dist/lib/workflow/validator.d.ts +10 -0
  81. package/dist/lib/workflow/validator.js +189 -0
  82. package/dist/tasks/index.d.ts +4 -0
  83. package/dist/tasks/index.js +4 -0
  84. package/dist/tasks/scaffold-tasks.d.ts +3 -0
  85. package/dist/tasks/scaffold-tasks.js +14 -0
  86. package/dist/tasks/task-id.d.ts +30 -0
  87. package/dist/tasks/task-id.js +55 -0
  88. package/dist/tasks/task-tracker.d.ts +15 -0
  89. package/dist/tasks/task-tracker.js +81 -0
  90. package/dist/types/index.d.ts +252 -5
  91. package/dist/types/index.js +11 -1
  92. package/dist/ui/AnalyzeUI.d.ts +1 -1
  93. package/dist/ui/AnalyzeUI.js +38 -39
  94. package/dist/ui/App.d.ts +5 -3
  95. package/dist/ui/App.js +92 -46
  96. package/dist/ui/AutoSkills.d.ts +9 -0
  97. package/dist/ui/AutoSkills.js +124 -0
  98. package/dist/ui/CI.d.ts +2 -2
  99. package/dist/ui/CI.js +24 -26
  100. package/dist/ui/CIContext.d.ts +1 -1
  101. package/dist/ui/CIContext.js +3 -2
  102. package/dist/ui/CISelector.d.ts +2 -2
  103. package/dist/ui/CISelector.js +23 -15
  104. package/dist/ui/Doctor.d.ts +1 -1
  105. package/dist/ui/Doctor.js +35 -29
  106. package/dist/ui/Header.d.ts +1 -1
  107. package/dist/ui/Header.js +14 -14
  108. package/dist/ui/HookProfileSelector.d.ts +9 -0
  109. package/dist/ui/HookProfileSelector.js +54 -0
  110. package/dist/ui/LlmsTxt.d.ts +1 -1
  111. package/dist/ui/LlmsTxt.js +31 -22
  112. package/dist/ui/MemorySelector.d.ts +2 -2
  113. package/dist/ui/MemorySelector.js +28 -16
  114. package/dist/ui/NameInput.d.ts +1 -1
  115. package/dist/ui/NameInput.js +21 -21
  116. package/dist/ui/OptionSelector.d.ts +8 -2
  117. package/dist/ui/OptionSelector.js +83 -26
  118. package/dist/ui/Plugin.d.ts +4 -3
  119. package/dist/ui/Plugin.js +89 -29
  120. package/dist/ui/Progress.d.ts +3 -3
  121. package/dist/ui/Progress.js +23 -22
  122. package/dist/ui/Skills.d.ts +11 -0
  123. package/dist/ui/Skills.js +148 -0
  124. package/dist/ui/StackSelector.d.ts +2 -2
  125. package/dist/ui/StackSelector.js +26 -16
  126. package/dist/ui/Summary.d.ts +3 -3
  127. package/dist/ui/Summary.js +60 -50
  128. package/dist/ui/Welcome.d.ts +1 -1
  129. package/dist/ui/Welcome.js +15 -16
  130. package/dist/ui/theme.d.ts +1 -1
  131. package/dist/ui/theme.js +6 -6
  132. package/package.json +9 -6
  133. package/templates/common/atlassian/mcp-atlassian-snippet.json +16 -0
  134. package/templates/common/repoforge/mcp-repoforge-snippet.json +11 -0
  135. package/templates/common/repoforge/repoforge.yaml +34 -0
  136. package/templates/github/deploy-docker-zero-downtime.yml +140 -0
  137. package/templates/github/repoforge-graph.yml +45 -0
  138. package/templates/gitlab/deploy-docker-zero-downtime.yml +57 -0
  139. package/templates/local-ai/.env.example +17 -0
  140. package/templates/local-ai/docker-compose.yml +95 -0
  141. package/templates/security-hooks/claude-settings-security.json +30 -0
  142. package/templates/security-hooks/commit-msg-signing +29 -0
  143. package/templates/security-hooks/pre-commit-permissions +74 -0
  144. package/templates/security-hooks/pre-commit-secrets +74 -0
  145. package/templates/security-hooks/pre-push-branch-protection +62 -0
  146. package/templates/security-hooks/pre-push-deps +83 -0
  147. package/templates/security-hooks/pre-push-signing +67 -0
  148. package/templates/woodpecker/deploy-docker-zero-downtime.yml +50 -0
  149. package/templates/workflows/ci-pipeline.dot +15 -0
  150. package/templates/workflows/feature-flow.dot +21 -0
  151. package/templates/workflows/release.dot +16 -0
  152. package/dist/__integration__/helpers.d.ts +0 -20
  153. package/dist/__integration__/helpers.d.ts.map +0 -1
  154. package/dist/__integration__/helpers.js +0 -31
  155. package/dist/__integration__/helpers.js.map +0 -1
  156. package/dist/commands/analyze.d.ts.map +0 -1
  157. package/dist/commands/analyze.js.map +0 -1
  158. package/dist/commands/ci.d.ts.map +0 -1
  159. package/dist/commands/ci.js.map +0 -1
  160. package/dist/commands/doctor.d.ts.map +0 -1
  161. package/dist/commands/doctor.js.map +0 -1
  162. package/dist/commands/init.d.ts.map +0 -1
  163. package/dist/commands/init.js.map +0 -1
  164. package/dist/commands/llmstxt.d.ts.map +0 -1
  165. package/dist/commands/llmstxt.js.map +0 -1
  166. package/dist/commands/plugin.d.ts.map +0 -1
  167. package/dist/commands/plugin.js.map +0 -1
  168. package/dist/constants.d.ts.map +0 -1
  169. package/dist/constants.js.map +0 -1
  170. package/dist/index.d.ts.map +0 -1
  171. package/dist/index.js.map +0 -1
  172. package/dist/lib/common.d.ts.map +0 -1
  173. package/dist/lib/common.js.map +0 -1
  174. package/dist/lib/docker.d.ts.map +0 -1
  175. package/dist/lib/docker.js.map +0 -1
  176. package/dist/lib/frontmatter.d.ts.map +0 -1
  177. package/dist/lib/frontmatter.js.map +0 -1
  178. package/dist/lib/plugin.d.ts.map +0 -1
  179. package/dist/lib/plugin.js.map +0 -1
  180. package/dist/lib/template.d.ts.map +0 -1
  181. package/dist/lib/template.js.map +0 -1
  182. package/dist/types/index.d.ts.map +0 -1
  183. package/dist/types/index.js.map +0 -1
  184. package/dist/ui/AnalyzeUI.d.ts.map +0 -1
  185. package/dist/ui/AnalyzeUI.js.map +0 -1
  186. package/dist/ui/App.d.ts.map +0 -1
  187. package/dist/ui/App.js.map +0 -1
  188. package/dist/ui/CI.d.ts.map +0 -1
  189. package/dist/ui/CI.js.map +0 -1
  190. package/dist/ui/CIContext.d.ts.map +0 -1
  191. package/dist/ui/CIContext.js.map +0 -1
  192. package/dist/ui/CISelector.d.ts.map +0 -1
  193. package/dist/ui/CISelector.js.map +0 -1
  194. package/dist/ui/Doctor.d.ts.map +0 -1
  195. package/dist/ui/Doctor.js.map +0 -1
  196. package/dist/ui/Header.d.ts.map +0 -1
  197. package/dist/ui/Header.js.map +0 -1
  198. package/dist/ui/LlmsTxt.d.ts.map +0 -1
  199. package/dist/ui/LlmsTxt.js.map +0 -1
  200. package/dist/ui/MemorySelector.d.ts.map +0 -1
  201. package/dist/ui/MemorySelector.js.map +0 -1
  202. package/dist/ui/NameInput.d.ts.map +0 -1
  203. package/dist/ui/NameInput.js.map +0 -1
  204. package/dist/ui/OptionSelector.d.ts.map +0 -1
  205. package/dist/ui/OptionSelector.js.map +0 -1
  206. package/dist/ui/Plugin.d.ts.map +0 -1
  207. package/dist/ui/Plugin.js.map +0 -1
  208. package/dist/ui/Progress.d.ts.map +0 -1
  209. package/dist/ui/Progress.js.map +0 -1
  210. package/dist/ui/StackSelector.d.ts.map +0 -1
  211. package/dist/ui/StackSelector.js.map +0 -1
  212. package/dist/ui/Summary.d.ts.map +0 -1
  213. package/dist/ui/Summary.js.map +0 -1
  214. package/dist/ui/Welcome.d.ts.map +0 -1
  215. package/dist/ui/Welcome.js.map +0 -1
  216. package/dist/ui/theme.d.ts.map +0 -1
  217. package/dist/ui/theme.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import type { PluginValidationResult, InstalledPlugin, PluginRegistryEntry } from '../types/index.js';
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"
@@ -1,8 +1,10 @@
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';
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: [{ path: PLUGIN_MANIFEST_FILE, message: 'plugin.json not found' }],
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: 'invalid JSON' }],
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 !== 'string' || !manifest.name) {
38
- errors.push({ path: 'name', message: 'name is required' });
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: 'name', message: 'name must be kebab-case' });
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: 'name', message: 'name must be 2-60 characters' });
48
+ errors.push({ path: "name", message: "name must be 2-60 characters" });
45
49
  }
46
- if (typeof manifest.version !== 'string' || !manifest.version) {
47
- errors.push({ path: 'version', message: 'version is required' });
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({ path: 'version', message: 'version must be semver (e.g. 1.0.0)' });
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 !== 'string' || !manifest.description) {
53
- errors.push({ path: 'description', message: 'description is required' });
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({ path: 'description', message: 'description must be at least 10 characters' });
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({ path: 'description', message: 'description must be at most 200 characters' });
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({ path: assetType, message: `declared ${assetType}/ directory not found` });
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({ path: `${assetType}/${entry}`, message: `declared entry not found` });
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: 'tags', message: 'tags must be an array' });
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: 'tags', message: 'max 10 tags allowed' });
103
+ errors.push({ path: "tags", message: "max 10 tags allowed" });
85
104
  }
86
- return { valid: errors.length === 0, errors, manifest: errors.length === 0 ? manifest : manifest };
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 { success: false, error: `invalid source: ${source}. Use org/repo or a GitHub URL` };
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, '.tmp', `install-${Date.now()}`);
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('git', ['clone', '--depth', '1', gitUrl, tmpDir], {
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
- ? { valid: true, errors: [], manifest: { name: source.split('/').pop() ?? 'unknown', version: '0.0.0', description: 'dry-run placeholder' } }
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.map(e => ` ${e.path}: ${e.message}`).join('\n');
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, '.installed.json'), installedPlugin, { spaces: 2 });
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, '.installed.json');
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 { /* skip corrupt entries */ }
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('https://github.com/')) {
215
- return source.endsWith('.git') ? source : `${source}.git`;
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('github.com/')) {
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