@gmickel/gno 1.24.0 → 1.26.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.
- package/README.md +18 -4
- package/assets/skill/SKILL.md +40 -17
- package/assets/skill/recipes/capture-and-file.md +20 -5
- package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip.sha256 +1 -0
- package/browser-extension/dist/PRIVACY.md +55 -0
- package/browser-extension/dist/chunk-vn5f663b.js +50 -0
- package/browser-extension/dist/chunk-ydfx5d7p.css +1 -0
- package/browser-extension/dist/content.js +1 -0
- package/browser-extension/dist/manifest.json +25 -0
- package/browser-extension/dist/preview.html +13 -0
- package/browser-extension/dist/service-worker.js +40 -0
- package/package.json +13 -3
- package/spec/cli.md +196 -4
- package/spec/db/schema.sql +102 -1
- package/spec/mcp.md +10 -0
- package/spec/output-schemas/browser-clip-preview.schema.json +83 -0
- package/spec/output-schemas/browser-clip.schema.json +586 -0
- package/spec/output-schemas/capture-receipt.schema.json +22 -1
- package/spec/output-schemas/clipper-csrf.schema.json +12 -0
- package/spec/output-schemas/clipper-error.schema.json +46 -0
- package/spec/output-schemas/clipper-pair-approval.schema.json +17 -0
- package/spec/output-schemas/clipper-pair-start.schema.json +26 -0
- package/spec/output-schemas/clipper-pair-status.schema.json +46 -0
- package/spec/output-schemas/clipper-revoke.schema.json +28 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +12 -1
- package/spec/output-schemas/project-profile-apply.schema.json +209 -0
- package/spec/output-schemas/project-profile-command.schema.json +160 -0
- package/spec/output-schemas/query-diagnose.schema.json +7 -1
- package/spec/output-schemas/setup-profile-result.schema.json +87 -0
- package/spec/project-profile.schema.json +301 -0
- package/src/cli/commands/collection/add.ts +39 -45
- package/src/cli/commands/collection/remove.ts +28 -28
- package/src/cli/commands/collection/rename.ts +55 -73
- package/src/cli/commands/context/add.ts +37 -26
- package/src/cli/commands/context/rm.ts +47 -20
- package/src/cli/commands/init.ts +55 -125
- package/src/cli/commands/models/use.ts +43 -38
- package/src/cli/commands/profile-apply.ts +334 -0
- package/src/cli/commands/profile.ts +409 -0
- package/src/cli/commands/setup-activation.ts +205 -54
- package/src/cli/commands/setup-profile.ts +223 -0
- package/src/cli/commands/setup.ts +3 -0
- package/src/cli/program.ts +110 -9
- package/src/config/index.ts +3 -0
- package/src/config/project-profile.ts +367 -0
- package/src/config/saver.ts +16 -7
- package/src/config/types.ts +42 -0
- package/src/core/browser-clip-provenance.ts +139 -0
- package/src/core/browser-clip.ts +473 -0
- package/src/core/capture-write.ts +5 -0
- package/src/core/capture.ts +75 -18
- package/src/core/config-mutation.ts +138 -76
- package/src/core/config-write-lock.ts +89 -0
- package/src/core/context-identity.ts +16 -0
- package/src/core/context-resolver.ts +2 -12
- package/src/core/file-lock.ts +20 -6
- package/src/core/folder-setup-planning.ts +6 -21
- package/src/core/folder-setup.ts +30 -2
- package/src/core/path-rules.ts +53 -0
- package/src/core/project-affinity-surface.ts +102 -7
- package/src/core/project-profile-apply-state.ts +268 -0
- package/src/core/project-profile-apply-validation.ts +95 -0
- package/src/core/project-profile-apply.ts +408 -0
- package/src/core/project-profile-canonical.ts +71 -0
- package/src/core/project-profile-diff.ts +302 -0
- package/src/core/project-profile-discovery.ts +519 -0
- package/src/core/project-profile-file.ts +37 -0
- package/src/core/project-profile-parser.ts +98 -0
- package/src/core/project-profile.ts +490 -0
- package/src/ingestion/walker.ts +85 -44
- package/src/llm/cache.ts +21 -0
- package/src/serve/capture-service.ts +420 -0
- package/src/serve/clipper-body.ts +62 -0
- package/src/serve/clipper-capture.ts +248 -0
- package/src/serve/clipper-contract.ts +57 -0
- package/src/serve/clipper-idempotency.ts +35 -0
- package/src/serve/clipper-pairing.ts +297 -0
- package/src/serve/clipper-security-errors.ts +23 -0
- package/src/serve/clipper-security.ts +449 -0
- package/src/serve/config-sync.ts +2 -2
- package/src/serve/public/app.tsx +8 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/index.html +1 -0
- package/src/serve/public/lib/clipper-approval.ts +206 -0
- package/src/serve/public/pages/ClipperPairing.tsx +210 -0
- package/src/serve/resident-runtime.ts +1 -0
- package/src/serve/routes/api.ts +20 -115
- package/src/serve/routes/clipper.ts +394 -0
- package/src/serve/server.ts +22 -0
- package/src/store/migrations/020-browser-clipper-security.ts +128 -0
- package/src/store/migrations/021-multi-context-identity.ts +37 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +83 -0
- package/src/store/sqlite/clipper-store-types.ts +104 -0
- package/src/store/sqlite/clipper-store.ts +496 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock-safe, create/update-only application of a compiled project profile.
|
|
3
|
+
*
|
|
4
|
+
* Profile files are inputs only. Config, locks, index state, and receipts stay
|
|
5
|
+
* in user runtime directories outside the trusted profile root.
|
|
6
|
+
*
|
|
7
|
+
* @module src/core/project-profile-apply
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// node:fs/promises provides realpath; Bun has no equivalent for canonical path identity.
|
|
11
|
+
import { realpath } from "node:fs/promises";
|
|
12
|
+
// node:path provides path containment primitives; Bun has no path utilities.
|
|
13
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
14
|
+
|
|
15
|
+
import type { Config } from "../config/types";
|
|
16
|
+
import type { SqliteAdapter } from "../store/sqlite/adapter";
|
|
17
|
+
import type { ProjectProfileDiagnostic } from "./project-profile";
|
|
18
|
+
import type { ProjectProfileApplyResource } from "./project-profile-apply-state";
|
|
19
|
+
import type {
|
|
20
|
+
ProjectProfileDiff,
|
|
21
|
+
ProjectProfileDiffDiagnostic,
|
|
22
|
+
} from "./project-profile-diff";
|
|
23
|
+
|
|
24
|
+
import { createDefaultConfig } from "../config";
|
|
25
|
+
import { saveTextToPath } from "../config/saver";
|
|
26
|
+
import { applyConfigChange } from "./config-mutation";
|
|
27
|
+
import {
|
|
28
|
+
canonicalOperationalPath,
|
|
29
|
+
getConfigWriteLockPath,
|
|
30
|
+
} from "./config-write-lock";
|
|
31
|
+
import { compileProjectProfileYaml } from "./project-profile";
|
|
32
|
+
import {
|
|
33
|
+
applyProjectProfileDesiredState,
|
|
34
|
+
buildProjectProfileResources,
|
|
35
|
+
canonicalProfileStateEqual,
|
|
36
|
+
} from "./project-profile-apply-state";
|
|
37
|
+
import { buildProjectProfileDiff } from "./project-profile-diff";
|
|
38
|
+
import { PROJECT_PROFILE_RELATIVE_PATH } from "./project-profile-discovery";
|
|
39
|
+
|
|
40
|
+
export type {
|
|
41
|
+
ProjectProfileApplyDisposition,
|
|
42
|
+
ProjectProfileApplyResource,
|
|
43
|
+
} from "./project-profile-apply-state";
|
|
44
|
+
|
|
45
|
+
export const PROJECT_PROFILE_APPLY_SCHEMA_VERSION = "1.0" as const;
|
|
46
|
+
|
|
47
|
+
export interface ProjectProfileApplyReceipt {
|
|
48
|
+
schemaVersion: typeof PROJECT_PROFILE_APPLY_SCHEMA_VERSION;
|
|
49
|
+
command: "apply";
|
|
50
|
+
status: "applied" | "unchanged";
|
|
51
|
+
profile: {
|
|
52
|
+
fingerprint: string;
|
|
53
|
+
};
|
|
54
|
+
diff: ProjectProfileDiff;
|
|
55
|
+
resources: ProjectProfileApplyResource[];
|
|
56
|
+
pendingIndexing: string[];
|
|
57
|
+
diagnostics: ProjectProfileDiffDiagnostic[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type ProjectProfileApplyErrorCode =
|
|
61
|
+
| "CONFIG_LOAD_FAILED"
|
|
62
|
+
| "CONFIG_SAVE_FAILED"
|
|
63
|
+
| "LOCKED"
|
|
64
|
+
| "PROFILE_INVALID"
|
|
65
|
+
| "RECEIPT_WRITE_FAILED"
|
|
66
|
+
| "RUNTIME_PATH_OVERLAP"
|
|
67
|
+
| "STORE_SYNC_FAILED";
|
|
68
|
+
|
|
69
|
+
export interface ProjectProfileApplyError {
|
|
70
|
+
code: ProjectProfileApplyErrorCode;
|
|
71
|
+
message: string;
|
|
72
|
+
remediation: string;
|
|
73
|
+
diagnostics?: ProjectProfileDiagnostic[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ProjectProfileApplyResult =
|
|
77
|
+
| {
|
|
78
|
+
ok: true;
|
|
79
|
+
receipt: ProjectProfileApplyReceipt;
|
|
80
|
+
receiptPath: string;
|
|
81
|
+
}
|
|
82
|
+
| { ok: false; error: ProjectProfileApplyError };
|
|
83
|
+
|
|
84
|
+
export interface ProjectProfileApplyOptions {
|
|
85
|
+
profileYaml: string;
|
|
86
|
+
profileRoot: string;
|
|
87
|
+
/** Canonical discovered source when available; otherwise derived from root. */
|
|
88
|
+
profilePath?: string;
|
|
89
|
+
configPath: string;
|
|
90
|
+
dataDir: string;
|
|
91
|
+
store: SqliteAdapter;
|
|
92
|
+
runtimePaths?: ReadonlyArray<{ label: string; path: string }>;
|
|
93
|
+
canonicalizePath?: (path: string) => Promise<string>;
|
|
94
|
+
onConfigUpdated?: (config: Config) => void;
|
|
95
|
+
receiptWriter?: (
|
|
96
|
+
receipt: ProjectProfileApplyReceipt,
|
|
97
|
+
path: string
|
|
98
|
+
) => Promise<void>;
|
|
99
|
+
/** Test-only interruption seam after durable config save. */
|
|
100
|
+
failureInjection?: "after_config_save";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const isContained = (parent: string, candidate: string): boolean => {
|
|
104
|
+
const pathFromParent = relative(parent, candidate);
|
|
105
|
+
return (
|
|
106
|
+
pathFromParent === "" ||
|
|
107
|
+
(pathFromParent !== ".." &&
|
|
108
|
+
!pathFromParent.startsWith(`..${sep}`) &&
|
|
109
|
+
!isAbsolute(pathFromParent))
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export async function validateProjectProfileRuntimePaths(
|
|
114
|
+
profileRoot: string,
|
|
115
|
+
paths: ReadonlyArray<{ label: string; path: string }>
|
|
116
|
+
): Promise<ProjectProfileApplyError | null> {
|
|
117
|
+
const canonicalRoot = await canonicalOperationalPath(profileRoot);
|
|
118
|
+
for (const output of paths) {
|
|
119
|
+
const canonicalOutput = await canonicalOperationalPath(output.path);
|
|
120
|
+
if (isContained(canonicalRoot, canonicalOutput)) {
|
|
121
|
+
return {
|
|
122
|
+
code: "RUNTIME_PATH_OVERLAP",
|
|
123
|
+
message: `${output.label} must remain outside the project profile root.`,
|
|
124
|
+
remediation:
|
|
125
|
+
"Choose user config, data, cache, model, lock, and index paths outside the repository.",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const defaultReceiptWriter = async (
|
|
133
|
+
receipt: ProjectProfileApplyReceipt,
|
|
134
|
+
path: string
|
|
135
|
+
): Promise<void> => {
|
|
136
|
+
const saved = await saveTextToPath(
|
|
137
|
+
`${JSON.stringify(receipt, null, 2)}\n`,
|
|
138
|
+
path
|
|
139
|
+
);
|
|
140
|
+
if (!saved.ok) throw new Error(saved.error.message);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const mutationFailure = (
|
|
144
|
+
code: string,
|
|
145
|
+
message: string
|
|
146
|
+
): ProjectProfileApplyError => {
|
|
147
|
+
if (code === "LOCKED") {
|
|
148
|
+
return {
|
|
149
|
+
code: "LOCKED",
|
|
150
|
+
message: "The project profile apply lock is busy.",
|
|
151
|
+
remediation: "Wait for the concurrent apply to finish, then retry.",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (code === "SYNC_ERROR") {
|
|
155
|
+
return {
|
|
156
|
+
code: "STORE_SYNC_FAILED",
|
|
157
|
+
message,
|
|
158
|
+
remediation:
|
|
159
|
+
"Repair the local index store and rerun apply; the saved config is resumable.",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (code === "LOAD_ERROR") {
|
|
163
|
+
return {
|
|
164
|
+
code: "CONFIG_LOAD_FAILED",
|
|
165
|
+
message,
|
|
166
|
+
remediation: "Repair the selected user config and rerun apply.",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
code: "CONFIG_SAVE_FAILED",
|
|
171
|
+
message,
|
|
172
|
+
remediation:
|
|
173
|
+
"Repair user config/data-directory permissions and rerun apply.",
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export async function applyProjectProfile(
|
|
178
|
+
options: ProjectProfileApplyOptions
|
|
179
|
+
): Promise<ProjectProfileApplyResult> {
|
|
180
|
+
let canonicalProfilePath: string;
|
|
181
|
+
try {
|
|
182
|
+
canonicalProfilePath = await realpath(
|
|
183
|
+
options.profilePath ??
|
|
184
|
+
join(options.profileRoot, PROJECT_PROFILE_RELATIVE_PATH)
|
|
185
|
+
);
|
|
186
|
+
} catch {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
error: {
|
|
190
|
+
code: "PROFILE_INVALID",
|
|
191
|
+
message: "The selected project profile identity could not be resolved.",
|
|
192
|
+
remediation:
|
|
193
|
+
"Repair .gno/index.yml or select an exact readable project root.",
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const runtimeDir = join(options.dataDir, "project-profiles");
|
|
198
|
+
const receiptPath = join(runtimeDir, "apply-receipt.json");
|
|
199
|
+
const lockPath = await getConfigWriteLockPath(options.configPath);
|
|
200
|
+
const overlap = await validateProjectProfileRuntimePaths(
|
|
201
|
+
options.profileRoot,
|
|
202
|
+
[
|
|
203
|
+
{ label: "Config", path: options.configPath },
|
|
204
|
+
{ label: "Profile apply receipt", path: receiptPath },
|
|
205
|
+
{ label: "Profile apply lock", path: lockPath },
|
|
206
|
+
...(options.runtimePaths ?? []),
|
|
207
|
+
]
|
|
208
|
+
);
|
|
209
|
+
if (overlap) return { ok: false, error: overlap };
|
|
210
|
+
|
|
211
|
+
let profileFailure: ProjectProfileDiagnostic[] | null = null;
|
|
212
|
+
let receipt: ProjectProfileApplyReceipt | null = null;
|
|
213
|
+
let projection: {
|
|
214
|
+
collectionName: string;
|
|
215
|
+
contexts: Config["contexts"];
|
|
216
|
+
} | null = null;
|
|
217
|
+
let receiptWriteFailed = false;
|
|
218
|
+
let mutation;
|
|
219
|
+
try {
|
|
220
|
+
mutation = await applyConfigChange(
|
|
221
|
+
{
|
|
222
|
+
store: options.store,
|
|
223
|
+
configPath: options.configPath,
|
|
224
|
+
createConfigIfMissing: createDefaultConfig,
|
|
225
|
+
onConfigUpdated: options.onConfigUpdated ?? (() => undefined),
|
|
226
|
+
projectStore: async (store, config) => {
|
|
227
|
+
const selected = projection;
|
|
228
|
+
if (!selected) {
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
error: "Project profile store projection was not established.",
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const collection = config.collections.find(
|
|
235
|
+
(item) => item.name === selected.collectionName
|
|
236
|
+
);
|
|
237
|
+
if (!collection) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: "Project profile collection is missing after config save.",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const collectionsResult = await store.upsertCollections([collection]);
|
|
244
|
+
if (!collectionsResult.ok) {
|
|
245
|
+
return { ok: false, error: collectionsResult.error.message };
|
|
246
|
+
}
|
|
247
|
+
const contextsResult = await store.upsertContexts(selected.contexts);
|
|
248
|
+
return contextsResult.ok
|
|
249
|
+
? { ok: true }
|
|
250
|
+
: { ok: false, error: contextsResult.error.message };
|
|
251
|
+
},
|
|
252
|
+
...(options.failureInjection === "after_config_save"
|
|
253
|
+
? {
|
|
254
|
+
afterConfigSaved: () => {
|
|
255
|
+
throw new Error("INJECTED_AFTER_CONFIG_SAVE");
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
: {}),
|
|
259
|
+
afterStoreSynced: async () => {
|
|
260
|
+
if (!receipt) throw new Error("MISSING_APPLY_RECEIPT");
|
|
261
|
+
try {
|
|
262
|
+
await (options.receiptWriter ?? defaultReceiptWriter)(
|
|
263
|
+
receipt,
|
|
264
|
+
receiptPath
|
|
265
|
+
);
|
|
266
|
+
} catch {
|
|
267
|
+
receiptWriteFailed = true;
|
|
268
|
+
throw new Error("RECEIPT_WRITE_FAILED");
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
async (config) => {
|
|
273
|
+
const compiled = await compileProjectProfileYaml(options.profileYaml, {
|
|
274
|
+
profileRoot: options.profileRoot,
|
|
275
|
+
config,
|
|
276
|
+
});
|
|
277
|
+
if (!compiled.ok) {
|
|
278
|
+
profileFailure = compiled.diagnostics;
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
code: "PROFILE_INVALID",
|
|
282
|
+
error: "Project profile validation failed.",
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const built = await buildProjectProfileDiff({
|
|
286
|
+
desiredState: compiled.value.desiredState,
|
|
287
|
+
expectedCollectionRoot: compiled.value.resolvedPaths.collectionRoot,
|
|
288
|
+
profileBinding: {
|
|
289
|
+
path: canonicalProfilePath,
|
|
290
|
+
fingerprint: compiled.value.fingerprint,
|
|
291
|
+
collection: compiled.value.desiredState.collection.name,
|
|
292
|
+
},
|
|
293
|
+
config,
|
|
294
|
+
canonicalizePath: options.canonicalizePath ?? realpath,
|
|
295
|
+
});
|
|
296
|
+
const profileBinding = {
|
|
297
|
+
path: canonicalProfilePath,
|
|
298
|
+
fingerprint: compiled.value.fingerprint,
|
|
299
|
+
collection: compiled.value.desiredState.collection.name,
|
|
300
|
+
};
|
|
301
|
+
const nextConfig = applyProjectProfileDesiredState(
|
|
302
|
+
config,
|
|
303
|
+
compiled.value.desiredState,
|
|
304
|
+
compiled.value.resolvedPaths.collectionRoot,
|
|
305
|
+
profileBinding
|
|
306
|
+
);
|
|
307
|
+
projection = {
|
|
308
|
+
collectionName: compiled.value.desiredState.collection.name,
|
|
309
|
+
contexts: compiled.value.desiredState.contexts.map((context) => ({
|
|
310
|
+
scopeType: context.scopeType,
|
|
311
|
+
scopeKey: context.scopeKey,
|
|
312
|
+
text: context.text,
|
|
313
|
+
})),
|
|
314
|
+
};
|
|
315
|
+
const resources = buildProjectProfileResources(
|
|
316
|
+
config,
|
|
317
|
+
nextConfig,
|
|
318
|
+
built.diff,
|
|
319
|
+
compiled.value.desiredState,
|
|
320
|
+
profileBinding
|
|
321
|
+
);
|
|
322
|
+
const pendingIndexing = resources.some(
|
|
323
|
+
(resource) => resource.pendingIndexing
|
|
324
|
+
)
|
|
325
|
+
? [compiled.value.desiredState.collection.name]
|
|
326
|
+
: [];
|
|
327
|
+
receipt = {
|
|
328
|
+
schemaVersion: PROJECT_PROFILE_APPLY_SCHEMA_VERSION,
|
|
329
|
+
command: "apply",
|
|
330
|
+
status: resources.some(
|
|
331
|
+
(resource) =>
|
|
332
|
+
resource.disposition === "created" ||
|
|
333
|
+
resource.disposition === "updated"
|
|
334
|
+
)
|
|
335
|
+
? "applied"
|
|
336
|
+
: "unchanged",
|
|
337
|
+
profile: { fingerprint: compiled.value.fingerprint },
|
|
338
|
+
diff: built.diff,
|
|
339
|
+
resources,
|
|
340
|
+
pendingIndexing,
|
|
341
|
+
diagnostics: built.diagnostics,
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
ok: true,
|
|
345
|
+
config: nextConfig,
|
|
346
|
+
value: receipt,
|
|
347
|
+
skipSave: canonicalProfileStateEqual(config, nextConfig),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (receiptWriteFailed) {
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
error: {
|
|
356
|
+
code: "RECEIPT_WRITE_FAILED",
|
|
357
|
+
message:
|
|
358
|
+
"Config applied but the external runtime receipt was not saved.",
|
|
359
|
+
remediation:
|
|
360
|
+
"Repair the user data directory and rerun apply; no profile or index deletion occurred.",
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
error: {
|
|
367
|
+
code: "CONFIG_SAVE_FAILED",
|
|
368
|
+
message:
|
|
369
|
+
error instanceof Error &&
|
|
370
|
+
error.message === "INJECTED_AFTER_CONFIG_SAVE"
|
|
371
|
+
? "Apply was interrupted after the config save."
|
|
372
|
+
: "Project profile config mutation failed.",
|
|
373
|
+
remediation:
|
|
374
|
+
"Rerun apply; fresh-state diffing and store projection resume idempotently.",
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!mutation.ok) {
|
|
380
|
+
if (profileFailure) {
|
|
381
|
+
return {
|
|
382
|
+
ok: false,
|
|
383
|
+
error: {
|
|
384
|
+
code: "PROFILE_INVALID",
|
|
385
|
+
message: "Project profile validation failed.",
|
|
386
|
+
remediation: "Repair .gno/index.yml and rerun apply.",
|
|
387
|
+
diagnostics: profileFailure,
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
error: mutationFailure(mutation.code, mutation.error),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const completedReceipt = mutation.value ?? receipt;
|
|
397
|
+
if (!completedReceipt) {
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
error: {
|
|
401
|
+
code: "CONFIG_SAVE_FAILED",
|
|
402
|
+
message: "Apply completed without a deterministic receipt.",
|
|
403
|
+
remediation: "Rerun apply against the current local state.",
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return { ok: true, receipt: completedReceipt, receiptPath };
|
|
408
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ProjectProfileDesiredState } from "./project-profile";
|
|
2
|
+
|
|
3
|
+
import { PROJECT_PROFILE_FINGERPRINT_DOMAIN } from "../config/project-profile";
|
|
4
|
+
|
|
5
|
+
export const compareCodeUnits = (left: string, right: string): number =>
|
|
6
|
+
left < right ? -1 : left > right ? 1 : 0;
|
|
7
|
+
|
|
8
|
+
export const normalizeLogicalPath = (value: string): string => {
|
|
9
|
+
const normalized = value.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
10
|
+
return normalized || ".";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const sortedUnique = (values: readonly string[]): string[] =>
|
|
14
|
+
[...new Set(values)].sort(compareCodeUnits);
|
|
15
|
+
|
|
16
|
+
const canonicalize = (value: unknown): unknown => {
|
|
17
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
18
|
+
if (value !== null && typeof value === "object") {
|
|
19
|
+
const sorted: Record<string, unknown> = {};
|
|
20
|
+
for (const key of Object.keys(value).sort(compareCodeUnits)) {
|
|
21
|
+
const child = (value as Record<string, unknown>)[key];
|
|
22
|
+
if (child !== undefined) sorted[key] = canonicalize(child);
|
|
23
|
+
}
|
|
24
|
+
return sorted;
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const canonicalProjectProfileJson = (value: unknown): string =>
|
|
30
|
+
JSON.stringify(canonicalize(value));
|
|
31
|
+
|
|
32
|
+
const escapeOuterBraceDelimiter = (pattern: string): string => {
|
|
33
|
+
let braceDepth = 0;
|
|
34
|
+
let bracketDepth = 0;
|
|
35
|
+
let escaped = "";
|
|
36
|
+
for (const character of pattern) {
|
|
37
|
+
if (character === "[" && bracketDepth === 0) bracketDepth = 1;
|
|
38
|
+
else if (character === "]" && bracketDepth > 0) bracketDepth = 0;
|
|
39
|
+
else if (bracketDepth === 0 && character === "{") braceDepth += 1;
|
|
40
|
+
else if (bracketDepth === 0 && character === "}") braceDepth -= 1;
|
|
41
|
+
|
|
42
|
+
escaped +=
|
|
43
|
+
character === "," && braceDepth === 0 && bracketDepth === 0
|
|
44
|
+
? "\\,"
|
|
45
|
+
: character;
|
|
46
|
+
}
|
|
47
|
+
return escaped;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Encode portable include globs into the config's single pattern. FileWalker
|
|
52
|
+
* splits a whole-pattern union before calling Bun.Glob.scan().
|
|
53
|
+
*/
|
|
54
|
+
export const projectProfileIncludePattern = (
|
|
55
|
+
include: readonly string[]
|
|
56
|
+
): string => {
|
|
57
|
+
if (include.length === 1) return include[0] ?? "**/*";
|
|
58
|
+
return `{${include.map(escapeOuterBraceDelimiter).join(",")}}`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const sha256 = (value: string | Uint8Array): string =>
|
|
62
|
+
new Bun.CryptoHasher("sha256").update(value).digest("hex");
|
|
63
|
+
|
|
64
|
+
export const fingerprintProjectProfileState = (
|
|
65
|
+
desiredState: ProjectProfileDesiredState
|
|
66
|
+
): string =>
|
|
67
|
+
sha256(
|
|
68
|
+
`${PROJECT_PROFILE_FINGERPRINT_DOMAIN}${canonicalProjectProfileJson(
|
|
69
|
+
desiredState
|
|
70
|
+
)}`
|
|
71
|
+
);
|