@almadar/workspace 0.1.0-alpha.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/concurrency.test.d.ts +1 -0
- package/dist/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/__tests__/observer.test.d.ts +1 -0
- package/dist/__tests__/service.test.d.ts +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +1031 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/backends/index.d.ts +2 -0
- package/dist/internal/backends/local.d.ts +22 -0
- package/dist/internal/backends/memory.d.ts +25 -0
- package/dist/internal/git-client.d.ts +32 -0
- package/dist/internal/memory-files.d.ts +26 -0
- package/dist/internal/path-layout.d.ts +54 -0
- package/dist/internal/restore.d.ts +17 -0
- package/dist/internal/sink-manager.d.ts +20 -0
- package/dist/internal/templates.d.ts +18 -0
- package/dist/internal/types.d.ts +44 -0
- package/dist/internal/workspace-manager.d.ts +21 -0
- package/dist/internal/workspace-resolver.d.ts +20 -0
- package/dist/open-workspace.d.ts +11 -0
- package/dist/service.d.ts +86 -0
- package/dist/types.d.ts +210 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,1034 @@
|
|
|
1
|
+
import path2 from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { execFile } from 'child_process';
|
|
1
4
|
|
|
5
|
+
// src/open-workspace.ts
|
|
6
|
+
var LocalBackend = class {
|
|
7
|
+
async readFile(absPath) {
|
|
8
|
+
return fs.promises.readFile(absPath, "utf-8");
|
|
9
|
+
}
|
|
10
|
+
readFileSync(absPath) {
|
|
11
|
+
return fs.readFileSync(absPath, "utf-8");
|
|
12
|
+
}
|
|
13
|
+
async writeFile(absPath, content) {
|
|
14
|
+
await fs.promises.mkdir(path2.dirname(absPath), { recursive: true });
|
|
15
|
+
await fs.promises.writeFile(absPath, content, "utf-8");
|
|
16
|
+
}
|
|
17
|
+
writeFileSync(absPath, content) {
|
|
18
|
+
fs.mkdirSync(path2.dirname(absPath), { recursive: true });
|
|
19
|
+
fs.writeFileSync(absPath, content, "utf-8");
|
|
20
|
+
}
|
|
21
|
+
exists(absPath) {
|
|
22
|
+
return fs.existsSync(absPath);
|
|
23
|
+
}
|
|
24
|
+
async readdir(absPath) {
|
|
25
|
+
return fs.promises.readdir(absPath);
|
|
26
|
+
}
|
|
27
|
+
readdirSync(absPath) {
|
|
28
|
+
return fs.readdirSync(absPath);
|
|
29
|
+
}
|
|
30
|
+
async mkdir(absPath, opts) {
|
|
31
|
+
await fs.promises.mkdir(absPath, opts);
|
|
32
|
+
}
|
|
33
|
+
mkdirSync(absPath, opts) {
|
|
34
|
+
fs.mkdirSync(absPath, opts);
|
|
35
|
+
}
|
|
36
|
+
async unlink(absPath) {
|
|
37
|
+
await fs.promises.unlink(absPath);
|
|
38
|
+
}
|
|
39
|
+
async stat(absPath) {
|
|
40
|
+
const s = await fs.promises.stat(absPath);
|
|
41
|
+
return { size: s.size, mtimeMs: s.mtimeMs, isDirectory: s.isDirectory() };
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/internal/backends/memory.ts
|
|
46
|
+
var MemoryBackend = class {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.files = /* @__PURE__ */ new Map();
|
|
49
|
+
this.dirs = /* @__PURE__ */ new Set();
|
|
50
|
+
}
|
|
51
|
+
async readFile(absPath) {
|
|
52
|
+
return this.readFileSync(absPath);
|
|
53
|
+
}
|
|
54
|
+
readFileSync(absPath) {
|
|
55
|
+
const content = this.files.get(absPath);
|
|
56
|
+
if (content === void 0) throw new Error(`ENOENT: ${absPath}`);
|
|
57
|
+
return content;
|
|
58
|
+
}
|
|
59
|
+
async writeFile(absPath, content) {
|
|
60
|
+
this.writeFileSync(absPath, content);
|
|
61
|
+
}
|
|
62
|
+
writeFileSync(absPath, content) {
|
|
63
|
+
this.files.set(absPath, content);
|
|
64
|
+
}
|
|
65
|
+
exists(absPath) {
|
|
66
|
+
if (this.files.has(absPath) || this.dirs.has(absPath)) return true;
|
|
67
|
+
const prefix = absPath.endsWith("/") ? absPath : absPath + "/";
|
|
68
|
+
for (const k of this.files.keys()) {
|
|
69
|
+
if (k.startsWith(prefix)) return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
async readdir(absPath) {
|
|
74
|
+
return this.readdirSync(absPath);
|
|
75
|
+
}
|
|
76
|
+
readdirSync(absPath) {
|
|
77
|
+
const prefix = absPath.endsWith("/") ? absPath : absPath + "/";
|
|
78
|
+
const entries = /* @__PURE__ */ new Set();
|
|
79
|
+
for (const key of this.files.keys()) {
|
|
80
|
+
if (key.startsWith(prefix)) {
|
|
81
|
+
const rest = key.slice(prefix.length);
|
|
82
|
+
const firstSegment = rest.split("/")[0];
|
|
83
|
+
if (firstSegment) entries.add(firstSegment);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return Array.from(entries);
|
|
87
|
+
}
|
|
88
|
+
async mkdir(absPath) {
|
|
89
|
+
this.mkdirSync(absPath);
|
|
90
|
+
}
|
|
91
|
+
mkdirSync(absPath) {
|
|
92
|
+
this.dirs.add(absPath);
|
|
93
|
+
}
|
|
94
|
+
async unlink(absPath) {
|
|
95
|
+
this.files.delete(absPath);
|
|
96
|
+
}
|
|
97
|
+
async stat(absPath) {
|
|
98
|
+
const content = this.files.get(absPath);
|
|
99
|
+
if (content !== void 0) {
|
|
100
|
+
return { size: content.length, mtimeMs: Date.now(), isDirectory: false };
|
|
101
|
+
}
|
|
102
|
+
if (this.dirs.has(absPath)) return { size: 0, mtimeMs: Date.now(), isDirectory: true };
|
|
103
|
+
const prefix = absPath.endsWith("/") ? absPath : absPath + "/";
|
|
104
|
+
for (const k of this.files.keys()) {
|
|
105
|
+
if (k.startsWith(prefix)) return { size: 0, mtimeMs: Date.now(), isDirectory: true };
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`ENOENT: ${absPath}`);
|
|
108
|
+
}
|
|
109
|
+
// Test helpers
|
|
110
|
+
getAll() {
|
|
111
|
+
return new Map(this.files);
|
|
112
|
+
}
|
|
113
|
+
clear() {
|
|
114
|
+
this.files.clear();
|
|
115
|
+
this.dirs.clear();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/internal/sink-manager.ts
|
|
120
|
+
var SinkManager = class {
|
|
121
|
+
constructor() {
|
|
122
|
+
this.observers = [];
|
|
123
|
+
}
|
|
124
|
+
/** Register an observer; returns an unsubscribe function. */
|
|
125
|
+
subscribe(observer) {
|
|
126
|
+
this.observers.push(observer);
|
|
127
|
+
return () => {
|
|
128
|
+
const i = this.observers.indexOf(observer);
|
|
129
|
+
if (i >= 0) this.observers.splice(i, 1);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Snapshot of observer count — useful for diagnostics. */
|
|
133
|
+
get observerCount() {
|
|
134
|
+
return this.observers.length;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Notify all observers in parallel. Awaits all settlements. A rejected
|
|
138
|
+
* observer is captured in the result; subsequent writes still proceed.
|
|
139
|
+
*/
|
|
140
|
+
async notifyAll(event) {
|
|
141
|
+
if (this.observers.length === 0) return;
|
|
142
|
+
const settled = await Promise.allSettled(
|
|
143
|
+
this.observers.map(async (o) => o.onWrite(event))
|
|
144
|
+
);
|
|
145
|
+
for (const r of settled) {
|
|
146
|
+
if (r.status === "rejected") {
|
|
147
|
+
const reason = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
148
|
+
console.error(`[workspace] observer error on ${event.kind}: ${reason}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var WORKSPACE_LAYOUT = {
|
|
154
|
+
ALMADAR_DIR: ".almadar",
|
|
155
|
+
ORBITALS_DIR: "orbitals",
|
|
156
|
+
SESSIONS_DIR: ".almadar/sessions",
|
|
157
|
+
COORDINATOR_DIR: ".almadar/sessions/Coordinator",
|
|
158
|
+
TRACE_FILE: ".almadar/trace.jsonl",
|
|
159
|
+
SCHEMA_FILE: "schema.orb",
|
|
160
|
+
COMPILED_DIR: "apps",
|
|
161
|
+
APP_MARKER: ".almadar/app-marker.json",
|
|
162
|
+
USER_MEMORY: ".almadar/user.orb",
|
|
163
|
+
PROJECT_MEMORY: ".almadar/project.orb"
|
|
164
|
+
};
|
|
165
|
+
function orbitalFile(workDir, name) {
|
|
166
|
+
return path2.join(workDir, WORKSPACE_LAYOUT.ORBITALS_DIR, `${name}.orb`);
|
|
167
|
+
}
|
|
168
|
+
function archivedOrbitalFile(workDir, name) {
|
|
169
|
+
return path2.join(workDir, WORKSPACE_LAYOUT.ORBITALS_DIR, ".archived", `${name}.orb`);
|
|
170
|
+
}
|
|
171
|
+
function schemaFile(workDir) {
|
|
172
|
+
return path2.join(workDir, WORKSPACE_LAYOUT.SCHEMA_FILE);
|
|
173
|
+
}
|
|
174
|
+
function coordinatorDir(workDir) {
|
|
175
|
+
return path2.join(workDir, WORKSPACE_LAYOUT.COORDINATOR_DIR);
|
|
176
|
+
}
|
|
177
|
+
function coordinatorFile(workDir, basename) {
|
|
178
|
+
return path2.join(coordinatorDir(workDir), basename);
|
|
179
|
+
}
|
|
180
|
+
function orbitalSessionDir(workDir, orbital) {
|
|
181
|
+
return path2.join(workDir, WORKSPACE_LAYOUT.SESSIONS_DIR, orbital);
|
|
182
|
+
}
|
|
183
|
+
function orbitalSessionFile(workDir, orbital, basename) {
|
|
184
|
+
return path2.join(orbitalSessionDir(workDir, orbital), basename);
|
|
185
|
+
}
|
|
186
|
+
function traceFile(workDir) {
|
|
187
|
+
return path2.join(workDir, WORKSPACE_LAYOUT.TRACE_FILE);
|
|
188
|
+
}
|
|
189
|
+
function compiledDir(workDir) {
|
|
190
|
+
return path2.join(workDir, WORKSPACE_LAYOUT.COMPILED_DIR);
|
|
191
|
+
}
|
|
192
|
+
function compiledFile(workDir, relPath) {
|
|
193
|
+
return path2.join(compiledDir(workDir), relPath);
|
|
194
|
+
}
|
|
195
|
+
function appMarkerFile(workDir) {
|
|
196
|
+
return path2.join(workDir, WORKSPACE_LAYOUT.APP_MARKER);
|
|
197
|
+
}
|
|
198
|
+
function sandboxedPath(workDir, relPath) {
|
|
199
|
+
if (typeof relPath !== "string" || relPath.length === 0) {
|
|
200
|
+
throw new Error("sandbox: empty relative path");
|
|
201
|
+
}
|
|
202
|
+
if (path2.isAbsolute(relPath)) {
|
|
203
|
+
throw new Error(`sandbox: absolute paths are not allowed (${relPath})`);
|
|
204
|
+
}
|
|
205
|
+
if (/^[a-zA-Z]:[\\/]/.test(relPath)) {
|
|
206
|
+
throw new Error(`sandbox: drive-letter paths are not allowed (${relPath})`);
|
|
207
|
+
}
|
|
208
|
+
const rootAbs = path2.resolve(workDir);
|
|
209
|
+
const target = path2.resolve(rootAbs, relPath);
|
|
210
|
+
if (target !== rootAbs && !target.startsWith(rootAbs + path2.sep)) {
|
|
211
|
+
throw new Error(`sandbox: path escapes workspace (${relPath})`);
|
|
212
|
+
}
|
|
213
|
+
return target;
|
|
214
|
+
}
|
|
215
|
+
function assertOrbitalName(name) {
|
|
216
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
217
|
+
throw new Error("orbital name: empty");
|
|
218
|
+
}
|
|
219
|
+
if (name.includes("/") || name.includes("\\") || name === "." || name === "..") {
|
|
220
|
+
throw new Error(`orbital name: invalid (${name})`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/internal/templates.ts
|
|
225
|
+
function createSchemaOrbTemplate(name) {
|
|
226
|
+
return {
|
|
227
|
+
name,
|
|
228
|
+
version: "1.0.0",
|
|
229
|
+
description: "",
|
|
230
|
+
orbitals: []
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function createUserOrbTemplate(userId) {
|
|
234
|
+
return {
|
|
235
|
+
name: "UserMemory",
|
|
236
|
+
version: "1.0.0",
|
|
237
|
+
description: "User preferences and learned patterns",
|
|
238
|
+
orbitals: [
|
|
239
|
+
{
|
|
240
|
+
name: "PreferenceTracking",
|
|
241
|
+
entity: {
|
|
242
|
+
name: "UserPreference",
|
|
243
|
+
fields: [],
|
|
244
|
+
instances: [
|
|
245
|
+
{
|
|
246
|
+
id: `pref-${userId}`,
|
|
247
|
+
userId
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
traits: [],
|
|
252
|
+
pages: []
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function createProjectOrbTemplate(projectName, appId) {
|
|
258
|
+
return {
|
|
259
|
+
name: "ProjectMemory",
|
|
260
|
+
version: "1.0.0",
|
|
261
|
+
description: `Domain knowledge for ${projectName}`,
|
|
262
|
+
orbitals: [
|
|
263
|
+
{
|
|
264
|
+
name: "DomainKnowledge",
|
|
265
|
+
entity: {
|
|
266
|
+
name: "ProjectContext",
|
|
267
|
+
fields: [],
|
|
268
|
+
instances: [
|
|
269
|
+
{
|
|
270
|
+
id: appId ?? `project-${projectName}`,
|
|
271
|
+
appId: appId ?? "",
|
|
272
|
+
projectName,
|
|
273
|
+
lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
},
|
|
277
|
+
traits: [],
|
|
278
|
+
pages: []
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function serializeJson(value) {
|
|
284
|
+
return JSON.stringify(value, null, 2);
|
|
285
|
+
}
|
|
286
|
+
var GitClient = class {
|
|
287
|
+
constructor(cwd, backend) {
|
|
288
|
+
this.cwd = cwd;
|
|
289
|
+
this.backend = backend;
|
|
290
|
+
}
|
|
291
|
+
/** Is the working dir already a git repo? */
|
|
292
|
+
async isRepo() {
|
|
293
|
+
return this.backend.exists(path2.join(this.cwd, ".git"));
|
|
294
|
+
}
|
|
295
|
+
async init() {
|
|
296
|
+
if (await this.isRepo()) return;
|
|
297
|
+
await this.exec(["init"]);
|
|
298
|
+
await this.exec(["config", "user.email", "agent@almadar.io"]);
|
|
299
|
+
await this.exec(["config", "user.name", "Almadar Agent"]);
|
|
300
|
+
}
|
|
301
|
+
async addAll() {
|
|
302
|
+
await this.exec(["add", "-A"]);
|
|
303
|
+
}
|
|
304
|
+
async commit(message) {
|
|
305
|
+
try {
|
|
306
|
+
const out = await this.exec(["commit", "-m", message, "--allow-empty-message"]);
|
|
307
|
+
const match = out.match(/\[[\w/.-]+ ([a-f0-9]+)\]/);
|
|
308
|
+
return match ? match[1] : null;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (err instanceof Error && err.message.includes("nothing to commit")) return null;
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async tag(name, message) {
|
|
315
|
+
const args = ["tag"];
|
|
316
|
+
if (message) {
|
|
317
|
+
args.push("-a", name, "-m", message);
|
|
318
|
+
} else {
|
|
319
|
+
args.push(name);
|
|
320
|
+
}
|
|
321
|
+
await this.exec(args);
|
|
322
|
+
}
|
|
323
|
+
async push(remote = "origin", branch) {
|
|
324
|
+
const args = ["push", remote];
|
|
325
|
+
if (branch) args.push(branch);
|
|
326
|
+
await this.exec(args);
|
|
327
|
+
}
|
|
328
|
+
async pull(remote = "origin", branch) {
|
|
329
|
+
const args = ["pull", remote];
|
|
330
|
+
if (branch) args.push(branch);
|
|
331
|
+
await this.exec(args);
|
|
332
|
+
}
|
|
333
|
+
async hasRemote(remote = "origin") {
|
|
334
|
+
try {
|
|
335
|
+
const out = await this.exec(["remote"]);
|
|
336
|
+
return out.split("\n").map((s) => s.trim()).includes(remote);
|
|
337
|
+
} catch {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async addRemote(remote, url) {
|
|
342
|
+
await this.exec(["remote", "add", remote, url]);
|
|
343
|
+
}
|
|
344
|
+
async status() {
|
|
345
|
+
const out = await this.exec(["status", "--porcelain"]);
|
|
346
|
+
const lines = out.split("\n").filter(Boolean);
|
|
347
|
+
const staged = [];
|
|
348
|
+
const modified = [];
|
|
349
|
+
const untracked = [];
|
|
350
|
+
for (const line of lines) {
|
|
351
|
+
const index = line[0];
|
|
352
|
+
const work = line[1];
|
|
353
|
+
const file = line.slice(3);
|
|
354
|
+
if (index === "?" && work === "?") untracked.push(file);
|
|
355
|
+
else if (index !== " " && index !== "?") staged.push(file);
|
|
356
|
+
else if (work !== " ") modified.push(file);
|
|
357
|
+
}
|
|
358
|
+
return { clean: lines.length === 0, staged, modified, untracked };
|
|
359
|
+
}
|
|
360
|
+
async headSha() {
|
|
361
|
+
try {
|
|
362
|
+
const out = await this.exec(["rev-parse", "HEAD"]);
|
|
363
|
+
return out.trim() || null;
|
|
364
|
+
} catch {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
exec(args) {
|
|
369
|
+
return execGit(args, this.cwd);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
function execGit(args, cwd) {
|
|
373
|
+
return new Promise((resolve, reject) => {
|
|
374
|
+
execFile("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
375
|
+
if (err) {
|
|
376
|
+
const message = stderr?.trim() || stdout?.trim() || err.message;
|
|
377
|
+
reject(new Error(`git ${args[0]}: ${message}`));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
resolve(stdout);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/internal/workspace-manager.ts
|
|
386
|
+
async function ensureSkeleton(backend, workDir) {
|
|
387
|
+
await backend.mkdir(workDir, { recursive: true });
|
|
388
|
+
await backend.mkdir(path2.join(workDir, WORKSPACE_LAYOUT.ALMADAR_DIR), { recursive: true });
|
|
389
|
+
await backend.mkdir(path2.join(workDir, WORKSPACE_LAYOUT.ORBITALS_DIR), { recursive: true });
|
|
390
|
+
await backend.mkdir(path2.join(workDir, WORKSPACE_LAYOUT.SESSIONS_DIR), { recursive: true });
|
|
391
|
+
await backend.mkdir(coordinatorDir(workDir), { recursive: true });
|
|
392
|
+
}
|
|
393
|
+
async function ensureOrbitalSessionDir(backend, workDir, orbital) {
|
|
394
|
+
await backend.mkdir(orbitalSessionDir(workDir, orbital), { recursive: true });
|
|
395
|
+
}
|
|
396
|
+
async function writeMintTemplatesIfMissing(backend, workDir, userId, projectName, appId) {
|
|
397
|
+
const schemaPath = schemaFile(workDir);
|
|
398
|
+
if (!backend.exists(schemaPath)) {
|
|
399
|
+
await backend.writeFile(schemaPath, serializeJson(createSchemaOrbTemplate(projectName)));
|
|
400
|
+
}
|
|
401
|
+
const userPath = path2.join(workDir, WORKSPACE_LAYOUT.USER_MEMORY);
|
|
402
|
+
if (!backend.exists(userPath)) {
|
|
403
|
+
await backend.writeFile(userPath, serializeJson(createUserOrbTemplate(userId)));
|
|
404
|
+
}
|
|
405
|
+
const projectPath = path2.join(workDir, WORKSPACE_LAYOUT.PROJECT_MEMORY);
|
|
406
|
+
if (!backend.exists(projectPath)) {
|
|
407
|
+
await backend.writeFile(
|
|
408
|
+
projectPath,
|
|
409
|
+
serializeJson(createProjectOrbTemplate(projectName, appId))
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function ensureGitInit(backend, workDir) {
|
|
414
|
+
const git = new GitClient(workDir, backend);
|
|
415
|
+
await git.init();
|
|
416
|
+
return git;
|
|
417
|
+
}
|
|
418
|
+
function readAppMarker(backend, workDir) {
|
|
419
|
+
const markerPath = appMarkerFile(workDir);
|
|
420
|
+
if (!backend.exists(markerPath)) return null;
|
|
421
|
+
try {
|
|
422
|
+
const raw = backend.readFileSync(markerPath);
|
|
423
|
+
const parsed = JSON.parse(raw);
|
|
424
|
+
if (typeof parsed.appId !== "string" || typeof parsed.userId !== "string" || typeof parsed.createdAt !== "number") {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
const marker = {
|
|
428
|
+
appId: parsed.appId,
|
|
429
|
+
userId: parsed.userId,
|
|
430
|
+
createdAt: parsed.createdAt
|
|
431
|
+
};
|
|
432
|
+
if (typeof parsed.repoUrl === "string") marker.repoUrl = parsed.repoUrl;
|
|
433
|
+
return marker;
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function writeAppMarker(backend, workDir, marker) {
|
|
439
|
+
const markerPath = appMarkerFile(workDir);
|
|
440
|
+
await backend.mkdir(path2.dirname(markerPath), { recursive: true });
|
|
441
|
+
await backend.writeFile(markerPath, JSON.stringify(marker, null, 2));
|
|
442
|
+
}
|
|
443
|
+
async function findLocalWorkspaceDir(backend, workspacesRoot, userId, appId) {
|
|
444
|
+
const userDir = path2.join(workspacesRoot, userId);
|
|
445
|
+
if (!backend.exists(userDir)) return null;
|
|
446
|
+
let entries;
|
|
447
|
+
try {
|
|
448
|
+
entries = await backend.readdir(userDir);
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
for (const entry of entries) {
|
|
453
|
+
const sessionDir = path2.join(userDir, entry);
|
|
454
|
+
let isDir = false;
|
|
455
|
+
try {
|
|
456
|
+
const s = await backend.stat(sessionDir);
|
|
457
|
+
isDir = s.isDirectory;
|
|
458
|
+
} catch {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (!isDir) continue;
|
|
462
|
+
const marker = readAppMarker(backend, sessionDir);
|
|
463
|
+
if (marker && marker.appId === appId) return sessionDir;
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
function mintSessionDir(workspacesRoot, userId) {
|
|
468
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
469
|
+
const suffix = Date.now().toString(36);
|
|
470
|
+
return path2.join(workspacesRoot, userId, `${ts}_${suffix}`);
|
|
471
|
+
}
|
|
472
|
+
async function restoreWorkspace(backend, rootDir, restore) {
|
|
473
|
+
const rootAbs = path2.resolve(rootDir);
|
|
474
|
+
await backend.mkdir(rootAbs, { recursive: true });
|
|
475
|
+
const paths = await restore.listFiles();
|
|
476
|
+
let filesRestored = 0;
|
|
477
|
+
let bytesWritten = 0;
|
|
478
|
+
for (const relativePath of paths) {
|
|
479
|
+
const normalised = relativePath.replace(/\\/g, "/");
|
|
480
|
+
const target = path2.resolve(rootAbs, normalised);
|
|
481
|
+
if (target !== rootAbs && !target.startsWith(rootAbs + path2.sep)) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const content = await restore.readFile(normalised);
|
|
485
|
+
if (content === null) continue;
|
|
486
|
+
await backend.mkdir(path2.dirname(target), { recursive: true });
|
|
487
|
+
await backend.writeFile(target, content);
|
|
488
|
+
filesRestored++;
|
|
489
|
+
bytesWritten += Buffer.byteLength(content, "utf-8");
|
|
490
|
+
}
|
|
491
|
+
return { filesRestored, bytesWritten };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/internal/memory-files.ts
|
|
495
|
+
async function writeJsonFile(backend, absPath, value) {
|
|
496
|
+
await backend.writeFile(absPath, JSON.stringify(value, null, 2));
|
|
497
|
+
}
|
|
498
|
+
async function appendJsonLine(backend, absPath, value) {
|
|
499
|
+
const existing = backend.exists(absPath) ? await backend.readFile(absPath) : "";
|
|
500
|
+
const ending = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
501
|
+
await backend.writeFile(absPath, existing + ending + JSON.stringify(value) + "\n");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/service.ts
|
|
505
|
+
var COORD_FILES = {
|
|
506
|
+
analysis: "analysis.json",
|
|
507
|
+
plan: "plan.json",
|
|
508
|
+
clarifications: "clarifications.json",
|
|
509
|
+
messages: "messages.json"
|
|
510
|
+
};
|
|
511
|
+
var ORBITAL_FILES = {
|
|
512
|
+
spec: "spec.json",
|
|
513
|
+
memory: "memory.json",
|
|
514
|
+
history: "history.jsonl",
|
|
515
|
+
paramsHistory: "params-history.jsonl",
|
|
516
|
+
errors: "errors.json",
|
|
517
|
+
messages: "messages.json"
|
|
518
|
+
};
|
|
519
|
+
var WorkspaceServiceImpl = class {
|
|
520
|
+
constructor(args) {
|
|
521
|
+
/** Per-absolute-path serial queue. */
|
|
522
|
+
this.writeQueue = /* @__PURE__ */ new Map();
|
|
523
|
+
this.workDir = args.workDir;
|
|
524
|
+
this.backend = args.backend;
|
|
525
|
+
this.sinks = args.sinks;
|
|
526
|
+
this._appId = args.appId;
|
|
527
|
+
this.git = args.git;
|
|
528
|
+
this.github = args.github;
|
|
529
|
+
}
|
|
530
|
+
// === Identity ===
|
|
531
|
+
get appId() {
|
|
532
|
+
return this._appId;
|
|
533
|
+
}
|
|
534
|
+
setAppId(id) {
|
|
535
|
+
this._appId = id;
|
|
536
|
+
}
|
|
537
|
+
// === Helpers ===
|
|
538
|
+
/** Run `op` under a per-path serial lock. */
|
|
539
|
+
withLock(absPath, op) {
|
|
540
|
+
const prev = this.writeQueue.get(absPath) ?? Promise.resolve();
|
|
541
|
+
let resolved;
|
|
542
|
+
const next = prev.then(async () => {
|
|
543
|
+
resolved = await op();
|
|
544
|
+
});
|
|
545
|
+
const swallowed = next.catch(() => void 0);
|
|
546
|
+
this.writeQueue.set(absPath, swallowed);
|
|
547
|
+
return next.then(() => resolved);
|
|
548
|
+
}
|
|
549
|
+
/** Make sure the parent directory of `absPath` exists. */
|
|
550
|
+
async ensureParent(absPath) {
|
|
551
|
+
await this.backend.mkdir(path2.dirname(absPath), { recursive: true });
|
|
552
|
+
}
|
|
553
|
+
async emit(event) {
|
|
554
|
+
await this.sinks.notifyAll(event);
|
|
555
|
+
}
|
|
556
|
+
// === Orbital artifacts ===
|
|
557
|
+
readOrbital(name) {
|
|
558
|
+
assertOrbitalName(name);
|
|
559
|
+
const p = orbitalFile(this.workDir, name);
|
|
560
|
+
if (!this.backend.exists(p)) return null;
|
|
561
|
+
try {
|
|
562
|
+
return this.backend.readFileSync(p);
|
|
563
|
+
} catch {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async writeOrbital(name, content) {
|
|
568
|
+
assertOrbitalName(name);
|
|
569
|
+
const p = orbitalFile(this.workDir, name);
|
|
570
|
+
await this.withLock(p, async () => {
|
|
571
|
+
await this.ensureParent(p);
|
|
572
|
+
await this.backend.writeFile(p, content);
|
|
573
|
+
});
|
|
574
|
+
await this.emit({ kind: "orbital", name, content });
|
|
575
|
+
}
|
|
576
|
+
listOrbitals() {
|
|
577
|
+
const dir = path2.join(this.workDir, WORKSPACE_LAYOUT.ORBITALS_DIR);
|
|
578
|
+
if (!this.backend.exists(dir)) return [];
|
|
579
|
+
try {
|
|
580
|
+
return this.backend.readdirSync(dir).filter((f) => f.endsWith(".orb")).map((f) => f.replace(/\.orb$/, "")).sort();
|
|
581
|
+
} catch {
|
|
582
|
+
return [];
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async archiveOrbital(name) {
|
|
586
|
+
assertOrbitalName(name);
|
|
587
|
+
const src = orbitalFile(this.workDir, name);
|
|
588
|
+
if (!this.backend.exists(src)) return;
|
|
589
|
+
const content = await this.backend.readFile(src);
|
|
590
|
+
const dst = archivedOrbitalFile(this.workDir, name);
|
|
591
|
+
await this.withLock(src, async () => {
|
|
592
|
+
await this.ensureParent(dst);
|
|
593
|
+
await this.backend.writeFile(dst, content);
|
|
594
|
+
await this.backend.unlink(src);
|
|
595
|
+
});
|
|
596
|
+
await this.emit({ kind: "orbital-archived", name });
|
|
597
|
+
}
|
|
598
|
+
async renameOrbital(from, to) {
|
|
599
|
+
assertOrbitalName(from);
|
|
600
|
+
assertOrbitalName(to);
|
|
601
|
+
const src = orbitalFile(this.workDir, from);
|
|
602
|
+
const dst = orbitalFile(this.workDir, to);
|
|
603
|
+
if (!this.backend.exists(src)) return;
|
|
604
|
+
const content = await this.backend.readFile(src);
|
|
605
|
+
await this.withLock(src, async () => {
|
|
606
|
+
await this.ensureParent(dst);
|
|
607
|
+
await this.backend.writeFile(dst, content);
|
|
608
|
+
await this.backend.unlink(src);
|
|
609
|
+
});
|
|
610
|
+
await this.emit({ kind: "orbital-renamed", from, to });
|
|
611
|
+
}
|
|
612
|
+
// === Schema artifact ===
|
|
613
|
+
readSchema() {
|
|
614
|
+
const p = schemaFile(this.workDir);
|
|
615
|
+
if (!this.backend.exists(p)) return null;
|
|
616
|
+
try {
|
|
617
|
+
return this.backend.readFileSync(p);
|
|
618
|
+
} catch {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async writeSchema(content) {
|
|
623
|
+
const p = schemaFile(this.workDir);
|
|
624
|
+
await this.withLock(p, async () => {
|
|
625
|
+
await this.ensureParent(p);
|
|
626
|
+
await this.backend.writeFile(p, content);
|
|
627
|
+
});
|
|
628
|
+
await this.emit({ kind: "schema", content });
|
|
629
|
+
}
|
|
630
|
+
// === Coordinator session ===
|
|
631
|
+
readAnalysis() {
|
|
632
|
+
return readJsonFileSync(this.backend, coordinatorFile(this.workDir, COORD_FILES.analysis));
|
|
633
|
+
}
|
|
634
|
+
async writeAnalysis(analysis) {
|
|
635
|
+
const p = coordinatorFile(this.workDir, COORD_FILES.analysis);
|
|
636
|
+
await this.withLock(p, async () => {
|
|
637
|
+
await this.ensureParent(p);
|
|
638
|
+
await writeJsonFile(this.backend, p, analysis);
|
|
639
|
+
});
|
|
640
|
+
await this.emit({ kind: "analysis", content: analysis });
|
|
641
|
+
}
|
|
642
|
+
readPlan() {
|
|
643
|
+
return readJsonFileSync(this.backend, coordinatorFile(this.workDir, COORD_FILES.plan));
|
|
644
|
+
}
|
|
645
|
+
async writePlan(plan) {
|
|
646
|
+
const p = coordinatorFile(this.workDir, COORD_FILES.plan);
|
|
647
|
+
await this.withLock(p, async () => {
|
|
648
|
+
await this.ensureParent(p);
|
|
649
|
+
await writeJsonFile(this.backend, p, plan);
|
|
650
|
+
});
|
|
651
|
+
await this.emit({ kind: "plan", content: plan });
|
|
652
|
+
}
|
|
653
|
+
readClarificationAnswers() {
|
|
654
|
+
const p = coordinatorFile(this.workDir, COORD_FILES.clarifications);
|
|
655
|
+
if (!this.backend.exists(p)) return {};
|
|
656
|
+
try {
|
|
657
|
+
const parsed = JSON.parse(this.backend.readFileSync(p));
|
|
658
|
+
const out = {};
|
|
659
|
+
for (const k of Object.keys(parsed)) {
|
|
660
|
+
const v = parsed[k];
|
|
661
|
+
if (typeof v === "string") out[k] = v;
|
|
662
|
+
}
|
|
663
|
+
return out;
|
|
664
|
+
} catch {
|
|
665
|
+
return {};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async writeClarificationAnswers(answers) {
|
|
669
|
+
const p = coordinatorFile(this.workDir, COORD_FILES.clarifications);
|
|
670
|
+
const asJson = { ...answers };
|
|
671
|
+
await this.withLock(p, async () => {
|
|
672
|
+
await this.ensureParent(p);
|
|
673
|
+
await writeJsonFile(this.backend, p, asJson);
|
|
674
|
+
});
|
|
675
|
+
await this.emit({ kind: "clarifications", content: answers });
|
|
676
|
+
}
|
|
677
|
+
readCoordinatorMessages() {
|
|
678
|
+
const p = coordinatorFile(this.workDir, COORD_FILES.messages);
|
|
679
|
+
if (!this.backend.exists(p)) return null;
|
|
680
|
+
try {
|
|
681
|
+
const parsed = JSON.parse(this.backend.readFileSync(p));
|
|
682
|
+
if (!Array.isArray(parsed)) return null;
|
|
683
|
+
return parsed;
|
|
684
|
+
} catch {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
async writeCoordinatorMessages(messages) {
|
|
689
|
+
const p = coordinatorFile(this.workDir, COORD_FILES.messages);
|
|
690
|
+
await this.withLock(p, async () => {
|
|
691
|
+
await this.ensureParent(p);
|
|
692
|
+
await this.backend.writeFile(p, JSON.stringify(messages, null, 2));
|
|
693
|
+
});
|
|
694
|
+
await this.emit({ kind: "coordinator-messages", content: messages });
|
|
695
|
+
}
|
|
696
|
+
// === Per-orbital session ===
|
|
697
|
+
orbitalFile(orbital, basename) {
|
|
698
|
+
assertOrbitalName(orbital);
|
|
699
|
+
return orbitalSessionFile(this.workDir, orbital, basename);
|
|
700
|
+
}
|
|
701
|
+
readSpec(orbital) {
|
|
702
|
+
return readJsonFileSync(this.backend, this.orbitalFile(orbital, ORBITAL_FILES.spec));
|
|
703
|
+
}
|
|
704
|
+
async writeSpec(orbital, spec) {
|
|
705
|
+
const p = this.orbitalFile(orbital, ORBITAL_FILES.spec);
|
|
706
|
+
await this.withLock(p, async () => {
|
|
707
|
+
await ensureOrbitalSessionDir(this.backend, this.workDir, orbital);
|
|
708
|
+
await writeJsonFile(this.backend, p, spec);
|
|
709
|
+
});
|
|
710
|
+
await this.emit({ kind: "spec", orbital, content: spec });
|
|
711
|
+
}
|
|
712
|
+
readMemory(orbital) {
|
|
713
|
+
return readJsonFileSync(this.backend, this.orbitalFile(orbital, ORBITAL_FILES.memory));
|
|
714
|
+
}
|
|
715
|
+
async writeMemory(orbital, memory) {
|
|
716
|
+
const p = this.orbitalFile(orbital, ORBITAL_FILES.memory);
|
|
717
|
+
await this.withLock(p, async () => {
|
|
718
|
+
await ensureOrbitalSessionDir(this.backend, this.workDir, orbital);
|
|
719
|
+
await writeJsonFile(this.backend, p, memory);
|
|
720
|
+
});
|
|
721
|
+
await this.emit({ kind: "memory", orbital, content: memory });
|
|
722
|
+
}
|
|
723
|
+
async appendHistory(orbital, entry) {
|
|
724
|
+
const p = this.orbitalFile(orbital, ORBITAL_FILES.history);
|
|
725
|
+
await this.withLock(p, async () => {
|
|
726
|
+
await ensureOrbitalSessionDir(this.backend, this.workDir, orbital);
|
|
727
|
+
await appendJsonLine(this.backend, p, entry);
|
|
728
|
+
});
|
|
729
|
+
await this.emit({ kind: "history", orbital, entry });
|
|
730
|
+
}
|
|
731
|
+
readHistory(orbital) {
|
|
732
|
+
const p = this.orbitalFile(orbital, ORBITAL_FILES.history);
|
|
733
|
+
return readJsonLinesSync(this.backend, p);
|
|
734
|
+
}
|
|
735
|
+
async appendParamsHistory(orbital, row) {
|
|
736
|
+
const p = this.orbitalFile(orbital, ORBITAL_FILES.paramsHistory);
|
|
737
|
+
await this.withLock(p, async () => {
|
|
738
|
+
await ensureOrbitalSessionDir(this.backend, this.workDir, orbital);
|
|
739
|
+
await appendJsonLine(this.backend, p, row);
|
|
740
|
+
});
|
|
741
|
+
await this.emit({ kind: "params-history", orbital, row });
|
|
742
|
+
}
|
|
743
|
+
readErrors(orbital) {
|
|
744
|
+
const p = this.orbitalFile(orbital, ORBITAL_FILES.errors);
|
|
745
|
+
if (!this.backend.exists(p)) return [];
|
|
746
|
+
try {
|
|
747
|
+
const parsed = JSON.parse(this.backend.readFileSync(p));
|
|
748
|
+
if (!Array.isArray(parsed)) return [];
|
|
749
|
+
return parsed;
|
|
750
|
+
} catch {
|
|
751
|
+
return [];
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
async writeErrors(orbital, errors) {
|
|
755
|
+
const p = this.orbitalFile(orbital, ORBITAL_FILES.errors);
|
|
756
|
+
await this.withLock(p, async () => {
|
|
757
|
+
await ensureOrbitalSessionDir(this.backend, this.workDir, orbital);
|
|
758
|
+
await this.backend.writeFile(p, JSON.stringify(errors, null, 2));
|
|
759
|
+
});
|
|
760
|
+
await this.emit({ kind: "errors", orbital, content: errors });
|
|
761
|
+
}
|
|
762
|
+
readSubagentMessages(orbital) {
|
|
763
|
+
const p = this.orbitalFile(orbital, ORBITAL_FILES.messages);
|
|
764
|
+
if (!this.backend.exists(p)) return null;
|
|
765
|
+
try {
|
|
766
|
+
const parsed = JSON.parse(this.backend.readFileSync(p));
|
|
767
|
+
if (!Array.isArray(parsed)) return null;
|
|
768
|
+
return parsed;
|
|
769
|
+
} catch {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async writeSubagentMessages(orbital, messages) {
|
|
774
|
+
const p = this.orbitalFile(orbital, ORBITAL_FILES.messages);
|
|
775
|
+
await this.withLock(p, async () => {
|
|
776
|
+
await ensureOrbitalSessionDir(this.backend, this.workDir, orbital);
|
|
777
|
+
await this.backend.writeFile(p, JSON.stringify(messages, null, 2));
|
|
778
|
+
});
|
|
779
|
+
await this.emit({ kind: "subagent-messages", orbital, content: messages });
|
|
780
|
+
}
|
|
781
|
+
// === Trace ===
|
|
782
|
+
async emitTrace(event) {
|
|
783
|
+
const p = traceFile(this.workDir);
|
|
784
|
+
await this.withLock(p, async () => {
|
|
785
|
+
await this.ensureParent(p);
|
|
786
|
+
await appendJsonLine(this.backend, p, event);
|
|
787
|
+
});
|
|
788
|
+
await this.emit({ kind: "trace", event });
|
|
789
|
+
}
|
|
790
|
+
readTrace() {
|
|
791
|
+
const p = traceFile(this.workDir);
|
|
792
|
+
return readJsonLinesSync(this.backend, p);
|
|
793
|
+
}
|
|
794
|
+
// === Compiled output ===
|
|
795
|
+
async writeCompiled(relPath, content) {
|
|
796
|
+
if (relPath.includes("..") || path2.isAbsolute(relPath)) {
|
|
797
|
+
throw new Error(`compiled: invalid relPath (${relPath})`);
|
|
798
|
+
}
|
|
799
|
+
const p = compiledFile(this.workDir, relPath);
|
|
800
|
+
await this.withLock(p, async () => {
|
|
801
|
+
await this.ensureParent(p);
|
|
802
|
+
await this.backend.writeFile(p, content);
|
|
803
|
+
});
|
|
804
|
+
await this.emit({ kind: "compiled", relPath, content });
|
|
805
|
+
}
|
|
806
|
+
readCompiled(relPath) {
|
|
807
|
+
if (relPath.includes("..") || path2.isAbsolute(relPath)) return null;
|
|
808
|
+
const p = compiledFile(this.workDir, relPath);
|
|
809
|
+
if (!this.backend.exists(p)) return null;
|
|
810
|
+
try {
|
|
811
|
+
return this.backend.readFileSync(p);
|
|
812
|
+
} catch {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async clearCompiled() {
|
|
817
|
+
const dir = compiledDir(this.workDir);
|
|
818
|
+
if (!this.backend.exists(dir)) return;
|
|
819
|
+
await removeTree(this.backend, dir);
|
|
820
|
+
}
|
|
821
|
+
// === Sandboxed generic file I/O ===
|
|
822
|
+
async readFile(relPath) {
|
|
823
|
+
const abs = sandboxedPath(this.workDir, relPath);
|
|
824
|
+
if (!this.backend.exists(abs)) return null;
|
|
825
|
+
try {
|
|
826
|
+
return await this.backend.readFile(abs);
|
|
827
|
+
} catch {
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async writeFile(relPath, content) {
|
|
832
|
+
const abs = sandboxedPath(this.workDir, relPath);
|
|
833
|
+
await this.withLock(abs, async () => {
|
|
834
|
+
await this.ensureParent(abs);
|
|
835
|
+
await this.backend.writeFile(abs, content);
|
|
836
|
+
});
|
|
837
|
+
await this.emit({ kind: "file", relPath, content });
|
|
838
|
+
}
|
|
839
|
+
async listTree(relPath) {
|
|
840
|
+
const base = relPath === void 0 ? this.workDir : sandboxedPath(this.workDir, relPath);
|
|
841
|
+
if (!this.backend.exists(base)) return [];
|
|
842
|
+
const out = [];
|
|
843
|
+
await walk(this.backend, base, this.workDir, out);
|
|
844
|
+
return out;
|
|
845
|
+
}
|
|
846
|
+
async exists(relPath) {
|
|
847
|
+
let abs;
|
|
848
|
+
try {
|
|
849
|
+
abs = sandboxedPath(this.workDir, relPath);
|
|
850
|
+
} catch {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
return this.backend.exists(abs);
|
|
854
|
+
}
|
|
855
|
+
// === Git ===
|
|
856
|
+
async commitAndPush(opts) {
|
|
857
|
+
if (!this.git) return null;
|
|
858
|
+
await this.git.addAll();
|
|
859
|
+
const sha = await this.git.commit(opts.message);
|
|
860
|
+
if (sha === null) return null;
|
|
861
|
+
if (opts.tags) {
|
|
862
|
+
for (const t of opts.tags) await this.git.tag(t);
|
|
863
|
+
}
|
|
864
|
+
if (this.github && this.github.repoUrl) {
|
|
865
|
+
if (!await this.git.hasRemote("origin")) {
|
|
866
|
+
await this.git.addRemote("origin", this.github.repoUrl);
|
|
867
|
+
}
|
|
868
|
+
await this.git.push("origin", this.github.branch ?? "main");
|
|
869
|
+
}
|
|
870
|
+
return { sha };
|
|
871
|
+
}
|
|
872
|
+
async pullIfLinked() {
|
|
873
|
+
if (!this.git || !this.github) return false;
|
|
874
|
+
if (!await this.git.hasRemote("origin")) return false;
|
|
875
|
+
await this.git.pull("origin", this.github.branch ?? "main");
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
async gitStatus() {
|
|
879
|
+
if (!this.git) {
|
|
880
|
+
return { clean: true, staged: [], modified: [], untracked: [], linked: false };
|
|
881
|
+
}
|
|
882
|
+
const s = await this.git.status();
|
|
883
|
+
const linked = await this.git.hasRemote("origin");
|
|
884
|
+
return { ...s, linked };
|
|
885
|
+
}
|
|
886
|
+
// === Observation ===
|
|
887
|
+
subscribe(observer) {
|
|
888
|
+
return this.sinks.subscribe(observer);
|
|
889
|
+
}
|
|
890
|
+
// === Disposal ===
|
|
891
|
+
async dispose() {
|
|
892
|
+
const pending = Array.from(this.writeQueue.values());
|
|
893
|
+
await Promise.allSettled(pending);
|
|
894
|
+
if (this.git && this.github) {
|
|
895
|
+
try {
|
|
896
|
+
await this.commitAndPush({ message: "workspace: dispose" });
|
|
897
|
+
} catch (err) {
|
|
898
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
899
|
+
console.error(`[workspace] dispose: commitAndPush failed: ${msg}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
function readJsonFileSync(backend, absPath) {
|
|
905
|
+
if (!backend.exists(absPath)) return null;
|
|
906
|
+
try {
|
|
907
|
+
return JSON.parse(backend.readFileSync(absPath));
|
|
908
|
+
} catch {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function readJsonLinesSync(backend, absPath) {
|
|
913
|
+
if (!backend.exists(absPath)) return [];
|
|
914
|
+
try {
|
|
915
|
+
const raw = backend.readFileSync(absPath);
|
|
916
|
+
const out = [];
|
|
917
|
+
for (const line of raw.split("\n")) {
|
|
918
|
+
if (!line.trim()) continue;
|
|
919
|
+
try {
|
|
920
|
+
out.push(JSON.parse(line));
|
|
921
|
+
} catch {
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return out;
|
|
925
|
+
} catch {
|
|
926
|
+
return [];
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
async function walk(backend, dir, root, out) {
|
|
930
|
+
const entries = await backend.readdir(dir);
|
|
931
|
+
for (const name of entries) {
|
|
932
|
+
const full = path2.join(dir, name);
|
|
933
|
+
const s = await backend.stat(full);
|
|
934
|
+
const rel = path2.relative(root, full).split(path2.sep).join("/");
|
|
935
|
+
if (s.isDirectory) {
|
|
936
|
+
out.push({ path: rel, type: "directory", size: 0 });
|
|
937
|
+
await walk(backend, full, root, out);
|
|
938
|
+
} else {
|
|
939
|
+
out.push({ path: rel, type: "file", size: s.size });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async function removeTree(backend, dir) {
|
|
944
|
+
const entries = await backend.readdir(dir);
|
|
945
|
+
for (const name of entries) {
|
|
946
|
+
const full = path2.join(dir, name);
|
|
947
|
+
let isDir = false;
|
|
948
|
+
try {
|
|
949
|
+
isDir = (await backend.stat(full)).isDirectory;
|
|
950
|
+
} catch {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (isDir) {
|
|
954
|
+
await removeTree(backend, full);
|
|
955
|
+
} else {
|
|
956
|
+
try {
|
|
957
|
+
await backend.unlink(full);
|
|
958
|
+
} catch {
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/open-workspace.ts
|
|
965
|
+
async function openWorkspace(opts) {
|
|
966
|
+
const backend = opts.backend === "memory" ? new MemoryBackend() : new LocalBackend();
|
|
967
|
+
const sinks = new SinkManager();
|
|
968
|
+
const resolved = await resolveLifecycle(backend, opts);
|
|
969
|
+
await ensureSkeleton(backend, resolved.workDir);
|
|
970
|
+
await writeMintTemplatesIfMissing(
|
|
971
|
+
backend,
|
|
972
|
+
resolved.workDir,
|
|
973
|
+
opts.userId,
|
|
974
|
+
opts.projectName ?? "Untitled",
|
|
975
|
+
resolved.appId
|
|
976
|
+
);
|
|
977
|
+
if (resolved.appId !== void 0) {
|
|
978
|
+
const existing = readAppMarker(backend, resolved.workDir);
|
|
979
|
+
if (!existing || existing.appId !== resolved.appId) {
|
|
980
|
+
const marker = {
|
|
981
|
+
appId: resolved.appId,
|
|
982
|
+
userId: opts.userId,
|
|
983
|
+
createdAt: existing?.createdAt ?? Date.now()
|
|
984
|
+
};
|
|
985
|
+
if (opts.github?.repoUrl) marker.repoUrl = opts.github.repoUrl;
|
|
986
|
+
await writeAppMarker(backend, resolved.workDir, marker);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
let git;
|
|
990
|
+
if (opts.github && opts.backend !== "memory") {
|
|
991
|
+
git = await ensureGitInit(backend, resolved.workDir);
|
|
992
|
+
}
|
|
993
|
+
return new WorkspaceServiceImpl({
|
|
994
|
+
workDir: resolved.workDir,
|
|
995
|
+
backend,
|
|
996
|
+
sinks,
|
|
997
|
+
appId: resolved.appId,
|
|
998
|
+
git,
|
|
999
|
+
github: opts.github
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
async function resolveLifecycle(backend, opts) {
|
|
1003
|
+
if (opts.adopt) {
|
|
1004
|
+
const workDir2 = path2.resolve(opts.adopt);
|
|
1005
|
+
const marker = readAppMarker(backend, workDir2);
|
|
1006
|
+
return { workDir: workDir2, appId: marker?.appId ?? opts.appId };
|
|
1007
|
+
}
|
|
1008
|
+
if (opts.appId) {
|
|
1009
|
+
const local = await findLocalWorkspaceDir(
|
|
1010
|
+
backend,
|
|
1011
|
+
opts.root,
|
|
1012
|
+
opts.userId,
|
|
1013
|
+
opts.appId
|
|
1014
|
+
);
|
|
1015
|
+
if (local) return { workDir: local, appId: opts.appId };
|
|
1016
|
+
}
|
|
1017
|
+
if (opts.appId && opts.restore) {
|
|
1018
|
+
const workDir2 = mintSessionDir(opts.root, opts.userId);
|
|
1019
|
+
await backend.mkdir(workDir2, { recursive: true });
|
|
1020
|
+
await restoreWorkspace(backend, workDir2, opts.restore);
|
|
1021
|
+
return { workDir: workDir2, appId: opts.appId };
|
|
1022
|
+
}
|
|
1023
|
+
if (opts.appId && opts.github && opts.github.repoUrl) {
|
|
1024
|
+
const workDir2 = mintSessionDir(opts.root, opts.userId);
|
|
1025
|
+
await backend.mkdir(workDir2, { recursive: true });
|
|
1026
|
+
return { workDir: workDir2, appId: opts.appId };
|
|
1027
|
+
}
|
|
1028
|
+
const workDir = mintSessionDir(opts.root, opts.userId);
|
|
1029
|
+
return { workDir, appId: opts.appId };
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
export { openWorkspace };
|
|
2
1033
|
//# sourceMappingURL=index.js.map
|
|
3
1034
|
//# sourceMappingURL=index.js.map
|