@mestreyoda/fabrica 0.2.17 → 0.2.19
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 +2 -0
- package/defaults/fabrica/prompts/developer.md +20 -13
- package/dist/index.js +110 -21
- package/genesis/scripts/scaffold-project.sh +112 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -120,6 +120,8 @@ The plugin should load immediately after install, without manual remediation.
|
|
|
120
120
|
openclaw plugins inspect fabrica
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
If OpenClaw warns that `plugins.allow` is empty and non-bundled plugins may auto-load, that is a host trust-policy warning, not a Fabrica install failure. Fabrica can be installed and loadable while the OpenClaw operator still has to decide whether to keep open discovery or set an explicit trusted plugin list in `plugins.allow`.
|
|
124
|
+
|
|
123
125
|
**4. Configure Fabrica for a workspace**:
|
|
124
126
|
|
|
125
127
|
```bash
|
|
@@ -24,20 +24,25 @@ Read the comments carefully — they often contain clarifications, decisions, or
|
|
|
24
24
|
|
|
25
25
|
### 1. Use the assigned worktree
|
|
26
26
|
|
|
27
|
-
**NEVER work in the main checkout.**
|
|
27
|
+
**NEVER work in the main checkout.** The task message includes a `Repo:` / `Execution path:` line with the canonical repository path for this project. Start there first. If that path is missing, inaccessible, or points somewhere unexpected, stop and return `Work result: BLOCKED` instead of creating the project under another workspace.
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
# Example:
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
# Example: task message says Repo: /home/ubuntu/git/acme/myproject
|
|
31
|
+
REPO_ROOT="/absolute/path/from-task-message"
|
|
32
|
+
cd "$REPO_ROOT"
|
|
33
33
|
BRANCH="feature/<issue-id>-<slug>"
|
|
34
34
|
WORKTREE="${REPO_ROOT}.worktrees/${BRANCH}"
|
|
35
|
-
git worktree
|
|
36
|
-
cd "$WORKTREE"
|
|
35
|
+
if git worktree list --porcelain | grep -Fq "worktree ${WORKTREE}"; then
|
|
36
|
+
cd "$WORKTREE"
|
|
37
|
+
else
|
|
38
|
+
git worktree add "$WORKTREE" -b "$BRANCH"
|
|
39
|
+
cd "$WORKTREE"
|
|
40
|
+
fi
|
|
37
41
|
```
|
|
38
42
|
|
|
39
43
|
The `.worktrees/` directory sits NEXT TO the repo folder (not inside it). This keeps the main checkout clean for the orchestrator and other workers. If the assigned worktree already exists from a previous task on the same branch, verify it's clean and reuse it.
|
|
40
|
-
|
|
44
|
+
|
|
45
|
+
Never create or implement the project under `~/.openclaw/workspace/<slug>` unless the task message explicitly says that directory is the canonical repo path. If the repo already contains scaffolded files, do not re-initialize the project with `npm init`, `uv init`, `cargo init`, or a second skeleton generator — keep the existing stack and modify the scaffold inside the assigned worktree. Once you are in the assigned worktree, stay there for the rest of the task and do not switch back to the main checkout.
|
|
41
46
|
|
|
42
47
|
### 2. Implement the changes
|
|
43
48
|
|
|
@@ -110,16 +115,16 @@ When your task message includes a **PR Feedback** section, it means a reviewer r
|
|
|
110
115
|
|
|
111
116
|
1. Check out the existing branch from the PR (the branch name is in the feedback context)
|
|
112
117
|
2. If a worktree already exists for that branch, `cd` into it
|
|
113
|
-
3. If not, create a worktree
|
|
118
|
+
3. If not, create a local worktree that tracks the existing remote branch:
|
|
114
119
|
```bash
|
|
115
|
-
REPO_ROOT="
|
|
120
|
+
REPO_ROOT="/absolute/path/from-task-message"
|
|
116
121
|
BRANCH="<branch-from-pr>"
|
|
117
122
|
WORKTREE="${REPO_ROOT}.worktrees/${BRANCH}"
|
|
118
123
|
git fetch origin "$BRANCH"
|
|
119
|
-
git worktree add "$WORKTREE" "origin/$BRANCH"
|
|
124
|
+
git worktree add -b "$BRANCH" "$WORKTREE" "origin/$BRANCH"
|
|
120
125
|
cd "$WORKTREE"
|
|
121
126
|
```
|
|
122
|
-
4. Address **only** the reviewer's comments — do not re-implement the original issue from scratch
|
|
127
|
+
4. Address **only** the reviewer's comments on that same PR branch — do not switch to a new canonical issue branch and do not re-implement the original issue from scratch
|
|
123
128
|
5. Commit and push to the **same branch** — the existing PR updates automatically
|
|
124
129
|
6. End your response with the canonical developer result line described below
|
|
125
130
|
|
|
@@ -133,9 +138,11 @@ PR_NUM=$(gh pr list --head "$BRANCH" --json number -q '.[0].number')
|
|
|
133
138
|
QA_RAW=$(bash scripts/qa.sh 2>&1); QA_EXIT=$?
|
|
134
139
|
# MANDATORY: sanitize before embedding in PR — strip lines with tokens/keys/env vars/host paths
|
|
135
140
|
QA_OUTPUT=$(printf '%s' "$QA_RAW" | grep -v -iE '(TOKEN|SECRET|_KEY|PASSWORD|CREDENTIAL|PRIVATE|AUTH)=' | grep -v -E '(ghp_|gho_|github_pat_|sk-|xox[bprs]-|AIza|AKIA|glpat-)' | grep -v -E '^declare -x ' | grep -v -E '(/home/|~/.openclaw/)' | head -200)
|
|
136
|
-
|
|
141
|
+
REPO_SLUG=$(gh repo view --json nameWithOwner -q '.nameWithOwner')
|
|
142
|
+
CURRENT_BODY=$(gh api "repos/$REPO_SLUG/pulls/$PR_NUM" --jq '.body')
|
|
137
143
|
BODY_NO_QA=$(printf '%s' "$CURRENT_BODY" | perl -0pe 's/\n## QA Evidence\b[\s\S]*?(?=\n##\s|\z)//g')
|
|
138
|
-
|
|
144
|
+
NEW_BODY=$(printf '%s\n\n## QA Evidence\n\n```\n%s\n```\n\nExit code: %d\n' "$BODY_NO_QA" "$QA_OUTPUT" "$QA_EXIT")
|
|
145
|
+
gh api --method PATCH "repos/$REPO_SLUG/pulls/$PR_NUM" -f body="$NEW_BODY" >/dev/null
|
|
139
146
|
```
|
|
140
147
|
|
|
141
148
|
**NEVER bypass the sanitization step.** Never embed raw, unfiltered command output in PR descriptions.
|
package/dist/index.js
CHANGED
|
@@ -113905,8 +113905,8 @@ import fsSync from "node:fs";
|
|
|
113905
113905
|
import path5 from "node:path";
|
|
113906
113906
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
113907
113907
|
function getCurrentVersion() {
|
|
113908
|
-
if ("0.2.
|
|
113909
|
-
return "0.2.
|
|
113908
|
+
if ("0.2.19") {
|
|
113909
|
+
return "0.2.19";
|
|
113910
113910
|
}
|
|
113911
113911
|
try {
|
|
113912
113912
|
const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
|
|
@@ -126233,6 +126233,13 @@ async function getCurrentBranch(repoPath, runCommand) {
|
|
|
126233
126233
|
});
|
|
126234
126234
|
return result.stdout.trim();
|
|
126235
126235
|
}
|
|
126236
|
+
function isCurrentProjectBaseBranch(branchName, baseBranch) {
|
|
126237
|
+
const normalizedBranch = branchName.trim().toLowerCase();
|
|
126238
|
+
if (!normalizedBranch) return true;
|
|
126239
|
+
const normalizedBaseBranch = baseBranch?.trim().toLowerCase();
|
|
126240
|
+
if (normalizedBaseBranch) return normalizedBranch === normalizedBaseBranch;
|
|
126241
|
+
return ["main", "master", "develop", "development", "trunk"].includes(normalizedBranch);
|
|
126242
|
+
}
|
|
126236
126243
|
function throwInvalidQaEvidence(qaEvidence, actor) {
|
|
126237
126244
|
throw new Error(formatQaEvidenceValidationFailure(qaEvidence, actor));
|
|
126238
126245
|
}
|
|
@@ -126264,12 +126271,16 @@ async function isConflictResolutionCycle(workspaceDir, issueId, issueRuntime) {
|
|
|
126264
126271
|
}
|
|
126265
126272
|
return false;
|
|
126266
126273
|
}
|
|
126267
|
-
async function validatePrExistsForDeveloper(issueId, repoPath, provider, runCommand, workspaceDir, projectSlug, issueRuntime) {
|
|
126274
|
+
async function validatePrExistsForDeveloper(issueId, repoPath, provider, runCommand, workspaceDir, projectSlug, issueRuntime, baseBranch) {
|
|
126268
126275
|
const logger6 = getRootLogger().child({ issueId, phase: "work-finish" });
|
|
126269
126276
|
const branchName = await getCurrentBranch(repoPath, runCommand).catch(() => "");
|
|
126270
126277
|
try {
|
|
126271
|
-
const
|
|
126272
|
-
const
|
|
126278
|
+
const preferIssuePr = isCurrentProjectBaseBranch(branchName, baseBranch);
|
|
126279
|
+
const branchPr = !preferIssuePr ? await provider.findOpenPrForBranch(branchName) : null;
|
|
126280
|
+
const issuePr = await provider.getPrStatus(issueId);
|
|
126281
|
+
const issuePrIsReviewable = !!issuePr.url && issuePr.state !== PrState.MERGED && issuePr.state !== PrState.CLOSED;
|
|
126282
|
+
const branchPrIsReviewable = !!branchPr?.url && branchPr.state !== PrState.MERGED && branchPr.state !== PrState.CLOSED;
|
|
126283
|
+
const prStatus = preferIssuePr ? issuePrIsReviewable ? issuePr : branchPr ?? issuePr : branchPrIsReviewable ? branchPr : issuePr;
|
|
126273
126284
|
if (!prStatus.url || prStatus.state === PrState.MERGED || prStatus.state === PrState.CLOSED) {
|
|
126274
126285
|
const currentBranch = branchName || "current-branch";
|
|
126275
126286
|
const reason = !prStatus.url ? `\u2717 No PR found for branch: ${currentBranch}` : prStatus.state === PrState.MERGED ? `\u2717 Last linked PR is already merged: ${prStatus.url}` : `\u2717 Last linked PR is closed and not reviewable: ${prStatus.url}`;
|
|
@@ -126553,7 +126564,16 @@ function createWorkFinishTool(ctx) {
|
|
|
126553
126564
|
const pluginConfig = ctx.pluginConfig;
|
|
126554
126565
|
let developerPrStatus;
|
|
126555
126566
|
if (role === "developer" && result === "done") {
|
|
126556
|
-
developerPrStatus = await validatePrExistsForDeveloper(
|
|
126567
|
+
developerPrStatus = await validatePrExistsForDeveloper(
|
|
126568
|
+
issueId,
|
|
126569
|
+
repoPath,
|
|
126570
|
+
provider,
|
|
126571
|
+
ctx.runCommand,
|
|
126572
|
+
workspaceDir,
|
|
126573
|
+
project.slug,
|
|
126574
|
+
project.issueRuntime?.[String(issueId)],
|
|
126575
|
+
project.baseBranch
|
|
126576
|
+
);
|
|
126557
126577
|
await updateIssueRuntime(workspaceDir, project.slug, issueId, {
|
|
126558
126578
|
currentPrNodeId: developerPrStatus.nodeId ?? null,
|
|
126559
126579
|
currentPrNumber: developerPrStatus.number ?? null,
|
|
@@ -128020,8 +128040,10 @@ async function resolveEffectiveModelForGateway(requested, runCommand) {
|
|
|
128020
128040
|
|
|
128021
128041
|
// lib/dispatch/message-builder.ts
|
|
128022
128042
|
init_roles();
|
|
128043
|
+
function toBranchSlug(value) {
|
|
128044
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "task";
|
|
128045
|
+
}
|
|
128023
128046
|
function buildTaskMessage(opts) {
|
|
128024
|
-
const sanitizeRepoContext = (value) => value.startsWith("/") || value.startsWith("~/") ? "[repository workspace hidden]" : value;
|
|
128025
128047
|
const {
|
|
128026
128048
|
projectName,
|
|
128027
128049
|
channelId,
|
|
@@ -128033,8 +128055,10 @@ function buildTaskMessage(opts) {
|
|
|
128033
128055
|
repo,
|
|
128034
128056
|
baseBranch
|
|
128035
128057
|
} = opts;
|
|
128036
|
-
const repoDisplay =
|
|
128058
|
+
const repoDisplay = repo;
|
|
128037
128059
|
const isFeedbackCycle = !!opts.prFeedback;
|
|
128060
|
+
const branchName = opts.prFeedback?.branchName?.trim() ? opts.prFeedback.branchName.trim() : `feature/${issueId}-${toBranchSlug(projectName)}`;
|
|
128061
|
+
const worktreePath = `${repoDisplay}.worktrees/${branchName}`;
|
|
128038
128062
|
const parts = [
|
|
128039
128063
|
`${role.toUpperCase()} task for project "${projectName}" \u2014 Issue #${issueId}`,
|
|
128040
128064
|
``,
|
|
@@ -128051,6 +128075,17 @@ ${issueDescription}` : ""
|
|
|
128051
128075
|
`> When feedback conflicts with the original description, follow the PR feedback.`
|
|
128052
128076
|
);
|
|
128053
128077
|
}
|
|
128078
|
+
parts.push(
|
|
128079
|
+
``,
|
|
128080
|
+
`## Execution Setup (do this before editing files)`,
|
|
128081
|
+
`Repo: ${repoDisplay} | Base branch: ${baseBranch} | ${issueUrl}`,
|
|
128082
|
+
`Execution path: ${repoDisplay}`,
|
|
128083
|
+
`Required branch: ${branchName}`,
|
|
128084
|
+
`Required worktree: ${worktreePath}`,
|
|
128085
|
+
`Before editing any file, create or reuse the required worktree above and work only there.`,
|
|
128086
|
+
`Do not re-initialize or replace the scaffold in the main checkout (for example: do not run npm init, cargo init, uv init, or create a second project skeleton) when the repo already contains scaffolded files. Modify the existing scaffold inside the worktree instead.`,
|
|
128087
|
+
`If the repo path is missing or inaccessible, return the canonical blocked result instead of improvising in ~/.openclaw/workspace.`
|
|
128088
|
+
);
|
|
128054
128089
|
if (opts.followUpPrRequired) {
|
|
128055
128090
|
parts.push(
|
|
128056
128091
|
``,
|
|
@@ -128096,14 +128131,12 @@ ${issueDescription}` : ""
|
|
|
128096
128131
|
if (opts.attachmentContext) parts.push(opts.attachmentContext);
|
|
128097
128132
|
parts.push(
|
|
128098
128133
|
``,
|
|
128099
|
-
`Repo: ${repoDisplay} | Branch: ${baseBranch} | ${issueUrl}`,
|
|
128100
128134
|
`Project: ${projectName} | Channel: ${channelId}`
|
|
128101
128135
|
);
|
|
128102
128136
|
parts.push(...buildCompletionContract(role));
|
|
128103
128137
|
return parts.join("\n");
|
|
128104
128138
|
}
|
|
128105
128139
|
function buildConflictFixMessage(opts) {
|
|
128106
|
-
const sanitizeRepoContext = (value) => value.startsWith("/") || value.startsWith("~/") ? "[repository workspace hidden]" : value;
|
|
128107
128140
|
const {
|
|
128108
128141
|
projectName,
|
|
128109
128142
|
channelId,
|
|
@@ -128115,7 +128148,9 @@ function buildConflictFixMessage(opts) {
|
|
|
128115
128148
|
baseBranch,
|
|
128116
128149
|
prFeedback
|
|
128117
128150
|
} = opts;
|
|
128118
|
-
const repoDisplay =
|
|
128151
|
+
const repoDisplay = repo;
|
|
128152
|
+
const branchName = prFeedback.branchName?.trim() ? prFeedback.branchName.trim() : `feature/${issueId}-${toBranchSlug(projectName)}`;
|
|
128153
|
+
const worktreePath = `${repoDisplay}.worktrees/${branchName}`;
|
|
128119
128154
|
const parts = [
|
|
128120
128155
|
`${role.toUpperCase()} task for project "${projectName}" \u2014 Issue #${issueId}`,
|
|
128121
128156
|
``,
|
|
@@ -128126,8 +128161,12 @@ function buildConflictFixMessage(opts) {
|
|
|
128126
128161
|
parts.push(...formatPrFeedback(prFeedback, baseBranch));
|
|
128127
128162
|
parts.push(
|
|
128128
128163
|
``,
|
|
128129
|
-
`Repo: ${repoDisplay} |
|
|
128130
|
-
`Project: ${projectName} | Channel: ${channelId}
|
|
128164
|
+
`Repo: ${repoDisplay} | Base branch: ${baseBranch} | ${issueUrl}`,
|
|
128165
|
+
`Project: ${projectName} | Channel: ${channelId}`,
|
|
128166
|
+
`Execution path: ${repoDisplay}`,
|
|
128167
|
+
`Required branch: ${branchName}`,
|
|
128168
|
+
`Required worktree: ${worktreePath}`,
|
|
128169
|
+
`Start by changing into the canonical repo path above before reusing the PR branch or creating its worktree. Reuse the exact PR branch named above; do not switch to a new canonical issue branch during a feedback cycle. Do not resolve the issue inside ~/.openclaw/workspace unless the repo path itself points there.`
|
|
128131
128170
|
);
|
|
128132
128171
|
parts.push(...buildCompletionContract(role));
|
|
128133
128172
|
return parts.join("\n");
|
|
@@ -128717,7 +128756,7 @@ async function dispatchTask(opts) {
|
|
|
128717
128756
|
const securityChecklist = await loadSecurityChecklist(workspaceDir, project.name).catch(() => "");
|
|
128718
128757
|
const primaryChannelId = project.slug;
|
|
128719
128758
|
const isConflictFix = prFeedback?.reason === "merge_conflict";
|
|
128720
|
-
const repoContext = project.repoRemote?.replace(/\.git$/, "")
|
|
128759
|
+
const repoContext = project.repo ? resolveRepoPath(project.repo) : project.repoRemote?.replace(/\.git$/, "") || project.slug;
|
|
128721
128760
|
const taskMessage = isConflictFix && prFeedback ? buildConflictFixMessage({
|
|
128722
128761
|
projectName: project.name,
|
|
128723
128762
|
channelId: primaryChannelId,
|
|
@@ -130417,6 +130456,7 @@ init_labels();
|
|
|
130417
130456
|
// lib/services/worker-completion.ts
|
|
130418
130457
|
init_audit();
|
|
130419
130458
|
import fs24 from "node:fs/promises";
|
|
130459
|
+
init_workflow();
|
|
130420
130460
|
|
|
130421
130461
|
// lib/services/worker-result.ts
|
|
130422
130462
|
var ROLE_PREFIX = {
|
|
@@ -130823,7 +130863,8 @@ async function defaultValidateDeveloperDone(opts) {
|
|
|
130823
130863
|
opts.runCommand,
|
|
130824
130864
|
opts.workspaceDir,
|
|
130825
130865
|
opts.projectSlug,
|
|
130826
|
-
opts.issueRuntime
|
|
130866
|
+
opts.issueRuntime,
|
|
130867
|
+
opts.baseBranch
|
|
130827
130868
|
);
|
|
130828
130869
|
return { ok: true, prStatus };
|
|
130829
130870
|
} catch (error48) {
|
|
@@ -130915,19 +130956,68 @@ async function applyWorkerResult(opts) {
|
|
|
130915
130956
|
runCommand: opts.runCommand,
|
|
130916
130957
|
workspaceDir: opts.workspaceDir,
|
|
130917
130958
|
projectSlug: context2.projectSlug,
|
|
130918
|
-
issueRuntime: context2.issueRuntime
|
|
130959
|
+
issueRuntime: context2.issueRuntime,
|
|
130960
|
+
baseBranch: context2.project.baseBranch
|
|
130919
130961
|
});
|
|
130920
130962
|
if (!validation.ok) {
|
|
130963
|
+
const validationReason = validation.reason ?? "developer_validation_failed";
|
|
130964
|
+
const feedbackQueueLabel = getQueueLabels(workflow, "developer").find((label) => isFeedbackState(workflow, label)) ?? "To Improve";
|
|
130921
130965
|
await log(opts.workspaceDir, "worker_completion_skipped", {
|
|
130922
130966
|
sessionKey: context2.project.workers[context2.parsed.role]?.levels?.[context2.slotLevel]?.[context2.slotIndex]?.sessionKey ?? null,
|
|
130923
130967
|
projectSlug: context2.projectSlug,
|
|
130924
130968
|
issueId: context2.issueId,
|
|
130925
130969
|
role: context2.parsed.role,
|
|
130926
130970
|
result: opts.result.value,
|
|
130927
|
-
reason:
|
|
130971
|
+
reason: validationReason
|
|
130972
|
+
}).catch(() => {
|
|
130973
|
+
});
|
|
130974
|
+
await updateIssueRuntime(opts.workspaceDir, context2.projectSlug, context2.issueId, {
|
|
130975
|
+
inconclusiveCompletionAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
130976
|
+
inconclusiveCompletionReason: validationReason
|
|
130977
|
+
}).catch(() => {
|
|
130978
|
+
});
|
|
130979
|
+
await executeCompletion({
|
|
130980
|
+
workspaceDir: opts.workspaceDir,
|
|
130981
|
+
projectSlug: context2.projectSlug,
|
|
130982
|
+
role: context2.parsed.role,
|
|
130983
|
+
result: "blocked",
|
|
130984
|
+
issueId: context2.issueId,
|
|
130985
|
+
summary: `Automatic recovery: developer reported DONE but completion validation failed.
|
|
130986
|
+
|
|
130987
|
+
${validationReason}`,
|
|
130988
|
+
provider,
|
|
130989
|
+
repoPath,
|
|
130990
|
+
projectName: context2.project.name,
|
|
130991
|
+
channels: context2.project.channels,
|
|
130992
|
+
runtime: opts.runtime,
|
|
130993
|
+
workflow,
|
|
130994
|
+
level: context2.slotLevel,
|
|
130995
|
+
slotIndex: context2.slotIndex,
|
|
130996
|
+
overrideToLabel: feedbackQueueLabel,
|
|
130997
|
+
overrideReason: validationReason,
|
|
130998
|
+
runCommand: opts.runCommand
|
|
130999
|
+
});
|
|
131000
|
+
await recordIssueLifecycle({
|
|
131001
|
+
workspaceDir: opts.workspaceDir,
|
|
131002
|
+
slug: context2.projectSlug,
|
|
131003
|
+
issueId: context2.issueId,
|
|
131004
|
+
stage: "session_completed",
|
|
131005
|
+
sessionKey: context2.sessionKey,
|
|
131006
|
+
details: { role: context2.parsed.role, result: "blocked", source: "agent_end_recovery", reason: validationReason }
|
|
131007
|
+
}).catch(() => {
|
|
131008
|
+
});
|
|
131009
|
+
await log(opts.workspaceDir, "worker_completion_applied", {
|
|
131010
|
+
sessionKey: context2.sessionKey,
|
|
131011
|
+
projectSlug: context2.projectSlug,
|
|
131012
|
+
issueId: context2.issueId,
|
|
131013
|
+
role: context2.parsed.role,
|
|
131014
|
+
result: "BLOCKED",
|
|
131015
|
+
source: "agent_end_recovery",
|
|
131016
|
+
recoveredTo: "To Improve",
|
|
131017
|
+
reason: validationReason
|
|
130928
131018
|
}).catch(() => {
|
|
130929
131019
|
});
|
|
130930
|
-
return { applied:
|
|
131020
|
+
return { applied: true };
|
|
130931
131021
|
}
|
|
130932
131022
|
validatedPrStatus = validation.prStatus;
|
|
130933
131023
|
await persistDeveloperPrBinding({
|
|
@@ -138065,7 +138155,7 @@ var EXPRESS_GATES = {
|
|
|
138065
138155
|
};
|
|
138066
138156
|
var NODE_CLI_GATES = {
|
|
138067
138157
|
lint: "npm run lint",
|
|
138068
|
-
types: "npm run
|
|
138158
|
+
types: "npm run typecheck",
|
|
138069
138159
|
security: "npm audit --audit-level=moderate",
|
|
138070
138160
|
tests: "npm test",
|
|
138071
138161
|
coverage: "npm run coverage"
|
|
@@ -138974,7 +139064,6 @@ fi
|
|
|
138974
139064
|
}
|
|
138975
139065
|
|
|
138976
139066
|
// lib/intake/steps/scaffold.ts
|
|
138977
|
-
var PYTHON_STACKS3 = /* @__PURE__ */ new Set(["python-cli", "fastapi", "flask", "django"]);
|
|
138978
139067
|
var scaffoldStep = {
|
|
138979
139068
|
name: "scaffold",
|
|
138980
139069
|
shouldRun: (payload) => payload.impact?.is_greenfield === true && !payload.dry_run,
|
|
@@ -139000,7 +139089,7 @@ var scaffoldStep = {
|
|
|
139000
139089
|
mode: "scaffold",
|
|
139001
139090
|
runCommand: ctx.runCommand
|
|
139002
139091
|
});
|
|
139003
|
-
if (scaffold.stack &&
|
|
139092
|
+
if (scaffold.stack && payload.spec) {
|
|
139004
139093
|
try {
|
|
139005
139094
|
const contract = generateQaContract({
|
|
139006
139095
|
spec: payload.spec,
|
|
@@ -348,7 +348,7 @@ $objective
|
|
|
348
348
|
|
|
349
349
|
\`\`\`bash
|
|
350
350
|
$(case "$stack" in
|
|
351
|
-
nextjs|express) echo "npm install" ;;
|
|
351
|
+
nextjs|express|node-cli) echo "npm install" ;;
|
|
352
352
|
fastapi|flask|django) echo "python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt" ;;
|
|
353
353
|
python-cli) echo "python -m venv .venv && source .venv/bin/activate && pip install -e '.[dev]'" ;;
|
|
354
354
|
esac)
|
|
@@ -360,6 +360,7 @@ esac)
|
|
|
360
360
|
$(case "$stack" in
|
|
361
361
|
nextjs) echo "npm run dev" ;;
|
|
362
362
|
express) echo "npm run dev" ;;
|
|
363
|
+
node-cli) echo "npm run dev -- \"Hello World CLI\"" ;;
|
|
363
364
|
fastapi) echo "uvicorn app.main:app --reload" ;;
|
|
364
365
|
flask) echo "flask run --debug" ;;
|
|
365
366
|
django) echo "python manage.py runserver" ;;
|
|
@@ -460,21 +461,26 @@ scaffold_node_cli() {
|
|
|
460
461
|
"scripts": {
|
|
461
462
|
"dev": "tsx src/index.ts",
|
|
462
463
|
"build": "tsc",
|
|
464
|
+
"typecheck": "tsc --noEmit",
|
|
463
465
|
"start": "node dist/index.js",
|
|
464
|
-
"lint": "eslint
|
|
466
|
+
"lint": "eslint .",
|
|
465
467
|
"test": "vitest run",
|
|
466
|
-
"test:watch": "vitest"
|
|
468
|
+
"test:watch": "vitest",
|
|
469
|
+
"coverage": "vitest run --coverage --coverage.thresholds.lines=80"
|
|
467
470
|
},
|
|
468
471
|
"dependencies": {
|
|
469
472
|
"commander": "^14.0.0"
|
|
470
473
|
},
|
|
471
474
|
"devDependencies": {
|
|
475
|
+
"@eslint/js": "^9.0.0",
|
|
472
476
|
"@types/node": "^22.0.0",
|
|
477
|
+
"@vitest/coverage-v8": "^3.0.0",
|
|
473
478
|
"eslint": "^9.0.0",
|
|
474
|
-
"
|
|
479
|
+
"globals": "^15.0.0",
|
|
475
480
|
"tsx": "^4.0.0",
|
|
476
|
-
"
|
|
477
|
-
"
|
|
481
|
+
"typescript": "^5.7.0",
|
|
482
|
+
"typescript-eslint": "^8.0.0",
|
|
483
|
+
"vitest": "^3.0.0"
|
|
478
484
|
}
|
|
479
485
|
}
|
|
480
486
|
EOF
|
|
@@ -489,62 +495,127 @@ EOF
|
|
|
489
495
|
"module": "ESNext",
|
|
490
496
|
"moduleResolution": "bundler",
|
|
491
497
|
"outDir": "dist",
|
|
492
|
-
"rootDir": "
|
|
498
|
+
"rootDir": ".",
|
|
493
499
|
"strict": true,
|
|
494
500
|
"esModuleInterop": true,
|
|
495
501
|
"skipLibCheck": true,
|
|
496
502
|
"resolveJsonModule": true,
|
|
497
|
-
"declaration": true
|
|
503
|
+
"declaration": true,
|
|
504
|
+
"types": ["node"]
|
|
498
505
|
},
|
|
499
|
-
"include": ["src"],
|
|
500
|
-
"exclude": ["node_modules", "dist"
|
|
506
|
+
"include": ["src", "tests"],
|
|
507
|
+
"exclude": ["node_modules", "dist"]
|
|
501
508
|
}
|
|
502
509
|
EOF
|
|
503
510
|
FILES_CREATED+=("tsconfig.json")
|
|
504
511
|
|
|
512
|
+
cat > eslint.config.mjs <<'EOF'
|
|
513
|
+
import js from '@eslint/js';
|
|
514
|
+
import globals from 'globals';
|
|
515
|
+
import tseslint from 'typescript-eslint';
|
|
516
|
+
|
|
517
|
+
export default tseslint.config(
|
|
518
|
+
js.configs.recommended,
|
|
519
|
+
...tseslint.configs.recommended,
|
|
520
|
+
{
|
|
521
|
+
files: ['src/**/*.ts', 'tests/**/*.ts'],
|
|
522
|
+
languageOptions: {
|
|
523
|
+
globals: {
|
|
524
|
+
...globals.node,
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
rules: {
|
|
528
|
+
'no-console': 'off',
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
);
|
|
532
|
+
EOF
|
|
533
|
+
FILES_CREATED+=("eslint.config.mjs")
|
|
534
|
+
|
|
505
535
|
mkdir -p src
|
|
506
536
|
cat > src/index.ts <<'EOF'
|
|
507
537
|
#!/usr/bin/env node
|
|
508
538
|
import { Command } from 'commander';
|
|
539
|
+
import { pathToFileURL } from 'node:url';
|
|
540
|
+
|
|
541
|
+
export function toKebabCase(input: string): string {
|
|
542
|
+
return input
|
|
543
|
+
.trim()
|
|
544
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
545
|
+
.replace(/[\s_]+/g, '-')
|
|
546
|
+
.replace(/-+/g, '-')
|
|
547
|
+
.replace(/^-|-$/g, '')
|
|
548
|
+
.toLowerCase();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function createProgram(): Command {
|
|
552
|
+
return new Command()
|
|
553
|
+
.name(process.env.npm_package_name ?? 'cli')
|
|
554
|
+
.version(process.env.npm_package_version ?? '0.1.0')
|
|
555
|
+
.description('Convert a text argument to kebab-case')
|
|
556
|
+
.argument('<text>', 'text to convert')
|
|
557
|
+
.action((text: string) => {
|
|
558
|
+
console.log(toKebabCase(text));
|
|
559
|
+
});
|
|
560
|
+
}
|
|
509
561
|
|
|
510
|
-
|
|
562
|
+
export function main(argv = process.argv): void {
|
|
563
|
+
createProgram().parse(argv);
|
|
564
|
+
}
|
|
511
565
|
|
|
512
|
-
|
|
513
|
-
.
|
|
514
|
-
|
|
515
|
-
.description('CLI tool')
|
|
516
|
-
.argument('[args...]', 'arguments')
|
|
517
|
-
.action((args: string[]) => {
|
|
518
|
-
console.log('Hello from CLI', args.length ? args.join(' ') : '');
|
|
519
|
-
});
|
|
566
|
+
const isDirectRun = process.argv[1]
|
|
567
|
+
? import.meta.url === pathToFileURL(process.argv[1]).href
|
|
568
|
+
: false;
|
|
520
569
|
|
|
521
|
-
|
|
570
|
+
if (isDirectRun) {
|
|
571
|
+
main();
|
|
572
|
+
}
|
|
522
573
|
EOF
|
|
523
574
|
FILES_CREATED+=("src/index.ts")
|
|
524
575
|
|
|
525
576
|
mkdir -p tests
|
|
526
577
|
cat > tests/main.test.ts <<'EOF'
|
|
527
|
-
import { describe, it, expect } from 'vitest';
|
|
528
578
|
import { execFileSync } from 'node:child_process';
|
|
529
|
-
import {
|
|
579
|
+
import { fileURLToPath } from 'node:url';
|
|
580
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
581
|
+
import { createProgram, main, toKebabCase } from '../src/index.js';
|
|
530
582
|
|
|
531
|
-
const cli =
|
|
583
|
+
const cli = fileURLToPath(new URL('../src/index.ts', import.meta.url));
|
|
532
584
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
585
|
+
describe('toKebabCase', () => {
|
|
586
|
+
it('converts text with spaces and casing', () => {
|
|
587
|
+
expect(toKebabCase('Hello World CLI')).toBe('hello-world-cli');
|
|
588
|
+
expect(toKebabCase('Some_Mixed Case Text')).toBe('some-mixed-case-text');
|
|
589
|
+
expect(toKebabCase(' already-kebab ')).toBe('already-kebab');
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
describe('main', () => {
|
|
594
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
595
|
+
|
|
596
|
+
afterEach(() => {
|
|
597
|
+
logSpy.mockClear();
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('prints the kebab-case output', () => {
|
|
601
|
+
main(['node', 'hyphenator-cli', 'Hello World Example']);
|
|
602
|
+
expect(logSpy).toHaveBeenCalledWith('hello-world-example');
|
|
603
|
+
});
|
|
539
604
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
expect(
|
|
605
|
+
it('builds a command with metadata', () => {
|
|
606
|
+
const program = createProgram();
|
|
607
|
+
expect(program.name()).toBeTruthy();
|
|
608
|
+
expect(program.description()).toContain('kebab-case');
|
|
544
609
|
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
describe('CLI smoke', () => {
|
|
613
|
+
it('shows version with --version', () => {
|
|
614
|
+
const output = execFileSync('npx', ['tsx', cli, '--version'], {
|
|
615
|
+
encoding: 'utf-8',
|
|
616
|
+
env: { ...process.env, NODE_NO_WARNINGS: '1' },
|
|
617
|
+
}).trim();
|
|
545
618
|
|
|
546
|
-
it('should show version with --version', () => {
|
|
547
|
-
const output = run('--version');
|
|
548
619
|
expect(output).toMatch(/\d+\.\d+\.\d+/);
|
|
549
620
|
});
|
|
550
621
|
});
|
|
@@ -559,23 +630,19 @@ echo "=== QA Gate ==="
|
|
|
559
630
|
FAIL=0
|
|
560
631
|
|
|
561
632
|
echo "--- Lint ---"
|
|
562
|
-
|
|
633
|
+
npm run lint 2>&1 || { echo "LINT FAILED"; FAIL=1; }
|
|
563
634
|
|
|
564
635
|
echo "--- TypeScript ---"
|
|
565
|
-
|
|
636
|
+
npm run typecheck 2>&1 || { echo "TSC FAILED"; FAIL=1; }
|
|
566
637
|
|
|
567
638
|
echo "--- Tests ---"
|
|
568
|
-
|
|
639
|
+
npm test 2>&1 || { echo "TESTS FAILED"; FAIL=1; }
|
|
569
640
|
|
|
570
641
|
echo "--- Coverage (>=80%) ---"
|
|
571
|
-
|
|
642
|
+
npm run coverage 2>&1 || { echo "COVERAGE FAILED"; FAIL=1; }
|
|
572
643
|
|
|
573
|
-
echo "---
|
|
574
|
-
|
|
575
|
-
echo "SECRETS FOUND — FAIL"; FAIL=1
|
|
576
|
-
else
|
|
577
|
-
echo "No hardcoded secrets found"
|
|
578
|
-
fi
|
|
644
|
+
echo "--- Security audit ---"
|
|
645
|
+
npm audit --audit-level=moderate 2>&1 || { echo "AUDIT FAILED"; FAIL=1; }
|
|
579
646
|
|
|
580
647
|
exit $FAIL
|
|
581
648
|
QAEOF
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mestreyoda/fabrica",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.19",
|
|
4
4
|
"description": "Autonomous software engineering pipeline for OpenClaw. Turns ideas into deployed code via intake, dispatch, review, test, and merge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|