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.
Files changed (159) 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/docker/python.Dockerfile +7 -0
  6. package/ci-local/hooks/commit-msg +120 -44
  7. package/ci-local/hooks/commit-msg.test.sh +286 -0
  8. package/ci-local/hooks/pre-commit +9 -1
  9. package/ci-local/hooks/pre-push +13 -2
  10. package/ci-local/install.ps1 +367 -0
  11. package/ci-local/install.sh +151 -5
  12. package/ci-local/uninstall.ps1 +85 -0
  13. package/ci-local/uninstall.sh +95 -0
  14. package/dist/cli/dispatch/ci.d.ts +12 -0
  15. package/dist/cli/dispatch/ci.js +38 -0
  16. package/dist/cli/dispatch/security.d.ts +11 -0
  17. package/dist/cli/dispatch/security.js +68 -0
  18. package/dist/cli/dispatch/simple-renderers.d.ts +16 -0
  19. package/dist/cli/dispatch/simple-renderers.js +65 -0
  20. package/dist/cli/dispatch/skill-publish.d.ts +15 -0
  21. package/dist/cli/dispatch/skill-publish.js +44 -0
  22. package/dist/cli/dispatch/skills-cmd.d.ts +13 -0
  23. package/dist/cli/dispatch/skills-cmd.js +93 -0
  24. package/dist/cli/dispatch/tdd.d.ts +11 -0
  25. package/dist/cli/dispatch/tdd.js +45 -0
  26. package/dist/cli/dispatch/types.d.ts +29 -0
  27. package/dist/cli/dispatch/types.js +2 -0
  28. package/dist/cli/dispatch/workflow.d.ts +11 -0
  29. package/dist/cli/dispatch/workflow.js +67 -0
  30. package/dist/cli/help.d.ts +127 -0
  31. package/dist/cli/help.js +138 -0
  32. package/dist/cli/runtime.d.ts +16 -0
  33. package/dist/cli/runtime.js +38 -0
  34. package/dist/cli/validators.d.ts +17 -0
  35. package/dist/cli/validators.js +35 -0
  36. package/dist/commands/analyze.js +1 -3
  37. package/dist/commands/ci.d.ts +47 -0
  38. package/dist/commands/ci.js +380 -63
  39. package/dist/commands/doctor.js +2 -4
  40. package/dist/commands/init/report.d.ts +13 -0
  41. package/dist/commands/init/report.js +13 -0
  42. package/dist/commands/init/steps/agent-skills.d.ts +13 -0
  43. package/dist/commands/init/steps/agent-skills.js +45 -0
  44. package/dist/commands/init/steps/ai-sync.d.ts +15 -0
  45. package/dist/commands/init/steps/ai-sync.js +62 -0
  46. package/dist/commands/init/steps/ci.d.ts +24 -0
  47. package/dist/commands/init/steps/ci.js +74 -0
  48. package/dist/commands/init/steps/claude-md.d.ts +14 -0
  49. package/dist/commands/init/steps/claude-md.js +54 -0
  50. package/dist/commands/init/steps/code-graph.d.ts +16 -0
  51. package/dist/commands/init/steps/code-graph.js +68 -0
  52. package/dist/commands/init/steps/context-dir.d.ts +13 -0
  53. package/dist/commands/init/steps/context-dir.js +47 -0
  54. package/dist/commands/init/steps/docker-deploy.d.ts +16 -0
  55. package/dist/commands/init/steps/docker-deploy.js +60 -0
  56. package/dist/commands/init/steps/ghagga.d.ts +15 -0
  57. package/dist/commands/init/steps/ghagga.js +58 -0
  58. package/dist/commands/init/steps/git.d.ts +25 -0
  59. package/dist/commands/init/steps/git.js +81 -0
  60. package/dist/commands/init/steps/gitignore.d.ts +13 -0
  61. package/dist/commands/init/steps/gitignore.js +39 -0
  62. package/dist/commands/init/steps/local-ai.d.ts +14 -0
  63. package/dist/commands/init/steps/local-ai.js +53 -0
  64. package/dist/commands/init/steps/manifest.d.ts +13 -0
  65. package/dist/commands/init/steps/manifest.js +58 -0
  66. package/dist/commands/init/steps/memory.d.ts +15 -0
  67. package/dist/commands/init/steps/memory.js +60 -0
  68. package/dist/commands/init/steps/mock.d.ts +13 -0
  69. package/dist/commands/init/steps/mock.js +64 -0
  70. package/dist/commands/init/steps/sdd.d.ts +12 -0
  71. package/dist/commands/init/steps/sdd.js +40 -0
  72. package/dist/commands/init/steps/security.d.ts +28 -0
  73. package/dist/commands/init/steps/security.js +103 -0
  74. package/dist/commands/init/types.d.ts +30 -0
  75. package/dist/commands/init/types.js +2 -0
  76. package/dist/commands/init.d.ts +5 -4
  77. package/dist/commands/init.js +42 -630
  78. package/dist/commands/llmstxt.js +1 -1
  79. package/dist/commands/plugin.js +1 -1
  80. package/dist/commands/security.js +23 -4
  81. package/dist/commands/skills/analysis.d.ts +19 -0
  82. package/dist/commands/skills/analysis.js +211 -0
  83. package/dist/commands/skills/benchmark.d.ts +6 -0
  84. package/dist/commands/skills/benchmark.js +82 -0
  85. package/dist/commands/skills/constants.d.ts +10 -0
  86. package/dist/commands/skills/constants.js +27 -0
  87. package/dist/commands/skills/directives.d.ts +21 -0
  88. package/dist/commands/skills/directives.js +101 -0
  89. package/dist/commands/skills/parsing.d.ts +16 -0
  90. package/dist/commands/skills/parsing.js +108 -0
  91. package/dist/commands/skills/rules.d.ts +5 -0
  92. package/dist/commands/skills/rules.js +32 -0
  93. package/dist/commands/skills/scoring.d.ts +47 -0
  94. package/dist/commands/skills/scoring.js +306 -0
  95. package/dist/commands/skills.d.ts +7 -125
  96. package/dist/commands/skills.js +8 -874
  97. package/dist/commands/tdd-pipeline.js +1 -1
  98. package/dist/commands/tdd.js +1 -1
  99. package/dist/commands/workflow.js +2 -0
  100. package/dist/constants.js +2 -2
  101. package/dist/index.js +26 -473
  102. package/dist/lib/agent-skills.js +1 -1
  103. package/dist/{commands → lib}/atlassian-mcp.js +2 -2
  104. package/dist/lib/auto-skill-install.js +2 -3
  105. package/dist/lib/auto-wire.js +7 -7
  106. package/dist/lib/ci-config.d.ts +65 -0
  107. package/dist/lib/ci-config.js +297 -0
  108. package/dist/lib/claudemd.js +1 -1
  109. package/dist/lib/codex-export.js +4 -8
  110. package/dist/lib/common.js +2 -2
  111. package/dist/lib/context.js +2 -2
  112. package/dist/{commands → lib}/crash-recovery.js +4 -4
  113. package/dist/lib/docker.d.ts +22 -1
  114. package/dist/lib/docker.js +57 -49
  115. package/dist/lib/exec.d.ts +10 -0
  116. package/dist/lib/exec.js +11 -0
  117. package/dist/lib/frontmatter.js +2 -2
  118. package/dist/{commands → lib}/harness-audit.js +1 -1
  119. package/dist/{commands → lib}/parallel-batch.js +1 -3
  120. package/dist/lib/plugin.js +2 -4
  121. package/dist/{commands → lib}/secret-scanner.js +3 -1
  122. package/dist/{commands → lib}/security-analysis.js +4 -2
  123. package/dist/lib/skill-publish.js +23 -5
  124. package/dist/{commands → lib}/skill-scanner.d.ts +1 -1
  125. package/dist/{commands → lib}/skill-scanner.js +3 -3
  126. package/dist/lib/stack-detector.js +3 -3
  127. package/dist/{commands → lib}/supply-chain.js +1 -1
  128. package/dist/lib/template.js +2 -2
  129. package/dist/lib/workflow/discovery.d.ts +8 -0
  130. package/dist/lib/workflow/discovery.js +18 -1
  131. package/dist/lib/workflow/parser.js +15 -15
  132. package/dist/lib/workflow/validator.js +4 -4
  133. package/dist/tasks/index.d.ts +3 -3
  134. package/dist/tasks/index.js +3 -3
  135. package/dist/tasks/scaffold-tasks.d.ts +1 -1
  136. package/dist/tasks/scaffold-tasks.js +10 -10
  137. package/dist/tasks/task-id.d.ts +1 -1
  138. package/dist/tasks/task-id.js +9 -5
  139. package/dist/tasks/task-tracker.d.ts +1 -1
  140. package/dist/tasks/task-tracker.js +7 -7
  141. package/dist/ui/App.js +1 -1
  142. package/dist/ui/CIContext.js +1 -2
  143. package/dist/ui/Header.js +4 -4
  144. package/dist/ui/NameInput.js +1 -1
  145. package/dist/ui/Progress.d.ts +1 -1
  146. package/dist/ui/Progress.js +1 -1
  147. package/dist/ui/StackSelector.js +1 -1
  148. package/lib/common.psm1 +167 -0
  149. package/lib/common.sh +30 -0
  150. package/package.json +38 -19
  151. /package/dist/{commands → lib}/atlassian-mcp.d.ts +0 -0
  152. /package/dist/{commands → lib}/crash-recovery.d.ts +0 -0
  153. /package/dist/{commands → lib}/harness-audit.d.ts +0 -0
  154. /package/dist/{commands → lib}/parallel-batch.d.ts +0 -0
  155. /package/dist/{commands → lib}/secret-scanner.d.ts +0 -0
  156. /package/dist/{commands → lib}/security-analysis.d.ts +0 -0
  157. /package/dist/{commands → lib}/supply-chain.d.ts +0 -0
  158. /package/dist/{commands → lib}/team-presets.d.ts +0 -0
  159. /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
@@ -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
@@ -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["name"];
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["description"];
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 {
@@ -1,11 +1,9 @@
1
- import { execFile } from "child_process";
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
- const execFileAsync = promisify(execFile);
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 ? `${f.file}:${f.line}:${f.column}` : `${f.file}:${f.line}`;
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["name"] ?? path.basename(skillDir);
42
- const version = frontmatter["version"] ?? "1.0.0";
43
- const description = frontmatter["description"] ?? `${name} AI skill`;
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, filePath: string): SkillThreat[];
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['\"]|user|input|req)/i,
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, filePath) {
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["dependencies"] ?? {}),
99
- ...(pkg["devDependencies"] ?? {}),
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")))
@@ -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 += "\n" + fragment;
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].trim();
44
- const toId = edgeMatch[2].trim();
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["label"] });
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].trim();
54
+ const nodeId = nodeMatch[1]?.trim();
55
55
  const attrs = parseAttributes(nodeMatch[2]);
56
56
  const node = ensureNode(nodesMap, nodeId);
57
- if (attrs["label"])
58
- node.label = attrs["label"];
59
- if (attrs["check"])
60
- node.check = attrs["check"];
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].trim();
73
- const toId = parts[i + 1].trim();
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].match(/^(?:flowchart|graph)\s+(LR|TD|TB|RL|BT)/i);
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].trim();
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].trim();
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].trim().match(/^(\w[\w-]*)/);
154
- const toSeg = segments[s + 1].trim().match(/^(\w[\w-]*)/);
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["devDependencies"],
33
- ...pkg["dependencies"],
32
+ ...pkg.devDependencies,
33
+ ...pkg.dependencies,
34
34
  };
35
- if (deps["eslint"] || deps["biome"] || deps["oxlint"]) {
35
+ if (deps.eslint || deps.biome || deps.oxlint) {
36
36
  return { pass: true, detail: "Linter found in package.json" };
37
37
  }
38
38
  }
@@ -1,4 +1,4 @@
1
- export { generateTaskId, createTask, addSubtask, buildTaskTree, findTask, type TaskNode, type TaskStatus, } from './task-id.js';
2
- export { TaskTracker } from './task-tracker.js';
3
- export { getDefaultScaffoldTasks } from './scaffold-tasks.js';
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
@@ -1,4 +1,4 @@
1
- export { generateTaskId, createTask, addSubtask, buildTaskTree, findTask, } from './task-id.js';
2
- export { TaskTracker } from './task-tracker.js';
3
- export { getDefaultScaffoldTasks } from './scaffold-tasks.js';
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,3 +1,3 @@
1
- import { type TaskNode } from './task-id.js';
1
+ import { type TaskNode } from "./task-id.js";
2
2
  export declare function getDefaultScaffoldTasks(): TaskNode[];
3
3
  //# sourceMappingURL=scaffold-tasks.d.ts.map
@@ -1,14 +1,14 @@
1
- import { createTask, addSubtask } from './task-id.js';
1
+ import { addSubtask, createTask } from "./task-id.js";
2
2
  export function getDefaultScaffoldTasks() {
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' });
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
@@ -1,4 +1,4 @@
1
- export type TaskStatus = 'todo' | 'in-progress' | 'done';
1
+ export type TaskStatus = "todo" | "in-progress" | "done";
2
2
  export type TaskNode = {
3
3
  id: string;
4
4
  title: string;
@@ -1,7 +1,11 @@
1
- import { createHash } from 'node:crypto';
1
+ import { createHash } from "node:crypto";
2
2
  export function generateTaskId(content) {
3
- const input = [content.title, content.description ?? '', content.parentId ?? ''].join('|');
4
- return createHash('sha256').update(input).digest('hex').slice(0, 7);
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: 'todo',
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).children.push(node);
41
+ map.get(node.parentId)?.children.push(node);
38
42
  }
39
43
  else {
40
44
  roots.push(node);
@@ -1,4 +1,4 @@
1
- import { type TaskNode, type TaskStatus } from './task-id.js';
1
+ import { type TaskNode, type TaskStatus } from "./task-id.js";
2
2
  export declare class TaskTracker {
3
3
  private tasks;
4
4
  addTask(title: string, description?: string, parentId?: string): TaskNode;
@@ -1,4 +1,4 @@
1
- import { createTask } from './task-id.js';
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 !== 'done';
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 !== 'todo')
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 === 'done';
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 === 'done' ? 'x' : ' ';
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('\n');
67
+ .join("\n");
68
68
  }
69
69
  toJSON() {
70
70
  return JSON.stringify(this.tasks);
package/dist/ui/App.js CHANGED
@@ -1,5 +1,5 @@
1
+ import path from "node:path";
1
2
  import { Box } from "ink";
2
- import path from "path";
3
3
  import React, { useState } from "react";
4
4
  import { initProject } from "../commands/init.js";
5
5
  import CISelector from "./CISelector.js";
@@ -1,5 +1,4 @@
1
- import React from "react";
2
- import { createContext, useContext } from "react";
1
+ import React, { createContext, useContext } from "react";
3
2
  const CIContext = createContext({ isCI: false });
4
3
  export function useCIMode() {
5
4
  return useContext(CIContext).isCI;
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 = "\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;
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,
@@ -1,5 +1,5 @@
1
+ import path from "node:path";
1
2
  import { Box, Text, useInput } from "ink";
2
- import path from "path";
3
3
  import React, { useEffect, useState } from "react";
4
4
  import { useCIMode } from "./CIContext.js";
5
5
  import { theme } from "./theme.js";
@@ -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
@@ -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;
@@ -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((input, key) => {
39
+ useInput((_input, key) => {
40
40
  if (detecting)
41
41
  return;
42
42
  if (key.upArrow)