@mestreyoda/fabrica 0.2.18 → 0.2.20
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/defaults/fabrica/prompts/developer.md +5 -5
- package/dist/index.js +145 -18
- package/genesis/scripts/scaffold-project.sh +112 -45
- package/package.json +1 -1
|
@@ -42,7 +42,7 @@ fi
|
|
|
42
42
|
|
|
43
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.
|
|
44
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. Once you are in the assigned worktree, stay there for the rest of the task and do not switch back to the main checkout.
|
|
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.
|
|
46
46
|
|
|
47
47
|
### 2. Implement the changes
|
|
48
48
|
|
|
@@ -115,16 +115,16 @@ When your task message includes a **PR Feedback** section, it means a reviewer r
|
|
|
115
115
|
|
|
116
116
|
1. Check out the existing branch from the PR (the branch name is in the feedback context)
|
|
117
117
|
2. If a worktree already exists for that branch, `cd` into it
|
|
118
|
-
3. If not, create a worktree
|
|
118
|
+
3. If not, create a local worktree that tracks the existing remote branch:
|
|
119
119
|
```bash
|
|
120
|
-
REPO_ROOT="
|
|
120
|
+
REPO_ROOT="/absolute/path/from-task-message"
|
|
121
121
|
BRANCH="<branch-from-pr>"
|
|
122
122
|
WORKTREE="${REPO_ROOT}.worktrees/${BRANCH}"
|
|
123
123
|
git fetch origin "$BRANCH"
|
|
124
|
-
git worktree add "$WORKTREE" "origin/$BRANCH"
|
|
124
|
+
git worktree add -b "$BRANCH" "$WORKTREE" "origin/$BRANCH"
|
|
125
125
|
cd "$WORKTREE"
|
|
126
126
|
```
|
|
127
|
-
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
|
|
128
128
|
5. Commit and push to the **same branch** — the existing PR updates automatically
|
|
129
129
|
6. End your response with the canonical developer result line described below
|
|
130
130
|
|
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.20") {
|
|
113909
|
+
return "0.2.20";
|
|
113910
113910
|
}
|
|
113911
113911
|
try {
|
|
113912
113912
|
const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
|
|
@@ -126282,6 +126282,25 @@ async function validatePrExistsForDeveloper(issueId, repoPath, provider, runComm
|
|
|
126282
126282
|
const branchPrIsReviewable = !!branchPr?.url && branchPr.state !== PrState.MERGED && branchPr.state !== PrState.CLOSED;
|
|
126283
126283
|
const prStatus = preferIssuePr ? issuePrIsReviewable ? issuePr : branchPr ?? issuePr : branchPrIsReviewable ? branchPr : issuePr;
|
|
126284
126284
|
if (!prStatus.url || prStatus.state === PrState.MERGED || prStatus.state === PrState.CLOSED) {
|
|
126285
|
+
if (preferIssuePr && isCurrentProjectBaseBranch(branchName, baseBranch)) {
|
|
126286
|
+
const currentBase = branchName || baseBranch || "main";
|
|
126287
|
+
const suggestedBranch = `feature/${issueId}-${projectSlug.replace(/[^a-z0-9]+/g, "-").slice(0, 40)}`;
|
|
126288
|
+
throw new Error(
|
|
126289
|
+
`Cannot mark work_finish(done) while on the base branch ("${currentBase}") without an open PR.
|
|
126290
|
+
|
|
126291
|
+
You must implement changes on a feature branch and open a PR before calling work_finish.
|
|
126292
|
+
|
|
126293
|
+
Steps to fix:
|
|
126294
|
+
1. git worktree add ../${projectSlug}.worktrees/${suggestedBranch} -b ${suggestedBranch}
|
|
126295
|
+
2. cd ../${projectSlug}.worktrees/${suggestedBranch}
|
|
126296
|
+
3. Implement the changes there, commit, push, and create a PR:
|
|
126297
|
+
git push -u origin ${suggestedBranch}
|
|
126298
|
+
gh pr create --base ${baseBranch ?? "main"} --head ${suggestedBranch} --title "feat: ..." --body "Closes #${issueId}"
|
|
126299
|
+
4. Then call work_finish again.
|
|
126300
|
+
|
|
126301
|
+
If the worktree already exists, cd into it and continue from there.`
|
|
126302
|
+
);
|
|
126303
|
+
}
|
|
126285
126304
|
const currentBranch = branchName || "current-branch";
|
|
126286
126305
|
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}`;
|
|
126287
126306
|
throw new Error(
|
|
@@ -126290,7 +126309,7 @@ async function validatePrExistsForDeveloper(issueId, repoPath, provider, runComm
|
|
|
126290
126309
|
${reason}
|
|
126291
126310
|
|
|
126292
126311
|
Please create a PR first:
|
|
126293
|
-
gh pr create --base main --head ${currentBranch} --title "..." --body "
|
|
126312
|
+
gh pr create --base ${baseBranch ?? "main"} --head ${currentBranch} --title "..." --body "Closes #${issueId}"
|
|
126294
126313
|
|
|
126295
126314
|
Then call work_finish again.`
|
|
126296
126315
|
);
|
|
@@ -128040,6 +128059,9 @@ async function resolveEffectiveModelForGateway(requested, runCommand) {
|
|
|
128040
128059
|
|
|
128041
128060
|
// lib/dispatch/message-builder.ts
|
|
128042
128061
|
init_roles();
|
|
128062
|
+
function toBranchSlug(value) {
|
|
128063
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "task";
|
|
128064
|
+
}
|
|
128043
128065
|
function buildTaskMessage(opts) {
|
|
128044
128066
|
const {
|
|
128045
128067
|
projectName,
|
|
@@ -128054,6 +128076,8 @@ function buildTaskMessage(opts) {
|
|
|
128054
128076
|
} = opts;
|
|
128055
128077
|
const repoDisplay = repo;
|
|
128056
128078
|
const isFeedbackCycle = !!opts.prFeedback;
|
|
128079
|
+
const branchName = opts.prFeedback?.branchName?.trim() ? opts.prFeedback.branchName.trim() : `feature/${issueId}-${toBranchSlug(projectName)}`;
|
|
128080
|
+
const worktreePath = `${repoDisplay}.worktrees/${branchName}`;
|
|
128057
128081
|
const parts = [
|
|
128058
128082
|
`${role.toUpperCase()} task for project "${projectName}" \u2014 Issue #${issueId}`,
|
|
128059
128083
|
``,
|
|
@@ -128070,6 +128094,17 @@ ${issueDescription}` : ""
|
|
|
128070
128094
|
`> When feedback conflicts with the original description, follow the PR feedback.`
|
|
128071
128095
|
);
|
|
128072
128096
|
}
|
|
128097
|
+
parts.push(
|
|
128098
|
+
``,
|
|
128099
|
+
`## Execution Setup (do this before editing files)`,
|
|
128100
|
+
`Repo: ${repoDisplay} | Base branch: ${baseBranch} | ${issueUrl}`,
|
|
128101
|
+
`Execution path: ${repoDisplay}`,
|
|
128102
|
+
`Required branch: ${branchName}`,
|
|
128103
|
+
`Required worktree: ${worktreePath}`,
|
|
128104
|
+
`Before editing any file, create or reuse the required worktree above and work only there.`,
|
|
128105
|
+
`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.`,
|
|
128106
|
+
`If the repo path is missing or inaccessible, return the canonical blocked result instead of improvising in ~/.openclaw/workspace.`
|
|
128107
|
+
);
|
|
128073
128108
|
if (opts.followUpPrRequired) {
|
|
128074
128109
|
parts.push(
|
|
128075
128110
|
``,
|
|
@@ -128115,10 +128150,7 @@ ${issueDescription}` : ""
|
|
|
128115
128150
|
if (opts.attachmentContext) parts.push(opts.attachmentContext);
|
|
128116
128151
|
parts.push(
|
|
128117
128152
|
``,
|
|
128118
|
-
`
|
|
128119
|
-
`Project: ${projectName} | Channel: ${channelId}`,
|
|
128120
|
-
`Execution path: ${repoDisplay}`,
|
|
128121
|
-
`Start by changing into the canonical repo path above before creating or reusing a worktree. Do not create or implement the project under ~/.openclaw/workspace unless the repo path itself points there.`
|
|
128153
|
+
`Project: ${projectName} | Channel: ${channelId}`
|
|
128122
128154
|
);
|
|
128123
128155
|
parts.push(...buildCompletionContract(role));
|
|
128124
128156
|
return parts.join("\n");
|
|
@@ -128136,6 +128168,8 @@ function buildConflictFixMessage(opts) {
|
|
|
128136
128168
|
prFeedback
|
|
128137
128169
|
} = opts;
|
|
128138
128170
|
const repoDisplay = repo;
|
|
128171
|
+
const branchName = prFeedback.branchName?.trim() ? prFeedback.branchName.trim() : `feature/${issueId}-${toBranchSlug(projectName)}`;
|
|
128172
|
+
const worktreePath = `${repoDisplay}.worktrees/${branchName}`;
|
|
128139
128173
|
const parts = [
|
|
128140
128174
|
`${role.toUpperCase()} task for project "${projectName}" \u2014 Issue #${issueId}`,
|
|
128141
128175
|
``,
|
|
@@ -128146,10 +128180,12 @@ function buildConflictFixMessage(opts) {
|
|
|
128146
128180
|
parts.push(...formatPrFeedback(prFeedback, baseBranch));
|
|
128147
128181
|
parts.push(
|
|
128148
128182
|
``,
|
|
128149
|
-
`Repo: ${repoDisplay} |
|
|
128183
|
+
`Repo: ${repoDisplay} | Base branch: ${baseBranch} | ${issueUrl}`,
|
|
128150
128184
|
`Project: ${projectName} | Channel: ${channelId}`,
|
|
128151
128185
|
`Execution path: ${repoDisplay}`,
|
|
128152
|
-
`
|
|
128186
|
+
`Required branch: ${branchName}`,
|
|
128187
|
+
`Required worktree: ${worktreePath}`,
|
|
128188
|
+
`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.`
|
|
128153
128189
|
);
|
|
128154
128190
|
parts.push(...buildCompletionContract(role));
|
|
128155
128191
|
return parts.join("\n");
|
|
@@ -138138,7 +138174,7 @@ var EXPRESS_GATES = {
|
|
|
138138
138174
|
};
|
|
138139
138175
|
var NODE_CLI_GATES = {
|
|
138140
138176
|
lint: "npm run lint",
|
|
138141
|
-
types: "npm run
|
|
138177
|
+
types: "npm run typecheck",
|
|
138142
138178
|
security: "npm audit --audit-level=moderate",
|
|
138143
138179
|
tests: "npm test",
|
|
138144
138180
|
coverage: "npm run coverage"
|
|
@@ -139047,7 +139083,6 @@ fi
|
|
|
139047
139083
|
}
|
|
139048
139084
|
|
|
139049
139085
|
// lib/intake/steps/scaffold.ts
|
|
139050
|
-
var PYTHON_STACKS3 = /* @__PURE__ */ new Set(["python-cli", "fastapi", "flask", "django"]);
|
|
139051
139086
|
var scaffoldStep = {
|
|
139052
139087
|
name: "scaffold",
|
|
139053
139088
|
shouldRun: (payload) => payload.impact?.is_greenfield === true && !payload.dry_run,
|
|
@@ -139073,7 +139108,7 @@ var scaffoldStep = {
|
|
|
139073
139108
|
mode: "scaffold",
|
|
139074
139109
|
runCommand: ctx.runCommand
|
|
139075
139110
|
});
|
|
139076
|
-
if (scaffold.stack &&
|
|
139111
|
+
if (scaffold.stack && payload.spec) {
|
|
139077
139112
|
try {
|
|
139078
139113
|
const contract = generateQaContract({
|
|
139079
139114
|
spec: payload.spec,
|
|
@@ -139655,11 +139690,44 @@ var createTaskStep = {
|
|
|
139655
139690
|
};
|
|
139656
139691
|
|
|
139657
139692
|
// lib/intake/lib/triage-logic.ts
|
|
139658
|
-
function
|
|
139659
|
-
|
|
139660
|
-
|
|
139661
|
-
|
|
139662
|
-
|
|
139693
|
+
function detectRawIdeaComplexity(rawIdea) {
|
|
139694
|
+
const text = rawIdea.toLowerCase();
|
|
139695
|
+
const signals = [];
|
|
139696
|
+
const subsystemPatterns = [
|
|
139697
|
+
[/\b(worker|background.?job|queue|celery|bull|sidekiq|task.?runner)\b/i, "background-worker"],
|
|
139698
|
+
[/\b(websocket|server.?sent|sse|real.?time|realtime|socket\.io|push.?notif)\b/i, "realtime"],
|
|
139699
|
+
[/\b(auth|oauth|jwt|login|register|session|user.?account|signup)\b/i, "auth"],
|
|
139700
|
+
[/\b(notif|alert|email|sms|webhook|subscription|subscribe)\b/i, "notifications"],
|
|
139701
|
+
[/\b(database|banco|db|postgres|mysql|mongodb|redis|sqlite|orm)\b/i, "database"],
|
|
139702
|
+
[/\b(api\s+rest|rest\s+api|endpoint|rota|route|graphql|grpc)\b/i, "api-layer"],
|
|
139703
|
+
[/\b(docker|kubernetes|k8s|deploy|ci|cd|pipeline)\b/i, "infra"],
|
|
139704
|
+
[/\b(dashboard|frontend|interface|ui|tela|p[aá]gina)\b/i, "frontend"]
|
|
139705
|
+
];
|
|
139706
|
+
for (const [regex, label] of subsystemPatterns) {
|
|
139707
|
+
if (regex.test(text)) signals.push(label);
|
|
139708
|
+
}
|
|
139709
|
+
let floor = null;
|
|
139710
|
+
if (signals.length >= 4) floor = "large";
|
|
139711
|
+
else if (signals.length >= 3) floor = "medium";
|
|
139712
|
+
else if (signals.length >= 2) floor = "medium";
|
|
139713
|
+
return { floor, signals };
|
|
139714
|
+
}
|
|
139715
|
+
function calculateEffort(filesChanged, acCount, rawIdea) {
|
|
139716
|
+
let effort;
|
|
139717
|
+
if (filesChanged <= 3 && acCount <= 3) effort = "small";
|
|
139718
|
+
else if (filesChanged <= 10 && acCount <= 7) effort = "medium";
|
|
139719
|
+
else if (filesChanged <= 25 && acCount <= 15) effort = "large";
|
|
139720
|
+
else effort = "xlarge";
|
|
139721
|
+
if (rawIdea) {
|
|
139722
|
+
const { floor } = detectRawIdeaComplexity(rawIdea);
|
|
139723
|
+
if (floor) {
|
|
139724
|
+
const ORDER = ["small", "medium", "large", "xlarge"];
|
|
139725
|
+
if (ORDER.indexOf(floor) > ORDER.indexOf(effort)) {
|
|
139726
|
+
effort = floor;
|
|
139727
|
+
}
|
|
139728
|
+
}
|
|
139729
|
+
}
|
|
139730
|
+
return effort;
|
|
139663
139731
|
}
|
|
139664
139732
|
function calculatePriority(type, effort, totalRisks, matrix) {
|
|
139665
139733
|
for (const rule of matrix.priority_rules_v2) {
|
|
@@ -139726,7 +139794,7 @@ function determineLevel(effort, targetState) {
|
|
|
139726
139794
|
return level;
|
|
139727
139795
|
}
|
|
139728
139796
|
function runTriageLogic(input, matrix) {
|
|
139729
|
-
const effort = calculateEffort(input.filesChanged, input.acCount);
|
|
139797
|
+
const effort = calculateEffort(input.filesChanged, input.acCount, input.rawIdea);
|
|
139730
139798
|
const { priority, label: priorityLabel } = calculatePriority(input.type, effort, input.totalRisks, matrix);
|
|
139731
139799
|
const effortLabel = matrix.effort_rules[effort]?.label ?? `effort:${effort}`;
|
|
139732
139800
|
const typeLabel = matrix.auto_labels[input.type] ?? "";
|
|
@@ -140980,6 +141048,22 @@ var BOOTSTRAP_MESSAGES = {
|
|
|
140980
141048
|
pt: "Como voc\xEA quer chamar o projeto? Se preferir, posso escolher um nome.",
|
|
140981
141049
|
en: "What do you want to name the project? If you prefer, I can pick one."
|
|
140982
141050
|
},
|
|
141051
|
+
clarifyScope: {
|
|
141052
|
+
pt: (idea) => `Recebi! Seu pedido envolve v\xE1rios subsistemas (autentica\xE7\xE3o, notifica\xE7\xF5es, worker, banco de dados...). Para montar uma spec de qualidade, preciso de algumas defini\xE7\xF5es:
|
|
141053
|
+
|
|
141054
|
+
1. **Stack/linguagem**: qual prefere? (Python/FastAPI, Node.js/Express, Go...)
|
|
141055
|
+
2. **Banco de dados**: PostgreSQL, MongoDB, Redis, outro?
|
|
141056
|
+
3. **Autentica\xE7\xE3o**: JWT, OAuth2, sess\xE3o?
|
|
141057
|
+
|
|
141058
|
+
Se preferir deixar a escolha comigo, responda "livre" e eu decido.`,
|
|
141059
|
+
en: (idea) => `Got it! Your request involves multiple subsystems (auth, notifications, background worker, DB...). To produce a quality spec, I need a few decisions:
|
|
141060
|
+
|
|
141061
|
+
1. **Stack/language**: which do you prefer? (Python/FastAPI, Node.js/Express, Go...)
|
|
141062
|
+
2. **Database**: PostgreSQL, MongoDB, Redis, other?
|
|
141063
|
+
3. **Auth**: JWT, OAuth2, session?
|
|
141064
|
+
|
|
141065
|
+
If you want me to choose, reply "your call" and I'll decide.`
|
|
141066
|
+
},
|
|
140983
141067
|
registered: {
|
|
140984
141068
|
pt: (name, link) => `Projeto "${name}" registrado.
|
|
140985
141069
|
Vou continuar o fluxo em ${link}`,
|
|
@@ -140987,6 +141071,28 @@ Vou continuar o fluxo em ${link}`,
|
|
|
140987
141071
|
I'll continue the flow at ${link}`
|
|
140988
141072
|
}
|
|
140989
141073
|
};
|
|
141074
|
+
function detectScopeAmbiguity(rawIdea, stackHint) {
|
|
141075
|
+
const text = rawIdea.toLowerCase();
|
|
141076
|
+
if (/\b(livre|free.?choice|your.?call|pode.?escolher|voc[eê].?decide|qualquer)\b/i.test(text)) {
|
|
141077
|
+
return false;
|
|
141078
|
+
}
|
|
141079
|
+
const subsystems = [
|
|
141080
|
+
/\b(worker|background.?job|queue|task.?runner|celery|bull)\b/i,
|
|
141081
|
+
/\b(websocket|sse|real.?time|realtime|push.?notif)\b/i,
|
|
141082
|
+
/\b(auth|oauth|jwt|login|register|signup|autenticac)\b/i,
|
|
141083
|
+
/\b(notif|alert|assinatura|subscription|subscribe|email|sms)\b/i,
|
|
141084
|
+
/\b(banco|database|db|postgres|mysql|mongodb|redis|sqlite)\b/i,
|
|
141085
|
+
/\b(api\s+rest|rest\s+api|endpoint|graphql|grpc)\b/i,
|
|
141086
|
+
/\b(dashboard|frontend|interface|ui|tela)\b/i
|
|
141087
|
+
];
|
|
141088
|
+
const matchedSubsystems = subsystems.filter((r2) => r2.test(text)).length;
|
|
141089
|
+
if (matchedSubsystems < 3) return false;
|
|
141090
|
+
const hasExplicitDB = /\b(postgres(ql)?|mysql|mongodb|mongo|redis|sqlite|supabase|dynamodb|cockroach)\b/i.test(text);
|
|
141091
|
+
const hasExplicitAuth = /\b(jwt|oauth2?|basic.?auth|api.?key|session.?based|cookie.?auth)\b/i.test(text);
|
|
141092
|
+
const hasExplicitStack = !!stackHint && !["api", "rest-api", "backend"].includes(stackHint);
|
|
141093
|
+
const unspecifiedDimensions = [!hasExplicitDB, !hasExplicitAuth, !hasExplicitStack].filter(Boolean).length;
|
|
141094
|
+
return unspecifiedDimensions >= 2;
|
|
141095
|
+
}
|
|
140990
141096
|
function inferProjectSlug(text) {
|
|
140991
141097
|
let cleaned = text.replace(/^(create|build|crie|cria|criar|fazer?|quero|i need|i want)\s+(uma|um|me\s+a?|an|a)?\s*/i, "").replace(/\s+(that|which|que|para|for|pra)\s+.*/i, "").trim();
|
|
140992
141098
|
if (!cleaned) cleaned = text;
|
|
@@ -141455,6 +141561,27 @@ async function handleTelegramBootstrapDmMessage(ctx, rawConversationId, content)
|
|
|
141455
141561
|
));
|
|
141456
141562
|
return;
|
|
141457
141563
|
}
|
|
141564
|
+
if (detectScopeAmbiguity(content, parsed.stackHint)) {
|
|
141565
|
+
const existingSession2 = await readTelegramBootstrapSession(workspaceDir, conversationId);
|
|
141566
|
+
const alreadyAskedScope = existingSession2?.pendingClarification === "scope";
|
|
141567
|
+
if (!alreadyAskedScope) {
|
|
141568
|
+
await upsertTelegramBootstrapSession(workspaceDir, {
|
|
141569
|
+
conversationId,
|
|
141570
|
+
rawIdea: content,
|
|
141571
|
+
stackHint: parsed.stackHint ?? void 0,
|
|
141572
|
+
projectName: parsed.projectSlug ?? void 0,
|
|
141573
|
+
status: "clarifying",
|
|
141574
|
+
pendingClarification: "scope",
|
|
141575
|
+
language
|
|
141576
|
+
});
|
|
141577
|
+
const clarifyMsg = BOOTSTRAP_MESSAGES.clarifyScope[language](content);
|
|
141578
|
+
await sendTelegramText(ctx, rawConversationId, clarifyMsg);
|
|
141579
|
+
return;
|
|
141580
|
+
}
|
|
141581
|
+
if (existingSession2?.stackHint) {
|
|
141582
|
+
incomingRequest.stackHint = existingSession2.stackHint;
|
|
141583
|
+
}
|
|
141584
|
+
}
|
|
141458
141585
|
const handled = await runBootstrapPreflightOrFail(
|
|
141459
141586
|
ctx,
|
|
141460
141587
|
conversationId,
|
|
@@ -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.20",
|
|
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",
|