@xtrable-ltd/nanoesis 0.1.31 → 0.1.33

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 (30) hide show
  1. package/dist/adapter-azure-blob.d.ts +41 -4
  2. package/dist/adapter-azure-blob.js +90 -4
  3. package/dist/{chunk-BCWZRKMF.js → chunk-C3QPGXG5.js} +14 -4
  4. package/dist/{chunk-GFQT7BYP.js → chunk-P6NDWIKK.js} +201 -83
  5. package/dist/editor-api.d.ts +11 -5
  6. package/dist/editor-api.js +2 -2
  7. package/dist/index.d.ts +102 -2
  8. package/dist/index.js +9 -1
  9. package/dist/mcp.js +3 -3
  10. package/editor/assets/{MigrationsPane-Drxje7Nq.js → MigrationsPane-BuaKhsie.js} +1 -1
  11. package/editor/assets/{TemplatesPane-CxO1IknP.js → TemplatesPane-Bg_b0htA.js} +7 -7
  12. package/editor/assets/{cssMode-BPla51Av.js → cssMode-Bcwjp8T3.js} +1 -1
  13. package/editor/assets/{freemarker2-Bob6Bse-.js → freemarker2-Cgqj3dEx.js} +1 -1
  14. package/editor/assets/{handlebars-DsQdZgzp.js → handlebars-CLf2-RZf.js} +1 -1
  15. package/editor/assets/{html-BNBtLdPe.js → html-BrWGWRh6.js} +1 -1
  16. package/editor/assets/{htmlMode-6kyy4G1O.js → htmlMode-D9skx8gp.js} +1 -1
  17. package/editor/assets/index-CY5Kehuu.js +142 -0
  18. package/editor/assets/{javascript-Dx8xuybD.js → javascript-KaABDboM.js} +1 -1
  19. package/editor/assets/{jsonMode-DWRGUqMk.js → jsonMode-0usLs2ME.js} +1 -1
  20. package/editor/assets/{liquid-DlIa11aQ.js → liquid-DYna-Clp.js} +1 -1
  21. package/editor/assets/{mdx-DR0q2TJm.js → mdx-f7LAIls6.js} +1 -1
  22. package/editor/assets/{python-Cbswpzux.js → python-Bh21_mvq.js} +1 -1
  23. package/editor/assets/{razor-IYEYh5Ox.js → razor-D_530Ymg.js} +1 -1
  24. package/editor/assets/{tsMode-DQrbmWSC.js → tsMode-CTRVjt9n.js} +1 -1
  25. package/editor/assets/{typescript-DiBU2jVJ.js → typescript-D084tkOf.js} +1 -1
  26. package/editor/assets/{xml-CCxs0QRL.js → xml-BFulX1C6.js} +1 -1
  27. package/editor/assets/{yaml-C1tH0_iR.js → yaml-CgZsYmUt.js} +1 -1
  28. package/editor/index.html +1 -1
  29. package/package.json +1 -1
  30. package/editor/assets/index-BG-8SSzq.js +0 -142
@@ -1,5 +1,5 @@
1
1
  import { ContainerClient } from '@azure/storage-blob';
2
- import { BlobStore, ArtifactSink, Storage } from '@nanoesis/engine';
2
+ import { BlobStore, ConditionalBlobStore, ArtifactSink, Storage } from '@nanoesis/engine';
3
3
  export { contentTypeFor } from '@nanoesis/engine';
4
4
 
5
5
  /**
@@ -28,6 +28,23 @@ interface BlobContainer {
28
28
  write(name: string, data: Uint8Array, contentType: string, cacheControl?: string): Promise<void>;
29
29
  /** Delete a blob; a missing blob is a no-op (idempotent). */
30
30
  remove(name: string): Promise<void>;
31
+ /**
32
+ * A blob's bytes plus its current version tag (the Azure ETag), or null if absent. Pairs
33
+ * with {@link writeConditional} for the optimistic-concurrency the content index needs
34
+ * (DESIGN §11d): the bytes and the tag are captured together so the tag always describes
35
+ * the bytes returned.
36
+ */
37
+ readVersioned(name: string): Promise<{
38
+ bytes: Uint8Array;
39
+ version: string;
40
+ } | null>;
41
+ /**
42
+ * Write a blob only if its current version tag matches `expected` (or, when `expected` is
43
+ * null, only if it does not yet exist). Resolves to the new version tag on success, or
44
+ * `undefined` when the precondition failed because another writer got there first. A
45
+ * non-precondition failure still rejects.
46
+ */
47
+ writeConditional(name: string, data: Uint8Array, contentType: string, expected: string | null): Promise<string | undefined>;
31
48
  }
32
49
  /**
33
50
  * An in-memory {@link BlobContainer} backed by a name→bytes map. The test double for
@@ -36,11 +53,20 @@ interface BlobContainer {
36
53
  declare class InMemoryBlobContainer implements BlobContainer {
37
54
  private readonly blobs;
38
55
  private readonly cacheControls;
56
+ /** Per-blob version, bumped on every write, the in-memory stand-in for an ETag. */
57
+ private readonly versions;
58
+ private nextVersion;
39
59
  constructor(seed?: Readonly<Record<string, Uint8Array | string>>);
60
+ private store;
40
61
  list(prefix: string): Promise<readonly string[]>;
41
62
  read(name: string): Promise<Uint8Array | null>;
42
63
  write(name: string, data: Uint8Array, contentType?: string, cacheControl?: string): Promise<void>;
43
64
  remove(name: string): Promise<void>;
65
+ readVersioned(name: string): Promise<{
66
+ bytes: Uint8Array;
67
+ version: string;
68
+ } | null>;
69
+ writeConditional(name: string, data: Uint8Array, contentType: string, expected: string | null): Promise<string | undefined>;
44
70
  /** Test helper: the current blob names. */
45
71
  get names(): readonly string[];
46
72
  /** Test helper: the `Cache-Control` a blob was written with, if any. */
@@ -65,6 +91,11 @@ declare class AzureBlobContainer implements BlobContainer {
65
91
  read(name: string): Promise<Uint8Array | null>;
66
92
  write(name: string, data: Uint8Array, contentType: string, cacheControl?: string): Promise<void>;
67
93
  remove(name: string): Promise<void>;
94
+ readVersioned(name: string): Promise<{
95
+ bytes: Uint8Array;
96
+ version: string;
97
+ } | null>;
98
+ writeConditional(name: string, data: Uint8Array, contentType: string, expected: string | null): Promise<string | undefined>;
68
99
  }
69
100
 
70
101
  /**
@@ -74,12 +105,17 @@ declare class AzureBlobContainer implements BlobContainer {
74
105
  * in-memory container in tests (LSP). Keys are POSIX-style; `put` stamps a content type
75
106
  * from the key's extension so a published blob serves correctly as a static-website file.
76
107
  */
77
- declare class BlobContainerStore implements BlobStore {
108
+ declare class BlobContainerStore implements BlobStore, ConditionalBlobStore {
78
109
  private readonly container;
79
110
  constructor(container: BlobContainer);
80
111
  get(key: string): Promise<Uint8Array | undefined>;
81
112
  put(key: string, bytes: Uint8Array, cacheControl?: string): Promise<void>;
82
113
  delete(key: string): Promise<void>;
114
+ getVersioned(key: string): Promise<{
115
+ bytes: Uint8Array;
116
+ version: string;
117
+ } | undefined>;
118
+ putConditional(key: string, bytes: Uint8Array, expected: string | null): Promise<string | undefined>;
83
119
  }
84
120
 
85
121
  /**
@@ -99,12 +135,13 @@ declare class BlobArtifactSink implements ArtifactSink {
99
135
 
100
136
  /**
101
137
  * A ready-made {@link Storage} over an Azure Blob container: get/put/delete (via
102
- * {@link BlobContainerStore}) plus `wipe` (delete every blob). Pass it to `createEditor`
138
+ * {@link BlobContainerStore}) plus `wipe` (delete every blob) and `prune` (delete every blob
139
+ * not in a keep set, the zero-downtime publish sweep). Pass it to `createEditor`
103
140
  * as `editorFiles` (the working container) and `website` (the published `$web` container).
104
141
  * For the working container, also pass `enumerate: () => container.list('')` so the editor
105
142
  * can reconcile its content index.
106
143
  */
107
- declare function azureStorage(container: BlobContainer): Storage;
144
+ declare function azureStorage(container: BlobContainer): Storage & ConditionalBlobStore;
108
145
 
109
146
  /**
110
147
  * Pure path/name helper for the blob adapters. Blob storage is a *flat* namespace, a blob
@@ -1,16 +1,26 @@
1
1
  import {
2
2
  contentTypeFor
3
- } from "./chunk-GFQT7BYP.js";
3
+ } from "./chunk-P6NDWIKK.js";
4
4
 
5
5
  // ../../adapters/azure-blob/src/container.ts
6
6
  var InMemoryBlobContainer = class {
7
7
  blobs = /* @__PURE__ */ new Map();
8
8
  cacheControls = /* @__PURE__ */ new Map();
9
+ /** Per-blob version, bumped on every write, the in-memory stand-in for an ETag. */
10
+ versions = /* @__PURE__ */ new Map();
11
+ nextVersion = 1;
9
12
  constructor(seed = {}) {
10
13
  for (const [name, value] of Object.entries(seed)) {
11
- this.blobs.set(name, typeof value === "string" ? new TextEncoder().encode(value) : value);
14
+ this.store(name, typeof value === "string" ? new TextEncoder().encode(value) : value);
12
15
  }
13
16
  }
17
+ store(name, bytes) {
18
+ this.blobs.set(name, bytes);
19
+ const version = this.nextVersion;
20
+ this.nextVersion += 1;
21
+ this.versions.set(name, version);
22
+ return version;
23
+ }
14
24
  async list(prefix) {
15
25
  const names = [];
16
26
  for (const name of this.blobs.keys()) {
@@ -23,11 +33,24 @@ var InMemoryBlobContainer = class {
23
33
  }
24
34
  async write(name, data, contentType, cacheControl) {
25
35
  void contentType;
26
- this.blobs.set(name, data);
36
+ this.store(name, data);
27
37
  if (cacheControl !== void 0) this.cacheControls.set(name, cacheControl);
28
38
  }
29
39
  async remove(name) {
30
40
  this.blobs.delete(name);
41
+ this.versions.delete(name);
42
+ }
43
+ async readVersioned(name) {
44
+ const bytes = this.blobs.get(name);
45
+ if (bytes === void 0) return null;
46
+ return { bytes, version: String(this.versions.get(name)) };
47
+ }
48
+ async writeConditional(name, data, contentType, expected) {
49
+ void contentType;
50
+ const current = this.versions.get(name);
51
+ const currentTag = current === void 0 ? null : String(current);
52
+ if (currentTag !== expected) return void 0;
53
+ return String(this.store(name, data));
31
54
  }
32
55
  /** Test helper: the current blob names. */
33
56
  get names() {
@@ -41,8 +64,15 @@ var InMemoryBlobContainer = class {
41
64
 
42
65
  // ../../adapters/azure-blob/src/azure-container.ts
43
66
  import { BlobServiceClient } from "@azure/storage-blob";
67
+ function statusCodeOf(error) {
68
+ return typeof error === "object" && error !== null && "statusCode" in error ? error.statusCode : void 0;
69
+ }
44
70
  function isNotFound(error) {
45
- return typeof error === "object" && error !== null && "statusCode" in error && error.statusCode === 404;
71
+ return statusCodeOf(error) === 404;
72
+ }
73
+ function isPreconditionFailure(error) {
74
+ const status = statusCodeOf(error);
75
+ return status === 412 || status === 409;
46
76
  }
47
77
  var AzureBlobContainer = class _AzureBlobContainer {
48
78
  constructor(client) {
@@ -85,7 +115,41 @@ var AzureBlobContainer = class _AzureBlobContainer {
85
115
  async remove(name) {
86
116
  await this.client.getBlockBlobClient(name).deleteIfExists();
87
117
  }
118
+ async readVersioned(name) {
119
+ try {
120
+ const response = await this.client.getBlockBlobClient(name).download();
121
+ const buffer = await streamToBuffer(response.readableStreamBody);
122
+ if (response.etag === void 0) {
123
+ throw new Error(`Azure blob ${name} returned no ETag on download`);
124
+ }
125
+ return { bytes: new Uint8Array(buffer), version: response.etag };
126
+ } catch (error) {
127
+ if (isNotFound(error)) return null;
128
+ throw error;
129
+ }
130
+ }
131
+ async writeConditional(name, data, contentType, expected) {
132
+ const conditions = expected === null ? { ifNoneMatch: "*" } : { ifMatch: expected };
133
+ try {
134
+ const response = await this.client.getBlockBlobClient(name).uploadData(Buffer.from(data), {
135
+ blobHTTPHeaders: { blobContentType: contentType },
136
+ conditions
137
+ });
138
+ return response.etag;
139
+ } catch (error) {
140
+ if (isPreconditionFailure(error)) return void 0;
141
+ throw error;
142
+ }
143
+ }
88
144
  };
145
+ async function streamToBuffer(stream) {
146
+ if (stream === void 0) return Buffer.alloc(0);
147
+ const chunks = [];
148
+ for await (const chunk of stream) {
149
+ chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
150
+ }
151
+ return Buffer.concat(chunks);
152
+ }
89
153
 
90
154
  // ../../adapters/azure-blob/src/paths.ts
91
155
  function normalizeBlobName(path) {
@@ -108,6 +172,13 @@ var BlobContainerStore = class {
108
172
  async delete(key) {
109
173
  await this.container.remove(normalizeBlobName(key));
110
174
  }
175
+ async getVersioned(key) {
176
+ return await this.container.readVersioned(normalizeBlobName(key)) ?? void 0;
177
+ }
178
+ async putConditional(key, bytes, expected) {
179
+ const name = normalizeBlobName(key);
180
+ return this.container.writeConditional(name, bytes, contentTypeFor(name), expected);
181
+ }
111
182
  };
112
183
 
113
184
  // ../../adapters/azure-blob/src/blob-sink.ts
@@ -138,8 +209,23 @@ function azureStorage(container) {
138
209
  // strips the publish Cache-Control before it reaches the blob.
139
210
  put: (key, bytes, cacheControl) => store.put(key, bytes, cacheControl),
140
211
  delete: (key) => store.delete(key),
212
+ // Optimistic-concurrency capability (DESIGN §11d): surfaced so a horizontally-scaled
213
+ // host's IndexedStore compare-and-sets the content index instead of blind-overwriting
214
+ // it, which is what stops two instances losing each other's updates
215
+ // (NANOESIS-MCP-ISSUES Issue 1). Azure blobs back this with native ETag conditions.
216
+ getVersioned: (key) => store.getVersioned(key),
217
+ putConditional: (key, bytes, expected) => store.putConditional(key, bytes, expected),
141
218
  wipe: async () => {
142
219
  for (const name of await container.list("")) await container.remove(name);
220
+ },
221
+ // Zero-downtime sweep (DESIGN §11): the publish overwrites every artifact in place, then
222
+ // calls this with the just-written path set so only orphaned blobs (pages the new publish
223
+ // did not emit) are removed. Every live URL keeps serving throughout, the old wipe-first
224
+ // path blanked the whole site for the upload window.
225
+ prune: async (keep) => {
226
+ for (const name of await container.list("")) {
227
+ if (!keep.has(name)) await container.remove(name);
228
+ }
143
229
  }
144
230
  };
145
231
  }
@@ -18,7 +18,7 @@ import {
18
18
  renderReferenceMarkdown,
19
19
  validateSite,
20
20
  workingStoreRoundTripDiagnostic
21
- } from "./chunk-GFQT7BYP.js";
21
+ } from "./chunk-P6NDWIKK.js";
22
22
 
23
23
  // ../editor-api/src/scaffold.ts
24
24
  var HOME_HTML = `<!doctype html>
@@ -559,7 +559,7 @@ async function dispatchApi(deps, req) {
559
559
  return json(422, {
560
560
  ok: false,
561
561
  errors: result.validation.errors.map((e) => e.message),
562
- ...result.summary !== void 0 && { summary: result.summary }
562
+ ...result.summary !== void 0 && { wouldPublish: result.summary }
563
563
  });
564
564
  }
565
565
  case "/api/scaffold": {
@@ -949,12 +949,12 @@ function createEditor(config) {
949
949
  };
950
950
  try {
951
951
  const validation = await validateSite(working);
952
- if (validation.ok && wipeBeforePublish && config.website.wipe !== void 0) {
952
+ if (validation.ok && wipeBeforePublish && config.website.prune === void 0 && config.website.wipe !== void 0) {
953
953
  await config.website.wipe();
954
954
  }
955
955
  const dir = config.users === void 0 ? void 0 : authorDirectory(await config.users());
956
956
  const prebuild = typeof config.prebuild === "function" ? await config.prebuild() : config.prebuild;
957
- return await publishSite(working, sink, {
957
+ const result = await publishSite(working, sink, {
958
958
  ...config.images !== void 0 && { imageEncoder: config.images },
959
959
  ...config.purge !== void 0 && { purge: config.purge },
960
960
  ...config.baseUrl !== void 0 && { baseUrl: config.baseUrl },
@@ -962,6 +962,10 @@ function createEditor(config) {
962
962
  ...prebuild !== void 0 && { prebuild },
963
963
  onProgress: record
964
964
  });
965
+ if (result.ok && wipeBeforePublish && config.website.prune !== void 0) {
966
+ await config.website.prune(new Set(result.written));
967
+ }
968
+ return result;
965
969
  } finally {
966
970
  progressRunning = false;
967
971
  }
@@ -1170,6 +1174,12 @@ var TOOL_SPECS = [
1170
1174
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
1171
1175
  toRequest: (_args, token) => request("POST", "/api/publish", {}, void 0, token)
1172
1176
  },
1177
+ {
1178
+ name: "reconcile",
1179
+ description: "Rebuild the content index from the files actually in storage, recovering any that drifted out of the index. Use this if `validate`, `list_dir`, or a `reference-missing` error reports a file as absent that `read_file` can still return: the file is stored but unenumerable, and reconcile makes it visible again without re-writing it. Idempotent and a no-op when nothing drifted. Returns the count added/removed and the new total. Reports an error on a host that cannot enumerate its store.",
1180
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
1181
+ toRequest: (_args, token) => request("POST", "/api/reconcile", {}, void 0, token)
1182
+ },
1173
1183
  {
1174
1184
  name: "list_pending_migrations",
1175
1185
  description: "List content items whose JSON fields don't match their bound template's schema (orphan fields or missing required fields). Use this after a destructive template edit (you'll see a `stamped` record on write_file) to see what needs migrating, or any time to check site-wide drift.",