flavor-code 1.4.1 → 1.4.3-beta.2
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 +32 -2
- package/README.zh-CN.md +32 -2
- package/dist/{app-4YRKE7VI.js → app-G35YBHPR.js} +873 -641
- package/dist/apply-WOW3QU5I.js +51 -0
- package/dist/broker-cli-X5H46FZV.js +15 -0
- package/dist/check-CSKTV4O2.js +16 -0
- package/dist/chunk-3563SW4X.js +10513 -0
- package/dist/chunk-3UX2CGDW.js +1955 -0
- package/dist/chunk-4TMLXZVZ.js +75 -0
- package/dist/chunk-6F66UXBI.js +48 -0
- package/dist/{chunk-FAORPP4F.js → chunk-6URQ7FIF.js} +24 -88
- package/dist/chunk-6X62CGMA.js +193 -0
- package/dist/chunk-7Y64WNBB.js +8930 -0
- package/dist/chunk-AA4QNKTF.js +501 -0
- package/dist/chunk-BDX777JR.js +11 -0
- package/dist/chunk-CVP5OUHD.js +103 -0
- package/dist/chunk-EWD7FWTH.js +14547 -0
- package/dist/chunk-FAJ6YDZK.js +630 -0
- package/dist/{chunk-N5MXHJKD.js → chunk-GTFP2R5I.js} +1 -1
- package/dist/{chunk-7YFHRORG.js → chunk-HGTCIHWA.js} +383 -172
- package/dist/chunk-IDJAHEG6.js +43501 -0
- package/dist/chunk-IQEEFDEE.js +389 -0
- package/dist/chunk-IU3UBETC.js +30 -0
- package/dist/chunk-K7UGGWJV.js +1003 -0
- package/dist/chunk-KRWWCVXA.js +74 -0
- package/dist/chunk-OKHV7CTB.js +482 -0
- package/dist/chunk-PULWAH6E.js +474 -0
- package/dist/chunk-PZ5NKRVA.js +85 -0
- package/dist/chunk-RBAZLFKB.js +1022 -0
- package/dist/chunk-RV3EXUXM.js +30 -0
- package/dist/chunk-SJUIIHIA.js +2130 -0
- package/dist/{chunk-VMQWXQEC.js → chunk-T6RE76D2.js} +2003 -493
- package/dist/chunk-TIEOVQW6.js +61 -0
- package/dist/chunk-WLZUAVVU.js +15 -0
- package/dist/chunk-X442CJJH.js +114 -0
- package/dist/{chunk-RAYH6K2H.js → chunk-Y7EH4NVO.js} +1 -1
- package/dist/{chunk-S32JRDZK.js → chunk-ZKKVSR37.js} +6 -1
- package/dist/{claude-ink-JZXNSAQS.js → claude-ink-BE56DB6N.js} +9 -4
- package/dist/cli-3MF6EL2H.js +163 -0
- package/dist/cli-main.js +3805 -473
- package/dist/cli.js +50 -4
- package/dist/config/load.d.ts +8 -0
- package/dist/config/schema.d.ts +11 -0
- package/dist/context/manager.d.ts +2 -0
- package/dist/desktop/main.js +4303 -1171
- package/dist/desktop/preload.cjs +30 -0
- package/dist/desktop-renderer/assets/index-BWklSoOO.css +1 -0
- package/dist/desktop-renderer/assets/index-DCqIrF1S.js +153 -0
- package/dist/desktop-renderer/assets/{interactive-terminal-1agnYY5L.js → interactive-terminal-B6GbpzIn.js} +1 -1
- package/dist/desktop-renderer/index.html +2 -2
- package/dist/doctor-OHCGK52S.js +20 -0
- package/dist/load-QK6CMLDF.js +25 -0
- package/dist/log-ATFIPRNU.js +19 -0
- package/dist/manager-5SI4MPES.js +191 -0
- package/dist/models/anthropic.d.ts +3 -0
- package/dist/node-LOIXT44J.js +879 -0
- package/dist/plugin-worker-entry.js +1 -1
- package/dist/production-6DIZ6YLQ.js +46 -0
- package/dist/production.d.ts +28 -1
- package/dist/project-3QXE22TG.js +11 -0
- package/dist/react-M6JGXSJZ.js +7 -0
- package/dist/recorder-FAGJJH3N.js +11 -0
- package/dist/sdk/index.js +30 -8
- package/dist/server-2KLKPHPL.js +11 -0
- package/dist/session/store.d.ts +1 -0
- package/dist/store-ZIHN6363.js +18 -0
- package/dist/ui/commands.d.ts +1 -5
- package/dist/usage-summary-G5FBOJLK.js +13 -0
- package/dist/utils/execFileNoThrow.d.ts +1 -0
- package/dist/write-stream-CGD57URC.js +77 -0
- package/package.json +1 -1
- package/dist/chunk-O2PC44GY.js +0 -26120
- package/dist/chunk-ZJCAQXA7.js +0 -169
- package/dist/desktop-renderer/assets/index-BM9ugkh9.js +0 -154
- package/dist/desktop-renderer/assets/index-Bk6FYhyh.css +0 -1
- package/dist/load-UPOG3XQR.js +0 -19
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as createFlavorRequire } from "node:module"; const require = createFlavorRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/utils/log.ts
|
|
5
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
6
|
+
import { dirname, join, resolve } from "path";
|
|
7
|
+
var pending = Promise.resolve();
|
|
8
|
+
var usagePending = Promise.resolve();
|
|
9
|
+
function logError(error) {
|
|
10
|
+
if (process.env.DEBUG) process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
11
|
+
`);
|
|
12
|
+
}
|
|
13
|
+
function usageLogPath() {
|
|
14
|
+
return process.env.FLAVOR_USAGE_FILE ?? join(process.cwd(), ".flavor", "usage.jsonl");
|
|
15
|
+
}
|
|
16
|
+
var usageSessionId = "unknown";
|
|
17
|
+
var truncateOnNextAppend = false;
|
|
18
|
+
function setUsageSession(sessionId) {
|
|
19
|
+
usageSessionId = sessionId;
|
|
20
|
+
truncateOnNextAppend = true;
|
|
21
|
+
}
|
|
22
|
+
function currentUsageSession() {
|
|
23
|
+
return usageSessionId;
|
|
24
|
+
}
|
|
25
|
+
async function appendUsageLog(line) {
|
|
26
|
+
usagePending = usagePending.catch(() => void 0).then(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const path = usageLogPath();
|
|
29
|
+
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
30
|
+
const flag = truncateOnNextAppend ? "w" : "a";
|
|
31
|
+
truncateOnNextAppend = false;
|
|
32
|
+
await appendFile(path, `${line}
|
|
33
|
+
`, { encoding: "utf8", mode: 384, flag });
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return usagePending;
|
|
38
|
+
}
|
|
39
|
+
var AuditLogger = class {
|
|
40
|
+
#path;
|
|
41
|
+
#closed = false;
|
|
42
|
+
constructor(workspace) {
|
|
43
|
+
this.#path = join(resolve(workspace), ".flavor", "audit.jsonl");
|
|
44
|
+
}
|
|
45
|
+
get path() {
|
|
46
|
+
return this.#path;
|
|
47
|
+
}
|
|
48
|
+
async append(entry) {
|
|
49
|
+
if (this.#closed) return;
|
|
50
|
+
const line = `${JSON.stringify(entry)}
|
|
51
|
+
`;
|
|
52
|
+
pending = pending.catch(() => void 0).then(() => this.#write(line));
|
|
53
|
+
return pending;
|
|
54
|
+
}
|
|
55
|
+
async #write(line) {
|
|
56
|
+
try {
|
|
57
|
+
await mkdir(dirname(this.#path), { recursive: true, mode: 448 });
|
|
58
|
+
await appendFile(this.#path, line, { encoding: "utf8", mode: 384, flag: "a" });
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
close() {
|
|
63
|
+
this.#closed = true;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
logError,
|
|
69
|
+
usageLogPath,
|
|
70
|
+
setUsageSession,
|
|
71
|
+
currentUsageSession,
|
|
72
|
+
appendUsageLog,
|
|
73
|
+
AuditLogger
|
|
74
|
+
};
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as createFlavorRequire } from "node:module"; const require = createFlavorRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/init/project.ts
|
|
5
|
+
import { lstat, mkdir, readdir, readFile, realpath, writeFile } from "fs/promises";
|
|
6
|
+
import { basename, isAbsolute, join, relative, sep } from "path";
|
|
7
|
+
var GENERATED_START = "<!-- flavor-code:start -->";
|
|
8
|
+
var GENERATED_END = "<!-- flavor-code:end -->";
|
|
9
|
+
var MAX_MANIFEST_BYTES = 1e6;
|
|
10
|
+
var JAVASCRIPT_LOCKFILES = [
|
|
11
|
+
{ name: "package-lock.json", manager: "npm" },
|
|
12
|
+
{ name: "npm-shrinkwrap.json", manager: "npm" },
|
|
13
|
+
{ name: "pnpm-lock.yaml", manager: "pnpm" },
|
|
14
|
+
{ name: "yarn.lock", manager: "yarn" },
|
|
15
|
+
{ name: "bun.lock", manager: "bun" },
|
|
16
|
+
{ name: "bun.lockb", manager: "bun" }
|
|
17
|
+
];
|
|
18
|
+
var SOURCE_DIRECTORIES = [
|
|
19
|
+
"src",
|
|
20
|
+
"app",
|
|
21
|
+
"lib",
|
|
22
|
+
"packages",
|
|
23
|
+
"tests",
|
|
24
|
+
"test",
|
|
25
|
+
"spec"
|
|
26
|
+
];
|
|
27
|
+
var INSTRUCTION_FILES = [
|
|
28
|
+
"AGENTS.md",
|
|
29
|
+
"CLAUDE.md",
|
|
30
|
+
"GEMINI.md",
|
|
31
|
+
".github/copilot-instructions.md"
|
|
32
|
+
];
|
|
33
|
+
var CONFIG_FILES = [
|
|
34
|
+
"tsconfig.json",
|
|
35
|
+
"jsconfig.json",
|
|
36
|
+
"vitest.config.ts",
|
|
37
|
+
"vitest.config.js",
|
|
38
|
+
"jest.config.ts",
|
|
39
|
+
"jest.config.js",
|
|
40
|
+
"eslint.config.js",
|
|
41
|
+
"eslint.config.mjs",
|
|
42
|
+
".eslintrc",
|
|
43
|
+
".eslintrc.json",
|
|
44
|
+
"biome.json",
|
|
45
|
+
"prettier.config.js",
|
|
46
|
+
".prettierrc",
|
|
47
|
+
"pytest.ini",
|
|
48
|
+
"tox.ini",
|
|
49
|
+
".ruff.toml",
|
|
50
|
+
"ruff.toml",
|
|
51
|
+
"mypy.ini"
|
|
52
|
+
];
|
|
53
|
+
function isContainedPath(root, candidate) {
|
|
54
|
+
const pathFromRoot = relative(root, candidate);
|
|
55
|
+
return pathFromRoot === "" || pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`) && !isAbsolute(pathFromRoot);
|
|
56
|
+
}
|
|
57
|
+
async function pathKind(path, root) {
|
|
58
|
+
try {
|
|
59
|
+
const details = await lstat(path);
|
|
60
|
+
if (details.isSymbolicLink() || !isContainedPath(root, await realpath(path))) return void 0;
|
|
61
|
+
if (details.isFile()) return "file";
|
|
62
|
+
if (details.isDirectory()) return "directory";
|
|
63
|
+
return void 0;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (isMissingPathError(error)) return void 0;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function isMissingPathError(error) {
|
|
70
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
71
|
+
}
|
|
72
|
+
async function readTextIfSmall(path, root) {
|
|
73
|
+
const details = await lstat(path).catch((error) => {
|
|
74
|
+
if (isMissingPathError(error)) return void 0;
|
|
75
|
+
throw error;
|
|
76
|
+
});
|
|
77
|
+
if (details === void 0 || !details.isFile() || details.isSymbolicLink() || details.size > MAX_MANIFEST_BYTES || !isContainedPath(root, await realpath(path))) {
|
|
78
|
+
return void 0;
|
|
79
|
+
}
|
|
80
|
+
return readFile(path, "utf8");
|
|
81
|
+
}
|
|
82
|
+
function isRecord(value) {
|
|
83
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
84
|
+
}
|
|
85
|
+
function sanitizeProjectName(value) {
|
|
86
|
+
const sanitized = value.replace(/<!--/g, "").replace(/-->/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
87
|
+
return sanitized === "" ? void 0 : sanitized;
|
|
88
|
+
}
|
|
89
|
+
function joinWithAnd(values) {
|
|
90
|
+
if (values.length < 2) return values[0] ?? "";
|
|
91
|
+
return `${values.slice(0, -1).join(", ")} and ${values.at(-1)}`;
|
|
92
|
+
}
|
|
93
|
+
function packageCommand(manager, script) {
|
|
94
|
+
if (manager === "npm") {
|
|
95
|
+
return script === "test" ? "npm test" : `npm run ${script}`;
|
|
96
|
+
}
|
|
97
|
+
if (manager === "yarn") return `yarn ${script}`;
|
|
98
|
+
if (manager === "bun") return `bun run ${script}`;
|
|
99
|
+
return `${manager} ${script}`;
|
|
100
|
+
}
|
|
101
|
+
function addScriptCommand(target, scripts, manager, names) {
|
|
102
|
+
if (manager === void 0) return;
|
|
103
|
+
const script = names.find((name) => scripts[name] !== void 0);
|
|
104
|
+
if (script !== void 0) target.push(packageCommand(manager, script));
|
|
105
|
+
}
|
|
106
|
+
function detectPythonTools(pyproject, requirements, configFiles) {
|
|
107
|
+
const tools = /* @__PURE__ */ new Set();
|
|
108
|
+
const sections = pyproject?.matchAll(/^\s*\[tool\.([A-Za-z0-9_-]+)(?:\.|\])/gm) ?? [];
|
|
109
|
+
for (const match of sections) {
|
|
110
|
+
const tool = match[1];
|
|
111
|
+
if (tool !== void 0) tools.add(tool.toLowerCase());
|
|
112
|
+
}
|
|
113
|
+
if (configFiles.includes("pytest.ini")) tools.add("pytest");
|
|
114
|
+
if (configFiles.includes("ruff.toml") || configFiles.includes(".ruff.toml")) tools.add("ruff");
|
|
115
|
+
for (const line of requirements?.split(/\r?\n/) ?? []) {
|
|
116
|
+
const match = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(line);
|
|
117
|
+
if (match?.[1] !== void 0 && !line.trimStart().startsWith("#")) {
|
|
118
|
+
tools.add(match[1].toLowerCase().replace(/[-_.]+/g, "-"));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return tools;
|
|
122
|
+
}
|
|
123
|
+
async function sampleSourceExtensions(cwd, root, directories) {
|
|
124
|
+
const extensions = /* @__PURE__ */ new Set();
|
|
125
|
+
const sampleDirectories = ["", ...directories].slice(0, 12);
|
|
126
|
+
for (const relativeDirectory of sampleDirectories) {
|
|
127
|
+
const absoluteDirectory = join(cwd, relativeDirectory);
|
|
128
|
+
if (!isContainedPath(root, await realpath(absoluteDirectory))) continue;
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
entries = await readdir(absoluteDirectory, { withFileTypes: true });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (isMissingPathError(error)) continue;
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
for (const entry of entries.slice(0, 200)) {
|
|
137
|
+
if (!entry.isFile()) continue;
|
|
138
|
+
const match = /\.([A-Za-z0-9]+)$/.exec(entry.name);
|
|
139
|
+
if (match?.[1] !== void 0) extensions.add(match[1].toLowerCase());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return extensions;
|
|
143
|
+
}
|
|
144
|
+
async function inspectProject(cwd) {
|
|
145
|
+
const root = await realpath(cwd);
|
|
146
|
+
const sourceDirectories = [];
|
|
147
|
+
for (const directory of SOURCE_DIRECTORIES) {
|
|
148
|
+
if (await pathKind(join(cwd, directory), root) === "directory") sourceDirectories.push(directory);
|
|
149
|
+
}
|
|
150
|
+
const instructionFiles = [];
|
|
151
|
+
for (const file of INSTRUCTION_FILES) {
|
|
152
|
+
if (await pathKind(join(cwd, file), root) === "file") instructionFiles.push(file);
|
|
153
|
+
}
|
|
154
|
+
const configFiles = [];
|
|
155
|
+
for (const file of CONFIG_FILES) {
|
|
156
|
+
if (await pathKind(join(cwd, file), root) === "file") configFiles.push(file);
|
|
157
|
+
}
|
|
158
|
+
const manifestNames = [
|
|
159
|
+
"package.json",
|
|
160
|
+
...JAVASCRIPT_LOCKFILES.map(({ name }) => name),
|
|
161
|
+
"pyproject.toml",
|
|
162
|
+
"requirements.txt",
|
|
163
|
+
"uv.lock",
|
|
164
|
+
"poetry.lock",
|
|
165
|
+
"Pipfile"
|
|
166
|
+
];
|
|
167
|
+
const present = /* @__PURE__ */ new Set();
|
|
168
|
+
for (const manifest of manifestNames) {
|
|
169
|
+
if (await pathKind(join(cwd, manifest), root) === "file") present.add(manifest);
|
|
170
|
+
}
|
|
171
|
+
const detectedLockfiles = JAVASCRIPT_LOCKFILES.filter(({ name }) => present.has(name));
|
|
172
|
+
const detectedJsManagers = [
|
|
173
|
+
...new Set(detectedLockfiles.map(({ manager }) => manager))
|
|
174
|
+
];
|
|
175
|
+
const pythonManagers = [];
|
|
176
|
+
if (present.has("uv.lock")) {
|
|
177
|
+
pythonManagers.push("uv");
|
|
178
|
+
} else if (present.has("poetry.lock")) {
|
|
179
|
+
pythonManagers.push("poetry");
|
|
180
|
+
} else if (present.has("Pipfile")) {
|
|
181
|
+
pythonManagers.push("pipenv");
|
|
182
|
+
} else if (present.has("pyproject.toml") || present.has("requirements.txt")) {
|
|
183
|
+
pythonManagers.push("pip");
|
|
184
|
+
}
|
|
185
|
+
const cautions = [];
|
|
186
|
+
if (detectedLockfiles.length > 1) {
|
|
187
|
+
cautions.push(
|
|
188
|
+
`Multiple JavaScript lockfiles detected: ${detectedLockfiles.map(({ name }) => name).join(", ")}.`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const scripts = {};
|
|
192
|
+
let projectName = sanitizeProjectName(basename(cwd)) ?? "project";
|
|
193
|
+
let declaredJsManager;
|
|
194
|
+
if (present.has("package.json")) {
|
|
195
|
+
const text = await readTextIfSmall(join(cwd, "package.json"), root);
|
|
196
|
+
if (text === void 0) {
|
|
197
|
+
cautions.push("package.json was too large to inspect; its scripts were ignored.");
|
|
198
|
+
} else {
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(text);
|
|
201
|
+
if (!isRecord(parsed)) throw new Error("not an object");
|
|
202
|
+
const manifest = parsed;
|
|
203
|
+
if (typeof manifest.name === "string") {
|
|
204
|
+
projectName = sanitizeProjectName(manifest.name) ?? projectName;
|
|
205
|
+
}
|
|
206
|
+
if (typeof manifest.packageManager === "string") {
|
|
207
|
+
declaredJsManager = /^(npm|pnpm|yarn|bun)@/.exec(manifest.packageManager)?.[1];
|
|
208
|
+
}
|
|
209
|
+
if (isRecord(manifest.scripts)) {
|
|
210
|
+
for (const [name, command] of Object.entries(manifest.scripts).sort(
|
|
211
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
212
|
+
)) {
|
|
213
|
+
if (typeof command === "string") scripts[name] = command;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
cautions.push("package.json could not be parsed; its scripts were ignored.");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
let selectedJsManager;
|
|
222
|
+
if (declaredJsManager !== void 0) {
|
|
223
|
+
selectedJsManager = declaredJsManager;
|
|
224
|
+
const conflicts = detectedJsManagers.filter((manager) => manager !== declaredJsManager);
|
|
225
|
+
if (conflicts.length > 0) {
|
|
226
|
+
cautions.push(
|
|
227
|
+
`package.json declares ${declaredJsManager}, but lockfiles for ${joinWithAnd(conflicts)} were also detected; using the declaration.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
} else if (detectedJsManagers.length === 1) {
|
|
231
|
+
selectedJsManager = detectedJsManagers[0];
|
|
232
|
+
} else if (detectedJsManagers.length === 0 && present.has("package.json")) {
|
|
233
|
+
selectedJsManager = "npm";
|
|
234
|
+
} else if (detectedJsManagers.length > 1) {
|
|
235
|
+
cautions.push("JavaScript package manager is ambiguous; script commands were omitted.");
|
|
236
|
+
}
|
|
237
|
+
const orderedJsManagers = selectedJsManager === void 0 ? detectedJsManagers : [selectedJsManager, ...detectedJsManagers.filter((manager) => manager !== selectedJsManager)];
|
|
238
|
+
const packageManagers = [...orderedJsManagers, ...pythonManagers];
|
|
239
|
+
const packageManager = selectedJsManager ?? pythonManagers[0];
|
|
240
|
+
const jsManager = selectedJsManager;
|
|
241
|
+
const buildCommands = [];
|
|
242
|
+
const testCommands = [];
|
|
243
|
+
const lintCommands = [];
|
|
244
|
+
const formatCommands = [];
|
|
245
|
+
addScriptCommand(buildCommands, scripts, jsManager, ["build"]);
|
|
246
|
+
addScriptCommand(testCommands, scripts, jsManager, ["test"]);
|
|
247
|
+
addScriptCommand(lintCommands, scripts, jsManager, ["lint"]);
|
|
248
|
+
addScriptCommand(formatCommands, scripts, jsManager, ["format", "format:check"]);
|
|
249
|
+
const pyproject = present.has("pyproject.toml") ? await readTextIfSmall(join(cwd, "pyproject.toml"), root) : void 0;
|
|
250
|
+
const requirements = present.has("requirements.txt") ? await readTextIfSmall(join(cwd, "requirements.txt"), root) : void 0;
|
|
251
|
+
const pythonTools = detectPythonTools(pyproject, requirements, configFiles);
|
|
252
|
+
const pythonManager = pythonManagers[0];
|
|
253
|
+
const pythonPrefix = pythonManager === "uv" ? "uv run " : pythonManager === "poetry" ? "poetry run " : pythonManager === "pipenv" ? "pipenv run " : "";
|
|
254
|
+
if (pythonTools.has("pytest")) testCommands.push(`${pythonPrefix}pytest`);
|
|
255
|
+
if (pythonTools.has("ruff")) lintCommands.push(`${pythonPrefix}ruff check .`);
|
|
256
|
+
if (pythonTools.has("black")) formatCommands.push(`${pythonPrefix}black --check .`);
|
|
257
|
+
const extensions = await sampleSourceExtensions(cwd, root, sourceDirectories);
|
|
258
|
+
const languages = /* @__PURE__ */ new Set();
|
|
259
|
+
const hasTypeScript = configFiles.includes("tsconfig.json") || ["ts", "tsx", "mts", "cts"].some((extension) => extensions.has(extension));
|
|
260
|
+
if (hasTypeScript) {
|
|
261
|
+
languages.add("TypeScript");
|
|
262
|
+
} else if (present.has("package.json") || ["js", "jsx", "mjs", "cjs"].some((extension) => extensions.has(extension))) {
|
|
263
|
+
languages.add("JavaScript");
|
|
264
|
+
}
|
|
265
|
+
if (present.has("pyproject.toml") || present.has("requirements.txt") || extensions.has("py")) {
|
|
266
|
+
languages.add("Python");
|
|
267
|
+
}
|
|
268
|
+
const gitignorePath = await pathKind(join(cwd, ".gitignore"), root) === "file" ? ".gitignore" : void 0;
|
|
269
|
+
const hasCodeGraph = await pathKind(join(cwd, ".flavor", "astgraph", "index.db"), root) === "file";
|
|
270
|
+
return {
|
|
271
|
+
cwd,
|
|
272
|
+
projectName,
|
|
273
|
+
languages: [...languages].sort(),
|
|
274
|
+
...packageManager === void 0 ? {} : { packageManager },
|
|
275
|
+
packageManagers,
|
|
276
|
+
scripts,
|
|
277
|
+
buildCommands,
|
|
278
|
+
testCommands,
|
|
279
|
+
lintCommands,
|
|
280
|
+
formatCommands,
|
|
281
|
+
sourceDirectories,
|
|
282
|
+
instructionFiles,
|
|
283
|
+
configFiles,
|
|
284
|
+
...gitignorePath === void 0 ? {} : { gitignorePath },
|
|
285
|
+
hasCodeGraph,
|
|
286
|
+
cautions
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function bulletList(values, empty, code, newline) {
|
|
290
|
+
if (values.length === 0) return empty;
|
|
291
|
+
return values.map((value) => `- ${code ? `\`${value}\`` : value}`).join(newline);
|
|
292
|
+
}
|
|
293
|
+
function renderGeneratedSection(facts, newline) {
|
|
294
|
+
const languages = facts.languages.length === 0 ? "not detected" : facts.languages.join(", ");
|
|
295
|
+
const managers = facts.packageManagers.length === 0 ? "not detected" : facts.packageManagers.join(", ");
|
|
296
|
+
const qualityCommands = [...facts.lintCommands, ...facts.formatCommands];
|
|
297
|
+
const conventions = [
|
|
298
|
+
...facts.instructionFiles.map((file) => `Follow \`${file}\`.`),
|
|
299
|
+
...facts.configFiles.map((file) => `Respect \`${file}\`.`)
|
|
300
|
+
];
|
|
301
|
+
const cautions = [
|
|
302
|
+
"Do not read or copy secrets from environment files.",
|
|
303
|
+
"Do not inspect dependency directories or generated output unless explicitly required.",
|
|
304
|
+
...facts.cautions
|
|
305
|
+
];
|
|
306
|
+
const searchSection = facts.hasCodeGraph ? [
|
|
307
|
+
"",
|
|
308
|
+
"## Search",
|
|
309
|
+
"",
|
|
310
|
+
"- A code graph index is available (`.flavor/astgraph/index.db`): pair `ast_search` with `grep`/`glob` to locate symbols, and use `ast_callers`/`ast_callees`/`ast_impact`/`ast_context` to trace reachability instead of reading files broadly."
|
|
311
|
+
] : [];
|
|
312
|
+
return [
|
|
313
|
+
GENERATED_START,
|
|
314
|
+
"## Overview",
|
|
315
|
+
"",
|
|
316
|
+
`- Project: ${facts.projectName}`,
|
|
317
|
+
`- Languages: ${languages}`,
|
|
318
|
+
`- Package manager: ${managers}`,
|
|
319
|
+
"",
|
|
320
|
+
"## Layout",
|
|
321
|
+
"",
|
|
322
|
+
bulletList(facts.sourceDirectories, "No conventional source directories detected.", true, newline),
|
|
323
|
+
...searchSection,
|
|
324
|
+
"",
|
|
325
|
+
"## Build",
|
|
326
|
+
"",
|
|
327
|
+
bulletList(facts.buildCommands, "No verified build command detected.", true, newline),
|
|
328
|
+
"",
|
|
329
|
+
"## Test",
|
|
330
|
+
"",
|
|
331
|
+
bulletList(facts.testCommands, "No verified test command detected.", true, newline),
|
|
332
|
+
"",
|
|
333
|
+
"## Quality",
|
|
334
|
+
"",
|
|
335
|
+
bulletList(qualityCommands, "No verified lint or format command detected.", true, newline),
|
|
336
|
+
"",
|
|
337
|
+
"## Conventions",
|
|
338
|
+
"",
|
|
339
|
+
bulletList(conventions, "No additional project instruction or quality configuration files detected.", false, newline),
|
|
340
|
+
"",
|
|
341
|
+
"## Cautions",
|
|
342
|
+
"",
|
|
343
|
+
bulletList(cautions, "", false, newline),
|
|
344
|
+
GENERATED_END
|
|
345
|
+
].join(newline);
|
|
346
|
+
}
|
|
347
|
+
function detectNewline(text) {
|
|
348
|
+
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
349
|
+
}
|
|
350
|
+
function mergeGeneratedSection(existing, generated) {
|
|
351
|
+
if (existing === void 0 || existing === "") return `${generated}
|
|
352
|
+
`;
|
|
353
|
+
const newline = detectNewline(existing);
|
|
354
|
+
let searchFrom = 0;
|
|
355
|
+
let lastPair;
|
|
356
|
+
while (searchFrom < existing.length) {
|
|
357
|
+
const start = existing.indexOf(GENERATED_START, searchFrom);
|
|
358
|
+
if (start === -1) break;
|
|
359
|
+
const end = existing.indexOf(GENERATED_END, start + GENERATED_START.length);
|
|
360
|
+
if (end === -1) break;
|
|
361
|
+
lastPair = { start, end };
|
|
362
|
+
searchFrom = end + GENERATED_END.length;
|
|
363
|
+
}
|
|
364
|
+
if (lastPair !== void 0) {
|
|
365
|
+
return `${existing.slice(0, lastPair.start)}${generated}${existing.slice(lastPair.end + GENERATED_END.length)}`;
|
|
366
|
+
}
|
|
367
|
+
const separator = existing.endsWith("\n") ? newline : `${newline}${newline}`;
|
|
368
|
+
return `${existing}${separator}${generated}${newline}`;
|
|
369
|
+
}
|
|
370
|
+
var GITIGNORE_ENTRIES = [".flavor", "FLAVOR.md"];
|
|
371
|
+
function isEntryIgnored(lines, entry) {
|
|
372
|
+
return lines.some((line) => {
|
|
373
|
+
const normalized = line.trim().replace(/^\//, "").replace(/\/$/, "");
|
|
374
|
+
return normalized === entry;
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
async function addSessionsToGitignore(cwd) {
|
|
378
|
+
const path = join(cwd, ".gitignore");
|
|
379
|
+
const root = await realpath(cwd);
|
|
380
|
+
await assertSafeManagedPath(path, root);
|
|
381
|
+
let existing;
|
|
382
|
+
try {
|
|
383
|
+
existing = await readFile(path, "utf8");
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (!isMissingPathError(error)) throw error;
|
|
386
|
+
existing = "";
|
|
387
|
+
}
|
|
388
|
+
const lines = existing.split(/\r?\n/);
|
|
389
|
+
const newline = detectNewline(existing);
|
|
390
|
+
const missing = GITIGNORE_ENTRIES.filter((entry) => !isEntryIgnored(lines, entry));
|
|
391
|
+
if (missing.length === 0) return;
|
|
392
|
+
const separator = existing === "" || existing.endsWith("\n") ? "" : newline;
|
|
393
|
+
const append = missing.map((entry) => entry).join(newline);
|
|
394
|
+
await writeFile(path, `${existing}${separator}${append}${newline}`);
|
|
395
|
+
}
|
|
396
|
+
async function assertSafeManagedPath(path, root) {
|
|
397
|
+
try {
|
|
398
|
+
const details = await lstat(path);
|
|
399
|
+
if (details.isSymbolicLink()) {
|
|
400
|
+
throw new Error(`Refusing to overwrite managed file symbolic link: ${path}`);
|
|
401
|
+
}
|
|
402
|
+
if (!isContainedPath(root, await realpath(path))) {
|
|
403
|
+
throw new Error(`Refusing to overwrite managed file outside the project: ${path}`);
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (isMissingPathError(error)) return;
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function ensureFlavorDirectories(cwd) {
|
|
411
|
+
const root = await realpath(cwd);
|
|
412
|
+
const sessionsDir = join(cwd, ".flavor", "sessions");
|
|
413
|
+
const skillsDir = join(cwd, ".flavor", "skills");
|
|
414
|
+
const pluginsDir = join(cwd, ".flavor", "plugins");
|
|
415
|
+
await mkdir(sessionsDir, { recursive: true, mode: 448 });
|
|
416
|
+
await mkdir(skillsDir, { recursive: true, mode: 448 });
|
|
417
|
+
await mkdir(pluginsDir, { recursive: true, mode: 448 });
|
|
418
|
+
const canonicalRoot = await realpath(root);
|
|
419
|
+
for (const dir of [sessionsDir, skillsDir, pluginsDir]) {
|
|
420
|
+
const canonical = await realpath(dir);
|
|
421
|
+
if (!isContainedPath(canonicalRoot, canonical)) {
|
|
422
|
+
throw new Error(`Directory escapes the workspace: ${dir}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async function createExampleFlavorConfig(cwd) {
|
|
427
|
+
const configPath = join(cwd, ".flavor", "flavor.json");
|
|
428
|
+
const root = await realpath(cwd);
|
|
429
|
+
await assertSafeManagedPath(configPath, root);
|
|
430
|
+
try {
|
|
431
|
+
await lstat(configPath);
|
|
432
|
+
return;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (!isMissingPathError(error)) throw error;
|
|
435
|
+
}
|
|
436
|
+
const example = {
|
|
437
|
+
providers: {
|
|
438
|
+
pkce: {
|
|
439
|
+
type: "oauth-callback",
|
|
440
|
+
apiType: "anthropic",
|
|
441
|
+
authorizationUrl: "http://127.0.0.1:8091/authorize",
|
|
442
|
+
tokenUrl: "http://127.0.0.1:8091/token",
|
|
443
|
+
clientId: "flavor-code-cli",
|
|
444
|
+
scope: "models:read models:use",
|
|
445
|
+
thinkingEffort: "high"
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
agents: {
|
|
449
|
+
main: { model: "pkce:bootstrap" },
|
|
450
|
+
subagent: { model: "pkce:bootstrap" }
|
|
451
|
+
},
|
|
452
|
+
maxSubagents: 3,
|
|
453
|
+
permissionMode: "default",
|
|
454
|
+
language: "zh-CN"
|
|
455
|
+
};
|
|
456
|
+
await writeFile(configPath, JSON.stringify(example, null, 2) + "\n");
|
|
457
|
+
}
|
|
458
|
+
async function initializeFlavor(cwd) {
|
|
459
|
+
await ensureFlavorDirectories(cwd);
|
|
460
|
+
await createExampleFlavorConfig(cwd);
|
|
461
|
+
const facts = await inspectProject(cwd);
|
|
462
|
+
const path = join(cwd, "FLAVOR.md");
|
|
463
|
+
const root = await realpath(cwd);
|
|
464
|
+
await assertSafeManagedPath(path, root);
|
|
465
|
+
let existing;
|
|
466
|
+
try {
|
|
467
|
+
existing = await readFile(path, "utf8");
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (!isMissingPathError(error)) throw error;
|
|
470
|
+
}
|
|
471
|
+
const newline = existing === void 0 ? "\n" : detectNewline(existing);
|
|
472
|
+
const generated = renderGeneratedSection(facts, newline);
|
|
473
|
+
const content = mergeGeneratedSection(existing, generated);
|
|
474
|
+
await writeFile(path, content);
|
|
475
|
+
await addSessionsToGitignore(cwd);
|
|
476
|
+
return { path, created: existing === void 0, content, facts };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export {
|
|
480
|
+
inspectProject,
|
|
481
|
+
initializeFlavor
|
|
482
|
+
};
|