@almadar/workspace 0.9.0 → 0.10.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.
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -10,6 +10,6 @@ export { openWorkspace } from './open-workspace.js';
10
10
  export { listWorkspaces } from './list-workspaces.js';
11
11
  export { deleteWorkspace } from './delete-workspace.js';
12
12
  export { openAccount } from './account.js';
13
- export type { WorkspaceService, WorkspaceObserver, WorkspaceWriteEvent, WorkspaceWatchEvent, OpenWorkspaceOptions, ListWorkspacesOptions, WorkspaceSummary, RestoreBackend, GitHubConfig, GitStatusInfo, FileTreeNode, AccountService, AccountConfig, ProviderCredential, AuthToken, AccountIdentity, OpenAccountOptions, } from './types.js';
13
+ export type { WorkspaceService, WorkspaceObserver, WorkspaceWriteEvent, WorkspaceWatchEvent, OpenWorkspaceOptions, ListWorkspacesOptions, WorkspaceSummary, RestoreBackend, GitHubConfig, GitStatusInfo, SnapshotMeta, WorkspaceArchiveBackend, FileTreeNode, AccountService, AccountConfig, ProviderCredential, AuthToken, AccountIdentity, OpenAccountOptions, } from './types.js';
14
14
  export type { WorkspaceIndex, EmbedderPort, ResolveResult, ResolveOptions, TraitRefEmit, WorkspaceIndexStats, OrbitalIndexEntry, ExtraTraitIdentity, RetrievalResult, RetrievalOptions, RecentlyEditedOptions, EventEdge, EntityBinding, RuleBinding, RecencyEntry, IntentMaps, ComposedMaps, BM25Document, BM25Table, BM25Options, WorkspaceIndexManifest, } from './workspace-index/types.js';
15
15
  export { DEFAULT_COERCION_THRESHOLD, DEFAULT_RETRIEVAL_TOP_K, RRF_K, WORKSPACE_INDEX_SCHEMA_VERSION, } from './workspace-index/types.js';
package/dist/index.js CHANGED
@@ -21,6 +21,13 @@ var LocalBackend = class {
21
21
  fs.mkdirSync(path2.dirname(absPath), { recursive: true });
22
22
  fs.writeFileSync(absPath, content, "utf-8");
23
23
  }
24
+ async readFileBytes(absPath) {
25
+ return fs.promises.readFile(absPath);
26
+ }
27
+ async writeFileBytes(absPath, bytes) {
28
+ await fs.promises.mkdir(path2.dirname(absPath), { recursive: true });
29
+ await fs.promises.writeFile(absPath, bytes);
30
+ }
24
31
  exists(absPath) {
25
32
  return fs.existsSync(absPath);
26
33
  }
@@ -77,6 +84,7 @@ var LocalBackend = class {
77
84
  var MemoryBackend = class {
78
85
  constructor() {
79
86
  this.files = /* @__PURE__ */ new Map();
87
+ this.bytes = /* @__PURE__ */ new Map();
80
88
  this.dirs = /* @__PURE__ */ new Set();
81
89
  }
82
90
  async readFile(absPath) {
@@ -93,6 +101,14 @@ var MemoryBackend = class {
93
101
  writeFileSync(absPath, content) {
94
102
  this.files.set(absPath, content);
95
103
  }
104
+ async readFileBytes(absPath) {
105
+ const b = this.bytes.get(absPath);
106
+ if (b === void 0) throw new Error(`ENOENT: ${absPath}`);
107
+ return b;
108
+ }
109
+ async writeFileBytes(absPath, bytes) {
110
+ this.bytes.set(absPath, bytes);
111
+ }
96
112
  exists(absPath) {
97
113
  if (this.files.has(absPath) || this.dirs.has(absPath)) return true;
98
114
  const prefix = absPath.endsWith("/") ? absPath : absPath + "/";
@@ -364,6 +380,7 @@ function createProjectOrbTemplate(projectName, appId) {
364
380
  function serializeJson(value) {
365
381
  return JSON.stringify(value, null, 2);
366
382
  }
383
+ var GIT_BINARY = process.env.ALMADAR_GIT_BINARY || "git";
367
384
  var GitClient = class {
368
385
  constructor(cwd, backend) {
369
386
  this.cwd = cwd;
@@ -384,13 +401,12 @@ var GitClient = class {
384
401
  }
385
402
  async commit(message) {
386
403
  try {
387
- const out = await this.exec(["commit", "-m", message, "--allow-empty-message"]);
388
- const match = out.match(/\[[\w/.-]+ ([a-f0-9]+)\]/);
389
- return match ? match[1] : null;
404
+ await this.exec(["commit", "-m", message, "--allow-empty-message"]);
390
405
  } catch (err) {
391
406
  if (err instanceof Error && err.message.includes("nothing to commit")) return null;
392
407
  throw err;
393
408
  }
409
+ return this.headSha();
394
410
  }
395
411
  async tag(name, message) {
396
412
  const args = ["tag"];
@@ -446,13 +462,51 @@ var GitClient = class {
446
462
  return null;
447
463
  }
448
464
  }
465
+ /**
466
+ * Commit history of `filepath` (newest first) — the snapshot list. NUL-delimited
467
+ * fields so commit subjects with tabs/spaces parse cleanly.
468
+ */
469
+ async log(filepath, limit) {
470
+ const args = ["log", "--format=%H%x00%at%x00%s"];
471
+ if (limit !== void 0) args.push("-n", String(limit));
472
+ if (filepath) args.push("--", filepath);
473
+ let out;
474
+ try {
475
+ out = await this.exec(args);
476
+ } catch {
477
+ return [];
478
+ }
479
+ const entries = [];
480
+ for (const line of out.split("\n")) {
481
+ if (!line) continue;
482
+ const [oid, ts, subject] = line.split("\0");
483
+ if (oid && ts) entries.push({ oid, timestamp: Number(ts) * 1e3, subject: subject ?? "" });
484
+ }
485
+ return entries;
486
+ }
487
+ /** Content of `filepath` at commit `oid` — the restore read. Null if absent. */
488
+ async show(oid, filepath) {
489
+ try {
490
+ return await this.exec(["show", `${oid}:${filepath}`]);
491
+ } catch {
492
+ return null;
493
+ }
494
+ }
495
+ /** Bundle the whole repo into a single delta-compressed file (the durable archive). */
496
+ async bundleAll(outAbsPath) {
497
+ await this.exec(["bundle", "create", outAbsPath, "--all"]);
498
+ }
499
+ /** Clone a bundle file into `targetDir` (hydrate a fresh workspace from the archive). */
500
+ async cloneBundle(bundleAbsPath, targetDir) {
501
+ await execGit(["clone", bundleAbsPath, targetDir], path2.dirname(bundleAbsPath));
502
+ }
449
503
  exec(args) {
450
504
  return execGit(args, this.cwd);
451
505
  }
452
506
  };
453
507
  function execGit(args, cwd) {
454
508
  return new Promise((resolve, reject) => {
455
- execFile("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
509
+ execFile(GIT_BINARY, args, { cwd, maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
456
510
  if (err) {
457
511
  const message = stderr?.trim() || stdout?.trim() || err.message;
458
512
  reject(new Error(`git ${args[0]}: ${message}`));
@@ -491,9 +545,25 @@ async function writeMintTemplatesIfMissing(backend, workDir, userId, projectName
491
545
  );
492
546
  }
493
547
  }
548
+ var GITIGNORE = [
549
+ "# Almadar \u2014 version only the app; ignore generated/churny state",
550
+ "/.almadar/sessions/",
551
+ "/.almadar/trace.jsonl",
552
+ "/.almadar/index.json",
553
+ "/.almadar/snapshots/",
554
+ "/.almadar/changesets/",
555
+ "/.almadar/history-meta.json",
556
+ "/apps/",
557
+ "/.almadar-archive.bundle",
558
+ ""
559
+ ].join("\n");
494
560
  async function ensureGitInit(backend, workDir) {
495
561
  const git = new GitClient(workDir, backend);
496
562
  await git.init();
563
+ const gitignorePath = path2.join(workDir, ".gitignore");
564
+ if (!backend.exists(gitignorePath)) {
565
+ await backend.writeFile(gitignorePath, GITIGNORE);
566
+ }
497
567
  return git;
498
568
  }
499
569
  function readAppMarker(backend, workDir) {
@@ -1700,6 +1770,7 @@ var WorkspaceServiceImpl = class {
1700
1770
  this._appId = args.appId;
1701
1771
  this.git = args.git;
1702
1772
  this.github = args.github;
1773
+ this.archiveBackend = args.archiveBackend;
1703
1774
  this.index = new WorkspaceIndexImpl({
1704
1775
  workDir: this.workDir,
1705
1776
  backend: this.backend,
@@ -2097,6 +2168,55 @@ var WorkspaceServiceImpl = class {
2097
2168
  const linked = await this.git.hasRemote("origin");
2098
2169
  return { ...s, linked };
2099
2170
  }
2171
+ // === Snapshots (git history of schema.orb) ===
2172
+ async snapshot(reason) {
2173
+ if (!this.git) return null;
2174
+ await this.git.addAll();
2175
+ const oid = await this.git.commit(`snapshot: ${reason}`);
2176
+ return oid ? { oid } : null;
2177
+ }
2178
+ async listSnapshots(opts) {
2179
+ if (!this.git) return [];
2180
+ const entries = await this.git.log(WORKSPACE_LAYOUT.SCHEMA_FILE, opts?.limit);
2181
+ return entries.map((e) => ({
2182
+ id: e.oid,
2183
+ timestamp: e.timestamp,
2184
+ reason: e.subject.startsWith("snapshot: ") ? e.subject.slice("snapshot: ".length) : e.subject
2185
+ }));
2186
+ }
2187
+ async restore(oid) {
2188
+ if (!this.git) return null;
2189
+ return this.git.show(oid, WORKSPACE_LAYOUT.SCHEMA_FILE);
2190
+ }
2191
+ async archive() {
2192
+ if (!this.git || !this.archiveBackend) return;
2193
+ const bundlePath = path2.join(this.workDir, ".almadar-archive.bundle");
2194
+ await this.git.bundleAll(bundlePath);
2195
+ const bytes = await this.backend.readFileBytes(bundlePath);
2196
+ await this.archiveBackend.saveBundle(bytes);
2197
+ await this.backend.rm(bundlePath).catch(() => {
2198
+ });
2199
+ }
2200
+ async hydrateFromArchive() {
2201
+ if (!this.git || !this.archiveBackend) return false;
2202
+ const bytes = await this.archiveBackend.loadBundle();
2203
+ if (!bytes) return false;
2204
+ const base = path2.basename(this.workDir);
2205
+ const bundlePath = path2.join(path2.dirname(this.workDir), `${base}.bundle`);
2206
+ const cloneDir = path2.join(path2.dirname(this.workDir), `${base}.hydrate`);
2207
+ await this.backend.writeFileBytes(bundlePath, bytes);
2208
+ try {
2209
+ await this.backend.rm(cloneDir, { recursive: true }).catch(() => {
2210
+ });
2211
+ await this.git.cloneBundle(bundlePath, cloneDir);
2212
+ await this.backend.rm(this.workDir, { recursive: true });
2213
+ await this.backend.rename(cloneDir, this.workDir);
2214
+ return true;
2215
+ } finally {
2216
+ await this.backend.rm(bundlePath).catch(() => {
2217
+ });
2218
+ }
2219
+ }
2100
2220
  // === Observation ===
2101
2221
  subscribe(observer) {
2102
2222
  return this.sinks.subscribe(observer);
@@ -2252,8 +2372,12 @@ async function openWorkspaceInternal(opts) {
2252
2372
  }
2253
2373
  }
2254
2374
  let git;
2255
- if (!bare && opts.github && opts.backend !== "memory") {
2256
- git = await ensureGitInit(backend, resolved.workDir);
2375
+ if (!bare && opts.backend !== "memory") {
2376
+ try {
2377
+ git = await ensureGitInit(backend, resolved.workDir);
2378
+ } catch {
2379
+ git = void 0;
2380
+ }
2257
2381
  }
2258
2382
  const service = new WorkspaceServiceImpl({
2259
2383
  workDir: resolved.workDir,
@@ -2262,7 +2386,8 @@ async function openWorkspaceInternal(opts) {
2262
2386
  embedder: opts.embedder ?? createDefaultEmbedder(),
2263
2387
  appId: resolved.appId,
2264
2388
  git,
2265
- github: opts.github
2389
+ github: opts.github,
2390
+ archiveBackend: opts.archiveBackend
2266
2391
  });
2267
2392
  if (!bare) {
2268
2393
  await service.index.warm();