javi-forge 1.6.1 → 1.7.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 (158) hide show
  1. package/README.md +45 -1
  2. package/ci-local/README.md +56 -11
  3. package/ci-local/ci-local.ps1 +445 -0
  4. package/ci-local/ci-local.sh +52 -9
  5. package/ci-local/hooks/commit-msg +120 -44
  6. package/ci-local/hooks/commit-msg.test.sh +286 -0
  7. package/ci-local/hooks/pre-commit +9 -1
  8. package/ci-local/hooks/pre-push +13 -2
  9. package/ci-local/install.ps1 +367 -0
  10. package/ci-local/install.sh +151 -5
  11. package/ci-local/uninstall.ps1 +85 -0
  12. package/ci-local/uninstall.sh +95 -0
  13. package/dist/cli/dispatch/ci.d.ts +12 -0
  14. package/dist/cli/dispatch/ci.js +38 -0
  15. package/dist/cli/dispatch/security.d.ts +11 -0
  16. package/dist/cli/dispatch/security.js +68 -0
  17. package/dist/cli/dispatch/simple-renderers.d.ts +16 -0
  18. package/dist/cli/dispatch/simple-renderers.js +65 -0
  19. package/dist/cli/dispatch/skill-publish.d.ts +15 -0
  20. package/dist/cli/dispatch/skill-publish.js +44 -0
  21. package/dist/cli/dispatch/skills-cmd.d.ts +13 -0
  22. package/dist/cli/dispatch/skills-cmd.js +93 -0
  23. package/dist/cli/dispatch/tdd.d.ts +11 -0
  24. package/dist/cli/dispatch/tdd.js +45 -0
  25. package/dist/cli/dispatch/types.d.ts +29 -0
  26. package/dist/cli/dispatch/types.js +2 -0
  27. package/dist/cli/dispatch/workflow.d.ts +11 -0
  28. package/dist/cli/dispatch/workflow.js +67 -0
  29. package/dist/cli/help.d.ts +127 -0
  30. package/dist/cli/help.js +138 -0
  31. package/dist/cli/runtime.d.ts +16 -0
  32. package/dist/cli/runtime.js +38 -0
  33. package/dist/cli/validators.d.ts +17 -0
  34. package/dist/cli/validators.js +35 -0
  35. package/dist/commands/analyze.js +1 -3
  36. package/dist/commands/ci.d.ts +47 -0
  37. package/dist/commands/ci.js +386 -67
  38. package/dist/commands/doctor.js +2 -4
  39. package/dist/commands/init/report.d.ts +13 -0
  40. package/dist/commands/init/report.js +13 -0
  41. package/dist/commands/init/steps/agent-skills.d.ts +13 -0
  42. package/dist/commands/init/steps/agent-skills.js +45 -0
  43. package/dist/commands/init/steps/ai-sync.d.ts +15 -0
  44. package/dist/commands/init/steps/ai-sync.js +62 -0
  45. package/dist/commands/init/steps/ci.d.ts +24 -0
  46. package/dist/commands/init/steps/ci.js +74 -0
  47. package/dist/commands/init/steps/claude-md.d.ts +14 -0
  48. package/dist/commands/init/steps/claude-md.js +54 -0
  49. package/dist/commands/init/steps/code-graph.d.ts +16 -0
  50. package/dist/commands/init/steps/code-graph.js +68 -0
  51. package/dist/commands/init/steps/context-dir.d.ts +13 -0
  52. package/dist/commands/init/steps/context-dir.js +47 -0
  53. package/dist/commands/init/steps/docker-deploy.d.ts +16 -0
  54. package/dist/commands/init/steps/docker-deploy.js +60 -0
  55. package/dist/commands/init/steps/ghagga.d.ts +15 -0
  56. package/dist/commands/init/steps/ghagga.js +58 -0
  57. package/dist/commands/init/steps/git.d.ts +25 -0
  58. package/dist/commands/init/steps/git.js +81 -0
  59. package/dist/commands/init/steps/gitignore.d.ts +13 -0
  60. package/dist/commands/init/steps/gitignore.js +39 -0
  61. package/dist/commands/init/steps/local-ai.d.ts +14 -0
  62. package/dist/commands/init/steps/local-ai.js +53 -0
  63. package/dist/commands/init/steps/manifest.d.ts +13 -0
  64. package/dist/commands/init/steps/manifest.js +58 -0
  65. package/dist/commands/init/steps/memory.d.ts +15 -0
  66. package/dist/commands/init/steps/memory.js +60 -0
  67. package/dist/commands/init/steps/mock.d.ts +13 -0
  68. package/dist/commands/init/steps/mock.js +64 -0
  69. package/dist/commands/init/steps/sdd.d.ts +12 -0
  70. package/dist/commands/init/steps/sdd.js +40 -0
  71. package/dist/commands/init/steps/security.d.ts +28 -0
  72. package/dist/commands/init/steps/security.js +103 -0
  73. package/dist/commands/init/types.d.ts +30 -0
  74. package/dist/commands/init/types.js +2 -0
  75. package/dist/commands/init.d.ts +5 -4
  76. package/dist/commands/init.js +42 -630
  77. package/dist/commands/llmstxt.js +1 -1
  78. package/dist/commands/plugin.js +1 -1
  79. package/dist/commands/security.js +23 -4
  80. package/dist/commands/skills/analysis.d.ts +19 -0
  81. package/dist/commands/skills/analysis.js +211 -0
  82. package/dist/commands/skills/benchmark.d.ts +6 -0
  83. package/dist/commands/skills/benchmark.js +82 -0
  84. package/dist/commands/skills/constants.d.ts +10 -0
  85. package/dist/commands/skills/constants.js +27 -0
  86. package/dist/commands/skills/directives.d.ts +21 -0
  87. package/dist/commands/skills/directives.js +101 -0
  88. package/dist/commands/skills/parsing.d.ts +16 -0
  89. package/dist/commands/skills/parsing.js +108 -0
  90. package/dist/commands/skills/rules.d.ts +5 -0
  91. package/dist/commands/skills/rules.js +32 -0
  92. package/dist/commands/skills/scoring.d.ts +47 -0
  93. package/dist/commands/skills/scoring.js +306 -0
  94. package/dist/commands/skills.d.ts +7 -125
  95. package/dist/commands/skills.js +8 -874
  96. package/dist/commands/tdd-pipeline.js +1 -1
  97. package/dist/commands/tdd.js +1 -1
  98. package/dist/commands/workflow.js +2 -0
  99. package/dist/constants.js +2 -2
  100. package/dist/index.js +26 -473
  101. package/dist/lib/agent-skills.js +1 -1
  102. package/dist/{commands → lib}/atlassian-mcp.js +2 -2
  103. package/dist/lib/auto-skill-install.js +2 -3
  104. package/dist/lib/auto-wire.js +7 -7
  105. package/dist/lib/ci-config.d.ts +65 -0
  106. package/dist/lib/ci-config.js +297 -0
  107. package/dist/lib/claudemd.js +1 -1
  108. package/dist/lib/codex-export.js +4 -8
  109. package/dist/lib/common.js +2 -2
  110. package/dist/lib/context.js +2 -2
  111. package/dist/{commands → lib}/crash-recovery.js +4 -4
  112. package/dist/lib/docker.d.ts +22 -1
  113. package/dist/lib/docker.js +57 -49
  114. package/dist/lib/exec.d.ts +10 -0
  115. package/dist/lib/exec.js +11 -0
  116. package/dist/lib/frontmatter.js +2 -2
  117. package/dist/{commands → lib}/harness-audit.js +1 -1
  118. package/dist/{commands → lib}/parallel-batch.js +1 -3
  119. package/dist/lib/plugin.js +2 -4
  120. package/dist/{commands → lib}/secret-scanner.js +3 -1
  121. package/dist/{commands → lib}/security-analysis.js +4 -2
  122. package/dist/lib/skill-publish.js +23 -5
  123. package/dist/{commands → lib}/skill-scanner.d.ts +1 -1
  124. package/dist/{commands → lib}/skill-scanner.js +3 -3
  125. package/dist/lib/stack-detector.js +3 -3
  126. package/dist/{commands → lib}/supply-chain.js +1 -1
  127. package/dist/lib/template.js +2 -2
  128. package/dist/lib/workflow/discovery.d.ts +8 -0
  129. package/dist/lib/workflow/discovery.js +18 -1
  130. package/dist/lib/workflow/parser.js +15 -15
  131. package/dist/lib/workflow/validator.js +4 -4
  132. package/dist/tasks/index.d.ts +3 -3
  133. package/dist/tasks/index.js +3 -3
  134. package/dist/tasks/scaffold-tasks.d.ts +1 -1
  135. package/dist/tasks/scaffold-tasks.js +10 -10
  136. package/dist/tasks/task-id.d.ts +1 -1
  137. package/dist/tasks/task-id.js +9 -5
  138. package/dist/tasks/task-tracker.d.ts +1 -1
  139. package/dist/tasks/task-tracker.js +7 -7
  140. package/dist/ui/App.js +1 -1
  141. package/dist/ui/CIContext.js +1 -2
  142. package/dist/ui/Header.js +4 -4
  143. package/dist/ui/NameInput.js +1 -1
  144. package/dist/ui/Progress.d.ts +1 -1
  145. package/dist/ui/Progress.js +1 -1
  146. package/dist/ui/StackSelector.js +1 -1
  147. package/lib/common.psm1 +167 -0
  148. package/lib/common.sh +30 -0
  149. package/package.json +38 -19
  150. /package/dist/{commands → lib}/atlassian-mcp.d.ts +0 -0
  151. /package/dist/{commands → lib}/crash-recovery.d.ts +0 -0
  152. /package/dist/{commands → lib}/harness-audit.d.ts +0 -0
  153. /package/dist/{commands → lib}/parallel-batch.d.ts +0 -0
  154. /package/dist/{commands → lib}/secret-scanner.d.ts +0 -0
  155. /package/dist/{commands → lib}/security-analysis.d.ts +0 -0
  156. /package/dist/{commands → lib}/supply-chain.d.ts +0 -0
  157. /package/dist/{commands → lib}/team-presets.d.ts +0 -0
  158. /package/dist/{commands → lib}/team-presets.js +0 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Versioned mixed-runner CI config (`.javi-forge/ci.yaml`) — schema and loader.
3
+ *
4
+ * Hybrid repositories declare every required runner explicitly and in order.
5
+ * Single-stack repositories need no config: auto-detection stays the default.
6
+ *
7
+ * Validation is fail-closed: any schema violation produces a CIConfigError
8
+ * listing every problem, and no partially-valid config is ever returned.
9
+ * YAML parsing uses the `yaml` package, already a runtime dependency.
10
+ */
11
+ import type { Stack } from "../types/index.js";
12
+ export declare const CI_CONFIG_VERSION = 1;
13
+ /** Default config locations, in discovery order, relative to the project root. */
14
+ export declare const CI_CONFIG_CANDIDATES: readonly [".javi-forge/ci.yaml", ".javi-forge/ci.yml"];
15
+ export interface CIRunnerConfig {
16
+ /** Unique runner name (used in step ids and reports) */
17
+ name: string;
18
+ /** Toolchain stack — selects the default CI image and default commands */
19
+ stack: Stack;
20
+ /** Working directory relative to the project root (default: ".") */
21
+ directory: string;
22
+ /** Explicit container image (mutually exclusive with `buildContext`) */
23
+ image?: string;
24
+ /** Docker build context directory (mutually exclusive with `image`) */
25
+ buildContext?: string;
26
+ /** Dependency setup commands, run before lint/build/test */
27
+ setup: string[];
28
+ lint: string[];
29
+ build: string[];
30
+ test: string[];
31
+ security: string[];
32
+ /** Tools that must exist in the runner environment (validated fail-closed) */
33
+ requires: string[];
34
+ }
35
+ export interface CIConfig {
36
+ version: number;
37
+ runners: CIRunnerConfig[];
38
+ }
39
+ export interface CIConfigValidationError {
40
+ path: string;
41
+ message: string;
42
+ }
43
+ export declare class CIConfigError extends Error {
44
+ readonly errors: CIConfigValidationError[];
45
+ constructor(errors: CIConfigValidationError[], source?: string);
46
+ }
47
+ /** Stacks accepted by the CI config schema and the --stack override. */
48
+ export declare const CI_STACKS: readonly string[];
49
+ /**
50
+ * Parse and validate CI config YAML text. Throws CIConfigError listing every
51
+ * validation problem; never returns a partially valid config.
52
+ */
53
+ export declare function parseCIConfig(rawYaml: string, source?: string): CIConfig;
54
+ /**
55
+ * Load and validate a CI config file. Fails closed: a missing file or any
56
+ * validation error throws — no config is ever silently ignored.
57
+ */
58
+ export declare function loadCIConfig(configPath: string): Promise<CIConfig>;
59
+ /**
60
+ * Discover the default CI config for a project directory.
61
+ * Returns the config path, or null when the project has no config
62
+ * (single-stack auto-detection stays the default).
63
+ */
64
+ export declare function findCIConfig(projectDir: string): Promise<string | null>;
65
+ //# sourceMappingURL=ci-config.d.ts.map
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Versioned mixed-runner CI config (`.javi-forge/ci.yaml`) — schema and loader.
3
+ *
4
+ * Hybrid repositories declare every required runner explicitly and in order.
5
+ * Single-stack repositories need no config: auto-detection stays the default.
6
+ *
7
+ * Validation is fail-closed: any schema violation produces a CIConfigError
8
+ * listing every problem, and no partially-valid config is ever returned.
9
+ * YAML parsing uses the `yaml` package, already a runtime dependency.
10
+ */
11
+ import path from "node:path";
12
+ import fs from "fs-extra";
13
+ import YAML from "yaml";
14
+ // =============================================================================
15
+ // Types
16
+ // =============================================================================
17
+ export const CI_CONFIG_VERSION = 1;
18
+ /** Default config locations, in discovery order, relative to the project root. */
19
+ export const CI_CONFIG_CANDIDATES = [
20
+ ".javi-forge/ci.yaml",
21
+ ".javi-forge/ci.yml",
22
+ ];
23
+ export class CIConfigError extends Error {
24
+ errors;
25
+ constructor(errors, source) {
26
+ const where = source ? ` in ${source}` : "";
27
+ const lines = errors.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
28
+ super(`Invalid CI config${where}:\n${lines}`);
29
+ this.name = "CIConfigError";
30
+ this.errors = errors;
31
+ }
32
+ }
33
+ // =============================================================================
34
+ // Validation
35
+ // =============================================================================
36
+ const VALID_STACKS = [
37
+ "node",
38
+ "python",
39
+ "go",
40
+ "rust",
41
+ "java-gradle",
42
+ "java-maven",
43
+ "elixir",
44
+ ];
45
+ /** Stacks accepted by the CI config schema and the --stack override. */
46
+ export const CI_STACKS = VALID_STACKS;
47
+ const TOP_LEVEL_FIELDS = new Set(["version", "runners"]);
48
+ /** Runner names become docker image tags — keep them tag-safe. */
49
+ const RUNNER_NAME_RE = /^[a-zA-Z0-9_][a-zA-Z0-9._-]*$/;
50
+ /** Required tools are checked via `command -v <tool>` — no shell metachars. */
51
+ const TOOL_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._+~-]*$/;
52
+ const RUNNER_FIELDS = new Set([
53
+ "name",
54
+ "stack",
55
+ "directory",
56
+ "image",
57
+ "build-context",
58
+ "setup",
59
+ "lint",
60
+ "build",
61
+ "test",
62
+ "security",
63
+ "requires",
64
+ ]);
65
+ function isRecord(value) {
66
+ return typeof value === "object" && value !== null && !Array.isArray(value);
67
+ }
68
+ function normalizeCommands(value, fieldPath, errors) {
69
+ if (value === undefined)
70
+ return [];
71
+ if (typeof value === "string")
72
+ return value.trim() ? [value] : [];
73
+ if (Array.isArray(value)) {
74
+ const bad = value.some((v) => typeof v !== "string" || !v.trim());
75
+ if (!bad)
76
+ return value;
77
+ }
78
+ errors.push({
79
+ path: fieldPath,
80
+ message: "must be a non-empty string or a list of non-empty strings",
81
+ });
82
+ return [];
83
+ }
84
+ function normalizeStringList(value, fieldPath, errors) {
85
+ if (value === undefined)
86
+ return [];
87
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
88
+ return value;
89
+ }
90
+ errors.push({ path: fieldPath, message: "must be a list of strings" });
91
+ return [];
92
+ }
93
+ function validateRunner(raw, index, errors) {
94
+ const base = `runners[${index}]`;
95
+ if (!isRecord(raw)) {
96
+ errors.push({ path: base, message: "runner must be an object" });
97
+ return null;
98
+ }
99
+ for (const key of Object.keys(raw)) {
100
+ if (!RUNNER_FIELDS.has(key)) {
101
+ errors.push({
102
+ path: `${base}.${key}`,
103
+ message: `unknown field "${key}"`,
104
+ });
105
+ }
106
+ }
107
+ const name = raw.name;
108
+ if (typeof name !== "string" || !name.trim()) {
109
+ errors.push({
110
+ path: `${base}.name`,
111
+ message: "name is required and must be a non-empty string",
112
+ });
113
+ }
114
+ else if (!RUNNER_NAME_RE.test(name.trim())) {
115
+ errors.push({
116
+ path: `${base}.name`,
117
+ message: "name must start with a letter, digit or underscore and contain only [a-zA-Z0-9._-] (it is used as a docker image tag)",
118
+ });
119
+ }
120
+ const stack = raw.stack;
121
+ if (typeof stack !== "string" || !VALID_STACKS.includes(stack)) {
122
+ errors.push({
123
+ path: `${base}.stack`,
124
+ message: `stack is required and must be one of: ${VALID_STACKS.join(", ")}`,
125
+ });
126
+ }
127
+ let directory = ".";
128
+ if (raw.directory !== undefined) {
129
+ if (typeof raw.directory !== "string" || !raw.directory.trim()) {
130
+ errors.push({
131
+ path: `${base}.directory`,
132
+ message: "directory must be a non-empty string",
133
+ });
134
+ }
135
+ else {
136
+ const normalized = path.posix.normalize(raw.directory);
137
+ if (path.isAbsolute(raw.directory) ||
138
+ normalized === ".." ||
139
+ normalized.startsWith("../")) {
140
+ errors.push({
141
+ path: `${base}.directory`,
142
+ message: "directory must stay inside the project root",
143
+ });
144
+ }
145
+ else {
146
+ directory = normalized;
147
+ }
148
+ }
149
+ }
150
+ let image;
151
+ if (raw.image !== undefined) {
152
+ if (typeof raw.image !== "string" || !raw.image.trim()) {
153
+ errors.push({
154
+ path: `${base}.image`,
155
+ message: "image must be a non-empty string",
156
+ });
157
+ }
158
+ else {
159
+ image = raw.image;
160
+ }
161
+ }
162
+ let buildContext;
163
+ if (raw["build-context"] !== undefined) {
164
+ if (typeof raw["build-context"] !== "string" ||
165
+ !raw["build-context"].trim()) {
166
+ errors.push({
167
+ path: `${base}.build-context`,
168
+ message: "build-context must be a non-empty string (directory)",
169
+ });
170
+ }
171
+ else {
172
+ buildContext = raw["build-context"];
173
+ }
174
+ }
175
+ if (image && buildContext) {
176
+ errors.push({
177
+ path: `${base}.image`,
178
+ message: "image and build-context are mutually exclusive — set only one",
179
+ });
180
+ }
181
+ const requires = normalizeStringList(raw.requires, `${base}.requires`, errors);
182
+ for (const tool of requires) {
183
+ if (!TOOL_NAME_RE.test(tool)) {
184
+ errors.push({
185
+ path: `${base}.requires`,
186
+ message: `requires entry "${tool}" has unsafe characters — expected a plain tool name like python, ruff or node`,
187
+ });
188
+ }
189
+ }
190
+ // If anything failed, the caller discards all runners (fail closed).
191
+ return {
192
+ name: typeof name === "string" ? name.trim() : "",
193
+ stack: stack,
194
+ directory,
195
+ image,
196
+ buildContext,
197
+ setup: normalizeCommands(raw.setup, `${base}.setup`, errors),
198
+ lint: normalizeCommands(raw.lint, `${base}.lint`, errors),
199
+ build: normalizeCommands(raw.build, `${base}.build`, errors),
200
+ test: normalizeCommands(raw.test, `${base}.test`, errors),
201
+ security: normalizeCommands(raw.security, `${base}.security`, errors),
202
+ requires,
203
+ };
204
+ }
205
+ // =============================================================================
206
+ // Public API
207
+ // =============================================================================
208
+ /**
209
+ * Parse and validate CI config YAML text. Throws CIConfigError listing every
210
+ * validation problem; never returns a partially valid config.
211
+ */
212
+ export function parseCIConfig(rawYaml, source) {
213
+ let doc;
214
+ try {
215
+ doc = YAML.parse(rawYaml);
216
+ }
217
+ catch (e) {
218
+ throw new CIConfigError([
219
+ {
220
+ path: "<document>",
221
+ message: `invalid YAML: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`,
222
+ },
223
+ ], source);
224
+ }
225
+ const errors = [];
226
+ if (!isRecord(doc)) {
227
+ throw new CIConfigError([{ path: "<document>", message: "config must be a YAML object/mapping" }], source);
228
+ }
229
+ for (const key of Object.keys(doc)) {
230
+ if (!TOP_LEVEL_FIELDS.has(key)) {
231
+ errors.push({ path: key, message: `unknown field "${key}"` });
232
+ }
233
+ }
234
+ if (doc.version !== CI_CONFIG_VERSION) {
235
+ errors.push({
236
+ path: "version",
237
+ message: `version is required and must be the number ${CI_CONFIG_VERSION}`,
238
+ });
239
+ }
240
+ if (!Array.isArray(doc.runners) || doc.runners.length === 0) {
241
+ errors.push({
242
+ path: "runners",
243
+ message: "runners is required and must be a non-empty list",
244
+ });
245
+ }
246
+ const runners = [];
247
+ if (Array.isArray(doc.runners)) {
248
+ doc.runners.forEach((raw, index) => {
249
+ const runner = validateRunner(raw, index, errors);
250
+ if (runner)
251
+ runners.push(runner);
252
+ });
253
+ const seen = new Set();
254
+ for (const runner of runners) {
255
+ if (!runner.name)
256
+ continue;
257
+ if (seen.has(runner.name)) {
258
+ errors.push({
259
+ path: "runners",
260
+ message: `duplicate runner name "${runner.name}"`,
261
+ });
262
+ }
263
+ seen.add(runner.name);
264
+ }
265
+ }
266
+ if (errors.length > 0) {
267
+ throw new CIConfigError(errors, source);
268
+ }
269
+ return { version: CI_CONFIG_VERSION, runners };
270
+ }
271
+ /**
272
+ * Load and validate a CI config file. Fails closed: a missing file or any
273
+ * validation error throws — no config is ever silently ignored.
274
+ */
275
+ export async function loadCIConfig(configPath) {
276
+ if (!(await fs.pathExists(configPath))) {
277
+ throw new CIConfigError([
278
+ { path: configPath, message: "config file not found" },
279
+ ]);
280
+ }
281
+ const raw = await fs.readFile(configPath, "utf-8");
282
+ return parseCIConfig(raw, configPath);
283
+ }
284
+ /**
285
+ * Discover the default CI config for a project directory.
286
+ * Returns the config path, or null when the project has no config
287
+ * (single-stack auto-detection stays the default).
288
+ */
289
+ export async function findCIConfig(projectDir) {
290
+ for (const candidate of CI_CONFIG_CANDIDATES) {
291
+ const candidatePath = path.join(projectDir, candidate);
292
+ if (await fs.pathExists(candidatePath))
293
+ return candidatePath;
294
+ }
295
+ return null;
296
+ }
297
+ //# sourceMappingURL=ci-config.js.map
@@ -48,7 +48,7 @@ const PLUGIN_INSTRUCTIONS = {
48
48
  // Internal helpers
49
49
  // =============================================================================
50
50
  function getStackClaudeMd(stack) {
51
- return STACK_CLAUDEMD_MAP[stack] ?? STACK_CLAUDEMD_MAP["default"];
51
+ return STACK_CLAUDEMD_MAP[stack] ?? STACK_CLAUDEMD_MAP.default;
52
52
  }
53
53
  export function buildClaudeMd(projectName, stack, entry, contextDir, modules) {
54
54
  const lines = [];
@@ -1,5 +1,5 @@
1
+ import path from "node:path";
1
2
  import fs from "fs-extra";
2
- import path from "path";
3
3
  import { PLUGIN_MANIFEST_FILE, PLUGINS_DIR } from "../constants.js";
4
4
  import { parseFrontmatter } from "./frontmatter.js";
5
5
  const DEFAULT_MODEL = "o4-mini";
@@ -12,15 +12,11 @@ export function skillToCodexToml(raw) {
12
12
  const parsed = parseFrontmatter(raw);
13
13
  if (!parsed)
14
14
  return null;
15
- const name = parsed.data["name"];
15
+ const name = parsed.data.name;
16
16
  if (typeof name !== "string" || !name)
17
17
  return null;
18
- const description = typeof parsed.data["description"] === "string"
19
- ? parsed.data["description"]
20
- : "";
21
- const model = typeof parsed.data["model"] === "string"
22
- ? parsed.data["model"]
23
- : DEFAULT_MODEL;
18
+ const description = typeof parsed.data.description === "string" ? parsed.data.description : "";
19
+ const model = typeof parsed.data.model === "string" ? parsed.data.model : DEFAULT_MODEL;
24
20
  const instructions = description
25
21
  ? `${description}\n\n${parsed.content}`.trim()
26
22
  : parsed.content.trim();
@@ -1,5 +1,5 @@
1
+ import path from "node:path";
1
2
  import fs from "fs-extra";
2
- import path from "path";
3
3
  /**
4
4
  * Detect the project stack by looking for well-known build/config files.
5
5
  * Returns the first match (precedence order matters).
@@ -18,7 +18,7 @@ export async function detectStack(projectDir) {
18
18
  }
19
19
  // Node.js
20
20
  if (await exists("package.json")) {
21
- const pkg = await fs
21
+ const _pkg = await fs
22
22
  .readJson(path.join(projectDir, "package.json"))
23
23
  .catch(() => ({}));
24
24
  const buildTool = (await exists("pnpm-lock.yaml"))
@@ -1,11 +1,11 @@
1
+ import path from "node:path";
1
2
  import fs from "fs-extra";
2
- import path from "path";
3
3
  import { STACK_CONTEXT_MAP } from "../constants.js";
4
4
  // =============================================================================
5
5
  // Internal helpers
6
6
  // =============================================================================
7
7
  function getStackContext(stack) {
8
- return STACK_CONTEXT_MAP[stack] ?? STACK_CONTEXT_MAP["default"];
8
+ return STACK_CONTEXT_MAP[stack] ?? STACK_CONTEXT_MAP.default;
9
9
  }
10
10
  export function buildIndexMd(projectName, stackCtx, ciProvider, memory) {
11
11
  return `# ${projectName} — Project Index
@@ -3,9 +3,7 @@
3
3
  * when checkpoint files are corrupted or lost. Git is the single
4
4
  * source of truth for what was actually done.
5
5
  */
6
- import { execFile } from "child_process";
7
- import { promisify } from "util";
8
- const execFileAsync = promisify(execFile);
6
+ import { execFileAsync } from "./exec.js";
9
7
  // ── Git operations ──
10
8
  export async function getGitLog(projectDir, options = {}) {
11
9
  const args = [
@@ -41,7 +39,7 @@ const CONVENTIONAL_RE = /^(feat|fix|test|refactor|docs|chore|ci|perf|style|build
41
39
  const TASK_ID_RE = /#(\d+)|task[- ](\d+[.\d]*)/i;
42
40
  export function parseCommitPhase(message) {
43
41
  const match = CONVENTIONAL_RE.exec(message);
44
- return match ? match[1] : null;
42
+ return match ? (match[1] ?? null) : null;
45
43
  }
46
44
  export function extractTaskId(message) {
47
45
  const match = TASK_ID_RE.exec(message);
@@ -59,6 +57,8 @@ export function parseGitLog(raw) {
59
57
  if (lines.length === 0)
60
58
  continue;
61
59
  const headerLine = lines[0];
60
+ if (!headerLine)
61
+ continue;
62
62
  const parts = headerLine.split("|");
63
63
  if (parts.length < 3)
64
64
  continue;
@@ -2,6 +2,12 @@ import type { Stack } from "../types/index.js";
2
2
  export interface DockerRunOptions {
3
3
  /** Absolute path to mount as /home/runner/work */
4
4
  projectDir: string;
5
+ /**
6
+ * Pre-resolved image from the CI runner (required). Passed through
7
+ * verbatim, including digest pins (`name@sha256:...`). runInContainer
8
+ * never performs marker detection — resolution happens once, upstream.
9
+ */
10
+ image: string;
5
11
  /** Command to run inside the container */
6
12
  command: string;
7
13
  /** Timeout in seconds (default: 600) */
@@ -22,6 +28,14 @@ export interface DockerImageOptions {
22
28
  javaVersion?: string;
23
29
  /** Directory where Dockerfiles are stored (defaults to package-bundled dir) */
24
30
  dockerfilesDir?: string;
31
+ /**
32
+ * Custom build context directory (must contain a Dockerfile, which is
33
+ * the source of truth and is never overwritten). Mutually exclusive
34
+ * with dockerfilesDir.
35
+ */
36
+ buildContext?: string;
37
+ /** Image tag for build-context builds (default: derived from the stack) */
38
+ imageTag?: string;
25
39
  }
26
40
  export declare function getImageName(stack: Stack): string;
27
41
  export declare function getDockerfileContent(stack: Stack): string;
@@ -30,6 +44,12 @@ export declare function isDockerAvailable(): Promise<boolean>;
30
44
  * Ensure a CI Docker image exists and is up-to-date.
31
45
  * Rebuilds only if the Dockerfile content has changed (hash-based staleness check).
32
46
  * Returns the image name.
47
+ *
48
+ * Two modes:
49
+ * - per-stack (default): the Dockerfile is generated/managed by javi-forge
50
+ * - build-context: the configured context directory provides its own
51
+ * Dockerfile (source of truth, never overwritten); fails closed if
52
+ * the Dockerfile is missing.
33
53
  */
34
54
  export declare function ensureImage(options: DockerImageOptions): Promise<string>;
35
55
  /**
@@ -40,6 +60,7 @@ export declare function ensureImage(options: DockerImageOptions): Promise<string
40
60
  export declare function runInContainer(options: DockerRunOptions): Promise<DockerRunResult>;
41
61
  /**
42
62
  * Open an interactive shell inside the CI container.
63
+ * The image must be pre-resolved by the caller (no marker detection here).
43
64
  */
44
- export declare function openShell(projectDir: string): Promise<void>;
65
+ export declare function openShell(projectDir: string, image: string): Promise<void>;
45
66
  //# sourceMappingURL=docker.d.ts.map
@@ -1,9 +1,8 @@
1
- import { execFile, spawn } from "child_process";
2
- import crypto from "crypto";
1
+ import { spawn } from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import path from "node:path";
3
4
  import fs from "fs-extra";
4
- import path from "path";
5
- import { promisify } from "util";
6
- const execFileAsync = promisify(execFile);
5
+ import { execFileAsync } from "./exec.js";
7
6
  // =============================================================================
8
7
  // Image name
9
8
  // =============================================================================
@@ -96,18 +95,40 @@ export async function isDockerAvailable() {
96
95
  * Ensure a CI Docker image exists and is up-to-date.
97
96
  * Rebuilds only if the Dockerfile content has changed (hash-based staleness check).
98
97
  * Returns the image name.
98
+ *
99
+ * Two modes:
100
+ * - per-stack (default): the Dockerfile is generated/managed by javi-forge
101
+ * - build-context: the configured context directory provides its own
102
+ * Dockerfile (source of truth, never overwritten); fails closed if
103
+ * the Dockerfile is missing.
99
104
  */
100
105
  export async function ensureImage(options) {
101
- const { stack, javaVersion, dockerfilesDir } = options;
102
- const imageName = getImageName(stack);
103
- // Resolve Dockerfile path
104
- const dockerDir = dockerfilesDir ??
105
- path.join(path.dirname(new URL(import.meta.url).pathname), "../../ci-local/docker");
106
- const dockerfilePath = path.join(dockerDir, `${stack}.Dockerfile`);
107
- // Write Dockerfile if it doesn't exist yet (first run)
108
- if (!(await fs.pathExists(dockerfilePath))) {
109
- await fs.ensureDir(dockerDir);
110
- await fs.writeFile(dockerfilePath, getDockerfileContent(stack), "utf-8");
106
+ const { stack, javaVersion, dockerfilesDir, buildContext, imageTag } = options;
107
+ let imageName;
108
+ let dockerfilePath;
109
+ let contextDir;
110
+ if (buildContext) {
111
+ imageName = imageTag ?? getImageName(stack);
112
+ dockerfilePath = path.join(buildContext, "Dockerfile");
113
+ contextDir = buildContext;
114
+ // Fail closed: a build context without a Dockerfile is a config error,
115
+ // never something to silently work around.
116
+ if (!(await fs.pathExists(dockerfilePath))) {
117
+ throw new Error(`build-context "${buildContext}" has no Dockerfile — ` +
118
+ "add one or use image/stack defaults instead");
119
+ }
120
+ }
121
+ else {
122
+ imageName = getImageName(stack);
123
+ const dockerDir = dockerfilesDir ??
124
+ path.join(path.dirname(new URL(import.meta.url).pathname), "../../ci-local/docker");
125
+ dockerfilePath = path.join(dockerDir, `${stack}.Dockerfile`);
126
+ contextDir = dockerDir;
127
+ // Write Dockerfile if it doesn't exist yet (first run)
128
+ if (!(await fs.pathExists(dockerfilePath))) {
129
+ await fs.ensureDir(dockerDir);
130
+ await fs.writeFile(dockerfilePath, getDockerfileContent(stack), "utf-8");
131
+ }
111
132
  }
112
133
  // Staleness check: compare Dockerfile hash with the one embedded in the image label
113
134
  const content = await fs.readFile(dockerfilePath, "utf-8");
@@ -138,10 +159,12 @@ export async function ensureImage(options) {
138
159
  "-t",
139
160
  imageName,
140
161
  ];
141
- if (javaVersion && (stack === "java-gradle" || stack === "java-maven")) {
162
+ if (!buildContext &&
163
+ javaVersion &&
164
+ (stack === "java-gradle" || stack === "java-maven")) {
142
165
  buildArgs.push("--build-arg", `JAVA_VERSION=${javaVersion}`);
143
166
  }
144
- buildArgs.push(dockerDir);
167
+ buildArgs.push(contextDir);
145
168
  await new Promise((resolve, reject) => {
146
169
  const proc = spawn("docker", buildArgs, { stdio: "inherit" });
147
170
  proc.on("close", (code) => code === 0
@@ -160,10 +183,16 @@ export async function ensureImage(options) {
160
183
  * Streams output to process.stdout/stderr by default.
161
184
  */
162
185
  export async function runInContainer(options) {
163
- const { projectDir, command, timeout = 600, stream = true, user } = options;
164
- const stack = await detectStackFromDir(projectDir);
165
- const imageName = getImageName(stack);
186
+ const { projectDir, image, command, timeout = 600, stream = true, user, } = options;
187
+ // The image is always pre-resolved by the caller (resolveCIRunners →
188
+ // ensureImage or an explicit/digest-pinned config image). No marker
189
+ // detection happens on this path — ever.
190
+ const imageName = image;
166
191
  const isInteractive = process.stdin.isTTY && stream;
192
+ // Use --mount instead of -v: the -v form parses the value as a single
193
+ // "src:dst[:opt]" colon-separated string, which breaks (and could be
194
+ // hijacked) when projectDir itself contains a colon. --mount takes
195
+ // comma-separated key=value pairs and is colon-safe.
167
196
  const dockerArgs = [
168
197
  "run",
169
198
  "--rm",
@@ -173,8 +202,8 @@ export async function runInContainer(options) {
173
202
  "--entrypoint",
174
203
  "",
175
204
  ...(user ? ["--user", user] : []),
176
- "-v",
177
- `${projectDir}:/home/runner/work`,
205
+ "--mount",
206
+ `type=bind,source=${projectDir},target=/home/runner/work`,
178
207
  "-e",
179
208
  "CI=true",
180
209
  imageName,
@@ -204,10 +233,10 @@ export async function runInContainer(options) {
204
233
  }
205
234
  /**
206
235
  * Open an interactive shell inside the CI container.
236
+ * The image must be pre-resolved by the caller (no marker detection here).
207
237
  */
208
- export async function openShell(projectDir) {
209
- const stack = await detectStackFromDir(projectDir);
210
- const imageName = getImageName(stack);
238
+ export async function openShell(projectDir, image) {
239
+ const imageName = image;
211
240
  await new Promise((resolve, reject) => {
212
241
  const proc = spawn("docker", [
213
242
  "run",
@@ -215,8 +244,9 @@ export async function openShell(projectDir) {
215
244
  "-it",
216
245
  "--entrypoint",
217
246
  "",
218
- "-v",
219
- `${projectDir}:/home/runner/work`,
247
+ // --mount is colon-safe; see runInContainer for the rationale.
248
+ "--mount",
249
+ `type=bind,source=${projectDir},target=/home/runner/work`,
220
250
  "-e",
221
251
  "CI=true",
222
252
  imageName,
@@ -228,26 +258,4 @@ export async function openShell(projectDir) {
228
258
  proc.on("error", reject);
229
259
  });
230
260
  }
231
- // =============================================================================
232
- // Internal helpers
233
- // =============================================================================
234
- async function detectStackFromDir(projectDir) {
235
- if (await fs.pathExists(path.join(projectDir, "build.gradle.kts")))
236
- return "java-gradle";
237
- if (await fs.pathExists(path.join(projectDir, "build.gradle")))
238
- return "java-gradle";
239
- if (await fs.pathExists(path.join(projectDir, "pom.xml")))
240
- return "java-maven";
241
- if (await fs.pathExists(path.join(projectDir, "package.json")))
242
- return "node";
243
- if (await fs.pathExists(path.join(projectDir, "go.mod")))
244
- return "go";
245
- if (await fs.pathExists(path.join(projectDir, "Cargo.toml")))
246
- return "rust";
247
- if ((await fs.pathExists(path.join(projectDir, "pyproject.toml"))) ||
248
- (await fs.pathExists(path.join(projectDir, "requirements.txt"))) ||
249
- (await fs.pathExists(path.join(projectDir, "setup.py"))))
250
- return "python";
251
- return "node"; // fallback
252
- }
253
261
  //# sourceMappingURL=docker.js.map