@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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/dist/index.js +1238 -63
  3. 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 lstat4, mkdir as mkdir2, readFile as readFile3, rename, rm, writeFile as writeFile2 } from "fs/promises";
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 lstat4(file);
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 lstat4(file)).isSymbolicLink())
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 join4, resolve as resolve4 } from "path";
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 = join4(root, ".difflab");
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 (resolve4(root, await readlink(link)) !== resolve4(target))
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 = join4(root, ".difflab");
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 (resolve4(root, await readlink(link)) !== resolve4(target))
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 = join4(root, ".difflab");
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 join5 } from "path";
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 = join5(userPaths(home).projects, project.id, repository.slug);
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
- return serializeToolResult(await handler(config.inputSchema.parse(untrustedInput)), config.outputSchema);
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(value);
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 join7 } from "path";
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 = join7(home, ".claude.json");
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 join8 } from "path";
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 = join8(home, ".codex", "config.toml");
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 join9 } from "path";
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) => join9(setupHome(context), ".config", "mcp", "mcp.json")),
48367
- cursor: jsonAgentAdapter("cursor", (context) => join9(setupHome(context), ".cursor", "mcp.json")),
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 join9(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
49536
+ return join10(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
48375
49537
  }
48376
49538
  if (platform === "win32") {
48377
- return join9(process.env.APPDATA ?? join9(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
49539
+ return join10(process.env.APPDATA ?? join10(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
48378
49540
  }
48379
- return join9(home, ".config", "Claude", "claude_desktop_config.json");
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 join10 } from "path";
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 = join10(userPaths(options.home).projects, key, repositorySlug(origin));
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.3.0";
51712
+ var version2 = "0.4.0";
50538
51713
  try {
50539
51714
  await createProgram(console.log, version2).parseAsync(process.argv);
50540
51715
  } catch (error) {