@almadar/workspace 0.8.0 → 0.10.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.
@@ -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,
@@ -1718,6 +1789,14 @@ var WorkspaceServiceImpl = class {
1718
1789
  setAppId(id) {
1719
1790
  this._appId = id;
1720
1791
  }
1792
+ /**
1793
+ * Set (or clear) the GitHub config after open. Lets a consumer resolve the
1794
+ * link/token lazily — e.g. from a workspace file read post-open — and wire
1795
+ * `commitAndPush`/`pullIfLinked` without knowing it at construction time.
1796
+ */
1797
+ setGitHub(config) {
1798
+ this.github = config;
1799
+ }
1721
1800
  // === Helpers ===
1722
1801
  /** Run `op` under a per-path serial lock. */
1723
1802
  withLock(absPath, op) {
@@ -2089,6 +2168,55 @@ var WorkspaceServiceImpl = class {
2089
2168
  const linked = await this.git.hasRemote("origin");
2090
2169
  return { ...s, linked };
2091
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
+ }
2092
2220
  // === Observation ===
2093
2221
  subscribe(observer) {
2094
2222
  return this.sinks.subscribe(observer);
@@ -2244,7 +2372,7 @@ async function openWorkspaceInternal(opts) {
2244
2372
  }
2245
2373
  }
2246
2374
  let git;
2247
- if (!bare && opts.github && opts.backend !== "memory") {
2375
+ if (!bare && opts.backend !== "memory") {
2248
2376
  git = await ensureGitInit(backend, resolved.workDir);
2249
2377
  }
2250
2378
  const service = new WorkspaceServiceImpl({
@@ -2254,7 +2382,8 @@ async function openWorkspaceInternal(opts) {
2254
2382
  embedder: opts.embedder ?? createDefaultEmbedder(),
2255
2383
  appId: resolved.appId,
2256
2384
  git,
2257
- github: opts.github
2385
+ github: opts.github,
2386
+ archiveBackend: opts.archiveBackend
2258
2387
  });
2259
2388
  if (!bare) {
2260
2389
  await service.index.warm();