@applica-software-guru/sdd-core 1.3.3 → 1.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 (53) hide show
  1. package/dist/errors.d.ts +7 -0
  2. package/dist/errors.d.ts.map +1 -1
  3. package/dist/errors.js +17 -1
  4. package/dist/errors.js.map +1 -1
  5. package/dist/index.d.ts +9 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +27 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/prompt/apply-prompt-generator.d.ts +2 -1
  10. package/dist/prompt/apply-prompt-generator.d.ts.map +1 -1
  11. package/dist/prompt/apply-prompt-generator.js +51 -2
  12. package/dist/prompt/apply-prompt-generator.js.map +1 -1
  13. package/dist/prompt/draft-prompt-generator.d.ts +8 -0
  14. package/dist/prompt/draft-prompt-generator.d.ts.map +1 -0
  15. package/dist/prompt/draft-prompt-generator.js +59 -0
  16. package/dist/prompt/draft-prompt-generator.js.map +1 -0
  17. package/dist/remote/api-client.d.ts +38 -0
  18. package/dist/remote/api-client.d.ts.map +1 -0
  19. package/dist/remote/api-client.js +101 -0
  20. package/dist/remote/api-client.js.map +1 -0
  21. package/dist/remote/state.d.ts +4 -0
  22. package/dist/remote/state.d.ts.map +1 -0
  23. package/dist/remote/state.js +35 -0
  24. package/dist/remote/state.js.map +1 -0
  25. package/dist/remote/sync-engine.d.ts +7 -0
  26. package/dist/remote/sync-engine.d.ts.map +1 -0
  27. package/dist/remote/sync-engine.js +257 -0
  28. package/dist/remote/sync-engine.js.map +1 -0
  29. package/dist/remote/types.d.ts +93 -0
  30. package/dist/remote/types.d.ts.map +1 -0
  31. package/dist/remote/types.js +3 -0
  32. package/dist/remote/types.js.map +1 -0
  33. package/dist/sdd.d.ts +13 -0
  34. package/dist/sdd.d.ts.map +1 -1
  35. package/dist/sdd.js +99 -6
  36. package/dist/sdd.js.map +1 -1
  37. package/dist/types.d.ts +9 -4
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/errors.ts +16 -0
  41. package/src/index.ts +23 -1
  42. package/src/prompt/apply-prompt-generator.ts +61 -2
  43. package/src/prompt/draft-prompt-generator.ts +74 -0
  44. package/src/remote/api-client.ts +138 -0
  45. package/src/remote/state.ts +35 -0
  46. package/src/remote/sync-engine.ts +296 -0
  47. package/src/remote/types.ts +102 -0
  48. package/src/sdd.ts +114 -6
  49. package/src/types.ts +10 -4
  50. package/tests/api-client.test.ts +198 -0
  51. package/tests/cr.test.ts +27 -10
  52. package/tests/remote-state.test.ts +90 -0
  53. package/tests/sync-engine.test.ts +341 -0
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEvE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,gBAAgB,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,KAAK;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,UAAU,GAAG,KAAK,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,KAAK,CAAC;QACX,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;QACjD,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,SAAS,CAAC;AAEtD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,mBAAmB,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,wBAAwB,CAAC;IACtC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,UAAU,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,GAAG;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,cAAc,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applica-software-guru/sdd-core",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Core library for Story Driven Development",
5
5
  "author": "Bruno Fortunato <bruno.fortunato@applica.guru>",
6
6
  "main": "dist/index.js",
package/src/errors.ts CHANGED
@@ -25,3 +25,19 @@ export class ProjectNotInitializedError extends SDDError {
25
25
  this.name = 'ProjectNotInitializedError';
26
26
  }
27
27
  }
28
+
29
+ export class RemoteError extends SDDError {
30
+ public statusCode: number;
31
+ constructor(statusCode: number, message: string) {
32
+ super(`Remote error (${statusCode}): ${message}`);
33
+ this.name = 'RemoteError';
34
+ this.statusCode = statusCode;
35
+ }
36
+ }
37
+
38
+ export class RemoteNotConfiguredError extends SDDError {
39
+ constructor() {
40
+ super('Remote not configured. Run "sdd remote init" first.');
41
+ this.name = 'RemoteNotConfiguredError';
42
+ }
43
+ }
package/src/index.ts CHANGED
@@ -16,8 +16,9 @@ export type {
16
16
  Bug,
17
17
  BugFrontmatter,
18
18
  BugStatus,
19
+ RemoteConfig,
19
20
  } from "./types.js";
20
- export { SDDError, LockFileNotFoundError, ParseError, ProjectNotInitializedError } from "./errors.js";
21
+ export { SDDError, LockFileNotFoundError, ParseError, ProjectNotInitializedError, RemoteError, RemoteNotConfiguredError } from "./errors.js";
21
22
  export type { ProjectInfo } from "./scaffold/templates.js";
22
23
  export { isSDDProject, readConfig, writeConfig } from "./config/config-manager.js";
23
24
  export { runAgent } from "./agent/agent-runner.js";
@@ -32,3 +33,24 @@ export type {
32
33
  AdapterFileChange,
33
34
  SyncAdaptersResult,
34
35
  } from "./scaffold/skill-adapters.js";
36
+
37
+ // Remote sync
38
+ export { generateDraftEnrichmentPrompt } from "./prompt/draft-prompt-generator.js";
39
+ export type { DraftElements } from "./prompt/draft-prompt-generator.js";
40
+ export { resolveApiKey, buildApiConfig, pullDocs, pushDocs, fetchPendingCRs, fetchOpenBugs, markCRAppliedRemote, markBugResolvedRemote, markDocEnriched, markCREnriched, markBugEnriched } from "./remote/api-client.js";
41
+ export type { ApiClientConfig } from "./remote/api-client.js";
42
+ export { readRemoteState, writeRemoteState } from "./remote/state.js";
43
+ export { pushToRemote, pullFromRemote, pullCRsFromRemote, pullBugsFromRemote, getRemoteStatus } from "./remote/sync-engine.js";
44
+ export type {
45
+ RemoteDocResponse,
46
+ RemoteDocBulkResponse,
47
+ RemoteCRResponse,
48
+ RemoteBugResponse,
49
+ RemoteState,
50
+ RemoteDocState,
51
+ PushResult,
52
+ PullResult,
53
+ PullConflict,
54
+ PullEntitiesResult,
55
+ RemoteStatusResult,
56
+ } from "./remote/types.js";
@@ -1,18 +1,77 @@
1
1
  import type { Bug, ChangeRequest, StoryFile } from '../types.js';
2
2
  import { getFileDiff } from '../git/git.js';
3
+ import type { DraftElements } from './draft-prompt-generator.js';
3
4
 
4
5
  export function generateApplyPrompt(
5
6
  bugs: Bug[],
6
7
  changeRequests: ChangeRequest[],
7
8
  pendingFiles: StoryFile[],
8
- root: string
9
+ root: string,
10
+ drafts?: DraftElements,
11
+ projectContext?: StoryFile[],
12
+ projectDescription?: string,
9
13
  ): string | null {
10
- if (bugs.length === 0 && changeRequests.length === 0 && pendingFiles.length === 0) {
14
+ const hasDrafts = drafts && (drafts.docs.length > 0 || drafts.crs.length > 0 || drafts.bugs.length > 0);
15
+ if (bugs.length === 0 && changeRequests.length === 0 && pendingFiles.length === 0 && !hasDrafts) {
11
16
  return null;
12
17
  }
13
18
 
14
19
  const sections: string[] = [];
15
20
 
21
+ // Draft enrichment section (takes priority)
22
+ if (hasDrafts) {
23
+ sections.push(`# Draft Enrichment\n`);
24
+
25
+ if (projectDescription) {
26
+ sections.push(`## Project\n\n${projectDescription}\n`);
27
+ }
28
+
29
+ // Global context for enrichment
30
+ if (projectContext && projectContext.length > 0) {
31
+ const ctxLines = [`## Project context (${projectContext.length} documents)\n`];
32
+ ctxLines.push('Use the following existing documents as context to produce complete, coherent documentation.\n');
33
+ for (const f of projectContext) {
34
+ ctxLines.push(`### \`${f.relativePath}\` — ${f.frontmatter.title}\n`);
35
+ ctxLines.push(f.body.trim());
36
+ ctxLines.push('');
37
+ }
38
+ sections.push(ctxLines.join('\n'));
39
+ }
40
+
41
+ if (drafts!.docs.length > 0) {
42
+ const lines = [`## Draft documents to enrich (${drafts!.docs.length})\n`];
43
+ lines.push('Each draft below contains incomplete human-written content. Produce a complete version for each document, preserving the original intent while adding missing details based on project context.\n');
44
+ for (const f of drafts!.docs) {
45
+ lines.push(`### \`${f.relativePath}\` — ${f.frontmatter.title}\n`);
46
+ lines.push(f.body.trim());
47
+ lines.push('');
48
+ }
49
+ sections.push(lines.join('\n'));
50
+ }
51
+
52
+ if (drafts!.crs.length > 0) {
53
+ const lines = [`## Draft change requests to enrich (${drafts!.crs.length})\n`];
54
+ lines.push('Each draft CR contains a rough description of requested changes. Produce a complete, actionable change request for each.\n');
55
+ for (const cr of drafts!.crs) {
56
+ lines.push(`### \`${cr.relativePath}\` — ${cr.frontmatter.title}\n`);
57
+ lines.push(cr.body.trim());
58
+ lines.push('');
59
+ }
60
+ sections.push(lines.join('\n'));
61
+ }
62
+
63
+ if (drafts!.bugs.length > 0) {
64
+ const lines = [`## Draft bugs to enrich (${drafts!.bugs.length})\n`];
65
+ lines.push('Each draft bug contains a rough description of an issue. Produce a complete bug report for each.\n');
66
+ for (const bug of drafts!.bugs) {
67
+ lines.push(`### \`${bug.relativePath}\` — ${bug.frontmatter.title}\n`);
68
+ lines.push(bug.body.trim());
69
+ lines.push('');
70
+ }
71
+ sections.push(lines.join('\n'));
72
+ }
73
+ }
74
+
16
75
  // Bugs
17
76
  if (bugs.length > 0) {
18
77
  const lines = [`## Open bugs (${bugs.length})\n`];
@@ -0,0 +1,74 @@
1
+ import type { Bug, ChangeRequest, StoryFile } from '../types.js';
2
+
3
+ export interface DraftElements {
4
+ docs: StoryFile[];
5
+ crs: ChangeRequest[];
6
+ bugs: Bug[];
7
+ }
8
+
9
+ export function generateDraftEnrichmentPrompt(
10
+ drafts: DraftElements,
11
+ projectContext: StoryFile[],
12
+ projectDescription: string,
13
+ ): string | null {
14
+ const totalDrafts = drafts.docs.length + drafts.crs.length + drafts.bugs.length;
15
+ if (totalDrafts === 0) {
16
+ return null;
17
+ }
18
+
19
+ const sections: string[] = [];
20
+
21
+ // Project context header
22
+ sections.push(`# Draft Enrichment\n`);
23
+ sections.push(`## Project\n\n${projectDescription}\n`);
24
+
25
+ // Global context: all non-draft documents
26
+ if (projectContext.length > 0) {
27
+ const ctxLines = [`## Project context (${projectContext.length} documents)\n`];
28
+ ctxLines.push('Use the following existing documents as context to produce complete, coherent documentation.\n');
29
+ for (const f of projectContext) {
30
+ ctxLines.push(`### \`${f.relativePath}\` — ${f.frontmatter.title}\n`);
31
+ ctxLines.push(f.body.trim());
32
+ ctxLines.push('');
33
+ }
34
+ sections.push(ctxLines.join('\n'));
35
+ }
36
+
37
+ // Draft documents
38
+ if (drafts.docs.length > 0) {
39
+ const lines = [`## Draft documents to enrich (${drafts.docs.length})\n`];
40
+ lines.push('Each draft below contains incomplete human-written content. Produce a complete version for each document, preserving the original intent while adding missing details based on project context.\n');
41
+ for (const f of drafts.docs) {
42
+ lines.push(`### \`${f.relativePath}\` — ${f.frontmatter.title}\n`);
43
+ lines.push(f.body.trim());
44
+ lines.push('');
45
+ }
46
+ sections.push(lines.join('\n'));
47
+ }
48
+
49
+ // Draft change requests
50
+ if (drafts.crs.length > 0) {
51
+ const lines = [`## Draft change requests to enrich (${drafts.crs.length})\n`];
52
+ lines.push('Each draft CR contains a rough description of requested changes. Produce a complete, actionable change request for each, specifying which documents are affected and what changes should be made.\n');
53
+ for (const cr of drafts.crs) {
54
+ lines.push(`### \`${cr.relativePath}\` — ${cr.frontmatter.title}\n`);
55
+ lines.push(cr.body.trim());
56
+ lines.push('');
57
+ }
58
+ sections.push(lines.join('\n'));
59
+ }
60
+
61
+ // Draft bugs
62
+ if (drafts.bugs.length > 0) {
63
+ const lines = [`## Draft bugs to enrich (${drafts.bugs.length})\n`];
64
+ lines.push('Each draft bug contains a rough description of an issue. Produce a complete bug report for each, including affected components, expected vs actual behavior, and steps to reproduce when possible.\n');
65
+ for (const bug of drafts.bugs) {
66
+ lines.push(`### \`${bug.relativePath}\` — ${bug.frontmatter.title}\n`);
67
+ lines.push(bug.body.trim());
68
+ lines.push('');
69
+ }
70
+ sections.push(lines.join('\n'));
71
+ }
72
+
73
+ return sections.join('\n\n');
74
+ }
@@ -0,0 +1,138 @@
1
+ import type { SDDConfig } from '../types.js';
2
+ import { RemoteError, RemoteNotConfiguredError } from '../errors.js';
3
+ import type {
4
+ RemoteDocResponse,
5
+ RemoteDocBulkResponse,
6
+ RemoteCRResponse,
7
+ RemoteBugResponse,
8
+ } from './types.js';
9
+
10
+ export interface ApiClientConfig {
11
+ baseUrl: string;
12
+ apiKey: string;
13
+ }
14
+
15
+ /**
16
+ * Resolve API key: SDD_API_KEY env var > config.remote.api-key > null
17
+ */
18
+ export function resolveApiKey(config: SDDConfig): string | null {
19
+ const envKey = process.env.SDD_API_KEY;
20
+ if (envKey) return envKey;
21
+ return config.remote?.['api-key'] ?? null;
22
+ }
23
+
24
+ /**
25
+ * Build an ApiClientConfig from the SDD project config.
26
+ * Throws RemoteNotConfiguredError if URL or API key is missing.
27
+ */
28
+ export function buildApiConfig(config: SDDConfig): ApiClientConfig {
29
+ if (!config.remote?.url) {
30
+ throw new RemoteNotConfiguredError();
31
+ }
32
+ const apiKey = resolveApiKey(config);
33
+ if (!apiKey) {
34
+ throw new RemoteNotConfiguredError();
35
+ }
36
+ return {
37
+ baseUrl: config.remote.url.replace(/\/+$/, ''),
38
+ apiKey,
39
+ };
40
+ }
41
+
42
+ async function request<T>(
43
+ config: ApiClientConfig,
44
+ method: string,
45
+ path: string,
46
+ body?: unknown,
47
+ ): Promise<T> {
48
+ const url = `${config.baseUrl}${path}`;
49
+ const headers: Record<string, string> = {
50
+ Authorization: `Bearer ${config.apiKey}`,
51
+ 'Content-Type': 'application/json',
52
+ };
53
+
54
+ const res = await fetch(url, {
55
+ method,
56
+ headers,
57
+ body: body != null ? JSON.stringify(body) : undefined,
58
+ });
59
+
60
+ if (!res.ok) {
61
+ let message: string;
62
+ try {
63
+ const err = (await res.json()) as { detail?: string };
64
+ message = err.detail ?? res.statusText;
65
+ } catch {
66
+ message = res.statusText;
67
+ }
68
+ throw new RemoteError(res.status, message);
69
+ }
70
+
71
+ return (await res.json()) as T;
72
+ }
73
+
74
+ /** GET /cli/pull-docs */
75
+ export async function pullDocs(config: ApiClientConfig): Promise<RemoteDocResponse[]> {
76
+ return request<RemoteDocResponse[]>(config, 'GET', '/cli/pull-docs');
77
+ }
78
+
79
+ /** POST /cli/push-docs */
80
+ export async function pushDocs(
81
+ config: ApiClientConfig,
82
+ documents: Array<{ path: string; title: string; content: string }>,
83
+ ): Promise<RemoteDocBulkResponse> {
84
+ return request<RemoteDocBulkResponse>(config, 'POST', '/cli/push-docs', { documents });
85
+ }
86
+
87
+ /** GET /cli/pending-crs */
88
+ export async function fetchPendingCRs(config: ApiClientConfig): Promise<RemoteCRResponse[]> {
89
+ return request<RemoteCRResponse[]>(config, 'GET', '/cli/pending-crs');
90
+ }
91
+
92
+ /** GET /cli/open-bugs */
93
+ export async function fetchOpenBugs(config: ApiClientConfig): Promise<RemoteBugResponse[]> {
94
+ return request<RemoteBugResponse[]>(config, 'GET', '/cli/open-bugs');
95
+ }
96
+
97
+ /** POST /cli/crs/:crId/applied */
98
+ export async function markCRAppliedRemote(
99
+ config: ApiClientConfig,
100
+ crId: string,
101
+ ): Promise<RemoteCRResponse> {
102
+ return request<RemoteCRResponse>(config, 'POST', `/cli/crs/${crId}/applied`);
103
+ }
104
+
105
+ /** POST /cli/bugs/:bugId/resolved */
106
+ export async function markBugResolvedRemote(
107
+ config: ApiClientConfig,
108
+ bugId: string,
109
+ ): Promise<RemoteBugResponse> {
110
+ return request<RemoteBugResponse>(config, 'POST', `/cli/bugs/${bugId}/resolved`);
111
+ }
112
+
113
+ /** POST /cli/docs/:docId/enriched — Notify remote that a draft doc has been enriched */
114
+ export async function markDocEnriched(
115
+ config: ApiClientConfig,
116
+ docId: string,
117
+ content: string,
118
+ ): Promise<RemoteDocResponse> {
119
+ return request<RemoteDocResponse>(config, 'POST', `/cli/docs/${docId}/enriched`, { content });
120
+ }
121
+
122
+ /** POST /cli/crs/:crId/enriched — Notify remote that a draft CR has been enriched */
123
+ export async function markCREnriched(
124
+ config: ApiClientConfig,
125
+ crId: string,
126
+ body: string,
127
+ ): Promise<RemoteCRResponse> {
128
+ return request<RemoteCRResponse>(config, 'POST', `/cli/crs/${crId}/enriched`, { body });
129
+ }
130
+
131
+ /** POST /cli/bugs/:bugId/enriched — Notify remote that a draft bug has been enriched */
132
+ export async function markBugEnriched(
133
+ config: ApiClientConfig,
134
+ bugId: string,
135
+ body: string,
136
+ ): Promise<RemoteBugResponse> {
137
+ return request<RemoteBugResponse>(config, 'POST', `/cli/bugs/${bugId}/enriched`, { body });
138
+ }
@@ -0,0 +1,35 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import type { RemoteState } from './types.js';
5
+
6
+ const REMOTE_STATE_FILE = 'remote-state.json';
7
+
8
+ function stateFilePath(root: string): string {
9
+ return resolve(root, '.sdd', REMOTE_STATE_FILE);
10
+ }
11
+
12
+ function emptyState(): RemoteState {
13
+ return { documents: {} };
14
+ }
15
+
16
+ export async function readRemoteState(root: string): Promise<RemoteState> {
17
+ const path = stateFilePath(root);
18
+ if (!existsSync(path)) {
19
+ return emptyState();
20
+ }
21
+ const content = await readFile(path, 'utf-8');
22
+ try {
23
+ return JSON.parse(content) as RemoteState;
24
+ } catch {
25
+ return emptyState();
26
+ }
27
+ }
28
+
29
+ export async function writeRemoteState(root: string, state: RemoteState): Promise<void> {
30
+ const dir = resolve(root, '.sdd');
31
+ if (!existsSync(dir)) {
32
+ await mkdir(dir, { recursive: true });
33
+ }
34
+ await writeFile(stateFilePath(root), JSON.stringify(state, null, 2), 'utf-8');
35
+ }