@difflab/difflab-cli 0.3.0 → 0.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/README.md +13 -0
- package/dist/index.js +1238 -63
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -33,6 +33,19 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
33
33
|
return to;
|
|
34
34
|
};
|
|
35
35
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
36
|
+
var __returnValue = (v) => v;
|
|
37
|
+
function __exportSetter(name, newValue) {
|
|
38
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
39
|
+
}
|
|
40
|
+
var __export = (target, all) => {
|
|
41
|
+
for (var name in all)
|
|
42
|
+
__defProp(target, name, {
|
|
43
|
+
get: all[name],
|
|
44
|
+
enumerable: true,
|
|
45
|
+
configurable: true,
|
|
46
|
+
set: __exportSetter.bind(all, name)
|
|
47
|
+
});
|
|
48
|
+
};
|
|
36
49
|
var __esm = (fn, res, err) => () => {
|
|
37
50
|
if (fn)
|
|
38
51
|
try {
|
|
@@ -26630,25 +26643,65 @@ function hasErrorCode(error, code) {
|
|
|
26630
26643
|
}
|
|
26631
26644
|
var init_osx = () => {};
|
|
26632
26645
|
|
|
26646
|
+
// src/extensions/pathx.ts
|
|
26647
|
+
import { lstat as lstat4, realpath, stat } from "fs/promises";
|
|
26648
|
+
import { isAbsolute as isAbsolute2, join as join4, relative, resolve as resolve4, sep } from "path";
|
|
26649
|
+
async function resolvePath(cwd, path) {
|
|
26650
|
+
if (!isAbsolute2(cwd))
|
|
26651
|
+
throw new Error("cwd must be an absolute path to an existing directory");
|
|
26652
|
+
if (!path.trim() || isAbsolute2(path))
|
|
26653
|
+
throw new Error("path must be a non-empty relative file path");
|
|
26654
|
+
const root = await realpath(cwd);
|
|
26655
|
+
if (!(await stat(root)).isDirectory())
|
|
26656
|
+
throw new Error("cwd must be an existing directory");
|
|
26657
|
+
const file = resolve4(root, path);
|
|
26658
|
+
const inside = relative(root, file);
|
|
26659
|
+
if (!inside || inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute2(inside)) {
|
|
26660
|
+
throw new Error("path must name a file inside cwd; parent traversal outside cwd is not allowed");
|
|
26661
|
+
}
|
|
26662
|
+
let current = root;
|
|
26663
|
+
for (const segment of inside.split(sep)) {
|
|
26664
|
+
current = join4(current, segment);
|
|
26665
|
+
try {
|
|
26666
|
+
if ((await lstat4(current)).isSymbolicLink()) {
|
|
26667
|
+
throw new Error(`Symlink paths are not allowed for files: ${current}`);
|
|
26668
|
+
}
|
|
26669
|
+
} catch (error) {
|
|
26670
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
26671
|
+
break;
|
|
26672
|
+
throw error;
|
|
26673
|
+
}
|
|
26674
|
+
}
|
|
26675
|
+
return file;
|
|
26676
|
+
}
|
|
26677
|
+
var init_pathx = __esm(() => {
|
|
26678
|
+
init_osx();
|
|
26679
|
+
});
|
|
26680
|
+
|
|
26633
26681
|
// src/extensions/fsx.ts
|
|
26634
26682
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
26635
|
-
import { lstat as
|
|
26683
|
+
import { lstat as lstat5, mkdir as mkdir2, readFile as readFile3, rename, rm, writeFile as writeFile2 } from "fs/promises";
|
|
26636
26684
|
import { dirname } from "path";
|
|
26637
26685
|
async function lstatOrNull(file) {
|
|
26638
26686
|
try {
|
|
26639
|
-
return await
|
|
26687
|
+
return await lstat5(file);
|
|
26640
26688
|
} catch (error) {
|
|
26641
26689
|
if (hasErrorCode(error, "ENOENT"))
|
|
26642
26690
|
return null;
|
|
26643
26691
|
throw error;
|
|
26644
26692
|
}
|
|
26645
26693
|
}
|
|
26694
|
+
async function ensurePathDirsExist(path, cwd = process.cwd()) {
|
|
26695
|
+
const file = await resolvePath(cwd, path);
|
|
26696
|
+
await mkdir2(dirname(file), { recursive: true });
|
|
26697
|
+
return resolvePath(cwd, path);
|
|
26698
|
+
}
|
|
26646
26699
|
function isObject3(value) {
|
|
26647
26700
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
26648
26701
|
}
|
|
26649
26702
|
async function readJsonFile(file) {
|
|
26650
26703
|
try {
|
|
26651
|
-
if ((await
|
|
26704
|
+
if ((await lstat5(file)).isSymbolicLink())
|
|
26652
26705
|
throw new Error(`JSON file at ${file} is a symlink`);
|
|
26653
26706
|
const parsed = JSON.parse(await readFile3(file, "utf8"));
|
|
26654
26707
|
if (!isObject3(parsed))
|
|
@@ -26674,11 +26727,12 @@ async function patchJsonToFile(file, patch) {
|
|
|
26674
26727
|
}
|
|
26675
26728
|
var init_fsx = __esm(() => {
|
|
26676
26729
|
init_osx();
|
|
26730
|
+
init_pathx();
|
|
26677
26731
|
});
|
|
26678
26732
|
|
|
26679
26733
|
// src/projects/repo-store.ts
|
|
26680
26734
|
import { mkdir as mkdir3, readlink, symlink } from "fs/promises";
|
|
26681
|
-
import { dirname as dirname2, join as
|
|
26735
|
+
import { dirname as dirname2, join as join5, resolve as resolve5 } from "path";
|
|
26682
26736
|
async function preflightRepoStore(root, target) {
|
|
26683
26737
|
for (const path of [root, dirname2(target)]) {
|
|
26684
26738
|
const entry = await lstatOrNull(path);
|
|
@@ -26688,23 +26742,23 @@ async function preflightRepoStore(root, target) {
|
|
|
26688
26742
|
const targetStat = await lstatOrNull(target);
|
|
26689
26743
|
if (targetStat && !targetStat.isDirectory())
|
|
26690
26744
|
throw new RepoStoreError(`Refusing existing target: ${target}`);
|
|
26691
|
-
const link =
|
|
26745
|
+
const link = join5(root, ".difflab");
|
|
26692
26746
|
const entry = await lstatOrNull(link);
|
|
26693
26747
|
if (entry) {
|
|
26694
26748
|
if (!entry.isSymbolicLink())
|
|
26695
26749
|
throw new RepoStoreError(`Refusing to replace existing .difflab path: ${link}`);
|
|
26696
|
-
if (
|
|
26750
|
+
if (resolve5(root, await readlink(link)) !== resolve5(target))
|
|
26697
26751
|
throw new RepoStoreError(`.difflab already links elsewhere: ${link}`);
|
|
26698
26752
|
}
|
|
26699
26753
|
}
|
|
26700
26754
|
async function checkRepoStore(root, target, excludePath) {
|
|
26701
|
-
const link =
|
|
26755
|
+
const link = join5(root, ".difflab");
|
|
26702
26756
|
const entry = await lstatOrNull(link);
|
|
26703
26757
|
if (!entry)
|
|
26704
26758
|
throw new RepoStoreError(`Missing .difflab symlink at ${link}`);
|
|
26705
26759
|
if (!entry.isSymbolicLink())
|
|
26706
26760
|
throw new RepoStoreError(`Refusing to replace existing .difflab path: ${link}`);
|
|
26707
|
-
if (
|
|
26761
|
+
if (resolve5(root, await readlink(link)) !== resolve5(target))
|
|
26708
26762
|
throw new RepoStoreError(`.difflab already links elsewhere: ${link}`);
|
|
26709
26763
|
const targetStat = await lstatOrNull(target);
|
|
26710
26764
|
if (!targetStat?.isDirectory())
|
|
@@ -26722,7 +26776,7 @@ async function ensureRepoStore(root, target, excludePath) {
|
|
|
26722
26776
|
throw error;
|
|
26723
26777
|
}
|
|
26724
26778
|
}
|
|
26725
|
-
const link =
|
|
26779
|
+
const link = join5(root, ".difflab");
|
|
26726
26780
|
if (!await lstatOrNull(link)) {
|
|
26727
26781
|
try {
|
|
26728
26782
|
await symlink(target, link);
|
|
@@ -26740,7 +26794,7 @@ var init_repo_store = __esm(() => {
|
|
|
26740
26794
|
});
|
|
26741
26795
|
|
|
26742
26796
|
// src/context.ts
|
|
26743
|
-
import { join as
|
|
26797
|
+
import { join as join6 } from "path";
|
|
26744
26798
|
async function readProjectContext(cwd = process.cwd(), home) {
|
|
26745
26799
|
const git = discoverGitRepository(cwd);
|
|
26746
26800
|
const config = await readRepoConfig(git.root);
|
|
@@ -26761,7 +26815,7 @@ async function getProjectContext(git, projectId, home) {
|
|
|
26761
26815
|
const repository = project.repositories.find((item) => item.url === origin);
|
|
26762
26816
|
if (!repository)
|
|
26763
26817
|
throw new RepoStoreError("Repository is not registered in the selected project database");
|
|
26764
|
-
const target =
|
|
26818
|
+
const target = join6(userPaths(home).projects, project.id, repository.slug);
|
|
26765
26819
|
await checkRepoStore(git.root, target);
|
|
26766
26820
|
return {
|
|
26767
26821
|
project: { id: project.id, name: project.name },
|
|
@@ -47818,7 +47872,8 @@ function defineTool(name, config, handler, options = {}) {
|
|
|
47818
47872
|
},
|
|
47819
47873
|
callback: async (untrustedInput) => {
|
|
47820
47874
|
try {
|
|
47821
|
-
|
|
47875
|
+
const value = await handler(config.inputSchema.parse(untrustedInput));
|
|
47876
|
+
return serializeToolResult(value, config.outputSchema, options.toStructuredContent?.(value) ?? value);
|
|
47822
47877
|
} catch (error) {
|
|
47823
47878
|
return {
|
|
47824
47879
|
...serializeToolResult({
|
|
@@ -47830,49 +47885,14 @@ function defineTool(name, config, handler, options = {}) {
|
|
|
47830
47885
|
}
|
|
47831
47886
|
};
|
|
47832
47887
|
}
|
|
47833
|
-
function serializeToolResult(value, outputSchema) {
|
|
47834
|
-
const structuredContent = outputSchema?.parse(
|
|
47888
|
+
function serializeToolResult(value, outputSchema, content = value) {
|
|
47889
|
+
const structuredContent = outputSchema?.parse(content);
|
|
47835
47890
|
return {
|
|
47836
47891
|
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
47837
47892
|
...structuredContent ? { structuredContent } : {}
|
|
47838
47893
|
};
|
|
47839
47894
|
}
|
|
47840
47895
|
|
|
47841
|
-
// src/extensions/pathx.ts
|
|
47842
|
-
import { lstat as lstat5, realpath, stat } from "fs/promises";
|
|
47843
|
-
import { isAbsolute as isAbsolute2, join as join6, relative, resolve as resolve5, sep } from "path";
|
|
47844
|
-
async function resolvePath(cwd, path) {
|
|
47845
|
-
if (!isAbsolute2(cwd))
|
|
47846
|
-
throw new Error("cwd must be an absolute path to an existing directory");
|
|
47847
|
-
if (!path.trim() || isAbsolute2(path))
|
|
47848
|
-
throw new Error("path must be a non-empty relative file path");
|
|
47849
|
-
const root = await realpath(cwd);
|
|
47850
|
-
if (!(await stat(root)).isDirectory())
|
|
47851
|
-
throw new Error("cwd must be an existing directory");
|
|
47852
|
-
const file = resolve5(root, path);
|
|
47853
|
-
const inside = relative(root, file);
|
|
47854
|
-
if (!inside || inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute2(inside)) {
|
|
47855
|
-
throw new Error("path must name a file inside cwd; parent traversal outside cwd is not allowed");
|
|
47856
|
-
}
|
|
47857
|
-
let current = root;
|
|
47858
|
-
for (const segment of inside.split(sep)) {
|
|
47859
|
-
current = join6(current, segment);
|
|
47860
|
-
try {
|
|
47861
|
-
if ((await lstat5(current)).isSymbolicLink()) {
|
|
47862
|
-
throw new Error(`Symlink paths are not allowed for files: ${current}`);
|
|
47863
|
-
}
|
|
47864
|
-
} catch (error) {
|
|
47865
|
-
if (hasErrorCode(error, "ENOENT"))
|
|
47866
|
-
break;
|
|
47867
|
-
throw error;
|
|
47868
|
-
}
|
|
47869
|
-
}
|
|
47870
|
-
return file;
|
|
47871
|
-
}
|
|
47872
|
-
var init_pathx = __esm(() => {
|
|
47873
|
-
init_osx();
|
|
47874
|
-
});
|
|
47875
|
-
|
|
47876
47896
|
// src/stores/json-file-store.ts
|
|
47877
47897
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
47878
47898
|
import { mkdir as mkdir4, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
@@ -48193,10 +48213,1151 @@ var init_tools2 = __esm(() => {
|
|
|
48193
48213
|
projectTools = createProjectTools();
|
|
48194
48214
|
});
|
|
48195
48215
|
|
|
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
|
+
// templates/spec-driven-plan.md
|
|
48227
|
+
var spec_driven_plan_default = `# [Outcome to deliver]
|
|
48228
|
+
|
|
48229
|
+
- **Status:** draft
|
|
48230
|
+
- **Branch:** [branch]
|
|
48231
|
+
- **Issue:** [issue or none]
|
|
48232
|
+
|
|
48233
|
+
## Intent
|
|
48234
|
+
|
|
48235
|
+
[Who needs this change, what should be possible afterward, and why now?]
|
|
48236
|
+
|
|
48237
|
+
## Requirements
|
|
48238
|
+
|
|
48239
|
+
- [Observable outcome and how to tell whether it works.]
|
|
48240
|
+
- [Limits and explicit non-goals.]
|
|
48241
|
+
|
|
48242
|
+
## Design
|
|
48243
|
+
|
|
48244
|
+
### Big Ideas
|
|
48245
|
+
|
|
48246
|
+
[Describe the intended behavior and the main architectural choices.]
|
|
48247
|
+
|
|
48248
|
+
### Infrastructure Changes
|
|
48249
|
+
|
|
48250
|
+
[Any infra changes. Optional. Only include if there's actual infra being handled in repo.]
|
|
48251
|
+
|
|
48252
|
+
### Interfaces
|
|
48253
|
+
|
|
48254
|
+
[CLI commands, API endpoint, data contracts, db entities, modules, apps, libs, project tasks added, changed or removed. Summary of how a user's interaction with the app/lib/cli/sdk/etc. will change as a result.]
|
|
48255
|
+
|
|
48256
|
+
### Consequences
|
|
48257
|
+
|
|
48258
|
+
[Trade-offs, risks, compatibility, and what is deferred.]
|
|
48259
|
+
|
|
48260
|
+
## Phases
|
|
48261
|
+
|
|
48262
|
+
[Instructions broken down granularly, specifying modules/classes/methods/files to be added/updated/removed, and relevant technical work done with terse reasoning. Terse for human readers, sufficiently detailed for delegating implementation to less capable language models i.e. a fool proof guide for a junior developer.]
|
|
48263
|
+
|
|
48264
|
+
### Phase 1: [Outcome]
|
|
48265
|
+
|
|
48266
|
+
- [ ] [Task A]
|
|
48267
|
+
- [ ] [Task B]
|
|
48268
|
+
- [ ] [Task C]
|
|
48269
|
+
- [ ] [Task D]
|
|
48270
|
+
- [ ] Validation
|
|
48271
|
+
- [ ] Lint, format, etc.
|
|
48272
|
+
- [ ] Commit, CI green (if expected)
|
|
48273
|
+
|
|
48274
|
+
### Phase 2: [Outcome]
|
|
48275
|
+
|
|
48276
|
+
- [ ] [Task A]
|
|
48277
|
+
- [ ] [Task B]
|
|
48278
|
+
- [ ] [Task C]
|
|
48279
|
+
- [ ] [Task D]
|
|
48280
|
+
- [ ] Validation
|
|
48281
|
+
- [ ] Lint, format, etc.
|
|
48282
|
+
- [ ] Commit, CI green (if expected)
|
|
48283
|
+
|
|
48284
|
+
### Phase 3: [Outcome]
|
|
48285
|
+
|
|
48286
|
+
- [ ] [Task A]
|
|
48287
|
+
- [ ] [Task B]
|
|
48288
|
+
- [ ] [Task C]
|
|
48289
|
+
- [ ] [Task D]
|
|
48290
|
+
- [ ] Validation
|
|
48291
|
+
- [ ] Lint, format, etc.
|
|
48292
|
+
- [ ] Commit, CI green (if expected)
|
|
48293
|
+
|
|
48294
|
+
## References
|
|
48295
|
+
|
|
48296
|
+
- [Source or related decision.]
|
|
48297
|
+
- [External references, docs supporting the implementation plan.]
|
|
48298
|
+
`;
|
|
48299
|
+
var init_spec_driven_plan = () => {};
|
|
48300
|
+
|
|
48301
|
+
// templates/software-architecture-design.md
|
|
48302
|
+
var software_architecture_design_default = `# [System or feature] \u2014 software architecture
|
|
48303
|
+
|
|
48304
|
+
## Overview
|
|
48305
|
+
|
|
48306
|
+
[Purpose, system boundary, users, and explicit non-goals.]
|
|
48307
|
+
|
|
48308
|
+
## Requirement
|
|
48309
|
+
|
|
48310
|
+
### Functional
|
|
48311
|
+
|
|
48312
|
+
[Functionality app, lib or product must perform.]
|
|
48313
|
+
|
|
48314
|
+
- FR1
|
|
48315
|
+
- FR2
|
|
48316
|
+
- ...
|
|
48317
|
+
|
|
48318
|
+
### Non-Functional
|
|
48319
|
+
|
|
48320
|
+
[Performance, security, cost and other non-functional characteristics which the system must fulfil.]
|
|
48321
|
+
|
|
48322
|
+
- NFR1
|
|
48323
|
+
- NFR2
|
|
48324
|
+
|
|
48325
|
+
## Design
|
|
48326
|
+
|
|
48327
|
+
### Components and Responsibilities
|
|
48328
|
+
|
|
48329
|
+
[System diagram(s) with bullet list enumerating and explaining components and their relationships. Prefer mermaid diagrams unless user/AGENTS.md provde other stated preferences.]
|
|
48330
|
+
|
|
48331
|
+
> @agent: add example mermaid diagram
|
|
48332
|
+
|
|
48333
|
+
\`\`\`mermaid
|
|
48334
|
+
|
|
48335
|
+
\`\`\`
|
|
48336
|
+
|
|
48337
|
+
- Component A does this.
|
|
48338
|
+
- It then calls component B to do this.
|
|
48339
|
+
- And so on.
|
|
48340
|
+
|
|
48341
|
+
### Data and Control Flow
|
|
48342
|
+
|
|
48343
|
+
[Describe the main request/event path and where state lives. Include flow charts or sequence diagrams if applicable.]
|
|
48344
|
+
|
|
48345
|
+
### Interfaces and Contracts
|
|
48346
|
+
|
|
48347
|
+
[Database entities, input/output schemas, endpoints, core types, and public methods. Include ERD where applicable.]
|
|
48348
|
+
|
|
48349
|
+
## Implementation
|
|
48350
|
+
|
|
48351
|
+
### Important topic 1
|
|
48352
|
+
|
|
48353
|
+
[Owned modules, dependency direction, and extension points.]
|
|
48354
|
+
|
|
48355
|
+
### Important topic 2
|
|
48356
|
+
|
|
48357
|
+
[More important topics.]
|
|
48358
|
+
|
|
48359
|
+
## References
|
|
48360
|
+
|
|
48361
|
+
- [Relevant code, ADR, or external specification.]
|
|
48362
|
+
`;
|
|
48363
|
+
var init_software_architecture_design = () => {};
|
|
48364
|
+
|
|
48365
|
+
// templates/architecture-decision-record.md
|
|
48366
|
+
var architecture_decision_record_default = `# [Decision: problem and chosen solution]
|
|
48367
|
+
|
|
48368
|
+
- **Status:** proposed
|
|
48369
|
+
- **Deciders:** [people or team]
|
|
48370
|
+
- **Date:** [YYYY-MM-DD]
|
|
48371
|
+
|
|
48372
|
+
## Context and Problem Statement
|
|
48373
|
+
|
|
48374
|
+
[What decision is needed, and what happens if we do nothing?]
|
|
48375
|
+
|
|
48376
|
+
## Decision Drivers
|
|
48377
|
+
|
|
48378
|
+
- [Constraint or quality attribute.]
|
|
48379
|
+
|
|
48380
|
+
## Considered Options
|
|
48381
|
+
|
|
48382
|
+
### [Option A]
|
|
48383
|
+
|
|
48384
|
+
- Good: [reason.]
|
|
48385
|
+
- Bad: [reason.]
|
|
48386
|
+
|
|
48387
|
+
### [Option B]
|
|
48388
|
+
|
|
48389
|
+
- Good: [reason.]
|
|
48390
|
+
- Bad: [reason.]
|
|
48391
|
+
|
|
48392
|
+
## Decision Outcome
|
|
48393
|
+
|
|
48394
|
+
Chosen option: [option], because [reason and evidence].
|
|
48395
|
+
|
|
48396
|
+
### Positive Consequences
|
|
48397
|
+
|
|
48398
|
+
- [Benefit.]
|
|
48399
|
+
|
|
48400
|
+
### Negative Consequences
|
|
48401
|
+
|
|
48402
|
+
- [Cost or follow-up work.]
|
|
48403
|
+
|
|
48404
|
+
## References
|
|
48405
|
+
|
|
48406
|
+
- [Related decision, issue, or evidence.]
|
|
48407
|
+
|
|
48408
|
+
## Appendix
|
|
48409
|
+
|
|
48410
|
+
### Appendix A
|
|
48411
|
+
|
|
48412
|
+
Useful background information.
|
|
48413
|
+
|
|
48414
|
+
### Appendix B
|
|
48415
|
+
|
|
48416
|
+
More useful background information.
|
|
48417
|
+
`;
|
|
48418
|
+
var init_architecture_decision_record = () => {};
|
|
48419
|
+
|
|
48420
|
+
// templates/product-requirements-document.md
|
|
48421
|
+
var product_requirements_document_default = `# [User outcome or product change]
|
|
48422
|
+
|
|
48423
|
+
> @agent: make this closer to the PRD under prd dir!
|
|
48424
|
+
|
|
48425
|
+
- **Status:** draft
|
|
48426
|
+
- **Owner:** [person or team]
|
|
48427
|
+
- **Date:** [YYYY-MM-DD]
|
|
48428
|
+
|
|
48429
|
+
## Summary
|
|
48430
|
+
|
|
48431
|
+
[What changes for users, and why?]
|
|
48432
|
+
|
|
48433
|
+
## Problem and Evidence
|
|
48434
|
+
|
|
48435
|
+
[Who has the problem? Give observed evidence or mark assumptions.]
|
|
48436
|
+
|
|
48437
|
+
## Users and Use Cases
|
|
48438
|
+
|
|
48439
|
+
- [User]: [job to be done and context.]
|
|
48440
|
+
|
|
48441
|
+
## Goals
|
|
48442
|
+
|
|
48443
|
+
- [User outcome to achieve.]
|
|
48444
|
+
|
|
48445
|
+
## Non-Goals
|
|
48446
|
+
|
|
48447
|
+
- [Related need that this work does not address.]
|
|
48448
|
+
|
|
48449
|
+
## Proposed Experience
|
|
48450
|
+
|
|
48451
|
+
[Describe the user journey, inputs, outputs, errors, and recovery.]
|
|
48452
|
+
|
|
48453
|
+
## Requirements
|
|
48454
|
+
|
|
48455
|
+
### [Capability]
|
|
48456
|
+
|
|
48457
|
+
[Required user-facing behavior, edge cases, and constraints.]
|
|
48458
|
+
|
|
48459
|
+
**Acceptance:** [Observable way to check the requirement.]
|
|
48460
|
+
|
|
48461
|
+
## Success Measures
|
|
48462
|
+
|
|
48463
|
+
[Signal, baseline or assumption, target, and when/how it will be measured.]
|
|
48464
|
+
|
|
48465
|
+
## Risks and Dependencies
|
|
48466
|
+
|
|
48467
|
+
- [Risk or dependency]: [mitigation or owner.]
|
|
48468
|
+
|
|
48469
|
+
## Open Questions
|
|
48470
|
+
|
|
48471
|
+
- [Question and decision owner.]
|
|
48472
|
+
|
|
48473
|
+
## Rollout
|
|
48474
|
+
|
|
48475
|
+
[Release, feedback, migration, and rollback plan, if needed.]
|
|
48476
|
+
`;
|
|
48477
|
+
var init_product_requirements_document = () => {};
|
|
48478
|
+
|
|
48479
|
+
// templates/code-review.md
|
|
48480
|
+
var code_review_default = `# Review: [change or pull request]
|
|
48481
|
+
|
|
48482
|
+
- **Revision:** [commit or diff]
|
|
48483
|
+
- **Intent:** [stated objective and source]
|
|
48484
|
+
- **Reviewer:** [name]
|
|
48485
|
+
|
|
48486
|
+
## Intent Delivery
|
|
48487
|
+
|
|
48488
|
+
[Does the change actually deliver the intended behavior? What is missing or outside scope?]
|
|
48489
|
+
|
|
48490
|
+
## Readability
|
|
48491
|
+
|
|
48492
|
+
[Can a new reader follow the flow, names, and error paths? Is code readable for and understandable for an average mide-level developer at a glance?]
|
|
48493
|
+
|
|
48494
|
+
## Semantic Organization
|
|
48495
|
+
|
|
48496
|
+
[Are responsibilities, boundaries, and public APIs arranged by meaning? Are modules semantically meaningful? Is code in applications and libraries well sliced by concerns?]
|
|
48497
|
+
|
|
48498
|
+
## Maintainability
|
|
48499
|
+
|
|
48500
|
+
[Is the code DRY? Is the code over-engineered beyond required scope? Are well trusted / industry-standard libraries and frameworks used for standard tasks? Are domain specific conventions approprate for the codebase being followed? Is basic linting, formatting and static analysis hygiene present?]
|
|
48501
|
+
|
|
48502
|
+
## Reliability/Testing
|
|
48503
|
+
|
|
48504
|
+
[Behavior, edge cases, test coverage, and evidence from executed checks. Adhere to repo testing standards if specified in AGENTS.md or docs. Otherwise, generally prioritize e2e coverage and test readability and organization.]
|
|
48505
|
+
|
|
48506
|
+
## Performance
|
|
48507
|
+
|
|
48508
|
+
[Cost, allocations, I/O, scaling, and measured or expected impact. State SKIPPED with reasons for non-perf bugfixes, minor refactors, and non-performance critical applications.]
|
|
48509
|
+
|
|
48510
|
+
## Security
|
|
48511
|
+
|
|
48512
|
+
[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
|
+
|
|
48514
|
+
## Findings
|
|
48515
|
+
|
|
48516
|
+
| Severity | Location | Evidence and impact | Required change or question |
|
|
48517
|
+
| ------------------------ | ----------- | -------------------------- | --------------------------- |
|
|
48518
|
+
| [blocking/consider/note] | [file:line] | [reproducible observation] | [concrete action] |
|
|
48519
|
+
|
|
48520
|
+
## Overall Assessment
|
|
48521
|
+
|
|
48522
|
+
[Recommendation and reasons; distinguish findings from preferences.]
|
|
48523
|
+
|
|
48524
|
+
## Verification and Limits
|
|
48525
|
+
|
|
48526
|
+
- **Checks run:** [command and result.]
|
|
48527
|
+
- **Not verified:** [environment, path, or behavior not checked.]
|
|
48528
|
+
`;
|
|
48529
|
+
var init_code_review = () => {};
|
|
48530
|
+
|
|
48531
|
+
// templates/planning-intent.md
|
|
48532
|
+
var planning_intent_default = `# Intent: [short name]
|
|
48533
|
+
|
|
48534
|
+
## Objective
|
|
48535
|
+
|
|
48536
|
+
[What do you want to add/change?]
|
|
48537
|
+
|
|
48538
|
+
## Requirements/Instructions
|
|
48539
|
+
|
|
48540
|
+
[Requirements deliverable must meet. Alternately, imperative instructions which must be followed.]
|
|
48541
|
+
|
|
48542
|
+
## Constraints
|
|
48543
|
+
|
|
48544
|
+
[Implementation requirements (particularly non-functional ones), important invariants, behavioral must-haves or limits. Optional.]
|
|
48545
|
+
|
|
48546
|
+
## Open Questions
|
|
48547
|
+
|
|
48548
|
+
[Decisions you want help making. Optional.]
|
|
48549
|
+
`;
|
|
48550
|
+
var init_planning_intent = () => {};
|
|
48551
|
+
|
|
48552
|
+
// templates/ui-component-architecture.md
|
|
48553
|
+
var ui_component_architecture_default = `# [UI component name]
|
|
48554
|
+
|
|
48555
|
+
## Overview
|
|
48556
|
+
|
|
48557
|
+
[What does this component do, who uses it, and where does it appear?]
|
|
48558
|
+
|
|
48559
|
+
## Design
|
|
48560
|
+
|
|
48561
|
+
### Layout
|
|
48562
|
+
|
|
48563
|
+
\`\`\`text
|
|
48564
|
+
+---------------------------------------+
|
|
48565
|
+
| [Title] [Action] |
|
|
48566
|
+
|----------------------------------------|
|
|
48567
|
+
| [Main content] |
|
|
48568
|
+
| [Status or feedback] |
|
|
48569
|
+
+---------------------------------------+
|
|
48570
|
+
\`\`\`
|
|
48571
|
+
|
|
48572
|
+
[Replace the sketch with the actual component layout, including responsive changes. Could also be an image or inline svg.]
|
|
48573
|
+
|
|
48574
|
+
### Behavior
|
|
48575
|
+
|
|
48576
|
+
- **Default:** [Initial content and action.]
|
|
48577
|
+
- **Interaction:** [Inputs, focus changes, and responses.]
|
|
48578
|
+
- **Loading, empty, and error:** [What users see and how they recover.]
|
|
48579
|
+
- **Accessibility:** [Keyboard path, semantics, labels, and announcements.]
|
|
48580
|
+
|
|
48581
|
+
## Implementation
|
|
48582
|
+
|
|
48583
|
+
### Public API
|
|
48584
|
+
|
|
48585
|
+
[Props, events, slots, and example usage.]
|
|
48586
|
+
|
|
48587
|
+
### State and Data Flow
|
|
48588
|
+
|
|
48589
|
+
[Who owns state? What is derived? What is persisted?]
|
|
48590
|
+
|
|
48591
|
+
### Styling and Responsiveness
|
|
48592
|
+
|
|
48593
|
+
[Tokens, breakpoints, layout constraints, loading behavior, interaction design, etc. Optional. Often theme controlled.]
|
|
48594
|
+
|
|
48595
|
+
### Maintainer Notes
|
|
48596
|
+
|
|
48597
|
+
[Non-obvious invariants, testing strategy, performance concerns, and known limits.]
|
|
48598
|
+
`;
|
|
48599
|
+
var init_ui_component_architecture = () => {};
|
|
48600
|
+
|
|
48601
|
+
// templates/pull-request-description.md
|
|
48602
|
+
var pull_request_description_default = `## Intent
|
|
48603
|
+
|
|
48604
|
+
{{intent}}
|
|
48605
|
+
|
|
48606
|
+
## Changes
|
|
48607
|
+
|
|
48608
|
+
{{changes}}
|
|
48609
|
+
|
|
48610
|
+
## Validation
|
|
48611
|
+
|
|
48612
|
+
{{validation}}
|
|
48613
|
+
|
|
48614
|
+
## Further Work
|
|
48615
|
+
|
|
48616
|
+
{{further_work}}
|
|
48617
|
+
|
|
48618
|
+
## References
|
|
48619
|
+
|
|
48620
|
+
- Issue: {{issue_url}}
|
|
48621
|
+
`;
|
|
48622
|
+
var init_pull_request_description = () => {};
|
|
48623
|
+
|
|
48624
|
+
// templates/examples/plan-feature.md
|
|
48625
|
+
var plan_feature_default = `# Invite teammates to a project
|
|
48626
|
+
|
|
48627
|
+
- **Status:** draft
|
|
48628
|
+
- **Branch:** feature/project-invites
|
|
48629
|
+
- **Issue:** EX-12
|
|
48630
|
+
|
|
48631
|
+
## Intent
|
|
48632
|
+
|
|
48633
|
+
Project owners need to invite teammates without sharing credentials. Recipients should know which project they are joining before accepting.
|
|
48634
|
+
|
|
48635
|
+
## Requirements
|
|
48636
|
+
|
|
48637
|
+
- An owner can send an invite to a valid email address and see its pending state.
|
|
48638
|
+
- Only the intended recipient can accept; expired or used invites fail without changing membership.
|
|
48639
|
+
- Bulk invitations and organization-level roles are out of scope.
|
|
48640
|
+
|
|
48641
|
+
## Design
|
|
48642
|
+
|
|
48643
|
+
### Big Ideas
|
|
48644
|
+
|
|
48645
|
+
Store a single-use invitation tied to project, email, and expiry. Acceptance adds membership in the same transaction that consumes the invitation. Queue delivery after saving the invite so a temporary email failure cannot lose it.
|
|
48646
|
+
|
|
48647
|
+
### Interfaces
|
|
48648
|
+
|
|
48649
|
+
\`POST /projects/:id/invitations\` creates a pending invite; \`POST /invitations/:token/accept\` returns membership or a safe expired/used error. \`src/invitations/store.ts\` owns persistence; \`src/invitations/service.ts\` owns eligibility, expiry, and token consumption; \`src/http/invitations.ts\` maps results to responses.
|
|
48650
|
+
|
|
48651
|
+
### Consequences
|
|
48652
|
+
|
|
48653
|
+
Email delivery can fail after persistence, so queue retries and show a pending invitation. Rate-limit creation to avoid abuse. Resend and cancellation are deferred.
|
|
48654
|
+
|
|
48655
|
+
## Phases
|
|
48656
|
+
|
|
48657
|
+
### Phase 1: Persist and consume invitations
|
|
48658
|
+
|
|
48659
|
+
- [ ] Add a unique token hash, recipient email, project ID, expiry, and consumed timestamp to invitation storage in \`src/invitations/store.ts\`; never log raw tokens.
|
|
48660
|
+
- [ ] Implement transactional membership creation and token consumption in \`src/invitations/service.ts\`; reject expired, used, or wrong-recipient tokens without a membership change.
|
|
48661
|
+
- [ ] Add boundary, repeat-use, and rollback tests in \`src/invitations/service.test.ts\`.
|
|
48662
|
+
- [ ] Validation
|
|
48663
|
+
- [ ] Run invitation unit tests, lint, and format checks.
|
|
48664
|
+
- [ ] Commit this phase and confirm CI is green.
|
|
48665
|
+
|
|
48666
|
+
### Phase 2: Expose and deliver invitations
|
|
48667
|
+
|
|
48668
|
+
- [ ] Add owner creation and recipient acceptance handlers in \`src/http/invitations.ts\` with safe error responses and creation rate limits.
|
|
48669
|
+
- [ ] Queue delivery after persistence, retry transient mail failures, and expose the pending state to owners.
|
|
48670
|
+
- [ ] Add endpoint integration tests for authorization, expiry, used tokens, and delivery failures in \`src/http/invitations.spec.ts\`.
|
|
48671
|
+
- [ ] Validation
|
|
48672
|
+
- [ ] Run unit and integration tests, lint, and format checks.
|
|
48673
|
+
- [ ] Commit this phase and confirm CI is green.
|
|
48674
|
+
|
|
48675
|
+
## References
|
|
48676
|
+
|
|
48677
|
+
- EX-12 \u2014 sample product request.
|
|
48678
|
+
`;
|
|
48679
|
+
var init_plan_feature = () => {};
|
|
48680
|
+
|
|
48681
|
+
// templates/examples/plan-migration.md
|
|
48682
|
+
var plan_migration_default = `# Move activity history to a new store
|
|
48683
|
+
|
|
48684
|
+
- **Status:** draft
|
|
48685
|
+
- **Branch:** feature/activity-migration
|
|
48686
|
+
- **Issue:** EX-27
|
|
48687
|
+
|
|
48688
|
+
## Intent
|
|
48689
|
+
|
|
48690
|
+
Readers should retain access to historical activity while the team changes storage engines without downtime.
|
|
48691
|
+
|
|
48692
|
+
## Requirements
|
|
48693
|
+
|
|
48694
|
+
- Old and new records produce the same user-visible history, order, and cursors.
|
|
48695
|
+
- Rollback can restore the prior reader without losing activity written during migration.
|
|
48696
|
+
- No reader switches until backfill and comparison pass.
|
|
48697
|
+
|
|
48698
|
+
## Design
|
|
48699
|
+
|
|
48700
|
+
### Big Ideas
|
|
48701
|
+
|
|
48702
|
+
Write to both stores temporarily, backfill older records with stable IDs, compare counts and ordering, then switch readers behind a reversible flag. Keep the legacy reader authoritative until the new store is verified.
|
|
48703
|
+
|
|
48704
|
+
### Infrastructure Changes
|
|
48705
|
+
|
|
48706
|
+
Provision the new activity store and credentials before dual writes. Retain the legacy store and its capacity until the rollback window closes; monitor write failures for each store separately.
|
|
48707
|
+
|
|
48708
|
+
### Interfaces
|
|
48709
|
+
|
|
48710
|
+
\`listActivity(projectId, cursor)\` preserves ordering and cursor behavior during cutover. \`src/activity/writer.ts\` owns dual writes, \`src/activity/backfill.ts\` copies historical records, and \`src/activity/reader.ts\` selects the store behind the feature flag.
|
|
48711
|
+
|
|
48712
|
+
### Consequences
|
|
48713
|
+
|
|
48714
|
+
Dual writes increase latency and create a risk of partial failure. Record discrepancies and retry failed writes instead of hiding them. Removing the legacy store is outside this plan.
|
|
48715
|
+
|
|
48716
|
+
## Phases
|
|
48717
|
+
|
|
48718
|
+
### Phase 1: Provision and dual-write
|
|
48719
|
+
|
|
48720
|
+
- [ ] Provision the new store and credentials; add write-failure metrics for both stores.
|
|
48721
|
+
- [ ] Make \`src/activity/writer.ts\` write idempotently to both stores with stable activity IDs while retaining the legacy reader.
|
|
48722
|
+
- [ ] Test partial-write retries and duplicate events in \`src/activity/writer.test.ts\`.
|
|
48723
|
+
- [ ] Validation
|
|
48724
|
+
- [ ] Run writer tests, lint, and format checks; confirm write-failure metrics are visible.
|
|
48725
|
+
- [ ] Commit this phase and confirm CI is green.
|
|
48726
|
+
|
|
48727
|
+
### Phase 2: Backfill and compare
|
|
48728
|
+
|
|
48729
|
+
- [ ] Add resumable, idempotent batches in \`src/activity/backfill.ts\` without replacing newer dual-written records.
|
|
48730
|
+
- [ ] Compare record IDs, counts, and ordering per project; report mismatches before any reader switch.
|
|
48731
|
+
- [ ] Add backfill restart and parity tests in \`src/activity/backfill.test.ts\`.
|
|
48732
|
+
- [ ] Validation
|
|
48733
|
+
- [ ] Run backfill and writer tests, lint, and format checks; review mismatch reports.
|
|
48734
|
+
- [ ] Commit this phase and confirm CI is green.
|
|
48735
|
+
|
|
48736
|
+
### Phase 3: Switch reads with rollback
|
|
48737
|
+
|
|
48738
|
+
- [ ] Route \`listActivity(projectId, cursor)\` through a reversible flag in \`src/activity/reader.ts\` and preserve cursor compatibility.
|
|
48739
|
+
- [ ] Test new-store reads, cursor boundaries, and rollback to the legacy reader in \`src/activity/reader.spec.ts\`.
|
|
48740
|
+
- [ ] Keep dual writes and monitor parity through the rollback window.
|
|
48741
|
+
- [ ] Validation
|
|
48742
|
+
- [ ] Run unit and integration tests, lint, and format checks; verify flag rollback.
|
|
48743
|
+
- [ ] Commit this phase and confirm CI is green.
|
|
48744
|
+
|
|
48745
|
+
## References
|
|
48746
|
+
|
|
48747
|
+
- EX-27 \u2014 sample migration request.
|
|
48748
|
+
`;
|
|
48749
|
+
var init_plan_migration = () => {};
|
|
48750
|
+
|
|
48751
|
+
// templates/examples/design-service.md
|
|
48752
|
+
var design_service_default = `# Notification service \u2014 software architecture
|
|
48753
|
+
|
|
48754
|
+
## Overview
|
|
48755
|
+
|
|
48756
|
+
The service delivers email notifications for account events. It consumes messages from the application queue; it does not decide who should receive a message or block account requests on email delivery.
|
|
48757
|
+
|
|
48758
|
+
## Requirement
|
|
48759
|
+
|
|
48760
|
+
### Functional
|
|
48761
|
+
|
|
48762
|
+
- FR1: Consume and validate account events with an ID, type, recipient, and timestamp.
|
|
48763
|
+
- FR2: Deduplicate deliveries, send email, and record each attempt and its outcome.
|
|
48764
|
+
- FR3: Retry transient provider failures and report permanent rejections.
|
|
48765
|
+
|
|
48766
|
+
### Non-Functional
|
|
48767
|
+
|
|
48768
|
+
- NFR1: Queue redelivery must not send the same event twice; retain a durable event ID for deduplication.
|
|
48769
|
+
- NFR2: Provider credentials must stay outside event payloads and the account service.
|
|
48770
|
+
- NFR3: A burst of events must not delay account requests; alert when the queue grows.
|
|
48771
|
+
|
|
48772
|
+
## Design
|
|
48773
|
+
|
|
48774
|
+
### Components and Responsibilities
|
|
48775
|
+
|
|
48776
|
+
\`\`\`mermaid
|
|
48777
|
+
flowchart LR
|
|
48778
|
+
Account[Account service] -->|Account event| Queue[(Durable queue)]
|
|
48779
|
+
Queue --> Consumer[Notification consumer]
|
|
48780
|
+
Consumer --> Sender[Email sender]
|
|
48781
|
+
Sender --> Provider[Email API]
|
|
48782
|
+
Consumer --> Store[(Attempt store)]
|
|
48783
|
+
\`\`\`
|
|
48784
|
+
|
|
48785
|
+
- The account service publishes events and knows only the event contract.
|
|
48786
|
+
- The durable queue isolates delivery from account requests and retries transient failures.
|
|
48787
|
+
- The consumer validates and deduplicates events; the sender formats and submits mail to the provider.
|
|
48788
|
+
- The attempt store records event IDs, delivery outcomes, and retry attempts.
|
|
48789
|
+
|
|
48790
|
+
### Data and Control Flow
|
|
48791
|
+
|
|
48792
|
+
An account event enters the queue. The consumer validates it and checks the attempt store for its ID before the sender calls the provider. The consumer records the result; a transient failure is retried, while a permanent rejection is reported. Sending synchronously would avoid the queue but could delay account requests whenever the provider is slow.
|
|
48793
|
+
|
|
48794
|
+
### Interfaces and Contracts
|
|
48795
|
+
|
|
48796
|
+
\`AccountEvent\` contains \`id\`, \`type\`, \`recipient\`, and \`timestamp\`. \`handle(event)\` consumes the event, \`send(message)\` calls the provider, and \`record(attempt)\` persists its result. The event ID is the deduplication key; provider credentials never enter the payload.
|
|
48797
|
+
|
|
48798
|
+
## Implementation
|
|
48799
|
+
|
|
48800
|
+
### Consumer and storage
|
|
48801
|
+
|
|
48802
|
+
The notification service owns its consumer and attempt store; the account service imports only the event contract. Persist the deduplication key with the delivery outcome so queue redelivery is safe. Integration tests simulate duplicate events and provider timeouts.
|
|
48803
|
+
|
|
48804
|
+
### Delivery operations
|
|
48805
|
+
|
|
48806
|
+
Keep provider credentials in the notification service. Alert on a growing queue and repeated provider failures; disable sending and drain pending work if credentials are compromised. Load tests cover bursts without slowing the account service.
|
|
48807
|
+
|
|
48808
|
+
## References
|
|
48809
|
+
|
|
48810
|
+
- EX-34 \u2014 example notification requirement.
|
|
48811
|
+
`;
|
|
48812
|
+
var init_design_service = () => {};
|
|
48813
|
+
|
|
48814
|
+
// templates/examples/adr-storage.md
|
|
48815
|
+
var adr_storage_default = `# Use object storage for user exports
|
|
48816
|
+
|
|
48817
|
+
- **Status:** accepted
|
|
48818
|
+
- **Deciders:** Platform team
|
|
48819
|
+
- **Date:** 2026-01-14
|
|
48820
|
+
|
|
48821
|
+
## Context and Problem Statement
|
|
48822
|
+
|
|
48823
|
+
Exports may exceed the response size limit of the application server. Where should completed exports live until users download them?
|
|
48824
|
+
|
|
48825
|
+
## Decision Drivers
|
|
48826
|
+
|
|
48827
|
+
- Exports must expire after seven days.
|
|
48828
|
+
- The application server should not retain large files on local disk.
|
|
48829
|
+
|
|
48830
|
+
## Considered Options
|
|
48831
|
+
|
|
48832
|
+
### Object storage
|
|
48833
|
+
|
|
48834
|
+
- Good: Lifecycle rules remove expired exports without an application job.
|
|
48835
|
+
- Bad: Requires bucket credentials and signed download URLs.
|
|
48836
|
+
|
|
48837
|
+
### Database
|
|
48838
|
+
|
|
48839
|
+
- Good: Existing backup process covers exports.
|
|
48840
|
+
- Bad: Large blobs make backups slow and expensive.
|
|
48841
|
+
|
|
48842
|
+
## Decision Outcome
|
|
48843
|
+
|
|
48844
|
+
Chosen option: object storage, because it provides expiry policies and direct downloads without loading large files into application memory.
|
|
48845
|
+
|
|
48846
|
+
### Positive Consequences
|
|
48847
|
+
|
|
48848
|
+
- Large exports do not occupy application disks.
|
|
48849
|
+
|
|
48850
|
+
### Negative Consequences
|
|
48851
|
+
|
|
48852
|
+
- Download authorization requires short-lived signed URLs.
|
|
48853
|
+
|
|
48854
|
+
## References
|
|
48855
|
+
|
|
48856
|
+
- EX-41 \u2014 example export requirement.
|
|
48857
|
+
|
|
48858
|
+
## Appendix
|
|
48859
|
+
|
|
48860
|
+
### Appendix A
|
|
48861
|
+
|
|
48862
|
+
A seven-day lifecycle rule deletes exports even if a user never downloads them. Issue signed URLs only after checking that the requester owns the export.
|
|
48863
|
+
|
|
48864
|
+
### Appendix B
|
|
48865
|
+
|
|
48866
|
+
An export job records the object key and expiry time so the download endpoint can reject expired requests before generating a URL.
|
|
48867
|
+
`;
|
|
48868
|
+
var init_adr_storage = () => {};
|
|
48869
|
+
|
|
48870
|
+
// templates/examples/prd-onboarding.md
|
|
48871
|
+
var prd_onboarding_default = `# Help new members finish project onboarding
|
|
48872
|
+
|
|
48873
|
+
- **Status:** draft
|
|
48874
|
+
- **Owner:** Product team
|
|
48875
|
+
- **Date:** 2026-01-14
|
|
48876
|
+
|
|
48877
|
+
## Summary
|
|
48878
|
+
|
|
48879
|
+
Give invited members a short checklist so they can find their project and complete their first task.
|
|
48880
|
+
|
|
48881
|
+
## Problem and Evidence
|
|
48882
|
+
|
|
48883
|
+
New members frequently ask where to start after accepting an invite. This is a product assumption until interviews confirm the pattern.
|
|
48884
|
+
|
|
48885
|
+
## Users and Use Cases
|
|
48886
|
+
|
|
48887
|
+
- New project member: find the project, read its purpose, and finish a first task.
|
|
48888
|
+
|
|
48889
|
+
## Goals
|
|
48890
|
+
|
|
48891
|
+
- Members can find their first task without contacting an administrator.
|
|
48892
|
+
|
|
48893
|
+
## Non-Goals
|
|
48894
|
+
|
|
48895
|
+
- Training on every product feature.
|
|
48896
|
+
|
|
48897
|
+
## Proposed Experience
|
|
48898
|
+
|
|
48899
|
+
After joining, a member sees a three-step checklist with links to the project overview and an assigned starter task. Dismissal is reversible from the help menu.
|
|
48900
|
+
|
|
48901
|
+
## Requirements
|
|
48902
|
+
|
|
48903
|
+
### Visible next step
|
|
48904
|
+
|
|
48905
|
+
Show one task assigned to the member, or a clear empty state when none exists.
|
|
48906
|
+
|
|
48907
|
+
**Acceptance:** A member with no assigned task sees an actionable empty state, not a blank panel.
|
|
48908
|
+
|
|
48909
|
+
## Success Measures
|
|
48910
|
+
|
|
48911
|
+
Measure the share of invited members who open a project and complete a task within seven days; record a baseline before launch.
|
|
48912
|
+
|
|
48913
|
+
## Risks and Dependencies
|
|
48914
|
+
|
|
48915
|
+
- Task assignment data may be delayed: refresh the checklist when assignment changes.
|
|
48916
|
+
|
|
48917
|
+
## Open Questions
|
|
48918
|
+
|
|
48919
|
+
- Should dismissed checklists reappear for new projects? Product owner to decide.
|
|
48920
|
+
|
|
48921
|
+
## Rollout
|
|
48922
|
+
|
|
48923
|
+
Enable for a small cohort, compare completion and feedback, then expand or disable.
|
|
48924
|
+
`;
|
|
48925
|
+
var init_prd_onboarding = () => {};
|
|
48926
|
+
|
|
48927
|
+
// templates/examples/review-change.md
|
|
48928
|
+
var review_change_default = `# Review: invitation expiry change
|
|
48929
|
+
|
|
48930
|
+
- **Revision:** example-change-17
|
|
48931
|
+
- **Intent:** Reject expired invitations without creating membership.
|
|
48932
|
+
- **Reviewer:** Example reviewer
|
|
48933
|
+
|
|
48934
|
+
## Intent Delivery
|
|
48935
|
+
|
|
48936
|
+
The acceptance path checks expiry before membership creation. The resend path was not part of this change.
|
|
48937
|
+
|
|
48938
|
+
## Readability
|
|
48939
|
+
|
|
48940
|
+
Function names describe outcomes, and the request path is understandable without tracing helpers. Move the expiry guard before token decoding so a mid-level maintainer can see the rejection path at a glance.
|
|
48941
|
+
|
|
48942
|
+
## Semantic Organization
|
|
48943
|
+
|
|
48944
|
+
Invitation policy belongs in the invitation service, not the HTTP handler. The handler should own transport errors; the service should own token and membership rules.
|
|
48945
|
+
|
|
48946
|
+
## Maintainability
|
|
48947
|
+
|
|
48948
|
+
The shared expiry helper avoids duplicate rules in request and background paths. No new library or abstraction is needed for this change; formatting and static analysis should remain clean.
|
|
48949
|
+
|
|
48950
|
+
## Reliability/Testing
|
|
48951
|
+
|
|
48952
|
+
Tests cover expiry at the boundary and a used token. A clock-skew case is missing; add a readable test that checks the server clock source and confirms no membership is created on rejection.
|
|
48953
|
+
|
|
48954
|
+
## Performance
|
|
48955
|
+
|
|
48956
|
+
One lookup occurs per acceptance; no new unbounded loop is present.
|
|
48957
|
+
|
|
48958
|
+
## Security
|
|
48959
|
+
|
|
48960
|
+
The response does not reveal whether an address has an account. The recipient is authorized before the token is consumed, tokens are treated as opaque values, and failed acceptance does not create membership.
|
|
48961
|
+
|
|
48962
|
+
## Findings
|
|
48963
|
+
|
|
48964
|
+
| Severity | Location | Evidence and impact | Required change or question |
|
|
48965
|
+
| -------- | ----------------------- | ------------------------------------------------- | ----------------------------------------------------- |
|
|
48966
|
+
| consider | \`src/invitations.ts:52\` | Clock skew near expiry may reject valid requests. | Add a boundary test and document server clock source. |
|
|
48967
|
+
|
|
48968
|
+
## Overall Assessment
|
|
48969
|
+
|
|
48970
|
+
No blocking issue found in the reviewed path; address the boundary test before release.
|
|
48971
|
+
|
|
48972
|
+
## Verification and Limits
|
|
48973
|
+
|
|
48974
|
+
- **Checks run:** \`bun test src/invitations.test.ts\` \u2014 passed in this example.
|
|
48975
|
+
- **Not verified:** Real email delivery and production clock behavior.
|
|
48976
|
+
`;
|
|
48977
|
+
var init_review_change = () => {};
|
|
48978
|
+
|
|
48979
|
+
// templates/examples/intent-request.md
|
|
48980
|
+
var intent_request_default = `# Intent: Faster project search
|
|
48981
|
+
|
|
48982
|
+
## Objective
|
|
48983
|
+
|
|
48984
|
+
People with many projects need to find a project by name without scrolling through the full list. Support has received requests about this, but we do not yet have a measured search-time baseline.
|
|
48985
|
+
|
|
48986
|
+
## Requirements/Instructions
|
|
48987
|
+
|
|
48988
|
+
- Let users search accessible projects by name while keeping the existing project list available.
|
|
48989
|
+
- Do not add search for tasks or users.
|
|
48990
|
+
|
|
48991
|
+
## Constraints
|
|
48992
|
+
|
|
48993
|
+
Search must work from the keyboard and must not reveal projects a user cannot access.
|
|
48994
|
+
|
|
48995
|
+
## Open Questions
|
|
48996
|
+
|
|
48997
|
+
Should results include archived projects by default?
|
|
48998
|
+
`;
|
|
48999
|
+
var init_intent_request = () => {};
|
|
49000
|
+
|
|
49001
|
+
// templates/examples/component-dialog.md
|
|
49002
|
+
var component_dialog_default = `# ConfirmDeleteDialog
|
|
49003
|
+
|
|
49004
|
+
## Overview
|
|
49005
|
+
|
|
49006
|
+
A modal asks for confirmation before a destructive action. It appears only when the user chooses Delete.
|
|
49007
|
+
|
|
49008
|
+
## Design
|
|
49009
|
+
|
|
49010
|
+
### Layout
|
|
49011
|
+
|
|
49012
|
+
\`\`\`text
|
|
49013
|
+
+------------------------------------------+
|
|
49014
|
+
| Delete project? [X] |
|
|
49015
|
+
|------------------------------------------|
|
|
49016
|
+
| This removes the project and its tasks. |
|
|
49017
|
+
| [Cancel] [Delete] |
|
|
49018
|
+
+------------------------------------------+
|
|
49019
|
+
\`\`\`
|
|
49020
|
+
|
|
49021
|
+
The buttons stack on narrow screens while preserving Cancel before Delete in keyboard order.
|
|
49022
|
+
|
|
49023
|
+
### Behavior
|
|
49024
|
+
|
|
49025
|
+
- **Default:** Focus the title or Cancel button; explain the affected project.
|
|
49026
|
+
- **Interaction:** Cancel and Escape close without deleting; Delete submits once.
|
|
49027
|
+
- **Loading, empty, and error:** Disable repeat submit; report server failure and allow retry.
|
|
49028
|
+
- **Accessibility:** Use a labelled modal, trap focus while open, and restore focus to Delete trigger on close.
|
|
49029
|
+
|
|
49030
|
+
## Implementation
|
|
49031
|
+
|
|
49032
|
+
### Public API
|
|
49033
|
+
|
|
49034
|
+
\`ConfirmDeleteDialog({ projectName, open, onCancel, onConfirm })\` receives the project name and emits confirmation only once per open cycle.
|
|
49035
|
+
|
|
49036
|
+
### State and Data Flow
|
|
49037
|
+
|
|
49038
|
+
Parent owns \`open\` and performs deletion; the dialog owns only pending/error display.
|
|
49039
|
+
|
|
49040
|
+
### Styling and Responsiveness
|
|
49041
|
+
|
|
49042
|
+
Use shared spacing tokens and a 28rem maximum width; stack actions below 22rem.
|
|
49043
|
+
|
|
49044
|
+
### Maintainer Notes
|
|
49045
|
+
|
|
49046
|
+
Use the design system modal primitive for focus management. Test Escape, focus restoration, double-submit prevention, and the server error path.
|
|
49047
|
+
`;
|
|
49048
|
+
var init_component_dialog = () => {};
|
|
49049
|
+
|
|
49050
|
+
// templates/examples/component-navigation.md
|
|
49051
|
+
var component_navigation_default = `# ProjectNavigation
|
|
49052
|
+
|
|
49053
|
+
## Overview
|
|
49054
|
+
|
|
49055
|
+
A navigation rail lets members switch between project sections. The active section is always identifiable without color alone.
|
|
49056
|
+
|
|
49057
|
+
## Design
|
|
49058
|
+
|
|
49059
|
+
### Layout
|
|
49060
|
+
|
|
49061
|
+
\`\`\`text
|
|
49062
|
+
+---------------------+
|
|
49063
|
+
| Project name |
|
|
49064
|
+
|---------------------|
|
|
49065
|
+
| > Overview |
|
|
49066
|
+
| Tasks |
|
|
49067
|
+
| Activity |
|
|
49068
|
+
|---------------------|
|
|
49069
|
+
| Settings |
|
|
49070
|
+
+---------------------+
|
|
49071
|
+
\`\`\`
|
|
49072
|
+
|
|
49073
|
+
At narrow widths, the rail becomes a labelled navigation menu.
|
|
49074
|
+
|
|
49075
|
+
### Behavior
|
|
49076
|
+
|
|
49077
|
+
- **Default:** Highlight the route that matches the current page.
|
|
49078
|
+
- **Interaction:** Arrow keys move focus within the menu; Enter activates a route.
|
|
49079
|
+
- **Loading, empty, and error:** Render available routes while project metadata loads; show a safe fallback label on failure.
|
|
49080
|
+
- **Accessibility:** Use a \`nav\` landmark, accessible item names, and \`aria-current="page"\` for the active link.
|
|
49081
|
+
|
|
49082
|
+
## Implementation
|
|
49083
|
+
|
|
49084
|
+
### Public API
|
|
49085
|
+
|
|
49086
|
+
\`ProjectNavigation({ project, currentRoute, routes })\` receives visible routes already filtered for permissions.
|
|
49087
|
+
|
|
49088
|
+
### State and Data Flow
|
|
49089
|
+
|
|
49090
|
+
The router owns navigation state; the component derives the active item from \`currentRoute\`.
|
|
49091
|
+
|
|
49092
|
+
### Styling and Responsiveness
|
|
49093
|
+
|
|
49094
|
+
Use existing navigation tokens; collapse below the compact breakpoint.
|
|
49095
|
+
|
|
49096
|
+
### Maintainer Notes
|
|
49097
|
+
|
|
49098
|
+
Route links come from the application router; do not duplicate permission checks in display code. Test route changes, keyboard operation, permission-filtered items, and compact layout.
|
|
49099
|
+
`;
|
|
49100
|
+
var init_component_navigation = () => {};
|
|
49101
|
+
|
|
49102
|
+
// templates/examples/pull-request-feature.md
|
|
49103
|
+
var pull_request_feature_default = `## Intent
|
|
49104
|
+
|
|
49105
|
+
Let project owners invite teammates without sharing credentials.
|
|
49106
|
+
|
|
49107
|
+
## Changes
|
|
49108
|
+
|
|
49109
|
+
- Added a single-use invitation token with an expiry.
|
|
49110
|
+
- Added an acceptance endpoint that creates membership and consumes the token atomically.
|
|
49111
|
+
- Added owner and recipient error handling.
|
|
49112
|
+
|
|
49113
|
+
## Validation
|
|
49114
|
+
|
|
49115
|
+
- \`bun test src/invitations.test.ts\` \u2014 passed in this example.
|
|
49116
|
+
- Manual check: expired and already-used tokens did not grant membership.
|
|
49117
|
+
|
|
49118
|
+
## Further Work
|
|
49119
|
+
|
|
49120
|
+
Add invitation resend and cancellation in a separate change.
|
|
49121
|
+
|
|
49122
|
+
## References
|
|
49123
|
+
|
|
49124
|
+
- Issue: EX-12 (example)
|
|
49125
|
+
`;
|
|
49126
|
+
var init_pull_request_feature = () => {};
|
|
49127
|
+
|
|
49128
|
+
// src/templates/source.ts
|
|
49129
|
+
var catalog, contents, embeddedSource;
|
|
49130
|
+
var init_source = __esm(() => {
|
|
49131
|
+
init_spec_driven_plan();
|
|
49132
|
+
init_software_architecture_design();
|
|
49133
|
+
init_architecture_decision_record();
|
|
49134
|
+
init_product_requirements_document();
|
|
49135
|
+
init_code_review();
|
|
49136
|
+
init_planning_intent();
|
|
49137
|
+
init_ui_component_architecture();
|
|
49138
|
+
init_pull_request_description();
|
|
49139
|
+
init_plan_feature();
|
|
49140
|
+
init_plan_migration();
|
|
49141
|
+
init_design_service();
|
|
49142
|
+
init_adr_storage();
|
|
49143
|
+
init_prd_onboarding();
|
|
49144
|
+
init_review_change();
|
|
49145
|
+
init_intent_request();
|
|
49146
|
+
init_component_dialog();
|
|
49147
|
+
init_component_navigation();
|
|
49148
|
+
init_pull_request_feature();
|
|
49149
|
+
catalog = [
|
|
49150
|
+
{ name: "spec-driven-plan", description: "Spec-driven implementation plan" },
|
|
49151
|
+
{ name: "software-architecture-design", description: "Software architecture design" },
|
|
49152
|
+
{ name: "architecture-decision-record", description: "Architecture decision record" },
|
|
49153
|
+
{ name: "product-requirements-document", description: "Product requirements document" },
|
|
49154
|
+
{ name: "code-review", description: "Thorough code review" },
|
|
49155
|
+
{ name: "planning-intent", description: "Planning intent for a user to fill in" },
|
|
49156
|
+
{ name: "ui-component-architecture", description: "UI component design and implementation" },
|
|
49157
|
+
{ name: "pull-request-description", description: "Draft pull request description" }
|
|
49158
|
+
];
|
|
49159
|
+
contents = {
|
|
49160
|
+
"spec-driven-plan.md": spec_driven_plan_default,
|
|
49161
|
+
"software-architecture-design.md": software_architecture_design_default,
|
|
49162
|
+
"architecture-decision-record.md": architecture_decision_record_default,
|
|
49163
|
+
"product-requirements-document.md": product_requirements_document_default,
|
|
49164
|
+
"code-review.md": code_review_default,
|
|
49165
|
+
"planning-intent.md": planning_intent_default,
|
|
49166
|
+
"ui-component-architecture.md": ui_component_architecture_default,
|
|
49167
|
+
"pull-request-description.md": pull_request_description_default,
|
|
49168
|
+
"examples/plan-feature.md": plan_feature_default,
|
|
49169
|
+
"examples/plan-migration.md": plan_migration_default,
|
|
49170
|
+
"examples/design-service.md": design_service_default,
|
|
49171
|
+
"examples/adr-storage.md": adr_storage_default,
|
|
49172
|
+
"examples/prd-onboarding.md": prd_onboarding_default,
|
|
49173
|
+
"examples/review-change.md": review_change_default,
|
|
49174
|
+
"examples/intent-request.md": intent_request_default,
|
|
49175
|
+
"examples/component-dialog.md": component_dialog_default,
|
|
49176
|
+
"examples/component-navigation.md": component_navigation_default,
|
|
49177
|
+
"examples/pull-request-feature.md": pull_request_feature_default
|
|
49178
|
+
};
|
|
49179
|
+
embeddedSource = {
|
|
49180
|
+
catalog,
|
|
49181
|
+
assets: Object.keys(contents),
|
|
49182
|
+
async read(asset) {
|
|
49183
|
+
const content = contents[asset];
|
|
49184
|
+
if (content === undefined)
|
|
49185
|
+
throw new Error(`Missing embedded template asset: ${asset}`);
|
|
49186
|
+
return content;
|
|
49187
|
+
}
|
|
49188
|
+
};
|
|
49189
|
+
});
|
|
49190
|
+
|
|
49191
|
+
// src/templates/service.ts
|
|
49192
|
+
var exports_service = {};
|
|
49193
|
+
__export(exports_service, {
|
|
49194
|
+
TemplateService: () => TemplateService
|
|
49195
|
+
});
|
|
49196
|
+
import { constants } from "fs";
|
|
49197
|
+
import { lstat as lstat6, open as open2, rm as rm3 } from "fs/promises";
|
|
49198
|
+
import { isAbsolute as isAbsolute6, join as join7 } from "path";
|
|
49199
|
+
|
|
49200
|
+
class TemplateService {
|
|
49201
|
+
home;
|
|
49202
|
+
source;
|
|
49203
|
+
constructor(options = {}) {
|
|
49204
|
+
this.home = resolveHomeDirectory(options.home);
|
|
49205
|
+
this.source = options.source ?? embeddedSource;
|
|
49206
|
+
}
|
|
49207
|
+
async listTemplates() {
|
|
49208
|
+
await this.installMissing();
|
|
49209
|
+
return [...this.source.catalog];
|
|
49210
|
+
}
|
|
49211
|
+
async scaffoldFromTemplate(name, cwd, path, filename) {
|
|
49212
|
+
if (!this.source.catalog.some((item) => item.name === name)) {
|
|
49213
|
+
throw new UnknownTemplateError(`Unknown template: ${name}`);
|
|
49214
|
+
}
|
|
49215
|
+
if (!path.trim() || isAbsolute6(path)) {
|
|
49216
|
+
throw new TemplateError("path must be a non-empty relative directory inside cwd");
|
|
49217
|
+
}
|
|
49218
|
+
if (!filename.trim() || filename === "." || filename === ".." || /[/\\\0]/.test(filename)) {
|
|
49219
|
+
throw new TemplateError("filename must be a single non-empty file name");
|
|
49220
|
+
}
|
|
49221
|
+
await this.installMissing();
|
|
49222
|
+
const installed = await resolvePath(this.home, join7(".difflab", "templates", `${name}.md`));
|
|
49223
|
+
const contents = await readMarkdown(installed);
|
|
49224
|
+
const destination = await ensurePathDirsExist(join7(path, filename), cwd);
|
|
49225
|
+
try {
|
|
49226
|
+
await createExclusive(destination, contents);
|
|
49227
|
+
} catch (error) {
|
|
49228
|
+
if (hasErrorCode(error, "EEXIST")) {
|
|
49229
|
+
throw new TemplateDestinationExistsError(`Destination already exists: ${destination}`, {
|
|
49230
|
+
cause: error
|
|
49231
|
+
});
|
|
49232
|
+
}
|
|
49233
|
+
throw error;
|
|
49234
|
+
}
|
|
49235
|
+
return destination;
|
|
49236
|
+
}
|
|
49237
|
+
async installMissing() {
|
|
49238
|
+
for (const asset of this.source.assets) {
|
|
49239
|
+
const relative = join7(".difflab", "templates", asset);
|
|
49240
|
+
const destination = await ensurePathDirsExist(relative, this.home);
|
|
49241
|
+
try {
|
|
49242
|
+
if ((await lstat6(destination)).isSymbolicLink()) {
|
|
49243
|
+
throw new TemplateSourceError(`Installed template asset is a symlink: ${destination}`);
|
|
49244
|
+
}
|
|
49245
|
+
await readMarkdown(destination);
|
|
49246
|
+
} catch (error) {
|
|
49247
|
+
if (!hasErrorCode(error, "ENOENT"))
|
|
49248
|
+
throw error;
|
|
49249
|
+
const content = await readSource(this.source, asset);
|
|
49250
|
+
try {
|
|
49251
|
+
await createExclusive(destination, content);
|
|
49252
|
+
} catch (writeError) {
|
|
49253
|
+
if (!hasErrorCode(writeError, "EEXIST"))
|
|
49254
|
+
throw writeError;
|
|
49255
|
+
await readMarkdown(destination);
|
|
49256
|
+
}
|
|
49257
|
+
}
|
|
49258
|
+
}
|
|
49259
|
+
}
|
|
49260
|
+
}
|
|
49261
|
+
async function readSource(source, asset) {
|
|
49262
|
+
try {
|
|
49263
|
+
const content = await source.read(asset);
|
|
49264
|
+
if (!content.trim())
|
|
49265
|
+
throw new TemplateSourceError(`Template is empty: ${asset}`);
|
|
49266
|
+
return content;
|
|
49267
|
+
} catch (error) {
|
|
49268
|
+
if (error instanceof TemplateSourceError)
|
|
49269
|
+
throw error;
|
|
49270
|
+
throw new TemplateSourceError(`Cannot read template asset: ${asset}`, { cause: error });
|
|
49271
|
+
}
|
|
49272
|
+
}
|
|
49273
|
+
async function readMarkdown(path) {
|
|
49274
|
+
let handle;
|
|
49275
|
+
try {
|
|
49276
|
+
handle = await open2(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
49277
|
+
if (!(await handle.stat()).isFile())
|
|
49278
|
+
throw new TemplateSourceError(`Template is not a file: ${path}`);
|
|
49279
|
+
const content = await handle.readFile("utf8");
|
|
49280
|
+
if (!content.trim())
|
|
49281
|
+
throw new TemplateSourceError(`Template is empty: ${path}`);
|
|
49282
|
+
return content;
|
|
49283
|
+
} catch (error) {
|
|
49284
|
+
if (error instanceof TemplateSourceError)
|
|
49285
|
+
throw error;
|
|
49286
|
+
throw new TemplateSourceError(`Cannot read template asset: ${path}`, { cause: error });
|
|
49287
|
+
} finally {
|
|
49288
|
+
await handle?.close();
|
|
49289
|
+
}
|
|
49290
|
+
}
|
|
49291
|
+
async function createExclusive(path, content) {
|
|
49292
|
+
const handle = await open2(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
49293
|
+
try {
|
|
49294
|
+
await handle.writeFile(content, "utf8");
|
|
49295
|
+
} catch (error) {
|
|
49296
|
+
await handle.close();
|
|
49297
|
+
await rm3(path, { force: true });
|
|
49298
|
+
throw error;
|
|
49299
|
+
} finally {
|
|
49300
|
+
await handle.close();
|
|
49301
|
+
}
|
|
49302
|
+
}
|
|
49303
|
+
var TemplateError, UnknownTemplateError, TemplateSourceError, TemplateDestinationExistsError;
|
|
49304
|
+
var init_service = __esm(() => {
|
|
49305
|
+
init_fsx();
|
|
49306
|
+
init_osx();
|
|
49307
|
+
init_pathx();
|
|
49308
|
+
init_source();
|
|
49309
|
+
TemplateError = class TemplateError extends Error {
|
|
49310
|
+
};
|
|
49311
|
+
UnknownTemplateError = class UnknownTemplateError extends TemplateError {
|
|
49312
|
+
};
|
|
49313
|
+
TemplateSourceError = class TemplateSourceError extends TemplateError {
|
|
49314
|
+
};
|
|
49315
|
+
TemplateDestinationExistsError = class TemplateDestinationExistsError extends TemplateError {
|
|
49316
|
+
};
|
|
49317
|
+
});
|
|
49318
|
+
|
|
49319
|
+
// src/templates/tools.ts
|
|
49320
|
+
var templateInfo, templateListOutput, scaffoldOutput, templateTools;
|
|
49321
|
+
var init_tools3 = __esm(() => {
|
|
49322
|
+
init_zod();
|
|
49323
|
+
init_zodx();
|
|
49324
|
+
init_service();
|
|
49325
|
+
templateInfo = object({
|
|
49326
|
+
name: string2(),
|
|
49327
|
+
description: string2()
|
|
49328
|
+
});
|
|
49329
|
+
templateListOutput = object({ result: array(templateInfo) });
|
|
49330
|
+
scaffoldOutput = object({
|
|
49331
|
+
result: object({ name: string2(), path: string2() })
|
|
49332
|
+
});
|
|
49333
|
+
templateTools = [
|
|
49334
|
+
defineTool("template_list", {
|
|
49335
|
+
description: "List eight document templates; install any missing defaults and examples in the user templates directory without overwriting local edits.",
|
|
49336
|
+
inputSchema: strictObject({}),
|
|
49337
|
+
outputSchema: templateListOutput,
|
|
49338
|
+
annotations: { openWorldHint: false }
|
|
49339
|
+
}, async () => new TemplateService().listTemplates(), { toStructuredContent: (result) => ({ result }) }),
|
|
49340
|
+
defineTool("scaffold", {
|
|
49341
|
+
description: "Copy a named installed template into a new file under absolute cwd in relative directory path with the supplied filename. Create missing directories; never overwrite an existing file.",
|
|
49342
|
+
inputSchema: strictObject({
|
|
49343
|
+
name: string2().describe("Hyphenated template name from template_list, such as spec-driven-plan"),
|
|
49344
|
+
cwd: absolutePath.describe("Absolute path to an existing destination base directory"),
|
|
49345
|
+
path: relativePath.describe("Relative directory inside cwd; use . for cwd itself; missing directories are created"),
|
|
49346
|
+
filename: fileName.describe("Name of the new file inside path, such as my-plan.md; no separators")
|
|
49347
|
+
}),
|
|
49348
|
+
outputSchema: scaffoldOutput,
|
|
49349
|
+
annotations: { openWorldHint: false }
|
|
49350
|
+
}, async ({ name, cwd, path, filename }) => ({
|
|
49351
|
+
name,
|
|
49352
|
+
path: await new TemplateService().scaffoldFromTemplate(name, cwd, path, filename)
|
|
49353
|
+
}), { toStructuredContent: (result) => ({ result }) })
|
|
49354
|
+
];
|
|
49355
|
+
});
|
|
49356
|
+
|
|
48196
49357
|
// src/mcp/server.ts
|
|
48197
49358
|
async function serveMcp(version = "0.1.0") {
|
|
48198
49359
|
const server = new McpServer({ name: "difflab", version });
|
|
48199
|
-
for (const tool of [...todoTools, ...projectTools]) {
|
|
49360
|
+
for (const tool of [...todoTools, ...projectTools, ...templateTools]) {
|
|
48200
49361
|
server.registerTool(tool.name, tool.config, tool.callback);
|
|
48201
49362
|
}
|
|
48202
49363
|
await server.connect(new StdioServerTransport);
|
|
@@ -48206,6 +49367,7 @@ var init_server3 = __esm(() => {
|
|
|
48206
49367
|
init_stdio2();
|
|
48207
49368
|
init_tools();
|
|
48208
49369
|
init_tools2();
|
|
49370
|
+
init_tools3();
|
|
48209
49371
|
});
|
|
48210
49372
|
|
|
48211
49373
|
// src/extensions/processx.ts
|
|
@@ -48280,7 +49442,7 @@ var init_json_config = __esm(() => {
|
|
|
48280
49442
|
});
|
|
48281
49443
|
|
|
48282
49444
|
// src/agents/claude-code.ts
|
|
48283
|
-
import { join as
|
|
49445
|
+
import { join as join8 } from "path";
|
|
48284
49446
|
var claudeCodeAdapter;
|
|
48285
49447
|
var init_claude_code = __esm(() => {
|
|
48286
49448
|
init_adapter();
|
|
@@ -48290,7 +49452,7 @@ var init_claude_code = __esm(() => {
|
|
|
48290
49452
|
id: "claude-code",
|
|
48291
49453
|
async setupMcpConfig(context = {}) {
|
|
48292
49454
|
const home = setupHome(context);
|
|
48293
|
-
const file =
|
|
49455
|
+
const file = join8(home, ".claude.json");
|
|
48294
49456
|
const status = serverStatus(await readJsonConfig(file), file);
|
|
48295
49457
|
if (status === "matching")
|
|
48296
49458
|
return { file, status: "existing" };
|
|
@@ -48306,7 +49468,7 @@ var init_claude_code = __esm(() => {
|
|
|
48306
49468
|
|
|
48307
49469
|
// src/agents/codex.ts
|
|
48308
49470
|
import { readFile as readFile5 } from "fs/promises";
|
|
48309
|
-
import { join as
|
|
49471
|
+
import { join as join9 } from "path";
|
|
48310
49472
|
function codexServerStatus(content) {
|
|
48311
49473
|
const lines = content.split(/\r?\n/);
|
|
48312
49474
|
const start = lines.findIndex((line) => /^\s*\[mcp_servers\.(?:difflab|"difflab"|'difflab')\]\s*(?:#.*)?$/.test(line));
|
|
@@ -48330,7 +49492,7 @@ var init_codex = __esm(() => {
|
|
|
48330
49492
|
id: "codex",
|
|
48331
49493
|
async setupMcpConfig(context = {}) {
|
|
48332
49494
|
const home = setupHome(context);
|
|
48333
|
-
const file =
|
|
49495
|
+
const file = join9(home, ".codex", "config.toml");
|
|
48334
49496
|
let content = "";
|
|
48335
49497
|
try {
|
|
48336
49498
|
content = await readFile5(file, "utf8");
|
|
@@ -48352,7 +49514,7 @@ var init_codex = __esm(() => {
|
|
|
48352
49514
|
});
|
|
48353
49515
|
|
|
48354
49516
|
// src/agents/index.ts
|
|
48355
|
-
import { join as
|
|
49517
|
+
import { join as join10 } from "path";
|
|
48356
49518
|
function agentAdapter(name) {
|
|
48357
49519
|
return Object.hasOwn(agentAdapters, name) ? agentAdapters[name] : undefined;
|
|
48358
49520
|
}
|
|
@@ -48363,20 +49525,20 @@ var init_agents = __esm(() => {
|
|
|
48363
49525
|
init_codex();
|
|
48364
49526
|
init_json_config();
|
|
48365
49527
|
agentAdapters = {
|
|
48366
|
-
pi: jsonAgentAdapter("pi", (context) =>
|
|
48367
|
-
cursor: jsonAgentAdapter("cursor", (context) =>
|
|
49528
|
+
pi: jsonAgentAdapter("pi", (context) => join10(setupHome(context), ".config", "mcp", "mcp.json")),
|
|
49529
|
+
cursor: jsonAgentAdapter("cursor", (context) => join10(setupHome(context), ".cursor", "mcp.json")),
|
|
48368
49530
|
codex: codexAdapter,
|
|
48369
49531
|
"claude-code": claudeCodeAdapter,
|
|
48370
49532
|
"claude-desktop": jsonAgentAdapter("claude-desktop", (context) => {
|
|
48371
49533
|
const home = setupHome(context);
|
|
48372
49534
|
const platform = context.platform ?? process.platform;
|
|
48373
49535
|
if (platform === "darwin") {
|
|
48374
|
-
return
|
|
49536
|
+
return join10(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
48375
49537
|
}
|
|
48376
49538
|
if (platform === "win32") {
|
|
48377
|
-
return
|
|
49539
|
+
return join10(process.env.APPDATA ?? join10(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
48378
49540
|
}
|
|
48379
|
-
return
|
|
49541
|
+
return join10(home, ".config", "Claude", "claude_desktop_config.json");
|
|
48380
49542
|
})
|
|
48381
49543
|
};
|
|
48382
49544
|
agentNames = Object.keys(agentAdapters);
|
|
@@ -50464,7 +51626,7 @@ init_config();
|
|
|
50464
51626
|
init_context();
|
|
50465
51627
|
init_repo_store();
|
|
50466
51628
|
init_user_store();
|
|
50467
|
-
import { join as
|
|
51629
|
+
import { join as join11 } from "path";
|
|
50468
51630
|
function createProgram(write = console.log, version = "0.1.0", options = {}) {
|
|
50469
51631
|
const program = new Command2().name("difflab").description("Difflab CLI and local MCP server").version(version);
|
|
50470
51632
|
program.action(() => program.outputHelp());
|
|
@@ -50498,7 +51660,7 @@ function createProgram(write = console.log, version = "0.1.0", options = {}) {
|
|
|
50498
51660
|
if (config && config.project.id !== key)
|
|
50499
51661
|
throw new RepoStoreError(`Repository already belongs to project ${config.project.id}; requested ${key}`);
|
|
50500
51662
|
const origin = canonicalGitUrl(git.originUrl);
|
|
50501
|
-
const target =
|
|
51663
|
+
const target = join11(userPaths(options.home).projects, key, repositorySlug(origin));
|
|
50502
51664
|
await preflightRepoStore(git.root, target);
|
|
50503
51665
|
const project = await withUserStore(async (db) => {
|
|
50504
51666
|
const project = await db.projects.getProjectById(key);
|
|
@@ -50529,12 +51691,25 @@ function createProgram(write = console.log, version = "0.1.0", options = {}) {
|
|
|
50529
51691
|
await Promise.resolve().then(() => init_mcp2());
|
|
50530
51692
|
await setupMcpClients(client, write);
|
|
50531
51693
|
});
|
|
51694
|
+
const templates = program.command("templates").description("List and scaffold documents");
|
|
51695
|
+
const service = async () => options.templateService ?? new (await Promise.resolve().then(() => (init_service(), exports_service))).TemplateService;
|
|
51696
|
+
templates.command("list").description("List available document templates").action(async () => {
|
|
51697
|
+
const templatesService = await service();
|
|
51698
|
+
for (const template of await templatesService.listTemplates()) {
|
|
51699
|
+
write(`${template.name} \u2014 ${template.description}`);
|
|
51700
|
+
}
|
|
51701
|
+
});
|
|
51702
|
+
templates.command("scaffold <name> <path> <filename>").description("Copy a template into a new document inside a directory; never overwrite").option("--cwd <directory>", "absolute base directory for the relative destination path").action(async (name, path, filename, options) => {
|
|
51703
|
+
const templatesService = await service();
|
|
51704
|
+
const destination = await templatesService.scaffoldFromTemplate(name, options.cwd ?? process.cwd(), path, filename);
|
|
51705
|
+
write(destination);
|
|
51706
|
+
});
|
|
50532
51707
|
return program;
|
|
50533
51708
|
}
|
|
50534
51709
|
|
|
50535
51710
|
// src/index.ts
|
|
50536
51711
|
init_errors3();
|
|
50537
|
-
var version2 = "0.
|
|
51712
|
+
var version2 = "0.4.0";
|
|
50538
51713
|
try {
|
|
50539
51714
|
await createProgram(console.log, version2).parseAsync(process.argv);
|
|
50540
51715
|
} catch (error) {
|