javi-forge 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -1
- package/ci-local/README.md +56 -11
- package/ci-local/ci-local.ps1 +445 -0
- package/ci-local/ci-local.sh +52 -9
- package/ci-local/docker/python.Dockerfile +7 -0
- package/ci-local/hooks/commit-msg +120 -44
- package/ci-local/hooks/commit-msg.test.sh +286 -0
- package/ci-local/hooks/pre-commit +9 -1
- package/ci-local/hooks/pre-push +13 -2
- package/ci-local/install.ps1 +367 -0
- package/ci-local/install.sh +151 -5
- package/ci-local/uninstall.ps1 +85 -0
- package/ci-local/uninstall.sh +95 -0
- package/dist/cli/dispatch/ci.d.ts +12 -0
- package/dist/cli/dispatch/ci.js +38 -0
- package/dist/cli/dispatch/security.d.ts +11 -0
- package/dist/cli/dispatch/security.js +68 -0
- package/dist/cli/dispatch/simple-renderers.d.ts +16 -0
- package/dist/cli/dispatch/simple-renderers.js +65 -0
- package/dist/cli/dispatch/skill-publish.d.ts +15 -0
- package/dist/cli/dispatch/skill-publish.js +44 -0
- package/dist/cli/dispatch/skills-cmd.d.ts +13 -0
- package/dist/cli/dispatch/skills-cmd.js +93 -0
- package/dist/cli/dispatch/tdd.d.ts +11 -0
- package/dist/cli/dispatch/tdd.js +45 -0
- package/dist/cli/dispatch/types.d.ts +29 -0
- package/dist/cli/dispatch/types.js +2 -0
- package/dist/cli/dispatch/workflow.d.ts +11 -0
- package/dist/cli/dispatch/workflow.js +67 -0
- package/dist/cli/help.d.ts +127 -0
- package/dist/cli/help.js +138 -0
- package/dist/cli/runtime.d.ts +16 -0
- package/dist/cli/runtime.js +38 -0
- package/dist/cli/validators.d.ts +17 -0
- package/dist/cli/validators.js +35 -0
- package/dist/commands/analyze.js +1 -3
- package/dist/commands/ci.d.ts +47 -0
- package/dist/commands/ci.js +380 -63
- package/dist/commands/doctor.js +2 -4
- package/dist/commands/init/report.d.ts +13 -0
- package/dist/commands/init/report.js +13 -0
- package/dist/commands/init/steps/agent-skills.d.ts +13 -0
- package/dist/commands/init/steps/agent-skills.js +45 -0
- package/dist/commands/init/steps/ai-sync.d.ts +15 -0
- package/dist/commands/init/steps/ai-sync.js +62 -0
- package/dist/commands/init/steps/ci.d.ts +24 -0
- package/dist/commands/init/steps/ci.js +74 -0
- package/dist/commands/init/steps/claude-md.d.ts +14 -0
- package/dist/commands/init/steps/claude-md.js +54 -0
- package/dist/commands/init/steps/code-graph.d.ts +16 -0
- package/dist/commands/init/steps/code-graph.js +68 -0
- package/dist/commands/init/steps/context-dir.d.ts +13 -0
- package/dist/commands/init/steps/context-dir.js +47 -0
- package/dist/commands/init/steps/docker-deploy.d.ts +16 -0
- package/dist/commands/init/steps/docker-deploy.js +60 -0
- package/dist/commands/init/steps/ghagga.d.ts +15 -0
- package/dist/commands/init/steps/ghagga.js +58 -0
- package/dist/commands/init/steps/git.d.ts +25 -0
- package/dist/commands/init/steps/git.js +81 -0
- package/dist/commands/init/steps/gitignore.d.ts +13 -0
- package/dist/commands/init/steps/gitignore.js +39 -0
- package/dist/commands/init/steps/local-ai.d.ts +14 -0
- package/dist/commands/init/steps/local-ai.js +53 -0
- package/dist/commands/init/steps/manifest.d.ts +13 -0
- package/dist/commands/init/steps/manifest.js +58 -0
- package/dist/commands/init/steps/memory.d.ts +15 -0
- package/dist/commands/init/steps/memory.js +60 -0
- package/dist/commands/init/steps/mock.d.ts +13 -0
- package/dist/commands/init/steps/mock.js +64 -0
- package/dist/commands/init/steps/sdd.d.ts +12 -0
- package/dist/commands/init/steps/sdd.js +40 -0
- package/dist/commands/init/steps/security.d.ts +28 -0
- package/dist/commands/init/steps/security.js +103 -0
- package/dist/commands/init/types.d.ts +30 -0
- package/dist/commands/init/types.js +2 -0
- package/dist/commands/init.d.ts +5 -4
- package/dist/commands/init.js +42 -630
- package/dist/commands/llmstxt.js +1 -1
- package/dist/commands/plugin.js +1 -1
- package/dist/commands/security.js +23 -4
- package/dist/commands/skills/analysis.d.ts +19 -0
- package/dist/commands/skills/analysis.js +211 -0
- package/dist/commands/skills/benchmark.d.ts +6 -0
- package/dist/commands/skills/benchmark.js +82 -0
- package/dist/commands/skills/constants.d.ts +10 -0
- package/dist/commands/skills/constants.js +27 -0
- package/dist/commands/skills/directives.d.ts +21 -0
- package/dist/commands/skills/directives.js +101 -0
- package/dist/commands/skills/parsing.d.ts +16 -0
- package/dist/commands/skills/parsing.js +108 -0
- package/dist/commands/skills/rules.d.ts +5 -0
- package/dist/commands/skills/rules.js +32 -0
- package/dist/commands/skills/scoring.d.ts +47 -0
- package/dist/commands/skills/scoring.js +306 -0
- package/dist/commands/skills.d.ts +7 -125
- package/dist/commands/skills.js +8 -874
- package/dist/commands/tdd-pipeline.js +1 -1
- package/dist/commands/tdd.js +1 -1
- package/dist/commands/workflow.js +2 -0
- package/dist/constants.js +2 -2
- package/dist/index.js +26 -473
- package/dist/lib/agent-skills.js +1 -1
- package/dist/{commands → lib}/atlassian-mcp.js +2 -2
- package/dist/lib/auto-skill-install.js +2 -3
- package/dist/lib/auto-wire.js +7 -7
- package/dist/lib/ci-config.d.ts +65 -0
- package/dist/lib/ci-config.js +297 -0
- package/dist/lib/claudemd.js +1 -1
- package/dist/lib/codex-export.js +4 -8
- package/dist/lib/common.js +2 -2
- package/dist/lib/context.js +2 -2
- package/dist/{commands → lib}/crash-recovery.js +4 -4
- package/dist/lib/docker.d.ts +22 -1
- package/dist/lib/docker.js +57 -49
- package/dist/lib/exec.d.ts +10 -0
- package/dist/lib/exec.js +11 -0
- package/dist/lib/frontmatter.js +2 -2
- package/dist/{commands → lib}/harness-audit.js +1 -1
- package/dist/{commands → lib}/parallel-batch.js +1 -3
- package/dist/lib/plugin.js +2 -4
- package/dist/{commands → lib}/secret-scanner.js +3 -1
- package/dist/{commands → lib}/security-analysis.js +4 -2
- package/dist/lib/skill-publish.js +23 -5
- package/dist/{commands → lib}/skill-scanner.d.ts +1 -1
- package/dist/{commands → lib}/skill-scanner.js +3 -3
- package/dist/lib/stack-detector.js +3 -3
- package/dist/{commands → lib}/supply-chain.js +1 -1
- package/dist/lib/template.js +2 -2
- package/dist/lib/workflow/discovery.d.ts +8 -0
- package/dist/lib/workflow/discovery.js +18 -1
- package/dist/lib/workflow/parser.js +15 -15
- package/dist/lib/workflow/validator.js +4 -4
- package/dist/tasks/index.d.ts +3 -3
- package/dist/tasks/index.js +3 -3
- package/dist/tasks/scaffold-tasks.d.ts +1 -1
- package/dist/tasks/scaffold-tasks.js +10 -10
- package/dist/tasks/task-id.d.ts +1 -1
- package/dist/tasks/task-id.js +9 -5
- package/dist/tasks/task-tracker.d.ts +1 -1
- package/dist/tasks/task-tracker.js +7 -7
- package/dist/ui/App.js +1 -1
- package/dist/ui/CIContext.js +1 -2
- package/dist/ui/Header.js +4 -4
- package/dist/ui/NameInput.js +1 -1
- package/dist/ui/Progress.d.ts +1 -1
- package/dist/ui/Progress.js +1 -1
- package/dist/ui/StackSelector.js +1 -1
- package/lib/common.psm1 +167 -0
- package/lib/common.sh +30 -0
- package/package.json +38 -19
- /package/dist/{commands → lib}/atlassian-mcp.d.ts +0 -0
- /package/dist/{commands → lib}/crash-recovery.d.ts +0 -0
- /package/dist/{commands → lib}/harness-audit.d.ts +0 -0
- /package/dist/{commands → lib}/parallel-batch.d.ts +0 -0
- /package/dist/{commands → lib}/secret-scanner.d.ts +0 -0
- /package/dist/{commands → lib}/security-analysis.d.ts +0 -0
- /package/dist/{commands → lib}/supply-chain.d.ts +0 -0
- /package/dist/{commands → lib}/team-presets.d.ts +0 -0
- /package/dist/{commands → lib}/team-presets.js +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Promisified `execFile` for shelling out to subprocesses with arguments
|
|
4
|
+
* (safer than `exec` because arguments are NOT shell-interpreted).
|
|
5
|
+
*
|
|
6
|
+
* Centralized helper used across the codebase by command modules and
|
|
7
|
+
* init steps that invoke external CLIs (git, javi-ai, etc.).
|
|
8
|
+
*/
|
|
9
|
+
export declare const execFileAsync: typeof execFile.__promisify__;
|
|
10
|
+
//# sourceMappingURL=exec.d.ts.map
|
package/dist/lib/exec.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
/**
|
|
4
|
+
* Promisified `execFile` for shelling out to subprocesses with arguments
|
|
5
|
+
* (safer than `exec` because arguments are NOT shell-interpreted).
|
|
6
|
+
*
|
|
7
|
+
* Centralized helper used across the codebase by command modules and
|
|
8
|
+
* init steps that invoke external CLIs (git, javi-ai, etc.).
|
|
9
|
+
*/
|
|
10
|
+
export const execFileAsync = promisify(execFile);
|
|
11
|
+
//# sourceMappingURL=exec.js.map
|
package/dist/lib/frontmatter.js
CHANGED
|
@@ -29,7 +29,7 @@ const KEBAB_CASE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
|
29
29
|
export function validateFrontmatter(frontmatter, type) {
|
|
30
30
|
const errors = [];
|
|
31
31
|
// name: required, kebab-case, 2-60 chars
|
|
32
|
-
const name = frontmatter
|
|
32
|
+
const name = frontmatter.name;
|
|
33
33
|
if (typeof name !== "string" || !name) {
|
|
34
34
|
errors.push({
|
|
35
35
|
field: "name",
|
|
@@ -48,7 +48,7 @@ export function validateFrontmatter(frontmatter, type) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
// description: required, non-empty, min 10 chars
|
|
51
|
-
const desc = frontmatter
|
|
51
|
+
const desc = frontmatter.description;
|
|
52
52
|
if (typeof desc !== "string" || !desc) {
|
|
53
53
|
errors.push({
|
|
54
54
|
field: "description",
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* environment is (hooks, skills, memory, configs). Detects which AI
|
|
4
4
|
* agent harness is installed and generates appropriate config.
|
|
5
5
|
*/
|
|
6
|
+
import path from "node:path";
|
|
6
7
|
import fs from "fs-extra";
|
|
7
|
-
import path from "path";
|
|
8
8
|
// ── Harness Detection ──
|
|
9
9
|
const HARNESS_SIGNATURES = [
|
|
10
10
|
{
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Parallel batch sub-agents — spawn test/lint/build runners per module
|
|
3
3
|
* in parallel during quality scoring. Collects results and aggregates.
|
|
4
4
|
*/
|
|
5
|
+
import { execFileAsync } from "./exec.js";
|
|
5
6
|
export const DEFAULT_BATCH_CONFIG = {
|
|
6
7
|
maxParallel: 4,
|
|
7
8
|
timeoutMs: 60_000,
|
|
@@ -29,9 +30,6 @@ export function createJob(name, command, module) {
|
|
|
29
30
|
*/
|
|
30
31
|
export function createShellExecutor(timeoutMs = 60_000) {
|
|
31
32
|
return async (job) => {
|
|
32
|
-
const { execFile } = await import("child_process");
|
|
33
|
-
const { promisify } = await import("util");
|
|
34
|
-
const execFileAsync = promisify(execFile);
|
|
35
33
|
job.status = "running";
|
|
36
34
|
job.startedAt = Date.now();
|
|
37
35
|
try {
|
package/dist/lib/plugin.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { promisify } from "util";
|
|
5
3
|
import { PLUGIN_ASSET_DIRS, PLUGIN_MANIFEST_FILE, PLUGIN_REGISTRY_URL, PLUGINS_DIR, } from "../constants.js";
|
|
6
4
|
import { generateAgentSkillsManifest } from "./agent-skills.js";
|
|
7
5
|
import { autoWirePlugins } from "./auto-wire.js";
|
|
8
|
-
|
|
6
|
+
import { execFileAsync } from "./exec.js";
|
|
9
7
|
const KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
10
8
|
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
11
9
|
// ── Validation ──────────────────────────────────────────────────────────────
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Secret scanner — detects leaked credentials in source files using
|
|
3
3
|
* a curated regex bundle. Designed for pre-commit hooks and CI gates.
|
|
4
4
|
*/
|
|
5
|
+
import path from "node:path";
|
|
5
6
|
import fs from "fs-extra";
|
|
6
|
-
import path from "path";
|
|
7
7
|
// ── Regex Bundle ──
|
|
8
8
|
export const SECRET_PATTERNS = [
|
|
9
9
|
// AWS
|
|
@@ -219,6 +219,8 @@ export function scanContent(content, filePath, patterns = SECRET_PATTERNS) {
|
|
|
219
219
|
const lines = content.split("\n");
|
|
220
220
|
for (let i = 0; i < lines.length; i++) {
|
|
221
221
|
const line = lines[i];
|
|
222
|
+
if (line === undefined)
|
|
223
|
+
continue;
|
|
222
224
|
for (const pat of patterns) {
|
|
223
225
|
const match = pat.pattern.exec(line);
|
|
224
226
|
if (match) {
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* vulnerability detection, variant analysis, and structured reporting
|
|
6
6
|
* with severity levels (critical, high, medium, low).
|
|
7
7
|
*/
|
|
8
|
+
import path from "node:path";
|
|
8
9
|
import fs from "fs-extra";
|
|
9
|
-
import path from "path";
|
|
10
10
|
// =============================================================================
|
|
11
11
|
// Constants
|
|
12
12
|
// =============================================================================
|
|
@@ -473,7 +473,9 @@ export function formatReportText(report) {
|
|
|
473
473
|
if (findings.length > 0) {
|
|
474
474
|
lines.push("--- Findings ---");
|
|
475
475
|
for (const f of findings) {
|
|
476
|
-
const loc = f.column
|
|
476
|
+
const loc = f.column
|
|
477
|
+
? `${f.file}:${f.line}:${f.column}`
|
|
478
|
+
: `${f.file}:${f.line}`;
|
|
477
479
|
const cwe = f.cwe ? ` [${f.cwe}]` : "";
|
|
478
480
|
lines.push(`[${f.severity.toUpperCase()}] ${f.ruleId}${cwe} at ${loc}`);
|
|
479
481
|
lines.push(` ${f.message}`);
|
|
@@ -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 } from "../constants.js";
|
|
4
4
|
import { parseFrontmatter } from "./frontmatter.js";
|
|
5
5
|
// ── Core ───────────────────────────────────────────────────────────────────
|
|
@@ -27,6 +27,24 @@ export async function publishSkill(options) {
|
|
|
27
27
|
if (!(await fs.pathExists(skillMdPath))) {
|
|
28
28
|
return { success: false, error: `SKILL.md not found in ${skillDir}` };
|
|
29
29
|
}
|
|
30
|
+
// Refuse to follow a symlink. If SKILL.md is a symlink to a file outside
|
|
31
|
+
// the skill directory (e.g., /etc/passwd), readFile + the later fs.copy
|
|
32
|
+
// would expose the target's contents in the published manifest bundle.
|
|
33
|
+
// Mirrors the symlink-reject pattern in security.ts writeBaseline /
|
|
34
|
+
// ci.ts installCIHooks.
|
|
35
|
+
try {
|
|
36
|
+
const stat = await fs.lstat(skillMdPath);
|
|
37
|
+
if (stat.isSymbolicLink()) {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
error: `SKILL.md is a symlink; refusing to publish. Use a regular file.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// pathExists already returned true above; lstat failures here are
|
|
46
|
+
// unexpected. Fall through to readFile which will surface the error.
|
|
47
|
+
}
|
|
30
48
|
// Read and parse SKILL.md
|
|
31
49
|
let raw;
|
|
32
50
|
try {
|
|
@@ -38,9 +56,9 @@ export async function publishSkill(options) {
|
|
|
38
56
|
const parsed = parseFrontmatter(raw);
|
|
39
57
|
const frontmatter = parsed?.data ?? {};
|
|
40
58
|
// Extract metadata
|
|
41
|
-
const name = frontmatter
|
|
42
|
-
const version = frontmatter
|
|
43
|
-
const description = frontmatter
|
|
59
|
+
const name = frontmatter.name ?? path.basename(skillDir);
|
|
60
|
+
const version = frontmatter.version ?? "1.0.0";
|
|
61
|
+
const description = frontmatter.description ?? `${name} AI skill`;
|
|
44
62
|
// Validate name format (kebab-case)
|
|
45
63
|
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
46
64
|
return {
|
|
@@ -60,7 +78,7 @@ export async function publishSkill(options) {
|
|
|
60
78
|
name,
|
|
61
79
|
version,
|
|
62
80
|
description: description.length > 200
|
|
63
|
-
? description.slice(0, 197)
|
|
81
|
+
? `${description.slice(0, 197)}...`
|
|
64
82
|
: description,
|
|
65
83
|
skills: [name],
|
|
66
84
|
tags: tags.length > 0 ? tags : extractTagsFromDescription(description),
|
|
@@ -47,7 +47,7 @@ interface ProvenanceInfo {
|
|
|
47
47
|
hasDescription: boolean;
|
|
48
48
|
}
|
|
49
49
|
export declare function checkProvenance(content: string): ProvenanceInfo;
|
|
50
|
-
export declare function scanSkillContent(content: string,
|
|
50
|
+
export declare function scanSkillContent(content: string, _filePath: string): SkillThreat[];
|
|
51
51
|
export declare function computeVerdict(threats: SkillThreat[]): SkillScanVerdict;
|
|
52
52
|
export declare function computeScanSummary(threats: SkillThreat[]): SkillScanSummary;
|
|
53
53
|
export declare function extractSkillName(content: string, filePath: string): string;
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* patterns before skills are installed. Inspired by the SkillGuard skill
|
|
6
6
|
* (javi-ai) but implemented as a programmatic module with structured output.
|
|
7
7
|
*/
|
|
8
|
+
import path from "node:path";
|
|
8
9
|
import fs from "fs-extra";
|
|
9
|
-
import path from "path";
|
|
10
10
|
/**
|
|
11
11
|
* Ordered by severity (critical first). Each pattern is tested against
|
|
12
12
|
* every non-comment line in the skill file.
|
|
@@ -47,7 +47,7 @@ export const THREAT_PATTERNS = [
|
|
|
47
47
|
{
|
|
48
48
|
category: "code-injection",
|
|
49
49
|
severity: "critical",
|
|
50
|
-
pattern: /\b(?:subprocess\.(?:call|run|Popen)|os\.system|os\.popen)\s*\(\s*(?:f['
|
|
50
|
+
pattern: /\b(?:subprocess\.(?:call|run|Popen)|os\.system|os\.popen)\s*\(\s*(?:f['"]|user|input|req)/i,
|
|
51
51
|
message: "Python subprocess with user input — enables command injection",
|
|
52
52
|
},
|
|
53
53
|
// ── Critical: Data Exfiltration ──
|
|
@@ -183,7 +183,7 @@ export function checkProvenance(content) {
|
|
|
183
183
|
// =============================================================================
|
|
184
184
|
// Core scanner
|
|
185
185
|
// =============================================================================
|
|
186
|
-
export function scanSkillContent(content,
|
|
186
|
+
export function scanSkillContent(content, _filePath) {
|
|
187
187
|
const threats = [];
|
|
188
188
|
const lines = content.split("\n");
|
|
189
189
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import fs from "fs-extra";
|
|
2
|
-
import path from "path";
|
|
3
3
|
// ── Stack-to-Skills Mapping ────────────────────────────────────────────────
|
|
4
4
|
/**
|
|
5
5
|
* Maps detected project signals to recommended AI skills.
|
|
@@ -95,8 +95,8 @@ export async function detectProjectStack(projectDir) {
|
|
|
95
95
|
try {
|
|
96
96
|
const pkg = (await fs.readJson(pkgPath));
|
|
97
97
|
const allDeps = {
|
|
98
|
-
...(pkg
|
|
99
|
-
...(pkg
|
|
98
|
+
...(pkg.dependencies ?? {}),
|
|
99
|
+
...(pkg.devDependencies ?? {}),
|
|
100
100
|
};
|
|
101
101
|
for (const depName of Object.keys(allDeps)) {
|
|
102
102
|
const normalizedDep = depName.replace(/^@/, "").split("/")[0];
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* minimum package release age to protect against typosquatting,
|
|
4
4
|
* dependency confusion, and malicious package takeovers.
|
|
5
5
|
*/
|
|
6
|
+
import path from "node:path";
|
|
6
7
|
import fs from "fs-extra";
|
|
7
|
-
import path from "path";
|
|
8
8
|
// ── Detection ──
|
|
9
9
|
export function detectPackageManager(projectDir) {
|
|
10
10
|
if (fs.existsSync(path.join(projectDir, "pnpm-lock.yaml")))
|
package/dist/lib/template.js
CHANGED
|
@@ -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 { DEPENDABOT_FRAGMENTS_DIR, DEPLOY_DESTINATION_MAP, DEPLOY_TEMPLATE_MAP, STACK_CI_MAP, STACK_DEPENDABOT_MAP, TEMPLATES_DIR, } from "../constants.js";
|
|
4
4
|
/**
|
|
5
5
|
* Read a template file and replace __VAR_NAME__ placeholders.
|
|
@@ -33,7 +33,7 @@ export async function generateDependabotYml(stacks, includeGitHubActions = true)
|
|
|
33
33
|
const fragmentPath = path.join(DEPENDABOT_FRAGMENTS_DIR, `${fragmentName}.yml`);
|
|
34
34
|
if (await fs.pathExists(fragmentPath)) {
|
|
35
35
|
const fragment = await fs.readFile(fragmentPath, "utf-8");
|
|
36
|
-
content +=
|
|
36
|
+
content += `\n${fragment}`;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
return content;
|
|
@@ -11,6 +11,14 @@ export declare function discoverWorkflows(projectDir: string): Promise<WorkflowD
|
|
|
11
11
|
export declare function listBuiltinTemplates(): Promise<WorkflowDiscoveryEntry[]>;
|
|
12
12
|
/**
|
|
13
13
|
* Load a built-in template by name. Returns the file content or null if not found.
|
|
14
|
+
*
|
|
15
|
+
* The name argument is restricted to a strict kebab-case alphabet — anything
|
|
16
|
+
* with path separators, dots, or characters outside [a-z0-9-] is rejected
|
|
17
|
+
* outright. Without this check, an attacker (or a user with a typo) could
|
|
18
|
+
* pass --template "../../../etc/passwd" and walk out of WORKFLOW_TEMPLATES_DIR.
|
|
19
|
+
* The file would still need to end in .dot or .mermaid, but file-existence
|
|
20
|
+
* probes and arbitrary DOT-parse on attacker-controlled paths is enough to
|
|
21
|
+
* reject up front.
|
|
14
22
|
*/
|
|
15
23
|
export declare function loadBuiltinTemplate(name: string): Promise<{
|
|
16
24
|
content: string;
|
|
@@ -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 { TEMPLATES_DIR } from "../../constants.js";
|
|
4
4
|
const WORKFLOW_EXTENSIONS = {
|
|
5
5
|
".dot": "dot",
|
|
@@ -54,10 +54,27 @@ export async function listBuiltinTemplates() {
|
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Load a built-in template by name. Returns the file content or null if not found.
|
|
57
|
+
*
|
|
58
|
+
* The name argument is restricted to a strict kebab-case alphabet — anything
|
|
59
|
+
* with path separators, dots, or characters outside [a-z0-9-] is rejected
|
|
60
|
+
* outright. Without this check, an attacker (or a user with a typo) could
|
|
61
|
+
* pass --template "../../../etc/passwd" and walk out of WORKFLOW_TEMPLATES_DIR.
|
|
62
|
+
* The file would still need to end in .dot or .mermaid, but file-existence
|
|
63
|
+
* probes and arbitrary DOT-parse on attacker-controlled paths is enough to
|
|
64
|
+
* reject up front.
|
|
57
65
|
*/
|
|
58
66
|
export async function loadBuiltinTemplate(name) {
|
|
67
|
+
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(name)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
59
70
|
for (const [ext, format] of Object.entries(WORKFLOW_EXTENSIONS)) {
|
|
60
71
|
const templatePath = path.join(WORKFLOW_TEMPLATES_DIR, `${name}${ext}`);
|
|
72
|
+
// Defense in depth: even with the name regex above, ensure the
|
|
73
|
+
// resolved real path stays inside WORKFLOW_TEMPLATES_DIR.
|
|
74
|
+
const resolved = path.resolve(templatePath);
|
|
75
|
+
if (!resolved.startsWith(`${WORKFLOW_TEMPLATES_DIR}${path.sep}`)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
61
78
|
if (await fs.pathExists(templatePath)) {
|
|
62
79
|
const content = await fs.readFile(templatePath, "utf-8");
|
|
63
80
|
return { content, format };
|
|
@@ -40,24 +40,24 @@ export function parseDot(content, sourceName = "untitled") {
|
|
|
40
40
|
// Edge: A -> B [label="..."]
|
|
41
41
|
const edgeMatch = stmt.match(/^"?(\w[\w\s-]*?)"?\s*->\s*"?(\w[\w\s-]*?)"?\s*(?:\[([^\]]*)\])?\s*$/);
|
|
42
42
|
if (edgeMatch) {
|
|
43
|
-
const fromId = edgeMatch[1]
|
|
44
|
-
const toId = edgeMatch[2]
|
|
43
|
+
const fromId = edgeMatch[1]?.trim();
|
|
44
|
+
const toId = edgeMatch[2]?.trim();
|
|
45
45
|
const attrs = edgeMatch[3] ? parseAttributes(edgeMatch[3]) : {};
|
|
46
46
|
ensureNode(nodesMap, fromId);
|
|
47
47
|
ensureNode(nodesMap, toId);
|
|
48
|
-
edges.push({ from: fromId, to: toId, label: attrs
|
|
48
|
+
edges.push({ from: fromId, to: toId, label: attrs.label });
|
|
49
49
|
continue;
|
|
50
50
|
}
|
|
51
51
|
// Node: A [label="..." check="..."]
|
|
52
52
|
const nodeMatch = stmt.match(/^"?(\w[\w\s-]*?)"?\s*\[([^\]]+)\]\s*$/);
|
|
53
53
|
if (nodeMatch) {
|
|
54
|
-
const nodeId = nodeMatch[1]
|
|
54
|
+
const nodeId = nodeMatch[1]?.trim();
|
|
55
55
|
const attrs = parseAttributes(nodeMatch[2]);
|
|
56
56
|
const node = ensureNode(nodesMap, nodeId);
|
|
57
|
-
if (attrs
|
|
58
|
-
node.label = attrs
|
|
59
|
-
if (attrs
|
|
60
|
-
node.check = attrs
|
|
57
|
+
if (attrs.label)
|
|
58
|
+
node.label = attrs.label;
|
|
59
|
+
if (attrs.check)
|
|
60
|
+
node.check = attrs.check;
|
|
61
61
|
// Store remaining attrs as metadata
|
|
62
62
|
const { label: _l, check: _c, ...rest } = attrs;
|
|
63
63
|
if (Object.keys(rest).length > 0)
|
|
@@ -69,8 +69,8 @@ export function parseDot(content, sourceName = "untitled") {
|
|
|
69
69
|
if (chainMatch) {
|
|
70
70
|
const parts = stmt.split("->").map((p) => p.trim().replace(/^"|"$/g, ""));
|
|
71
71
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
72
|
-
const fromId = parts[i]
|
|
73
|
-
const toId = parts[i + 1]
|
|
72
|
+
const fromId = parts[i]?.trim();
|
|
73
|
+
const toId = parts[i + 1]?.trim();
|
|
74
74
|
ensureNode(nodesMap, fromId);
|
|
75
75
|
ensureNode(nodesMap, toId);
|
|
76
76
|
edges.push({ from: fromId, to: toId });
|
|
@@ -101,7 +101,7 @@ export function parseMermaid(content, sourceName = "untitled") {
|
|
|
101
101
|
throw new WorkflowParseError("Empty mermaid file");
|
|
102
102
|
}
|
|
103
103
|
// First line must be flowchart directive
|
|
104
|
-
const headerMatch = lines[0]
|
|
104
|
+
const headerMatch = lines[0]?.match(/^(?:flowchart|graph)\s+(LR|TD|TB|RL|BT)/i);
|
|
105
105
|
if (!headerMatch) {
|
|
106
106
|
throw new WorkflowParseError('Expected "flowchart LR|TD" or "graph LR|TD" at first line', 1);
|
|
107
107
|
}
|
|
@@ -120,7 +120,7 @@ export function parseMermaid(content, sourceName = "untitled") {
|
|
|
120
120
|
if (edgeLabelMatch) {
|
|
121
121
|
const fromId = edgeLabelMatch[1];
|
|
122
122
|
const fromLabel = edgeLabelMatch[2];
|
|
123
|
-
const edgeLabel = edgeLabelMatch[3]
|
|
123
|
+
const edgeLabel = edgeLabelMatch[3]?.trim();
|
|
124
124
|
const toId = edgeLabelMatch[4];
|
|
125
125
|
const toLabel = edgeLabelMatch[5];
|
|
126
126
|
const fromNode = ensureNode(nodesMap, fromId);
|
|
@@ -138,7 +138,7 @@ export function parseMermaid(content, sourceName = "untitled") {
|
|
|
138
138
|
if (edgeMatch && line.includes("-->")) {
|
|
139
139
|
const segments = line.split(/\s*-->\s*/);
|
|
140
140
|
for (let s = 0; s < segments.length; s++) {
|
|
141
|
-
const seg = segments[s]
|
|
141
|
+
const seg = segments[s]?.trim();
|
|
142
142
|
const segMatch = seg.match(/^(\w[\w-]*)(?:\[([^\]]+)\])?$/);
|
|
143
143
|
if (segMatch) {
|
|
144
144
|
const nodeId = segMatch[1];
|
|
@@ -150,8 +150,8 @@ export function parseMermaid(content, sourceName = "untitled") {
|
|
|
150
150
|
}
|
|
151
151
|
// Create edges between consecutive segments
|
|
152
152
|
for (let s = 0; s < segments.length - 1; s++) {
|
|
153
|
-
const fromSeg = segments[s]
|
|
154
|
-
const toSeg = segments[s + 1]
|
|
153
|
+
const fromSeg = segments[s]?.trim().match(/^(\w[\w-]*)/);
|
|
154
|
+
const toSeg = segments[s + 1]?.trim().match(/^(\w[\w-]*)/);
|
|
155
155
|
if (fromSeg && toSeg) {
|
|
156
156
|
edges.push({ from: fromSeg[1], to: toSeg[1] });
|
|
157
157
|
}
|
|
@@ -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
|
* Built-in check functions that map node `check` attributes to project validations.
|
|
5
5
|
* Each check receives the project directory and returns { pass, detail }.
|
|
@@ -29,10 +29,10 @@ const BUILTIN_CHECKS = {
|
|
|
29
29
|
try {
|
|
30
30
|
const pkg = (await fs.readJson(pkgPath));
|
|
31
31
|
const deps = {
|
|
32
|
-
...pkg
|
|
33
|
-
...pkg
|
|
32
|
+
...pkg.devDependencies,
|
|
33
|
+
...pkg.dependencies,
|
|
34
34
|
};
|
|
35
|
-
if (deps
|
|
35
|
+
if (deps.eslint || deps.biome || deps.oxlint) {
|
|
36
36
|
return { pass: true, detail: "Linter found in package.json" };
|
|
37
37
|
}
|
|
38
38
|
}
|
package/dist/tasks/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
1
|
+
export { getDefaultScaffoldTasks } from "./scaffold-tasks.js";
|
|
2
|
+
export { addSubtask, buildTaskTree, createTask, findTask, generateTaskId, type TaskNode, type TaskStatus, } from "./task-id.js";
|
|
3
|
+
export { TaskTracker } from "./task-tracker.js";
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/tasks/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
1
|
+
export { getDefaultScaffoldTasks } from "./scaffold-tasks.js";
|
|
2
|
+
export { addSubtask, buildTaskTree, createTask, findTask, generateTaskId, } from "./task-id.js";
|
|
3
|
+
export { TaskTracker } from "./task-tracker.js";
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { addSubtask, createTask } from "./task-id.js";
|
|
2
2
|
export function getDefaultScaffoldTasks() {
|
|
3
|
-
const setup = createTask({ title:
|
|
4
|
-
addSubtask(setup, { title:
|
|
5
|
-
addSubtask(setup, { title:
|
|
6
|
-
addSubtask(setup, { title:
|
|
7
|
-
const testing = createTask({ title:
|
|
8
|
-
addSubtask(testing, { title:
|
|
9
|
-
addSubtask(testing, { title:
|
|
10
|
-
const ci = createTask({ title:
|
|
11
|
-
const deploy = createTask({ title:
|
|
3
|
+
const setup = createTask({ title: "Setup project structure" });
|
|
4
|
+
addSubtask(setup, { title: "Initialize git repository" });
|
|
5
|
+
addSubtask(setup, { title: "Configure TypeScript" });
|
|
6
|
+
addSubtask(setup, { title: "Setup linter and formatter" });
|
|
7
|
+
const testing = createTask({ title: "Setup testing" });
|
|
8
|
+
addSubtask(testing, { title: "Configure test runner" });
|
|
9
|
+
addSubtask(testing, { title: "Write initial tests" });
|
|
10
|
+
const ci = createTask({ title: "Create CI pipeline" });
|
|
11
|
+
const deploy = createTask({ title: "Deploy to production" });
|
|
12
12
|
return [setup, testing, ci, deploy];
|
|
13
13
|
}
|
|
14
14
|
//# sourceMappingURL=scaffold-tasks.js.map
|
package/dist/tasks/task-id.d.ts
CHANGED
package/dist/tasks/task-id.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { createHash } from
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
2
|
export function generateTaskId(content) {
|
|
3
|
-
const input = [
|
|
4
|
-
|
|
3
|
+
const input = [
|
|
4
|
+
content.title,
|
|
5
|
+
content.description ?? "",
|
|
6
|
+
content.parentId ?? "",
|
|
7
|
+
].join("|");
|
|
8
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 7);
|
|
5
9
|
}
|
|
6
10
|
export function createTask(params) {
|
|
7
11
|
const now = new Date().toISOString();
|
|
@@ -9,7 +13,7 @@ export function createTask(params) {
|
|
|
9
13
|
id: generateTaskId(params),
|
|
10
14
|
title: params.title,
|
|
11
15
|
description: params.description,
|
|
12
|
-
status:
|
|
16
|
+
status: "todo",
|
|
13
17
|
parentId: params.parentId,
|
|
14
18
|
children: [],
|
|
15
19
|
dependencies: [],
|
|
@@ -34,7 +38,7 @@ export function buildTaskTree(flat) {
|
|
|
34
38
|
const roots = [];
|
|
35
39
|
for (const node of map.values()) {
|
|
36
40
|
if (node.parentId && map.has(node.parentId)) {
|
|
37
|
-
map.get(node.parentId)
|
|
41
|
+
map.get(node.parentId)?.children.push(node);
|
|
38
42
|
}
|
|
39
43
|
else {
|
|
40
44
|
roots.push(node);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTask } from
|
|
1
|
+
import { createTask } from "./task-id.js";
|
|
2
2
|
export class TaskTracker {
|
|
3
3
|
tasks = [];
|
|
4
4
|
addTask(title, description, parentId) {
|
|
@@ -41,30 +41,30 @@ export class TaskTracker {
|
|
|
41
41
|
return false;
|
|
42
42
|
return t.dependencies.some((depId) => {
|
|
43
43
|
const dep = this.findFlat(depId);
|
|
44
|
-
return !dep || dep.status !==
|
|
44
|
+
return !dep || dep.status !== "done";
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
getReadyTasks() {
|
|
49
49
|
return this.tasks.filter((t) => {
|
|
50
|
-
if (t.status !==
|
|
50
|
+
if (t.status !== "todo")
|
|
51
51
|
return false;
|
|
52
52
|
if (t.dependencies.length === 0)
|
|
53
53
|
return true;
|
|
54
54
|
return t.dependencies.every((depId) => {
|
|
55
55
|
const dep = this.findFlat(depId);
|
|
56
|
-
return dep?.status ===
|
|
56
|
+
return dep?.status === "done";
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
toMarkdown() {
|
|
61
61
|
return this.tasks
|
|
62
62
|
.map((t) => {
|
|
63
|
-
const check = t.status ===
|
|
64
|
-
const assignee = t.assignee ? ` @${t.assignee}` :
|
|
63
|
+
const check = t.status === "done" ? "x" : " ";
|
|
64
|
+
const assignee = t.assignee ? ` @${t.assignee}` : "";
|
|
65
65
|
return `- [${check}] ${t.title}${assignee}`;
|
|
66
66
|
})
|
|
67
|
-
.join(
|
|
67
|
+
.join("\n");
|
|
68
68
|
}
|
|
69
69
|
toJSON() {
|
|
70
70
|
return JSON.stringify(this.tasks);
|
package/dist/ui/App.js
CHANGED
package/dist/ui/CIContext.js
CHANGED
package/dist/ui/Header.js
CHANGED
|
@@ -10,10 +10,10 @@ function pad(content) {
|
|
|
10
10
|
return content + " ".repeat(Math.max(0, spaces));
|
|
11
11
|
}
|
|
12
12
|
export default function Header({ subtitle, dryRun }) {
|
|
13
|
-
const top =
|
|
14
|
-
const bottom =
|
|
15
|
-
const titleLine = pad(
|
|
16
|
-
const subLine = subtitle ? pad(
|
|
13
|
+
const top = `\u256d${"\u2500".repeat(BOX_WIDTH)}\u256e`;
|
|
14
|
+
const bottom = `\u2570${"\u2500".repeat(BOX_WIDTH)}\u256f`;
|
|
15
|
+
const titleLine = pad(` ${TITLE} `);
|
|
16
|
+
const subLine = subtitle ? pad(` ${subtitle} `) : null;
|
|
17
17
|
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
18
18
|
React.createElement(Text, { color: theme.muted }, top),
|
|
19
19
|
React.createElement(Box, null,
|
package/dist/ui/NameInput.js
CHANGED
package/dist/ui/Progress.d.ts
CHANGED
|
@@ -6,6 +6,6 @@ interface Props {
|
|
|
6
6
|
contextLine?: string;
|
|
7
7
|
onDone?: () => void;
|
|
8
8
|
}
|
|
9
|
-
export default function Progress({ steps, projectName, contextLine, onDone, }: Props): React.JSX.Element;
|
|
9
|
+
export default function Progress({ steps, projectName: _projectName, contextLine, onDone, }: Props): React.JSX.Element;
|
|
10
10
|
export {};
|
|
11
11
|
//# sourceMappingURL=Progress.d.ts.map
|
package/dist/ui/Progress.js
CHANGED
|
@@ -15,7 +15,7 @@ const STATUS_COLOR = {
|
|
|
15
15
|
error: theme.error,
|
|
16
16
|
skipped: theme.muted,
|
|
17
17
|
};
|
|
18
|
-
export default function Progress({ steps, projectName, contextLine, onDone, }) {
|
|
18
|
+
export default function Progress({ steps, projectName: _projectName, contextLine, onDone, }) {
|
|
19
19
|
const doneRef = useRef(false);
|
|
20
20
|
const total = steps.length;
|
|
21
21
|
const completed = steps.filter((s) => s.status === "done" || s.status === "skipped").length;
|
package/dist/ui/StackSelector.js
CHANGED
|
@@ -36,7 +36,7 @@ export default function StackSelector({ projectDir, onConfirm }) {
|
|
|
36
36
|
onConfirm(ALL_STACKS[cursor]);
|
|
37
37
|
}
|
|
38
38
|
}, [isCI, detecting]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
39
|
-
useInput((
|
|
39
|
+
useInput((_input, key) => {
|
|
40
40
|
if (detecting)
|
|
41
41
|
return;
|
|
42
42
|
if (key.upArrow)
|