@llm4ts/shell 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/flows/lib/pack-fork.js +173 -0
- package/flows/modernize-implement.js +5 -0
- package/flows/pack-fork.js +137 -0
- package/package.json +4 -4
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { ScriptUsage } from "@llm4ts/runner";
|
|
4
|
+
export const parseTargetKind = (environment) => {
|
|
5
|
+
const raw = environment.LLM4TS_TARGET_KIND?.trim().toLowerCase();
|
|
6
|
+
if (raw === "frontend" || raw === "backend") {
|
|
7
|
+
return Effect.succeed(raw);
|
|
8
|
+
}
|
|
9
|
+
return Effect.fail(ScriptUsage.make({
|
|
10
|
+
message: "LLM4TS_TARGET_KIND must be 'frontend' or 'backend' " +
|
|
11
|
+
`(got ${raw === undefined || raw.length === 0 ? "unset" : `'${raw}'`})`
|
|
12
|
+
}));
|
|
13
|
+
};
|
|
14
|
+
const forkAsPattern = /^[a-z][a-z0-9-]*$/;
|
|
15
|
+
export const parseForkAs = (environment) => {
|
|
16
|
+
const raw = environment.LLM4TS_FORK_AS?.trim();
|
|
17
|
+
if (raw !== undefined && forkAsPattern.test(raw)) {
|
|
18
|
+
return Effect.succeed(raw);
|
|
19
|
+
}
|
|
20
|
+
return Effect.fail(ScriptUsage.make({
|
|
21
|
+
message: "LLM4TS_FORK_AS must be a lowercase kebab-case name, e.g. 'acmecorp-nextjs' " +
|
|
22
|
+
`(got ${raw === undefined || raw.length === 0 ? "unset" : `'${raw}'`})`
|
|
23
|
+
}));
|
|
24
|
+
};
|
|
25
|
+
export const frontendPasses = [
|
|
26
|
+
{
|
|
27
|
+
heading: "Tech Stack & Dependencies",
|
|
28
|
+
instructions: "Identify the exact frontend framework and version (e.g. Next.js App Router vs " +
|
|
29
|
+
"Pages Router), the language (TypeScript/JavaScript), the package manager, and " +
|
|
30
|
+
"the state/data libraries already in use. List every direct dependency from the " +
|
|
31
|
+
"grounding files below that a coding agent adding a new feature is likely to " +
|
|
32
|
+
"need (forms, HTTP/data fetching, dates, validation) and name which package " +
|
|
33
|
+
"already covers it, so no duplicate gets added."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
heading: "Naming, Routing & Architecture",
|
|
37
|
+
instructions: "Explore the repository's folder structure, then describe: file and folder " +
|
|
38
|
+
"naming conventions (casing, suffixes), how new pages/routes are added and " +
|
|
39
|
+
"where they live, component naming conventions, and where shared " +
|
|
40
|
+
"utilities/constants/hooks live. Use real example paths from this repository, " +
|
|
41
|
+
"not generic advice."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
heading: "Shared Components, Design System & Style",
|
|
45
|
+
instructions: "Explore the repository for a shared component library or design system (its " +
|
|
46
|
+
"location, how components are composed and exported, whether there's theming " +
|
|
47
|
+
"or design tokens), and the CSS/styling approach in use (Tailwind, CSS " +
|
|
48
|
+
"Modules, styled-components, or other). Describe how an existing shared " +
|
|
49
|
+
"component should be found and reused rather than rebuilt. If there is no " +
|
|
50
|
+
"design system, say so explicitly rather than inventing one."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
heading: "Auth, Permissions & Data Fetching",
|
|
54
|
+
instructions: "Explore how authentication and authorization are implemented (the " +
|
|
55
|
+
"library/provider, where session/user state lives, how a route or component " +
|
|
56
|
+
"checks permissions), and how data is fetched/mutated (React Query, SWR, " +
|
|
57
|
+
"server actions, or plain fetch — and where that logic conventionally lives). " +
|
|
58
|
+
"Describe the pattern precisely enough that a new feature can follow it " +
|
|
59
|
+
"without inventing a new one."
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
export const backendPasses = [
|
|
63
|
+
{
|
|
64
|
+
heading: "Tech Stack & Dependencies",
|
|
65
|
+
instructions: "Identify the exact backend framework and version (e.g. Spring Boot), the JVM " +
|
|
66
|
+
"language, the build tool, and the database/persistence technology in use. " +
|
|
67
|
+
"List every direct dependency from the grounding files below that a coding " +
|
|
68
|
+
"agent adding a new feature is likely to need (HTTP client, JSON mapping, " +
|
|
69
|
+
"validation, testing) and name which dependency already covers it, so no " +
|
|
70
|
+
"duplicate gets added."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
heading: "Naming & Architecture",
|
|
74
|
+
instructions: "Explore the repository's package structure, then describe: package and class " +
|
|
75
|
+
"naming conventions (controllers, services, repositories, DTOs, entities), and " +
|
|
76
|
+
"the layering/module boundaries already established (e.g. controller → " +
|
|
77
|
+
"service → repository, or a domain-driven module split). Use real example " +
|
|
78
|
+
"package/class names from this repository."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
heading: "Data, Persistence & Shared Resources",
|
|
82
|
+
instructions: "Explore how entities/persistence are modeled (JPA conventions, migration " +
|
|
83
|
+
"tool such as Flyway/Liquibase, transaction handling), and where " +
|
|
84
|
+
"shared/common resources live (shared utility classes, common exception " +
|
|
85
|
+
"handling, cross-cutting concerns). Describe the pattern precisely enough " +
|
|
86
|
+
"that a new feature reuses what's there instead of rebuilding it."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
heading: "Auth, API & Testing Conventions",
|
|
90
|
+
instructions: "Explore how security/auth is configured (the security framework, JWT/OAuth " +
|
|
91
|
+
"setup, role-based or method-level access control), the REST API conventions " +
|
|
92
|
+
"in use (URL/versioning scheme, error response shape, OpenAPI usage), and the " +
|
|
93
|
+
"testing conventions (unit vs integration test structure, fixtures/mocking " +
|
|
94
|
+
"approach). Describe each precisely enough to follow without inventing a new " +
|
|
95
|
+
"one."
|
|
96
|
+
}
|
|
97
|
+
];
|
|
98
|
+
export const passesForTargetKind = (kind) => kind === "frontend" ? frontendPasses : backendPasses;
|
|
99
|
+
export const conventionPassAsk = (pass, grounding) => [
|
|
100
|
+
pass.instructions,
|
|
101
|
+
`Respond with a single markdown section starting with "## ${pass.heading}".`,
|
|
102
|
+
"If a topic doesn't apply to this repository, say so explicitly rather than " +
|
|
103
|
+
"inventing one.",
|
|
104
|
+
...(grounding === undefined || grounding.length === 0
|
|
105
|
+
? []
|
|
106
|
+
: [`Repository files for grounding:\n\n${grounding}`])
|
|
107
|
+
].join("\n\n");
|
|
108
|
+
export class ConventionSection extends Schema.Class("ConventionSection")({
|
|
109
|
+
markdown: Schema.String
|
|
110
|
+
}) {
|
|
111
|
+
}
|
|
112
|
+
export const conventionSectionJsonSchema = {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
markdown: {
|
|
116
|
+
type: "string",
|
|
117
|
+
description: "A markdown section (starting with a ## heading) describing the findings " +
|
|
118
|
+
"for this category."
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
required: ["markdown"]
|
|
122
|
+
};
|
|
123
|
+
export const techStackGroundingFiles = (kind) => kind === "frontend"
|
|
124
|
+
? ["package.json", "tsconfig.json"]
|
|
125
|
+
: ["pom.xml", "build.gradle", "build.gradle.kts"];
|
|
126
|
+
export const readGroundingFiles = (workspace, paths) => Effect.gen(function* () {
|
|
127
|
+
const sections = [];
|
|
128
|
+
for (const path of paths) {
|
|
129
|
+
const content = yield* workspace
|
|
130
|
+
.read(path)
|
|
131
|
+
.pipe(Effect.catch(() => Effect.succeed(undefined)));
|
|
132
|
+
if (content !== undefined) {
|
|
133
|
+
sections.push(`### ${path}\n\n\`\`\`\n${content}\n\`\`\``);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return sections.join("\n\n");
|
|
137
|
+
});
|
|
138
|
+
export const forkPackMarkdown = (sourcePackMd, forkedName) => {
|
|
139
|
+
const [, ...rest] = sourcePackMd.split("\n");
|
|
140
|
+
const withoutScaffold = rest.filter((line) => !/^scaffold:\s*/.test(line));
|
|
141
|
+
return [`# Pack: ${forkedName}`, ...withoutScaffold].join("\n");
|
|
142
|
+
};
|
|
143
|
+
export const forkedReadme = (sourcePackName, forkedName, kind, sourceRepo) => [
|
|
144
|
+
`# Forked pack — ${forkedName}`,
|
|
145
|
+
"",
|
|
146
|
+
`Forked by the pack-fork flow from '${sourcePackName}', analyzing '${sourceRepo}' ` +
|
|
147
|
+
`as a ${kind} target repository.`,
|
|
148
|
+
"",
|
|
149
|
+
"- pack.md — the source pack, copied verbatim except for the dropped `scaffold:` " +
|
|
150
|
+
"line (this pack targets a repository that already exists).",
|
|
151
|
+
"- conventions.md — this repository's own architecture, dependencies, naming, " +
|
|
152
|
+
"and design conventions, captured by analyzing the real target repository. " +
|
|
153
|
+
"Read directly by modernize-implement's generation prompt.",
|
|
154
|
+
"- reviewers/target-conventions.md — a post-hoc review lens checking new code " +
|
|
155
|
+
"against conventions.md.",
|
|
156
|
+
"",
|
|
157
|
+
"Review conventions.md against what you know of the target repository, then flip " +
|
|
158
|
+
"the marker below and point LLM4TS_PACK at this fork."
|
|
159
|
+
].join("\n");
|
|
160
|
+
export const targetConventionsReviewer = [
|
|
161
|
+
"Verify the diff stays inside this repository's established conventions — see " +
|
|
162
|
+
"conventions.md in this pack for the specifics captured when this pack was " +
|
|
163
|
+
"forked from the target repository.",
|
|
164
|
+
"",
|
|
165
|
+
"Flag, and only flag:",
|
|
166
|
+
"- A new dependency added when an existing one in this repo already covers the " + "same need.",
|
|
167
|
+
"- A new shared component, utility, or auth/permission pattern introduced instead " +
|
|
168
|
+
"of reusing the one this repo already has.",
|
|
169
|
+
"- Naming, folder, or style conventions that diverge from what conventions.md " + "describes.",
|
|
170
|
+
"",
|
|
171
|
+
"Do not flag anything conventions.md doesn't mention — silence there means this " +
|
|
172
|
+
"pack's fork never captured an opinion on it."
|
|
173
|
+
].join("\n");
|
|
@@ -206,6 +206,11 @@ const program = Effect.gen(function* () {
|
|
|
206
206
|
pack.lessons === undefined
|
|
207
207
|
? undefined
|
|
208
208
|
: `Lessons from previous modernization runs — apply them:\n${pack.lessons}`,
|
|
209
|
+
pack.conventions === undefined
|
|
210
|
+
? undefined
|
|
211
|
+
: "This repository's own established conventions, captured from the real " +
|
|
212
|
+
"target repository — reuse them, stay inside them:\n" +
|
|
213
|
+
pack.conventions,
|
|
209
214
|
playbook.length === 0
|
|
210
215
|
? undefined
|
|
211
216
|
: "Pattern cards cited by the specs — the translation playbook (advisory, the specs win):\n\n" +
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Forks an existing pack into a new, project-tier pack whose conventions.md
|
|
2
|
+
// and reviewers/target-conventions.md describe a REAL, already-in-production
|
|
3
|
+
// target repository — so a later modernize-implement run (LLM4TS_PACK
|
|
4
|
+
// pointed at the fork) writes new code that reuses this repo's own
|
|
5
|
+
// architecture, shared components, and conventions instead of guessing.
|
|
6
|
+
// See docs/adr/0018-pack-fork.md.
|
|
7
|
+
//
|
|
8
|
+
// Runs rooted at the TARGET repository (`--repo <target>`) — there is no
|
|
9
|
+
// legacy repository in this flow at all. Requires:
|
|
10
|
+
// LLM4TS_PACK=<source pack> resolved as for any flow (default cobol-springboot)
|
|
11
|
+
// LLM4TS_TARGET_KIND=frontend|backend
|
|
12
|
+
// LLM4TS_FORK_AS=<new pack name> lowercase kebab-case
|
|
13
|
+
//
|
|
14
|
+
// The forked pack lands at
|
|
15
|
+
// `<repo>/.llm4ts/kits/forked/packs/<LLM4TS_FORK_AS>/` — a dedicated
|
|
16
|
+
// project-tier kit named "forked", never the source pack's own kit name
|
|
17
|
+
// (Kits.ts dedupes kits by name across tiers; reusing the source kit's name
|
|
18
|
+
// would hide its other packs from this project). `scaffold:` is dropped:
|
|
19
|
+
// the whole point is that the target repository already exists.
|
|
20
|
+
//
|
|
21
|
+
// One-shot, not resumable: re-running overwrites the previous fork under
|
|
22
|
+
// the same LLM4TS_FORK_AS name.
|
|
23
|
+
import { basename, join, relative as relativePath, resolve as resolvePath } from "node:path";
|
|
24
|
+
import { rmSync } from "node:fs";
|
|
25
|
+
import * as Effect from "effect/Effect";
|
|
26
|
+
import { structuredAndPublish } from "@llm4ts/flow/Flow";
|
|
27
|
+
import { Info } from "@llm4ts/flow/FlowEvents";
|
|
28
|
+
import { withDraftApproval } from "@llm4ts/flow/Approval";
|
|
29
|
+
import { legacySourceWorkspaceLimits, workspaceLimitsFromEnv } from "@llm4ts/flow/Workspace";
|
|
30
|
+
import { asReadOnly, coderFromEnv, makeNodeWorkspace, nodePlainFileStore, openPack, resolveFlowInput, runFlowMain, runNode, ScriptUsage, stage } from "@llm4ts/runner";
|
|
31
|
+
import { ConventionSection, conventionPassAsk, conventionSectionJsonSchema, forkPackMarkdown, forkedReadme, parseForkAs, parseTargetKind, passesForTargetKind, readGroundingFiles, targetConventionsReviewer, techStackGroundingFiles } from "./lib/pack-fork.js";
|
|
32
|
+
const forkPackFiles = Effect.fn("flows/pack-fork.forkPackFiles")(function* (source, destinationAbs, files) {
|
|
33
|
+
// `source.dir` is "." for an absolute-path pack reference (Packs.ts's
|
|
34
|
+
// `locatePack`), "packs/<name>" for a relative-directory or kit pack
|
|
35
|
+
// reference — never assume it's non-empty. `join`/`relative` from
|
|
36
|
+
// node:path normalize the "." case correctly where manual string
|
|
37
|
+
// concatenation or `.slice()` would not.
|
|
38
|
+
const packMdPath = join(source.dir, "pack.md");
|
|
39
|
+
const entries = yield* source.workspace.discover(join(source.dir, "**"));
|
|
40
|
+
for (const relative_ of entries) {
|
|
41
|
+
if (relative_ === packMdPath) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const content = yield* source.workspace
|
|
45
|
+
.read(relative_)
|
|
46
|
+
.pipe(Effect.catch(() => Effect.succeed(undefined)));
|
|
47
|
+
if (content === undefined) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const suffix = relativePath(source.dir, relative_);
|
|
51
|
+
yield* files.writeAtomic(join(destinationAbs, suffix), content);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const forkKitPatterns = Effect.fn("flows/pack-fork.forkKitPatterns")(function* (source, destinationAbs, files) {
|
|
55
|
+
if (source.kit === undefined) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const entries = yield* source.workspace
|
|
59
|
+
.discover(join("patterns", "**"))
|
|
60
|
+
.pipe(Effect.catch(() => Effect.succeed([])));
|
|
61
|
+
for (const relative_ of entries) {
|
|
62
|
+
const content = yield* source.workspace
|
|
63
|
+
.read(relative_)
|
|
64
|
+
.pipe(Effect.catch(() => Effect.succeed(undefined)));
|
|
65
|
+
if (content === undefined) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
yield* files.writeAtomic(join(destinationAbs, relative_), content);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const program = Effect.gen(function* () {
|
|
72
|
+
const input = yield* resolveFlowInput("Analyze the target repository and fork the pack with its conventions.");
|
|
73
|
+
const targetKind = yield* parseTargetKind(process.env);
|
|
74
|
+
const forkAs = yield* parseForkAs(process.env);
|
|
75
|
+
const coder = asReadOnly(coderFromEnv(process.env));
|
|
76
|
+
const files = nodePlainFileStore;
|
|
77
|
+
yield* runNode({
|
|
78
|
+
workDir: input.workDir,
|
|
79
|
+
workspace: input.workspace,
|
|
80
|
+
userPrompt: input.prompt,
|
|
81
|
+
coder,
|
|
82
|
+
reasoning: coder,
|
|
83
|
+
environment: process.env
|
|
84
|
+
}, (context) => Effect.gen(function* () {
|
|
85
|
+
const repo = yield* makeNodeWorkspace(input.workDir, workspaceLimitsFromEnv(process.env, legacySourceWorkspaceLimits));
|
|
86
|
+
const opened = yield* stage(context.events, "pack", openPack({
|
|
87
|
+
environment: process.env,
|
|
88
|
+
launchDir: input.workspace,
|
|
89
|
+
flowDir: import.meta.dirname
|
|
90
|
+
}));
|
|
91
|
+
const destinationRel = join(".llm4ts", "kits", "forked", "packs", forkAs);
|
|
92
|
+
const destinationAbs = join(input.workDir, destinationRel);
|
|
93
|
+
// Guard BEFORE any read of the source pack or the "clean" rmSync
|
|
94
|
+
// below: if LLM4TS_PACK and LLM4TS_FORK_AS resolve to the same
|
|
95
|
+
// directory — reachable by cd-ing into the target repo and setting
|
|
96
|
+
// LLM4TS_PACK=forked/<name> for a re-fork, exactly as this flow's
|
|
97
|
+
// own closing message instructs — the clean stage would delete the
|
|
98
|
+
// pack this run is about to read, including any uncommitted human
|
|
99
|
+
// edits such as the approval-marker flip.
|
|
100
|
+
const sourceDirAbs = resolvePath(join(opened.workspace.root, opened.dir));
|
|
101
|
+
if (sourceDirAbs === resolvePath(destinationAbs)) {
|
|
102
|
+
return yield* ScriptUsage.make({
|
|
103
|
+
message: `LLM4TS_PACK resolves to '${sourceDirAbs}', the same directory ` +
|
|
104
|
+
`LLM4TS_FORK_AS='${forkAs}' would fork into — forking a pack into itself ` +
|
|
105
|
+
"would delete it before it's read. Point LLM4TS_PACK at a different source " +
|
|
106
|
+
"pack, or choose a different LLM4TS_FORK_AS."
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const grounding = yield* stage(context.events, "grounding", readGroundingFiles(repo, techStackGroundingFiles(targetKind)));
|
|
110
|
+
const passes = passesForTargetKind(targetKind);
|
|
111
|
+
const sections = [];
|
|
112
|
+
for (const [index, pass] of passes.entries()) {
|
|
113
|
+
const prompt = conventionPassAsk(pass, index === 0 ? grounding : undefined);
|
|
114
|
+
const result = yield* stage(context.events, pass.heading, structuredAndPublish(context.reasoning, context.events, prompt, ConventionSection, conventionSectionJsonSchema));
|
|
115
|
+
sections.push(result.markdown);
|
|
116
|
+
}
|
|
117
|
+
const conventionsMd = sections.join("\n\n");
|
|
118
|
+
yield* stage(context.events, "clean", Effect.sync(() => rmSync(destinationAbs, { recursive: true, force: true })));
|
|
119
|
+
yield* stage(context.events, "fork", forkPackFiles(opened, destinationAbs, files));
|
|
120
|
+
yield* stage(context.events, "fork patterns", forkKitPatterns(opened, destinationAbs, files));
|
|
121
|
+
const sourcePackMd = yield* opened.workspace.read(`${opened.dir}/pack.md`);
|
|
122
|
+
yield* files.writeAtomic(join(destinationAbs, "pack.md"), forkPackMarkdown(sourcePackMd, forkAs));
|
|
123
|
+
yield* files.writeAtomic(join(destinationAbs, "conventions.md"), conventionsMd);
|
|
124
|
+
yield* files.writeAtomic(join(destinationAbs, "reviewers", "target-conventions.md"), targetConventionsReviewer);
|
|
125
|
+
yield* files.writeAtomic(join(destinationAbs, "README.md"), withDraftApproval(forkedReadme(opened.pack.name, forkAs, targetKind, basename(input.workDir))));
|
|
126
|
+
yield* stage(context.events, "commit", context.git
|
|
127
|
+
.commitPaths(`pack-fork: fork '${opened.pack.name}' as '${forkAs}'`, [destinationRel])
|
|
128
|
+
.pipe(Effect.asVoid));
|
|
129
|
+
yield* context.events.publish(Info.make({
|
|
130
|
+
message: `forked pack ready — review ${destinationRel}/README.md and ` +
|
|
131
|
+
`${destinationRel}/conventions.md, set '- [x] Approved', then from ` +
|
|
132
|
+
`inside ${input.workDir} run modernize-implement with ` +
|
|
133
|
+
`LLM4TS_PACK=forked/${forkAs}`
|
|
134
|
+
}));
|
|
135
|
+
}));
|
|
136
|
+
});
|
|
137
|
+
runFlowMain(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llm4ts/shell",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Interactive shell and CLI for llm4ts: flow discovery, run-a-flow, and view",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@effect/platform-node": "4.0.0-rc.115",
|
|
54
54
|
"@effect/platform-node-shared": "4.0.0-rc.115",
|
|
55
|
-
"@llm4ts/
|
|
56
|
-
"@llm4ts/
|
|
57
|
-
"@llm4ts/
|
|
55
|
+
"@llm4ts/core": "2.4.0",
|
|
56
|
+
"@llm4ts/runner": "2.4.0",
|
|
57
|
+
"@llm4ts/flow": "2.4.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"effect": "4.0.0-rc.115"
|