@gmickel/gno 1.37.0 → 1.38.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 (40) hide show
  1. package/assets/spa-production.json.gz +0 -0
  2. package/browser-extension/artifacts/{gno-browser-clipper-v1.37.0.zip → gno-browser-clipper-v1.38.0.zip} +0 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.38.0.zip.sha256 +1 -0
  4. package/browser-extension/dist/manifest.json +1 -1
  5. package/package.json +1 -1
  6. package/spec/cli.md +35 -15
  7. package/src/cli/commands/cleanup.ts +8 -2
  8. package/src/cli/commands/collection/clear-embeddings.ts +6 -1
  9. package/src/cli/commands/doctor-activation.ts +5 -1
  10. package/src/cli/commands/doctor.ts +72 -2
  11. package/src/cli/commands/embed.ts +227 -194
  12. package/src/cli/commands/index-cmd.ts +74 -50
  13. package/src/cli/commands/init.ts +5 -1
  14. package/src/cli/commands/profile-apply.ts +5 -1
  15. package/src/cli/commands/setup-activation.ts +2 -1
  16. package/src/cli/commands/setup.ts +2 -1
  17. package/src/cli/commands/shared.ts +5 -1
  18. package/src/cli/commands/status.ts +5 -1
  19. package/src/cli/commands/tags.ts +18 -3
  20. package/src/cli/commands/update.ts +34 -27
  21. package/src/cli/commands/vec.ts +13 -4
  22. package/src/cli/errors.ts +3 -2
  23. package/src/cli/program.ts +345 -194
  24. package/src/config/defaults.ts +2 -0
  25. package/src/config/index.ts +3 -0
  26. package/src/config/types.ts +32 -1
  27. package/src/core/file-lock.ts +16 -4
  28. package/src/core/write-lease.ts +354 -0
  29. package/src/embed/backlog.ts +9 -1
  30. package/src/embed/retry.ts +116 -3
  31. package/src/sdk/client.ts +3 -1
  32. package/src/sdk/embed.ts +8 -3
  33. package/src/sdk/types.ts +2 -0
  34. package/src/serve/embed-scheduler.ts +8 -0
  35. package/src/serve/resident-runtime.ts +5 -1
  36. package/src/serve/spa-production-build.ts +53 -7
  37. package/src/store/sqlite/adapter.ts +28 -4
  38. package/src/store/sqlite/scoped-index.ts +5 -1
  39. package/src/store/vector/sqlite-vec.ts +2 -1
  40. package/browser-extension/artifacts/gno-browser-clipper-v1.37.0.zip.sha256 +0 -1
@@ -5,6 +5,11 @@
5
5
  * @module src/cli/commands/indexCmd
6
6
  */
7
7
 
8
+ import {
9
+ type CliWriteLeaseOptions,
10
+ type WriteLeaseContention,
11
+ withCliWriteLease,
12
+ } from "../../core/write-lease";
8
13
  import {
9
14
  defaultSyncService,
10
15
  type SyncResult,
@@ -15,7 +20,7 @@ import { formatSyncResultLines, initStore } from "./shared";
15
20
  /**
16
21
  * Options for index command.
17
22
  */
18
- export interface IndexOptions {
23
+ export interface IndexOptions extends CliWriteLeaseOptions {
19
24
  /** Override config path */
20
25
  configPath?: string;
21
26
  /** Index name */
@@ -44,66 +49,80 @@ export type IndexResult =
44
49
  success: true;
45
50
  syncResult: SyncResult;
46
51
  embedSkipped: boolean;
47
- embedResult?: { embedded: number; errors: number; duration: number };
52
+ embedResult?: {
53
+ embedded: number;
54
+ errors: number;
55
+ contentionErrors: number;
56
+ duration: number;
57
+ };
48
58
  }
49
- | { success: false; error: string };
59
+ | { success: false; error: string; contention?: WriteLeaseContention };
50
60
 
51
61
  /**
52
62
  * Execute gno index command.
53
63
  */
54
64
  export async function index(options: IndexOptions = {}): Promise<IndexResult> {
55
- const initResult = await initStore({
56
- configPath: options.configPath,
57
- indexName: options.indexName,
58
- collection: options.collection,
59
- });
60
- if (!initResult.ok) {
61
- return { success: false, error: initResult.error };
62
- }
65
+ return await withCliWriteLease(options, async () => {
66
+ const initResult = await initStore({
67
+ configPath: options.configPath,
68
+ indexName: options.indexName,
69
+ collection: options.collection,
70
+ });
71
+ if (!initResult.ok) {
72
+ return { success: false, error: initResult.error };
73
+ }
63
74
 
64
- const { store, collections, config } = initResult;
75
+ const { store, collections, config } = initResult;
65
76
 
66
- try {
67
- // Run sync service (update phase)
68
- const syncResult = await defaultSyncService.syncAll(
69
- collections,
70
- store,
71
- withContentTypeRules(
72
- {
73
- gitPull: options.gitPull,
74
- runUpdateCmd: true,
75
- },
76
- config
77
- )
78
- );
77
+ try {
78
+ // Run sync service (update phase)
79
+ const syncResult = await defaultSyncService.syncAll(
80
+ collections,
81
+ store,
82
+ withContentTypeRules(
83
+ {
84
+ gitPull: options.gitPull,
85
+ runUpdateCmd: true,
86
+ },
87
+ config
88
+ )
89
+ );
79
90
 
80
- // Embedding phase
81
- const embedSkipped = options.noEmbed ?? false;
82
- let embedResult:
83
- | { embedded: number; errors: number; duration: number }
84
- | undefined;
91
+ // Embedding phase
92
+ const embedSkipped = options.noEmbed ?? false;
93
+ let embedResult:
94
+ | {
95
+ embedded: number;
96
+ errors: number;
97
+ contentionErrors: number;
98
+ duration: number;
99
+ }
100
+ | undefined;
85
101
 
86
- if (!embedSkipped) {
87
- const { embed } = await import("./embed");
88
- const result = await embed({
89
- configPath: options.configPath,
90
- indexName: options.indexName,
91
- collection: options.collection,
92
- verbose: options.verbose,
93
- });
94
- if (result.success) {
95
- embedResult = {
96
- embedded: result.embedded,
97
- errors: result.errors,
98
- duration: result.duration,
99
- };
102
+ if (!embedSkipped) {
103
+ const { embed } = await import("./embed");
104
+ const result = await embed({
105
+ configPath: options.configPath,
106
+ indexName: options.indexName,
107
+ collection: options.collection,
108
+ verbose: options.verbose,
109
+ skipWriteLease: true,
110
+ });
111
+ if (result.success) {
112
+ embedResult = {
113
+ embedded: result.embedded,
114
+ errors: result.errors,
115
+ contentionErrors: result.contentionErrors,
116
+ duration: result.duration,
117
+ };
118
+ }
100
119
  }
101
- }
102
120
 
103
- return { success: true, syncResult, embedSkipped, embedResult };
104
- } finally {
105
- await store.close();
106
- }
121
+ return { success: true, syncResult, embedSkipped, embedResult };
122
+ } finally {
123
+ await store.close();
124
+ }
125
+ });
107
126
  }
108
127
 
109
128
  /**
@@ -147,13 +166,18 @@ export function formatIndex(
147
166
  lines.push("Embedding skipped (--no-embed)");
148
167
  } else if (result.embedResult) {
149
168
  lines.push("");
150
- const { embedded, errors, duration } = result.embedResult;
169
+ const { embedded, errors, contentionErrors, duration } = result.embedResult;
151
170
  lines.push(
152
171
  `Embedded ${embedded.toLocaleString()} chunks in ${formatDuration(duration)}`
153
172
  );
154
173
  if (errors > 0) {
155
174
  lines.push(`${errors.toLocaleString()} chunks failed to embed.`);
156
175
  }
176
+ if (contentionErrors > 0) {
177
+ lines.push(
178
+ `${contentionErrors.toLocaleString()} chunks deferred by index contention (SQLITE_BUSY) — not embedding failures. Rerun \`gno embed\` when the other writer finishes.`
179
+ );
180
+ }
157
181
  }
158
182
 
159
183
  return lines.join("\n");
@@ -141,7 +141,11 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
141
141
  }
142
142
 
143
143
  const store = new SqliteAdapter();
144
- const opened = await store.open(dbPath, mutation.config.ftsTokenizer);
144
+ const opened = await store.open(
145
+ dbPath,
146
+ mutation.config.ftsTokenizer,
147
+ mutation.config.busyTimeoutMs
148
+ );
145
149
  if (!opened.ok) {
146
150
  return {
147
151
  success: false,
@@ -218,7 +218,11 @@ export async function runProjectProfileApplyCommand(
218
218
  try {
219
219
  await mkdir(dataDir, { recursive: true });
220
220
  store.setConfigPath(configPath);
221
- const opened = await store.open(indexPath, startingConfig.ftsTokenizer);
221
+ const opened = await store.open(
222
+ indexPath,
223
+ startingConfig.ftsTokenizer,
224
+ startingConfig.busyTimeoutMs
225
+ );
222
226
  if (!opened.ok) {
223
227
  return {
224
228
  result: failedApplyResult("failed", discovery.summary, [
@@ -409,7 +409,8 @@ export async function setupWithActivation(
409
409
  store.setConfigPath(configPath);
410
410
  const opened = await store.open(
411
411
  getIndexDbPath(indexName),
412
- configResult.value.ftsTokenizer
412
+ configResult.value.ftsTokenizer,
413
+ configResult.value.busyTimeoutMs
413
414
  );
414
415
  if (!opened.ok) {
415
416
  return withProfileResult(
@@ -259,7 +259,8 @@ async function executeSetup(
259
259
  store.setConfigPath(configPath);
260
260
  const opened = await store.open(
261
261
  getIndexDbPath(indexName),
262
- configResult.value.ftsTokenizer
262
+ configResult.value.ftsTokenizer,
263
+ configResult.value.busyTimeoutMs
263
264
  );
264
265
  if (!opened.ok) {
265
266
  await store.close();
@@ -105,7 +105,11 @@ export async function initStore(
105
105
  // Set configPath for status output
106
106
  store.setConfigPath(actualConfigPath);
107
107
 
108
- const openResult = await store.open(dbPath, config.ftsTokenizer);
108
+ const openResult = await store.open(
109
+ dbPath,
110
+ config.ftsTokenizer,
111
+ config.busyTimeoutMs
112
+ );
109
113
  if (!openResult.ok) {
110
114
  return { ok: false, error: openResult.error.message };
111
115
  }
@@ -247,7 +247,11 @@ export async function status(
247
247
  // Set configPath for status output
248
248
  store.setConfigPath(options.configPath ?? paths.configFile);
249
249
 
250
- const openResult = await store.open(dbPath, config.ftsTokenizer);
250
+ const openResult = await store.open(
251
+ dbPath,
252
+ config.ftsTokenizer,
253
+ config.busyTimeoutMs
254
+ );
251
255
  if (!openResult.ok) {
252
256
  return { success: false, error: openResult.error.message };
253
257
  }
@@ -18,6 +18,8 @@ import { initStore } from "./shared";
18
18
  // ─────────────────────────────────────────────────────────────────────────────
19
19
 
20
20
  export interface TagsListOptions {
21
+ /** Index name */
22
+ indexName?: string;
21
23
  /** Override config path */
22
24
  configPath?: string;
23
25
  /** Filter by collection */
@@ -46,6 +48,8 @@ export interface TagsListResponse {
46
48
  export interface TagsAddOptions {
47
49
  /** Override config path */
48
50
  configPath?: string;
51
+ /** Index name */
52
+ indexName?: string;
49
53
  /** JSON output */
50
54
  json?: boolean;
51
55
  }
@@ -60,6 +64,8 @@ export type TagsAddResult =
60
64
  export interface TagsRmOptions {
61
65
  /** Override config path */
62
66
  configPath?: string;
67
+ /** Index name */
68
+ indexName?: string;
63
69
  /** JSON output */
64
70
  json?: boolean;
65
71
  }
@@ -339,7 +345,10 @@ function removeTagFromFrontmatter(content: string, tag: string): string {
339
345
  export async function tagsList(
340
346
  options: TagsListOptions = {}
341
347
  ): Promise<TagsListResult> {
342
- const initResult = await initStore({ configPath: options.configPath });
348
+ const initResult = await initStore({
349
+ configPath: options.configPath,
350
+ indexName: options.indexName,
351
+ });
343
352
  if (!initResult.ok) {
344
353
  return { success: false, error: initResult.error };
345
354
  }
@@ -394,7 +403,10 @@ export async function tagsAdd(
394
403
  };
395
404
  }
396
405
 
397
- const initResult = await initStore({ configPath: options.configPath });
406
+ const initResult = await initStore({
407
+ configPath: options.configPath,
408
+ indexName: options.indexName,
409
+ });
398
410
  if (!initResult.ok) {
399
411
  return { success: false, error: initResult.error };
400
412
  }
@@ -495,7 +507,10 @@ export async function tagsRm(
495
507
  ): Promise<TagsRmResult> {
496
508
  const normalized = normalizeTag(tag);
497
509
 
498
- const initResult = await initStore({ configPath: options.configPath });
510
+ const initResult = await initStore({
511
+ configPath: options.configPath,
512
+ indexName: options.indexName,
513
+ });
499
514
  if (!initResult.ok) {
500
515
  return { success: false, error: initResult.error };
501
516
  }
@@ -5,6 +5,11 @@
5
5
  * @module src/cli/commands/update
6
6
  */
7
7
 
8
+ import {
9
+ type CliWriteLeaseOptions,
10
+ type WriteLeaseContention,
11
+ withCliWriteLease,
12
+ } from "../../core/write-lease";
8
13
  import {
9
14
  defaultSyncService,
10
15
  type SyncResult,
@@ -15,7 +20,7 @@ import { formatSyncResultLines, initStore } from "./shared";
15
20
  /**
16
21
  * Options for update command.
17
22
  */
18
- export interface UpdateOptions {
23
+ export interface UpdateOptions extends CliWriteLeaseOptions {
19
24
  /** Override config path */
20
25
  configPath?: string;
21
26
  /** Index name */
@@ -33,7 +38,7 @@ export interface UpdateOptions {
33
38
  */
34
39
  export type UpdateResult =
35
40
  | { success: true; result: SyncResult }
36
- | { success: false; error: string };
41
+ | { success: false; error: string; contention?: WriteLeaseContention };
37
42
 
38
43
  /**
39
44
  * Execute gno update command.
@@ -41,34 +46,36 @@ export type UpdateResult =
41
46
  export async function update(
42
47
  options: UpdateOptions = {}
43
48
  ): Promise<UpdateResult> {
44
- const initResult = await initStore({
45
- configPath: options.configPath,
46
- indexName: options.indexName,
47
- });
48
- if (!initResult.ok) {
49
- return { success: false, error: initResult.error };
50
- }
49
+ return await withCliWriteLease(options, async () => {
50
+ const initResult = await initStore({
51
+ configPath: options.configPath,
52
+ indexName: options.indexName,
53
+ });
54
+ if (!initResult.ok) {
55
+ return { success: false, error: initResult.error };
56
+ }
51
57
 
52
- const { store, collections, config } = initResult;
58
+ const { store, collections, config } = initResult;
53
59
 
54
- try {
55
- // Run sync service
56
- const result = await defaultSyncService.syncAll(
57
- collections,
58
- store,
59
- withContentTypeRules(
60
- {
61
- gitPull: options.gitPull,
62
- runUpdateCmd: true,
63
- },
64
- config
65
- )
66
- );
60
+ try {
61
+ // Run sync service
62
+ const result = await defaultSyncService.syncAll(
63
+ collections,
64
+ store,
65
+ withContentTypeRules(
66
+ {
67
+ gitPull: options.gitPull,
68
+ runUpdateCmd: true,
69
+ },
70
+ config
71
+ )
72
+ );
67
73
 
68
- return { success: true, result };
69
- } finally {
70
- await store.close();
71
- }
74
+ return { success: true, result };
75
+ } finally {
76
+ await store.close();
77
+ }
78
+ });
72
79
  }
73
80
 
74
81
  /**
@@ -20,6 +20,7 @@ import {
20
20
 
21
21
  export interface VecOptions {
22
22
  configPath?: string;
23
+ indexName?: string;
23
24
  json?: boolean;
24
25
  }
25
26
 
@@ -91,11 +92,15 @@ export async function vecSync(
91
92
  const modelUri = preset.embed;
92
93
 
93
94
  const store = new SqliteAdapter();
94
- const dbPath = getIndexDbPath();
95
+ const dbPath = getIndexDbPath(options.indexName);
95
96
  const paths = getConfigPaths();
96
97
  store.setConfigPath(paths.configFile);
97
98
 
98
- const openResult = await store.open(dbPath, config.ftsTokenizer);
99
+ const openResult = await store.open(
100
+ dbPath,
101
+ config.ftsTokenizer,
102
+ config.busyTimeoutMs
103
+ );
99
104
  if (!openResult.ok) {
100
105
  return { success: false, error: openResult.error.message };
101
106
  }
@@ -165,11 +170,15 @@ export async function vecRebuild(
165
170
  const modelUri = preset.embed;
166
171
 
167
172
  const store = new SqliteAdapter();
168
- const dbPath = getIndexDbPath();
173
+ const dbPath = getIndexDbPath(options.indexName);
169
174
  const paths = getConfigPaths();
170
175
  store.setConfigPath(paths.configFile);
171
176
 
172
- const openResult = await store.open(dbPath, config.ftsTokenizer);
177
+ const openResult = await store.open(
178
+ dbPath,
179
+ config.ftsTokenizer,
180
+ config.busyTimeoutMs
181
+ );
173
182
  if (!openResult.ok) {
174
183
  return { success: false, error: openResult.error.message };
175
184
  }
package/src/cli/errors.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * CLI error model aligned to spec.
3
- * Exit codes: 0=success, 1=validation, 2=runtime
3
+ * Exit codes: 0=success, 1=validation, 2=runtime, 3=not-running, 4=busy/audit-findings, 5=audit-partial
4
4
  *
5
5
  * @module src/cli/errors
6
6
  */
@@ -13,6 +13,7 @@ export type CliErrorCode =
13
13
  | "VALIDATION"
14
14
  | "RUNTIME"
15
15
  | "NOT_RUNNING"
16
+ | "BUSY"
16
17
  | "AUDIT_FINDINGS"
17
18
  | "AUDIT_PARTIAL";
18
19
 
@@ -74,7 +75,7 @@ export function exitCodeFor(err: CliError): 1 | 2 | 3 | 4 | 5 {
74
75
  if (err.code === "NOT_RUNNING") {
75
76
  return 3;
76
77
  }
77
- if (err.code === "AUDIT_FINDINGS") return 4;
78
+ if (err.code === "BUSY" || err.code === "AUDIT_FINDINGS") return 4;
78
79
  if (err.code === "AUDIT_PARTIAL") return 5;
79
80
  return 2;
80
81
  }