@applica-software-guru/sdd-core 1.3.4 → 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
@@ -0,0 +1,296 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve, dirname, posix } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import matter from 'gray-matter';
6
+
7
+ import { readConfig } from '../config/config-manager.js';
8
+ import { parseAllStoryFiles } from '../parser/story-parser.js';
9
+ import { buildApiConfig, pullDocs, pushDocs, fetchPendingCRs, fetchOpenBugs } from './api-client.js';
10
+ import { readRemoteState, writeRemoteState } from './state.js';
11
+ import type {
12
+ PushResult,
13
+ PullResult,
14
+ PullConflict,
15
+ PullEntitiesResult,
16
+ RemoteStatusResult,
17
+ RemoteDocResponse,
18
+ } from './types.js';
19
+
20
+ function sha256(content: string): string {
21
+ return createHash('sha256').update(content, 'utf-8').digest('hex');
22
+ }
23
+
24
+ /** Normalize path separators to forward slash for remote consistency */
25
+ function normalizePath(p: string): string {
26
+ return p.split('\\').join('/');
27
+ }
28
+
29
+ /**
30
+ * Build a markdown file with frontmatter for a pulled document.
31
+ */
32
+ function buildStoryMarkdown(title: string, body: string, version: number, status: string = 'synced'): string {
33
+ const fm = {
34
+ title,
35
+ status,
36
+ author: 'remote',
37
+ 'last-modified': new Date().toISOString(),
38
+ version: `${version}.0`,
39
+ };
40
+ return matter.stringify(body, fm);
41
+ }
42
+
43
+ /**
44
+ * Build a markdown file for a pulled change request.
45
+ */
46
+ function buildCRMarkdown(title: string, body: string, createdAt: string, status: string = 'draft'): string {
47
+ const fm = {
48
+ title,
49
+ status,
50
+ author: 'remote',
51
+ 'created-at': createdAt,
52
+ };
53
+ return matter.stringify(body, fm);
54
+ }
55
+
56
+ /**
57
+ * Build a markdown file for a pulled bug.
58
+ */
59
+ function buildBugMarkdown(title: string, body: string, createdAt: string, status: string = 'open'): string {
60
+ const fm = {
61
+ title,
62
+ status,
63
+ author: 'remote',
64
+ 'created-at': createdAt,
65
+ };
66
+ return matter.stringify(body, fm);
67
+ }
68
+
69
+ // ─── Push ────────────────────────────────────────────────────────────────
70
+
71
+ export async function pushToRemote(root: string, paths?: string[]): Promise<PushResult> {
72
+ const config = await readConfig(root);
73
+ const api = buildApiConfig(config);
74
+
75
+ const files = await parseAllStoryFiles(root);
76
+ const toPush = files.filter((f) => {
77
+ if (paths && paths.length > 0) return paths.includes(f.relativePath);
78
+ return f.frontmatter.status !== 'synced';
79
+ });
80
+
81
+ if (toPush.length === 0) {
82
+ return { created: 0, updated: 0, pushed: [] };
83
+ }
84
+
85
+ const documents = toPush.map((f) => ({
86
+ path: normalizePath(f.relativePath),
87
+ title: f.frontmatter.title,
88
+ content: f.body,
89
+ }));
90
+
91
+ const result = await pushDocs(api, documents);
92
+
93
+ // Update remote state
94
+ const state = await readRemoteState(root);
95
+ for (const doc of result.documents) {
96
+ const localPath = doc.path;
97
+ const absPath = resolve(root, localPath);
98
+ const rawContent = existsSync(absPath) ? await readFile(absPath, 'utf-8') : '';
99
+ state.documents[localPath] = {
100
+ remoteId: doc.id,
101
+ remoteVersion: doc.version,
102
+ localHash: sha256(rawContent),
103
+ lastSynced: new Date().toISOString(),
104
+ };
105
+ }
106
+ state.lastPush = new Date().toISOString();
107
+ await writeRemoteState(root, state);
108
+
109
+ // Mark local files as synced (drafts are excluded — they need AI enrichment first)
110
+ for (const f of toPush) {
111
+ if (f.frontmatter.status === 'draft') continue;
112
+ const absPath = resolve(root, f.relativePath);
113
+ const content = await readFile(absPath, 'utf-8');
114
+ const updated = content.replace(/^status:\s*(new|changed)/m, 'status: synced');
115
+ if (updated !== content) {
116
+ await writeFile(absPath, updated, 'utf-8');
117
+ }
118
+ }
119
+
120
+ return {
121
+ created: result.created,
122
+ updated: result.updated,
123
+ pushed: toPush.map((f) => f.relativePath),
124
+ };
125
+ }
126
+
127
+ // ─── Pull Documents ──────────────────────────────────────────────────────
128
+
129
+ export async function pullFromRemote(root: string): Promise<PullResult> {
130
+ const config = await readConfig(root);
131
+ const api = buildApiConfig(config);
132
+
133
+ const remoteDocs = await pullDocs(api);
134
+ const state = await readRemoteState(root);
135
+
136
+ const created: string[] = [];
137
+ const updated: string[] = [];
138
+ const conflicts: PullConflict[] = [];
139
+
140
+ for (const doc of remoteDocs) {
141
+ const localPath = doc.path;
142
+ const absPath = resolve(root, localPath);
143
+ const tracked = state.documents[localPath];
144
+
145
+ if (!existsSync(absPath)) {
146
+ // New file — create locally, preserving remote status
147
+ const dir = dirname(absPath);
148
+ if (!existsSync(dir)) {
149
+ await mkdir(dir, { recursive: true });
150
+ }
151
+ const localStatus = doc.status === 'draft' ? 'draft' : 'synced';
152
+ const markdown = buildStoryMarkdown(doc.title, doc.content, doc.version, localStatus);
153
+ await writeFile(absPath, markdown, 'utf-8');
154
+ created.push(localPath);
155
+ updateDocState(state, doc, localPath, markdown);
156
+ } else if (!tracked || doc.version > tracked.remoteVersion) {
157
+ // Remote is newer — check for local changes
158
+ const localContent = await readFile(absPath, 'utf-8');
159
+ const localHash = sha256(localContent);
160
+
161
+ if (tracked && localHash !== tracked.localHash) {
162
+ // Local file changed since last sync AND remote changed → conflict
163
+ conflicts.push({
164
+ path: localPath,
165
+ localVersion: tracked.remoteVersion.toString(),
166
+ remoteVersion: doc.version,
167
+ reason: 'Both local and remote have changes since last sync',
168
+ });
169
+ } else {
170
+ // Local unchanged — safe to overwrite
171
+ const localStatus = doc.status === 'draft' ? 'draft' : 'synced';
172
+ const markdown = buildStoryMarkdown(doc.title, doc.content, doc.version, localStatus);
173
+ await writeFile(absPath, markdown, 'utf-8');
174
+ updated.push(localPath);
175
+ updateDocState(state, doc, localPath, markdown);
176
+ }
177
+ }
178
+ // If versions match, skip
179
+ }
180
+
181
+ state.lastPull = new Date().toISOString();
182
+ await writeRemoteState(root, state);
183
+
184
+ return { created, updated, conflicts };
185
+ }
186
+
187
+ function updateDocState(
188
+ state: { documents: Record<string, { remoteId: string; remoteVersion: number; localHash: string; lastSynced: string }> },
189
+ doc: RemoteDocResponse,
190
+ localPath: string,
191
+ markdownContent: string,
192
+ ): void {
193
+ state.documents[localPath] = {
194
+ remoteId: doc.id,
195
+ remoteVersion: doc.version,
196
+ localHash: sha256(markdownContent),
197
+ lastSynced: new Date().toISOString(),
198
+ };
199
+ }
200
+
201
+ // ─── Pull CRs ────────────────────────────────────────────────────────────
202
+
203
+ export async function pullCRsFromRemote(root: string): Promise<PullEntitiesResult> {
204
+ const config = await readConfig(root);
205
+ const api = buildApiConfig(config);
206
+
207
+ const remoteCRs = await fetchPendingCRs(api);
208
+ const crDir = resolve(root, 'change-requests');
209
+ if (!existsSync(crDir)) {
210
+ await mkdir(crDir, { recursive: true });
211
+ }
212
+
213
+ let created = 0;
214
+ let updated = 0;
215
+
216
+ for (const cr of remoteCRs) {
217
+ const filename = `CR-${cr.id.substring(0, 8)}.md`;
218
+ const absPath = resolve(crDir, filename);
219
+ const crStatus = cr.status === 'draft' ? 'draft' : 'pending';
220
+ const markdown = buildCRMarkdown(cr.title, cr.body, cr.created_at, crStatus);
221
+
222
+ if (existsSync(absPath)) {
223
+ updated++;
224
+ } else {
225
+ created++;
226
+ }
227
+ await writeFile(absPath, markdown, 'utf-8');
228
+ }
229
+
230
+ return { created, updated };
231
+ }
232
+
233
+ // ─── Pull Bugs ───────────────────────────────────────────────────────────
234
+
235
+ export async function pullBugsFromRemote(root: string): Promise<PullEntitiesResult> {
236
+ const config = await readConfig(root);
237
+ const api = buildApiConfig(config);
238
+
239
+ const remoteBugs = await fetchOpenBugs(api);
240
+ const bugsDir = resolve(root, 'bugs');
241
+ if (!existsSync(bugsDir)) {
242
+ await mkdir(bugsDir, { recursive: true });
243
+ }
244
+
245
+ let created = 0;
246
+ let updated = 0;
247
+
248
+ for (const bug of remoteBugs) {
249
+ const filename = `BUG-${bug.id.substring(0, 8)}.md`;
250
+ const absPath = resolve(bugsDir, filename);
251
+ const bugStatus = bug.status === 'draft' ? 'draft' : 'open';
252
+ const markdown = buildBugMarkdown(bug.title, bug.body, bug.created_at, bugStatus);
253
+
254
+ if (existsSync(absPath)) {
255
+ updated++;
256
+ } else {
257
+ created++;
258
+ }
259
+ await writeFile(absPath, markdown, 'utf-8');
260
+ }
261
+
262
+ return { created, updated };
263
+ }
264
+
265
+ // ─── Remote Status ───────────────────────────────────────────────────────
266
+
267
+ export async function getRemoteStatus(root: string): Promise<RemoteStatusResult> {
268
+ const config = await readConfig(root);
269
+
270
+ if (!config.remote?.url) {
271
+ return { configured: false, url: null, connected: false, localPending: 0, remoteDocs: 0 };
272
+ }
273
+
274
+ const files = await parseAllStoryFiles(root);
275
+ const localPending = files.filter((f) => f.frontmatter.status !== 'synced').length;
276
+
277
+ try {
278
+ const api = buildApiConfig(config);
279
+ const docs = await pullDocs(api);
280
+ return {
281
+ configured: true,
282
+ url: config.remote.url,
283
+ connected: true,
284
+ localPending,
285
+ remoteDocs: docs.length,
286
+ };
287
+ } catch {
288
+ return {
289
+ configured: true,
290
+ url: config.remote.url,
291
+ connected: false,
292
+ localPending,
293
+ remoteDocs: 0,
294
+ };
295
+ }
296
+ }
@@ -0,0 +1,102 @@
1
+ /** Mirrors the sdd-flow API response for documents */
2
+ export interface RemoteDocResponse {
3
+ id: string;
4
+ project_id: string;
5
+ path: string;
6
+ title: string;
7
+ status: string;
8
+ version: number;
9
+ content: string;
10
+ last_modified_by: string | null;
11
+ created_at: string;
12
+ updated_at: string;
13
+ }
14
+
15
+ /** Bulk push response from POST /cli/push-docs */
16
+ export interface RemoteDocBulkResponse {
17
+ created: number;
18
+ updated: number;
19
+ documents: RemoteDocResponse[];
20
+ }
21
+
22
+ /** Mirrors the sdd-flow API response for change requests */
23
+ export interface RemoteCRResponse {
24
+ id: string;
25
+ project_id: string;
26
+ title: string;
27
+ body: string;
28
+ status: string;
29
+ author_id: string;
30
+ assignee_id: string | null;
31
+ target_files: string[] | null;
32
+ closed_at: string | null;
33
+ created_at: string;
34
+ updated_at: string;
35
+ }
36
+
37
+ /** Mirrors the sdd-flow API response for bugs */
38
+ export interface RemoteBugResponse {
39
+ id: string;
40
+ project_id: string;
41
+ title: string;
42
+ body: string;
43
+ status: string;
44
+ severity: string;
45
+ author_id: string;
46
+ assignee_id: string | null;
47
+ closed_at: string | null;
48
+ created_at: string;
49
+ updated_at: string;
50
+ }
51
+
52
+ /** Tracks per-document remote sync state */
53
+ export interface RemoteDocState {
54
+ remoteId: string;
55
+ remoteVersion: number;
56
+ localHash: string;
57
+ lastSynced: string;
58
+ }
59
+
60
+ /** Persisted to .sdd/remote-state.json */
61
+ export interface RemoteState {
62
+ lastPull?: string;
63
+ lastPush?: string;
64
+ documents: Record<string, RemoteDocState>;
65
+ }
66
+
67
+ /** Result of a push operation */
68
+ export interface PushResult {
69
+ created: number;
70
+ updated: number;
71
+ pushed: string[];
72
+ }
73
+
74
+ /** A conflict detected during pull */
75
+ export interface PullConflict {
76
+ path: string;
77
+ localVersion: string;
78
+ remoteVersion: number;
79
+ reason: string;
80
+ }
81
+
82
+ /** Result of a pull operation */
83
+ export interface PullResult {
84
+ created: string[];
85
+ updated: string[];
86
+ conflicts: PullConflict[];
87
+ }
88
+
89
+ /** Result of a pull CRs/Bugs operation */
90
+ export interface PullEntitiesResult {
91
+ created: number;
92
+ updated: number;
93
+ }
94
+
95
+ /** Result of remote status check */
96
+ export interface RemoteStatusResult {
97
+ configured: boolean;
98
+ url: string | null;
99
+ connected: boolean;
100
+ localPending: number;
101
+ remoteDocs: number;
102
+ }
package/src/sdd.ts CHANGED
@@ -5,6 +5,7 @@ import { ProjectNotInitializedError } from "./errors.js";
5
5
  import { parseAllStoryFiles } from "./parser/story-parser.js";
6
6
  import { generatePrompt } from "./prompt/prompt-generator.js";
7
7
  import { generateApplyPrompt } from "./prompt/apply-prompt-generator.js";
8
+ import { generateDraftEnrichmentPrompt } from "./prompt/draft-prompt-generator.js";
8
9
  import { validate } from "./validate/validator.js";
9
10
  import { initProject } from "./scaffold/init.js";
10
11
  import { isSDDProject, readConfig, writeConfig } from "./config/config-manager.js";
@@ -17,6 +18,14 @@ import {
17
18
  type SyncAdaptersOptions,
18
19
  type SyncAdaptersResult,
19
20
  } from "./scaffold/skill-adapters.js";
21
+ import {
22
+ pushToRemote,
23
+ pullFromRemote,
24
+ pullCRsFromRemote,
25
+ pullBugsFromRemote,
26
+ getRemoteStatus,
27
+ } from "./remote/sync-engine.js";
28
+ import type { PushResult, PullResult, PullEntitiesResult, RemoteStatusResult } from "./remote/types.js";
20
29
 
21
30
  export class SDD {
22
31
  private root: string;
@@ -60,7 +69,7 @@ export class SDD {
60
69
  async pending(): Promise<import("./types.js").StoryFile[]> {
61
70
  this.ensureInitialized();
62
71
  const files = await parseAllStoryFiles(this.root);
63
- return files.filter((f) => f.frontmatter.status !== "synced");
72
+ return files.filter((f) => f.frontmatter.status !== "synced" && f.frontmatter.status !== "draft");
64
73
  }
65
74
 
66
75
  async sync(): Promise<string> {
@@ -70,12 +79,16 @@ export class SDD {
70
79
 
71
80
  async applyPrompt(): Promise<string | null> {
72
81
  this.ensureInitialized();
73
- const [bugs, changeRequests, pendingFiles] = await Promise.all([
82
+ const [bugs, changeRequests, pendingFiles, drafts, allFiles, config] = await Promise.all([
74
83
  this.openBugs(),
75
84
  this.pendingChangeRequests(),
76
85
  this.pending(),
86
+ this.drafts(),
87
+ parseAllStoryFiles(this.root),
88
+ readConfig(this.root),
77
89
  ]);
78
- return generateApplyPrompt(bugs, changeRequests, pendingFiles, this.root);
90
+ const projectContext = allFiles.filter((f) => f.frontmatter.status !== "draft");
91
+ return generateApplyPrompt(bugs, changeRequests, pendingFiles, this.root, drafts, projectContext, config.description);
79
92
  }
80
93
 
81
94
  async validate(): Promise<ValidationResult> {
@@ -120,7 +133,7 @@ export class SDD {
120
133
 
121
134
  async pendingChangeRequests(): Promise<ChangeRequest[]> {
122
135
  const all = await this.changeRequests();
123
- return all.filter((cr) => cr.frontmatter.status === "draft");
136
+ return all.filter((cr) => cr.frontmatter.status === "pending");
124
137
  }
125
138
 
126
139
  async markCRApplied(paths?: string[]): Promise<string[]> {
@@ -129,12 +142,12 @@ export class SDD {
129
142
  const marked: string[] = [];
130
143
 
131
144
  for (const cr of all) {
132
- if (cr.frontmatter.status === "applied") continue;
145
+ if (cr.frontmatter.status !== "pending") continue;
133
146
  if (paths && paths.length > 0 && !paths.includes(cr.relativePath)) continue;
134
147
 
135
148
  const absPath = resolve(this.root, cr.relativePath);
136
149
  const content = await readFile(absPath, "utf-8");
137
- const updated = content.replace(/^status:\s*draft/m, "status: applied");
150
+ const updated = content.replace(/^status:\s*pending/m, "status: applied");
138
151
  await writeFile(absPath, updated, "utf-8");
139
152
  marked.push(cr.relativePath);
140
153
  }
@@ -171,6 +184,101 @@ export class SDD {
171
184
  return marked;
172
185
  }
173
186
 
187
+ // ── Drafts ───────────────────────────────────────────────────────────
188
+
189
+ async drafts(): Promise<{ docs: import("./types.js").StoryFile[]; crs: ChangeRequest[]; bugs: Bug[] }> {
190
+ this.ensureInitialized();
191
+ const [files, crs, bugs] = await Promise.all([
192
+ parseAllStoryFiles(this.root),
193
+ this.changeRequests(),
194
+ this.bugs(),
195
+ ]);
196
+ return {
197
+ docs: files.filter((f) => f.frontmatter.status === "draft"),
198
+ crs: crs.filter((cr) => cr.frontmatter.status === "draft"),
199
+ bugs: bugs.filter((b) => b.frontmatter.status === "draft"),
200
+ };
201
+ }
202
+
203
+ async draftEnrichmentPrompt(): Promise<string | null> {
204
+ this.ensureInitialized();
205
+ const drafts = await this.drafts();
206
+ const allFiles = await parseAllStoryFiles(this.root);
207
+ const projectContext = allFiles.filter((f) => f.frontmatter.status !== "draft");
208
+ const config = await readConfig(this.root);
209
+ return generateDraftEnrichmentPrompt(drafts, projectContext, config.description);
210
+ }
211
+
212
+ async markDraftsEnriched(paths?: string[]): Promise<string[]> {
213
+ this.ensureInitialized();
214
+ const marked: string[] = [];
215
+
216
+ // Story files: draft → new
217
+ const files = await parseAllStoryFiles(this.root);
218
+ for (const file of files) {
219
+ if (file.frontmatter.status !== "draft") continue;
220
+ if (paths && paths.length > 0 && !paths.includes(file.relativePath)) continue;
221
+ const absPath = resolve(this.root, file.relativePath);
222
+ const content = await readFile(absPath, "utf-8");
223
+ const updated = content.replace(/^status:\s*draft/m, "status: new");
224
+ await writeFile(absPath, updated, "utf-8");
225
+ marked.push(file.relativePath);
226
+ }
227
+
228
+ // CRs: draft → pending
229
+ const crs = await this.changeRequests();
230
+ for (const cr of crs) {
231
+ if (cr.frontmatter.status !== "draft") continue;
232
+ if (paths && paths.length > 0 && !paths.includes(cr.relativePath)) continue;
233
+ const absPath = resolve(this.root, cr.relativePath);
234
+ const content = await readFile(absPath, "utf-8");
235
+ const updated = content.replace(/^status:\s*draft/m, "status: pending");
236
+ await writeFile(absPath, updated, "utf-8");
237
+ marked.push(cr.relativePath);
238
+ }
239
+
240
+ // Bugs: draft → open
241
+ const allBugs = await this.bugs();
242
+ for (const bug of allBugs) {
243
+ if (bug.frontmatter.status !== "draft") continue;
244
+ if (paths && paths.length > 0 && !paths.includes(bug.relativePath)) continue;
245
+ const absPath = resolve(this.root, bug.relativePath);
246
+ const content = await readFile(absPath, "utf-8");
247
+ const updated = content.replace(/^status:\s*draft/m, "status: open");
248
+ await writeFile(absPath, updated, "utf-8");
249
+ marked.push(bug.relativePath);
250
+ }
251
+
252
+ return marked;
253
+ }
254
+
255
+ // ── Remote sync ──────────────────────────────────────────────────────
256
+
257
+ async remoteStatus(): Promise<RemoteStatusResult> {
258
+ this.ensureInitialized();
259
+ return getRemoteStatus(this.root);
260
+ }
261
+
262
+ async push(paths?: string[]): Promise<PushResult> {
263
+ this.ensureInitialized();
264
+ return pushToRemote(this.root, paths);
265
+ }
266
+
267
+ async pull(): Promise<PullResult> {
268
+ this.ensureInitialized();
269
+ return pullFromRemote(this.root);
270
+ }
271
+
272
+ async pullCRs(): Promise<PullEntitiesResult> {
273
+ this.ensureInitialized();
274
+ return pullCRsFromRemote(this.root);
275
+ }
276
+
277
+ async pullBugs(): Promise<PullEntitiesResult> {
278
+ this.ensureInitialized();
279
+ return pullBugsFromRemote(this.root);
280
+ }
281
+
174
282
  private ensureInitialized(): void {
175
283
  if (!isSDDProject(this.root)) {
176
284
  throw new ProjectNotInitializedError(this.root);
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type StoryFileStatus = 'new' | 'changed' | 'deleted' | 'synced';
1
+ export type StoryFileStatus = 'draft' | 'new' | 'changed' | 'deleted' | 'synced';
2
2
 
3
3
  export interface StoryFrontmatter {
4
4
  title: string;
@@ -49,20 +49,26 @@ export interface ValidationIssue {
49
49
  export interface StoryStatus {
50
50
  files: Array<{
51
51
  relativePath: string;
52
- status: 'new' | 'changed' | 'deleted' | 'synced';
52
+ status: StoryFileStatus;
53
53
  version: string;
54
54
  lastModified: string;
55
55
  }>;
56
56
  }
57
57
 
58
+ export interface RemoteConfig {
59
+ url: string;
60
+ 'api-key'?: string;
61
+ }
62
+
58
63
  export interface SDDConfig {
59
64
  description: string;
60
65
  'last-sync-commit'?: string;
61
66
  agent?: string;
62
67
  agents?: Record<string, string>;
68
+ remote?: RemoteConfig;
63
69
  }
64
70
 
65
- export type ChangeRequestStatus = 'draft' | 'applied';
71
+ export type ChangeRequestStatus = 'draft' | 'pending' | 'applied';
66
72
 
67
73
  export interface ChangeRequestFrontmatter {
68
74
  title: string;
@@ -77,7 +83,7 @@ export interface ChangeRequest {
77
83
  body: string;
78
84
  }
79
85
 
80
- export type BugStatus = 'open' | 'resolved';
86
+ export type BugStatus = 'draft' | 'open' | 'resolved';
81
87
 
82
88
  export interface BugFrontmatter {
83
89
  title: string;