@difflab/difflab-cli 0.4.0 → 0.5.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/README.md +3 -1
- package/dist/index.js +161 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,9 @@ difflab-cli templates scaffold spec-driven-plan docs/plans first-plan.md --cwd /
|
|
|
57
57
|
|
|
58
58
|
The CLI defaults `--cwd` to its current directory. The MCP server exposes `template_list({})` and `scaffold({"name":"spec-driven-plan","cwd":"/absolute/project/path","path":"docs/plans","filename":"first-plan.md"})`. Both interfaces take a hyphenated template name, a relative destination **directory** (`path`, or `.` for `cwd`), and a separate **filename**. MCP always requires an absolute existing `cwd`. Missing destination directories are created. Available names: `spec-driven-plan`, `software-architecture-design`, `architecture-decision-record`, `product-requirements-document`, `code-review`, `planning-intent`, `ui-component-architecture`, and `pull-request-description` (adapted from Diffpi's PR template).
|
|
59
59
|
|
|
60
|
-
On first use, missing bundled templates and filled examples are copied to `~/.difflab/templates` and `~/.difflab/templates/examples`. Edit the installed templates to customize future documents. Later runs add missing files but never replace existing ones, even after an upgrade. Examples are for reference, not selectable templates. Copying is literal: prompts and placeholders are not substituted. An unknown name, missing or invalid source, unsafe path, symlink, or existing destination causes an error; destination files are never overwritten. The target directory must remain inside `cwd` and the filename must be a single file name without separators. To restore a bundled default, move your installed copy aside before listing again.
|
|
60
|
+
On first use, missing bundled templates and filled examples are copied to `~/.difflab/templates` and `~/.difflab/templates/examples`. Edit the installed templates to customize future documents. Later runs add missing files but never replace existing ones, even after an upgrade. Examples are for reference, not selectable templates. Copying is literal: prompts and placeholders are not substituted. An unknown name, missing or invalid source, unsafe path, symlink, or existing destination causes an error; destination files are never overwritten. The target directory must remain inside `cwd` and the filename must be a single file name without separators. The one symlink exception is an initialized repository's `.difflab` link to its own user project store: `templates scaffold spec-driven-plan .difflab/plans/YYMMDD-name PLAN.md --cwd /absolute/repo` works in Git worktrees as well. To restore a bundled default, move your installed copy aside before listing again.
|
|
61
|
+
|
|
62
|
+
The portable `skills/difflab-plan` and `skills/difflab-review` directories contain natural-language agent workflows for these templates. Install each skill in your client's Agent Skills directory. The MCP-only `log_append({"path":"/absolute/physical/project-store/plans/YYMMDD-name/logs.txt","message":"Phase started"})` tool creates an append-only progress log on first use; pass the physical path, not the `.difflab` symlink. There is no log CLI command.
|
|
61
63
|
|
|
62
64
|
## Develop
|
|
63
65
|
|
package/dist/index.js
CHANGED
|
@@ -12796,17 +12796,17 @@ var init_registry = __esm(() => {
|
|
|
12796
12796
|
// ../../libs/difflab-ts/src/gitx.ts
|
|
12797
12797
|
import { execFileSync } from "child_process";
|
|
12798
12798
|
import { appendFile, readFile } from "fs/promises";
|
|
12799
|
-
import { isAbsolute,
|
|
12799
|
+
import { isAbsolute, resolve as resolve2 } from "path";
|
|
12800
12800
|
function gitRoot(cwd) {
|
|
12801
12801
|
return runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
12802
12802
|
}
|
|
12803
12803
|
function discoverGitRepository(cwd) {
|
|
12804
12804
|
const root = gitRoot(cwd);
|
|
12805
|
-
const
|
|
12805
|
+
const exclude = runGit(root, ["rev-parse", "--git-path", "info/exclude"]);
|
|
12806
12806
|
return {
|
|
12807
12807
|
root,
|
|
12808
12808
|
originUrl: runGit(root, ["remote", "get-url", "origin"]),
|
|
12809
|
-
excludePath:
|
|
12809
|
+
excludePath: isAbsolute(exclude) ? exclude : resolve2(root, exclude)
|
|
12810
12810
|
};
|
|
12811
12811
|
}
|
|
12812
12812
|
async function addPathToGitExcludes(path, entry) {
|
|
@@ -26485,9 +26485,9 @@ var init_dist3 = __esm(() => {
|
|
|
26485
26485
|
|
|
26486
26486
|
// src/projects/config.ts
|
|
26487
26487
|
import { lstat as lstat2, readFile as readFile2, writeFile } from "fs/promises";
|
|
26488
|
-
import { join
|
|
26488
|
+
import { join } from "path";
|
|
26489
26489
|
function repoConfigPath(root = process.cwd()) {
|
|
26490
|
-
return
|
|
26490
|
+
return join(root, "difflab.yaml");
|
|
26491
26491
|
}
|
|
26492
26492
|
async function readRepoConfig(root = process.cwd()) {
|
|
26493
26493
|
const path = repoConfigPath(root);
|
|
@@ -26548,7 +26548,7 @@ var init_config = __esm(() => {
|
|
|
26548
26548
|
import { realpathSync } from "fs";
|
|
26549
26549
|
import { lstat as lstat3, mkdir } from "fs/promises";
|
|
26550
26550
|
import { homedir } from "os";
|
|
26551
|
-
import { join as
|
|
26551
|
+
import { join as join2, resolve as resolve3 } from "path";
|
|
26552
26552
|
function userPaths(home = process.env.HOME || homedir()) {
|
|
26553
26553
|
let tilde;
|
|
26554
26554
|
try {
|
|
@@ -26558,8 +26558,8 @@ function userPaths(home = process.env.HOME || homedir()) {
|
|
|
26558
26558
|
throw error;
|
|
26559
26559
|
tilde = resolve3(home);
|
|
26560
26560
|
}
|
|
26561
|
-
const root =
|
|
26562
|
-
return { root, projects:
|
|
26561
|
+
const root = join2(tilde, ".difflab");
|
|
26562
|
+
return { root, projects: join2(root, "projects"), database: join2(root, "difflab.sqlite") };
|
|
26563
26563
|
}
|
|
26564
26564
|
function createUserStore(home) {
|
|
26565
26565
|
return {
|
|
@@ -26645,7 +26645,7 @@ var init_osx = () => {};
|
|
|
26645
26645
|
|
|
26646
26646
|
// src/extensions/pathx.ts
|
|
26647
26647
|
import { lstat as lstat4, realpath, stat } from "fs/promises";
|
|
26648
|
-
import { isAbsolute as isAbsolute2, join as
|
|
26648
|
+
import { isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve4, sep } from "path";
|
|
26649
26649
|
async function resolvePath(cwd, path) {
|
|
26650
26650
|
if (!isAbsolute2(cwd))
|
|
26651
26651
|
throw new Error("cwd must be an absolute path to an existing directory");
|
|
@@ -26661,7 +26661,7 @@ async function resolvePath(cwd, path) {
|
|
|
26661
26661
|
}
|
|
26662
26662
|
let current = root;
|
|
26663
26663
|
for (const segment of inside.split(sep)) {
|
|
26664
|
-
current =
|
|
26664
|
+
current = join3(current, segment);
|
|
26665
26665
|
try {
|
|
26666
26666
|
if ((await lstat4(current)).isSymbolicLink()) {
|
|
26667
26667
|
throw new Error(`Symlink paths are not allowed for files: ${current}`);
|
|
@@ -26732,7 +26732,7 @@ var init_fsx = __esm(() => {
|
|
|
26732
26732
|
|
|
26733
26733
|
// src/projects/repo-store.ts
|
|
26734
26734
|
import { mkdir as mkdir3, readlink, symlink } from "fs/promises";
|
|
26735
|
-
import { dirname as dirname2, join as
|
|
26735
|
+
import { dirname as dirname2, join as join4, resolve as resolve5 } from "path";
|
|
26736
26736
|
async function preflightRepoStore(root, target) {
|
|
26737
26737
|
for (const path of [root, dirname2(target)]) {
|
|
26738
26738
|
const entry = await lstatOrNull(path);
|
|
@@ -26742,7 +26742,7 @@ async function preflightRepoStore(root, target) {
|
|
|
26742
26742
|
const targetStat = await lstatOrNull(target);
|
|
26743
26743
|
if (targetStat && !targetStat.isDirectory())
|
|
26744
26744
|
throw new RepoStoreError(`Refusing existing target: ${target}`);
|
|
26745
|
-
const link =
|
|
26745
|
+
const link = join4(root, ".difflab");
|
|
26746
26746
|
const entry = await lstatOrNull(link);
|
|
26747
26747
|
if (entry) {
|
|
26748
26748
|
if (!entry.isSymbolicLink())
|
|
@@ -26752,7 +26752,7 @@ async function preflightRepoStore(root, target) {
|
|
|
26752
26752
|
}
|
|
26753
26753
|
}
|
|
26754
26754
|
async function checkRepoStore(root, target, excludePath) {
|
|
26755
|
-
const link =
|
|
26755
|
+
const link = join4(root, ".difflab");
|
|
26756
26756
|
const entry = await lstatOrNull(link);
|
|
26757
26757
|
if (!entry)
|
|
26758
26758
|
throw new RepoStoreError(`Missing .difflab symlink at ${link}`);
|
|
@@ -26776,7 +26776,7 @@ async function ensureRepoStore(root, target, excludePath) {
|
|
|
26776
26776
|
throw error;
|
|
26777
26777
|
}
|
|
26778
26778
|
}
|
|
26779
|
-
const link =
|
|
26779
|
+
const link = join4(root, ".difflab");
|
|
26780
26780
|
if (!await lstatOrNull(link)) {
|
|
26781
26781
|
try {
|
|
26782
26782
|
await symlink(target, link);
|
|
@@ -26794,7 +26794,7 @@ var init_repo_store = __esm(() => {
|
|
|
26794
26794
|
});
|
|
26795
26795
|
|
|
26796
26796
|
// src/context.ts
|
|
26797
|
-
import { join as
|
|
26797
|
+
import { join as join5 } from "path";
|
|
26798
26798
|
async function readProjectContext(cwd = process.cwd(), home) {
|
|
26799
26799
|
const git = discoverGitRepository(cwd);
|
|
26800
26800
|
const config = await readRepoConfig(git.root);
|
|
@@ -26815,7 +26815,7 @@ async function getProjectContext(git, projectId, home) {
|
|
|
26815
26815
|
const repository = project.repositories.find((item) => item.url === origin);
|
|
26816
26816
|
if (!repository)
|
|
26817
26817
|
throw new RepoStoreError("Repository is not registered in the selected project database");
|
|
26818
|
-
const target =
|
|
26818
|
+
const target = join5(userPaths(home).projects, project.id, repository.slug);
|
|
26819
26819
|
await checkRepoStore(git.root, target);
|
|
26820
26820
|
return {
|
|
26821
26821
|
project: { id: project.id, name: project.name },
|
|
@@ -48181,14 +48181,100 @@ var init_tools = __esm(() => {
|
|
|
48181
48181
|
];
|
|
48182
48182
|
});
|
|
48183
48183
|
|
|
48184
|
-
// src/
|
|
48184
|
+
// src/extensions/zodx.ts
|
|
48185
48185
|
import { isAbsolute as isAbsolute4 } from "path";
|
|
48186
|
+
var absolutePath, relativePath, fileName;
|
|
48187
|
+
var init_zodx = __esm(() => {
|
|
48188
|
+
init_zod();
|
|
48189
|
+
absolutePath = string2().refine(isAbsolute4, "path must be absolute");
|
|
48190
|
+
relativePath = string2().min(1).refine((value) => value.trim().length > 0 && !isAbsolute4(value), "path must be relative");
|
|
48191
|
+
fileName = string2().refine((value) => value.trim().length > 0 && value !== "." && value !== ".." && !/[/\\\0]/.test(value), "filename must be a single non-empty file name");
|
|
48192
|
+
});
|
|
48193
|
+
|
|
48194
|
+
// src/logs/store.ts
|
|
48195
|
+
import { constants } from "fs";
|
|
48196
|
+
import { lstat as lstat6, open as open2, realpath as realpath2 } from "fs/promises";
|
|
48197
|
+
import { basename, dirname as dirname5, isAbsolute as isAbsolute5, resolve as resolve8 } from "path";
|
|
48198
|
+
async function appendProgressLog(path, message, now = new Date) {
|
|
48199
|
+
if (!isAbsolute5(path) || resolve8(path) !== path || basename(path) !== "logs.txt") {
|
|
48200
|
+
throw new ProgressLogError("path must be an absolute, normalized path ending in logs.txt");
|
|
48201
|
+
}
|
|
48202
|
+
if (!message.trim() || /[\r\n\v\f\0\u0085\u2028\u2029]/.test(message) || Buffer.byteLength(message, "utf8") > 2048) {
|
|
48203
|
+
throw new ProgressLogError("message must be one non-empty line of at most 2048 bytes");
|
|
48204
|
+
}
|
|
48205
|
+
const parent = dirname5(path);
|
|
48206
|
+
if (await realpath2(parent) !== parent) {
|
|
48207
|
+
throw new ProgressLogError("path must not contain symlinked directories");
|
|
48208
|
+
}
|
|
48209
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
48210
|
+
const stamp = `${pad(now.getFullYear() % 100)}${pad(now.getMonth() + 1)}${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
48211
|
+
const entry = `[${stamp}]: ${message}`;
|
|
48212
|
+
try {
|
|
48213
|
+
const existing = await lstat6(path);
|
|
48214
|
+
if (existing.isSymbolicLink())
|
|
48215
|
+
throw new ProgressLogError("logs.txt must not be a symlink");
|
|
48216
|
+
if (!existing.isFile())
|
|
48217
|
+
throw new ProgressLogError("logs.txt must be a regular file");
|
|
48218
|
+
} catch (error) {
|
|
48219
|
+
if (!hasErrorCode(error, "ENOENT"))
|
|
48220
|
+
throw error;
|
|
48221
|
+
}
|
|
48222
|
+
let handle;
|
|
48223
|
+
try {
|
|
48224
|
+
handle = await open2(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 384);
|
|
48225
|
+
} catch (error) {
|
|
48226
|
+
if (hasErrorCode(error, "ELOOP"))
|
|
48227
|
+
throw new ProgressLogError("logs.txt must not be a symlink");
|
|
48228
|
+
throw error;
|
|
48229
|
+
}
|
|
48230
|
+
try {
|
|
48231
|
+
if (!(await handle.stat()).isFile())
|
|
48232
|
+
throw new ProgressLogError("logs.txt must be a regular file");
|
|
48233
|
+
const line = Buffer.from(`${entry}
|
|
48234
|
+
`, "utf8");
|
|
48235
|
+
const { bytesWritten } = await handle.write(line);
|
|
48236
|
+
if (bytesWritten !== line.length)
|
|
48237
|
+
throw new ProgressLogError("Could not append the full log line");
|
|
48238
|
+
} finally {
|
|
48239
|
+
await handle.close();
|
|
48240
|
+
}
|
|
48241
|
+
return { path, entry };
|
|
48242
|
+
}
|
|
48243
|
+
var ProgressLogError;
|
|
48244
|
+
var init_store2 = __esm(() => {
|
|
48245
|
+
init_osx();
|
|
48246
|
+
ProgressLogError = class ProgressLogError extends Error {
|
|
48247
|
+
};
|
|
48248
|
+
});
|
|
48249
|
+
|
|
48250
|
+
// src/logs/tools.ts
|
|
48251
|
+
var logOutput, logTools;
|
|
48252
|
+
var init_tools2 = __esm(() => {
|
|
48253
|
+
init_zod();
|
|
48254
|
+
init_zodx();
|
|
48255
|
+
init_store2();
|
|
48256
|
+
logOutput = object({ result: object({ path: string2(), entry: string2() }) });
|
|
48257
|
+
logTools = [
|
|
48258
|
+
defineTool("log_append", {
|
|
48259
|
+
description: "Append one locally timestamped progress line to logs.txt at an absolute physical path. The parent directory must already exist; create the file if absent, never overwrite prior entries.",
|
|
48260
|
+
inputSchema: strictObject({
|
|
48261
|
+
path: absolutePath.describe("Absolute physical path to an existing plan directory/logs.txt"),
|
|
48262
|
+
message: string2().min(1).describe("Single-line progress message")
|
|
48263
|
+
}),
|
|
48264
|
+
outputSchema: logOutput,
|
|
48265
|
+
annotations: { openWorldHint: false }
|
|
48266
|
+
}, async ({ path, message }) => appendProgressLog(path, message), { toStructuredContent: (result) => ({ result }) })
|
|
48267
|
+
];
|
|
48268
|
+
});
|
|
48269
|
+
|
|
48270
|
+
// src/projects/tools.ts
|
|
48271
|
+
import { isAbsolute as isAbsolute6 } from "path";
|
|
48186
48272
|
function createProjectTools(home) {
|
|
48187
48273
|
return [
|
|
48188
48274
|
defineTool("project_context", {
|
|
48189
48275
|
description: "Read the configured Difflab project and GitHub repository for an existing absolute cwd. Never sets up or modifies a repository.",
|
|
48190
48276
|
inputSchema: strictObject({
|
|
48191
|
-
cwd: string2().refine(
|
|
48277
|
+
cwd: string2().refine(isAbsolute6, "cwd must be an absolute existing directory")
|
|
48192
48278
|
}),
|
|
48193
48279
|
outputSchema: projectContextOutput,
|
|
48194
48280
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
@@ -48196,7 +48282,7 @@ function createProjectTools(home) {
|
|
|
48196
48282
|
];
|
|
48197
48283
|
}
|
|
48198
48284
|
var setupHint = "Use the difflab-init skill to set up this repository with the Difflab CLI.", projectContextOutput, projectTools;
|
|
48199
|
-
var
|
|
48285
|
+
var init_tools3 = __esm(() => {
|
|
48200
48286
|
init_zod();
|
|
48201
48287
|
init_context();
|
|
48202
48288
|
projectContextOutput = strictObject({
|
|
@@ -48213,16 +48299,6 @@ var init_tools2 = __esm(() => {
|
|
|
48213
48299
|
projectTools = createProjectTools();
|
|
48214
48300
|
});
|
|
48215
48301
|
|
|
48216
|
-
// src/extensions/zodx.ts
|
|
48217
|
-
import { isAbsolute as isAbsolute5 } from "path";
|
|
48218
|
-
var absolutePath, relativePath, fileName;
|
|
48219
|
-
var init_zodx = __esm(() => {
|
|
48220
|
-
init_zod();
|
|
48221
|
-
absolutePath = string2().refine(isAbsolute5, "path must be absolute");
|
|
48222
|
-
relativePath = string2().min(1).refine((value) => value.trim().length > 0 && !isAbsolute5(value), "path must be relative");
|
|
48223
|
-
fileName = string2().refine((value) => value.trim().length > 0 && value !== "." && value !== ".." && !/[/\\\0]/.test(value), "filename must be a single non-empty file name");
|
|
48224
|
-
});
|
|
48225
|
-
|
|
48226
48302
|
// templates/spec-driven-plan.md
|
|
48227
48303
|
var spec_driven_plan_default = `# [Outcome to deliver]
|
|
48228
48304
|
|
|
@@ -48511,6 +48587,21 @@ var code_review_default = `# Review: [change or pull request]
|
|
|
48511
48587
|
|
|
48512
48588
|
[Evaluate authentication and autorization, permissions, secrets, dependencies, input validation, proper error handling, and trust boundaries. State SKIPPED if skipped with reason, for example for chores, simple scripting, pure cosmetic UI work, documentation, etc.]
|
|
48513
48589
|
|
|
48590
|
+
## Change Requests
|
|
48591
|
+
|
|
48592
|
+
Track actionable inline requests with one entry per request. End each generated request and response with its own model attribution quote; preserve human text unchanged.
|
|
48593
|
+
|
|
48594
|
+
### [file:line \u2014 request]
|
|
48595
|
+
|
|
48596
|
+
- **Status:** [open/in progress/resolved]
|
|
48597
|
+
- **Request:** [verbatim request or concise generated finding]
|
|
48598
|
+
|
|
48599
|
+
> Generated via [actual model name, only for a generated request]
|
|
48600
|
+
|
|
48601
|
+
- **Response:** [resolution, evidence, or reason this remains open]
|
|
48602
|
+
|
|
48603
|
+
> Generated via [actual model name, only for a generated response]
|
|
48604
|
+
|
|
48514
48605
|
## Findings
|
|
48515
48606
|
|
|
48516
48607
|
| Severity | Location | Evidence and impact | Required change or question |
|
|
@@ -49193,9 +49284,9 @@ var exports_service = {};
|
|
|
49193
49284
|
__export(exports_service, {
|
|
49194
49285
|
TemplateService: () => TemplateService
|
|
49195
49286
|
});
|
|
49196
|
-
import { constants } from "fs";
|
|
49197
|
-
import { lstat as
|
|
49198
|
-
import { isAbsolute as
|
|
49287
|
+
import { constants as constants2 } from "fs";
|
|
49288
|
+
import { lstat as lstat7, open as open3, rm as rm3 } from "fs/promises";
|
|
49289
|
+
import { isAbsolute as isAbsolute7, join as join6 } from "path";
|
|
49199
49290
|
|
|
49200
49291
|
class TemplateService {
|
|
49201
49292
|
home;
|
|
@@ -49212,16 +49303,16 @@ class TemplateService {
|
|
|
49212
49303
|
if (!this.source.catalog.some((item) => item.name === name)) {
|
|
49213
49304
|
throw new UnknownTemplateError(`Unknown template: ${name}`);
|
|
49214
49305
|
}
|
|
49215
|
-
if (!path.trim() ||
|
|
49306
|
+
if (!path.trim() || isAbsolute7(path)) {
|
|
49216
49307
|
throw new TemplateError("path must be a non-empty relative directory inside cwd");
|
|
49217
49308
|
}
|
|
49218
49309
|
if (!filename.trim() || filename === "." || filename === ".." || /[/\\\0]/.test(filename)) {
|
|
49219
49310
|
throw new TemplateError("filename must be a single non-empty file name");
|
|
49220
49311
|
}
|
|
49221
49312
|
await this.installMissing();
|
|
49222
|
-
const installed = await resolvePath(this.home,
|
|
49313
|
+
const installed = await resolvePath(this.home, join6(".difflab", "templates", `${name}.md`));
|
|
49223
49314
|
const contents = await readMarkdown(installed);
|
|
49224
|
-
const destination = await ensurePathDirsExist(
|
|
49315
|
+
const destination = path.startsWith(".difflab/") ? await this.projectDestination(cwd, path.slice(".difflab/".length), filename) : await ensurePathDirsExist(join6(path, filename), cwd);
|
|
49225
49316
|
try {
|
|
49226
49317
|
await createExclusive(destination, contents);
|
|
49227
49318
|
} catch (error) {
|
|
@@ -49234,12 +49325,23 @@ class TemplateService {
|
|
|
49234
49325
|
}
|
|
49235
49326
|
return destination;
|
|
49236
49327
|
}
|
|
49328
|
+
async projectDestination(cwd, path, filename) {
|
|
49329
|
+
if (!isAbsolute7(cwd))
|
|
49330
|
+
throw new TemplateError("cwd must be an absolute path");
|
|
49331
|
+
const git = discoverGitRepository(cwd);
|
|
49332
|
+
const config = await readRepoConfig(git.root);
|
|
49333
|
+
if (!config)
|
|
49334
|
+
throw new TemplateError(`Initialize this repository with difflab init: ${git.root}`);
|
|
49335
|
+
const target = join6(userPaths(this.home).projects, config.project.id, repositorySlug(canonicalGitUrl(git.originUrl)));
|
|
49336
|
+
await checkRepoStore(git.root, target);
|
|
49337
|
+
return ensurePathDirsExist(join6(path, filename), target);
|
|
49338
|
+
}
|
|
49237
49339
|
async installMissing() {
|
|
49238
49340
|
for (const asset of this.source.assets) {
|
|
49239
|
-
const relative =
|
|
49341
|
+
const relative = join6(".difflab", "templates", asset);
|
|
49240
49342
|
const destination = await ensurePathDirsExist(relative, this.home);
|
|
49241
49343
|
try {
|
|
49242
|
-
if ((await
|
|
49344
|
+
if ((await lstat7(destination)).isSymbolicLink()) {
|
|
49243
49345
|
throw new TemplateSourceError(`Installed template asset is a symlink: ${destination}`);
|
|
49244
49346
|
}
|
|
49245
49347
|
await readMarkdown(destination);
|
|
@@ -49273,7 +49375,7 @@ async function readSource(source, asset) {
|
|
|
49273
49375
|
async function readMarkdown(path) {
|
|
49274
49376
|
let handle;
|
|
49275
49377
|
try {
|
|
49276
|
-
handle = await
|
|
49378
|
+
handle = await open3(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
49277
49379
|
if (!(await handle.stat()).isFile())
|
|
49278
49380
|
throw new TemplateSourceError(`Template is not a file: ${path}`);
|
|
49279
49381
|
const content = await handle.readFile("utf8");
|
|
@@ -49289,7 +49391,7 @@ async function readMarkdown(path) {
|
|
|
49289
49391
|
}
|
|
49290
49392
|
}
|
|
49291
49393
|
async function createExclusive(path, content) {
|
|
49292
|
-
const handle = await
|
|
49394
|
+
const handle = await open3(path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
49293
49395
|
try {
|
|
49294
49396
|
await handle.writeFile(content, "utf8");
|
|
49295
49397
|
} catch (error) {
|
|
@@ -49305,6 +49407,10 @@ var init_service = __esm(() => {
|
|
|
49305
49407
|
init_fsx();
|
|
49306
49408
|
init_osx();
|
|
49307
49409
|
init_pathx();
|
|
49410
|
+
init_gitx2();
|
|
49411
|
+
init_config();
|
|
49412
|
+
init_repo_store();
|
|
49413
|
+
init_user_store();
|
|
49308
49414
|
init_source();
|
|
49309
49415
|
TemplateError = class TemplateError extends Error {
|
|
49310
49416
|
};
|
|
@@ -49318,7 +49424,7 @@ var init_service = __esm(() => {
|
|
|
49318
49424
|
|
|
49319
49425
|
// src/templates/tools.ts
|
|
49320
49426
|
var templateInfo, templateListOutput, scaffoldOutput, templateTools;
|
|
49321
|
-
var
|
|
49427
|
+
var init_tools4 = __esm(() => {
|
|
49322
49428
|
init_zod();
|
|
49323
49429
|
init_zodx();
|
|
49324
49430
|
init_service();
|
|
@@ -49357,7 +49463,7 @@ var init_tools3 = __esm(() => {
|
|
|
49357
49463
|
// src/mcp/server.ts
|
|
49358
49464
|
async function serveMcp(version = "0.1.0") {
|
|
49359
49465
|
const server = new McpServer({ name: "difflab", version });
|
|
49360
|
-
for (const tool of [...todoTools, ...projectTools, ...templateTools]) {
|
|
49466
|
+
for (const tool of [...todoTools, ...projectTools, ...templateTools, ...logTools]) {
|
|
49361
49467
|
server.registerTool(tool.name, tool.config, tool.callback);
|
|
49362
49468
|
}
|
|
49363
49469
|
await server.connect(new StdioServerTransport);
|
|
@@ -49368,6 +49474,7 @@ var init_server3 = __esm(() => {
|
|
|
49368
49474
|
init_tools();
|
|
49369
49475
|
init_tools2();
|
|
49370
49476
|
init_tools3();
|
|
49477
|
+
init_tools4();
|
|
49371
49478
|
});
|
|
49372
49479
|
|
|
49373
49480
|
// src/extensions/processx.ts
|
|
@@ -49442,7 +49549,7 @@ var init_json_config = __esm(() => {
|
|
|
49442
49549
|
});
|
|
49443
49550
|
|
|
49444
49551
|
// src/agents/claude-code.ts
|
|
49445
|
-
import { join as
|
|
49552
|
+
import { join as join7 } from "path";
|
|
49446
49553
|
var claudeCodeAdapter;
|
|
49447
49554
|
var init_claude_code = __esm(() => {
|
|
49448
49555
|
init_adapter();
|
|
@@ -49452,7 +49559,7 @@ var init_claude_code = __esm(() => {
|
|
|
49452
49559
|
id: "claude-code",
|
|
49453
49560
|
async setupMcpConfig(context = {}) {
|
|
49454
49561
|
const home = setupHome(context);
|
|
49455
|
-
const file =
|
|
49562
|
+
const file = join7(home, ".claude.json");
|
|
49456
49563
|
const status = serverStatus(await readJsonConfig(file), file);
|
|
49457
49564
|
if (status === "matching")
|
|
49458
49565
|
return { file, status: "existing" };
|
|
@@ -49468,7 +49575,7 @@ var init_claude_code = __esm(() => {
|
|
|
49468
49575
|
|
|
49469
49576
|
// src/agents/codex.ts
|
|
49470
49577
|
import { readFile as readFile5 } from "fs/promises";
|
|
49471
|
-
import { join as
|
|
49578
|
+
import { join as join8 } from "path";
|
|
49472
49579
|
function codexServerStatus(content) {
|
|
49473
49580
|
const lines = content.split(/\r?\n/);
|
|
49474
49581
|
const start = lines.findIndex((line) => /^\s*\[mcp_servers\.(?:difflab|"difflab"|'difflab')\]\s*(?:#.*)?$/.test(line));
|
|
@@ -49492,7 +49599,7 @@ var init_codex = __esm(() => {
|
|
|
49492
49599
|
id: "codex",
|
|
49493
49600
|
async setupMcpConfig(context = {}) {
|
|
49494
49601
|
const home = setupHome(context);
|
|
49495
|
-
const file =
|
|
49602
|
+
const file = join8(home, ".codex", "config.toml");
|
|
49496
49603
|
let content = "";
|
|
49497
49604
|
try {
|
|
49498
49605
|
content = await readFile5(file, "utf8");
|
|
@@ -49514,7 +49621,7 @@ var init_codex = __esm(() => {
|
|
|
49514
49621
|
});
|
|
49515
49622
|
|
|
49516
49623
|
// src/agents/index.ts
|
|
49517
|
-
import { join as
|
|
49624
|
+
import { join as join9 } from "path";
|
|
49518
49625
|
function agentAdapter(name) {
|
|
49519
49626
|
return Object.hasOwn(agentAdapters, name) ? agentAdapters[name] : undefined;
|
|
49520
49627
|
}
|
|
@@ -49525,20 +49632,20 @@ var init_agents = __esm(() => {
|
|
|
49525
49632
|
init_codex();
|
|
49526
49633
|
init_json_config();
|
|
49527
49634
|
agentAdapters = {
|
|
49528
|
-
pi: jsonAgentAdapter("pi", (context) =>
|
|
49529
|
-
cursor: jsonAgentAdapter("cursor", (context) =>
|
|
49635
|
+
pi: jsonAgentAdapter("pi", (context) => join9(setupHome(context), ".config", "mcp", "mcp.json")),
|
|
49636
|
+
cursor: jsonAgentAdapter("cursor", (context) => join9(setupHome(context), ".cursor", "mcp.json")),
|
|
49530
49637
|
codex: codexAdapter,
|
|
49531
49638
|
"claude-code": claudeCodeAdapter,
|
|
49532
49639
|
"claude-desktop": jsonAgentAdapter("claude-desktop", (context) => {
|
|
49533
49640
|
const home = setupHome(context);
|
|
49534
49641
|
const platform = context.platform ?? process.platform;
|
|
49535
49642
|
if (platform === "darwin") {
|
|
49536
|
-
return
|
|
49643
|
+
return join9(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
49537
49644
|
}
|
|
49538
49645
|
if (platform === "win32") {
|
|
49539
|
-
return
|
|
49646
|
+
return join9(process.env.APPDATA ?? join9(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
49540
49647
|
}
|
|
49541
|
-
return
|
|
49648
|
+
return join9(home, ".config", "Claude", "claude_desktop_config.json");
|
|
49542
49649
|
})
|
|
49543
49650
|
};
|
|
49544
49651
|
agentNames = Object.keys(agentAdapters);
|
|
@@ -51626,7 +51733,7 @@ init_config();
|
|
|
51626
51733
|
init_context();
|
|
51627
51734
|
init_repo_store();
|
|
51628
51735
|
init_user_store();
|
|
51629
|
-
import { join as
|
|
51736
|
+
import { join as join10 } from "path";
|
|
51630
51737
|
function createProgram(write = console.log, version = "0.1.0", options = {}) {
|
|
51631
51738
|
const program = new Command2().name("difflab").description("Difflab CLI and local MCP server").version(version);
|
|
51632
51739
|
program.action(() => program.outputHelp());
|
|
@@ -51660,7 +51767,7 @@ function createProgram(write = console.log, version = "0.1.0", options = {}) {
|
|
|
51660
51767
|
if (config && config.project.id !== key)
|
|
51661
51768
|
throw new RepoStoreError(`Repository already belongs to project ${config.project.id}; requested ${key}`);
|
|
51662
51769
|
const origin = canonicalGitUrl(git.originUrl);
|
|
51663
|
-
const target =
|
|
51770
|
+
const target = join10(userPaths(options.home).projects, key, repositorySlug(origin));
|
|
51664
51771
|
await preflightRepoStore(git.root, target);
|
|
51665
51772
|
const project = await withUserStore(async (db) => {
|
|
51666
51773
|
const project = await db.projects.getProjectById(key);
|
|
@@ -51709,7 +51816,7 @@ function createProgram(write = console.log, version = "0.1.0", options = {}) {
|
|
|
51709
51816
|
|
|
51710
51817
|
// src/index.ts
|
|
51711
51818
|
init_errors3();
|
|
51712
|
-
var version2 = "0.
|
|
51819
|
+
var version2 = "0.5.0";
|
|
51713
51820
|
try {
|
|
51714
51821
|
await createProgram(console.log, version2).parseAsync(process.argv);
|
|
51715
51822
|
} catch (error) {
|