@funnelsgrove/cli 0.1.20 → 0.1.26
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 +16 -0
- package/dist/analyticsOutput.d.ts +2 -1
- package/dist/analyticsOutput.js +35 -10
- package/dist/apiClient.d.ts +9 -1
- package/dist/apiClient.js +16 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +40 -15
- package/dist/cliIdentity.d.ts +13 -0
- package/dist/cliIdentity.js +43 -0
- package/dist/contractCompatibility.d.ts +30 -0
- package/dist/contractCompatibility.js +250 -0
- package/dist/experimentCreate.d.ts +100 -0
- package/dist/experimentCreate.js +887 -0
- package/dist/localSync.d.ts +8 -0
- package/dist/localSync.js +71 -11
- package/funnel-contract-compatibility.json +1036 -0
- package/package.json +2 -1
- package/template_docs/.funnelsgrove-docs.json +4 -4
- package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
- package/template_docs/docs/funnelsgrove/recipes/add-experiment.md +156 -7
- package/template_docs/funnel-docs.config.json +1 -1
- package/template_scaffold/.funnelsgrove-docs.json +4 -4
- package/template_scaffold/.funnelsgrove-scaffold.json +9 -9
- package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
- package/template_scaffold/docs/funnelsgrove/recipes/add-experiment.md +156 -7
- package/template_scaffold/funnel-agent-docs.test.ts +32 -3
- package/template_scaffold/funnel-docs.config.json +1 -1
- package/template_scaffold/package-lock.json +4 -4
- package/template_scaffold/package.json +1 -1
package/dist/localSync.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export type SourceFile = {
|
|
|
17
17
|
contentType?: string;
|
|
18
18
|
contentEncoding?: 'base64';
|
|
19
19
|
};
|
|
20
|
+
export type SafeRootWriteHooks = {
|
|
21
|
+
beforeDirectoryCreate?: (relativeDirectoryPath: string) => void | Promise<void>;
|
|
22
|
+
afterDirectoryEntrySync?: (relativeDirectoryPath: string) => void | Promise<void>;
|
|
23
|
+
};
|
|
20
24
|
export type ChangedSourceFiles = {
|
|
21
25
|
currentManifest: SyncManifest;
|
|
22
26
|
deletedPaths: string[];
|
|
@@ -80,7 +84,11 @@ export type SyncDownLocalLifecycle = {
|
|
|
80
84
|
writeSyncState: () => Promise<unknown>;
|
|
81
85
|
};
|
|
82
86
|
export declare function runSyncDownLocalLifecycle(lifecycle: SyncDownLocalLifecycle): Promise<void>;
|
|
87
|
+
export declare const assertSafeRootFileWriteTarget: (rootDir: string, relativePath: string) => Promise<void>;
|
|
88
|
+
export declare const readSafeRootFile: (rootDir: string, relativePath: string) => Promise<Buffer>;
|
|
89
|
+
export declare const writeSafeRootFile: (rootDir: string, relativePath: string, content: string | Buffer, hooks?: SafeRootWriteHooks) => Promise<void>;
|
|
83
90
|
export declare function normalizeSyncPath(filePath: string): string;
|
|
91
|
+
export declare function assertSafeSyncPath(filePath: string): string;
|
|
84
92
|
export declare function shouldSyncFile(filePath: string): boolean;
|
|
85
93
|
export declare function buildSyncManifest(rootDir: string, input: SyncManifestInput): Promise<SyncManifest>;
|
|
86
94
|
export declare function buildCommittedSyncManifest(input: {
|
package/dist/localSync.js
CHANGED
|
@@ -60,28 +60,83 @@ const resolveSafeRoot = async (rootDir, create) => {
|
|
|
60
60
|
}
|
|
61
61
|
return realpath(rootDir);
|
|
62
62
|
};
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
63
|
+
const syncDirectory = async (directoryPath) => {
|
|
64
|
+
const handle = await open(directoryPath, constants.O_RDONLY);
|
|
65
|
+
try {
|
|
66
|
+
await handle.sync();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await handle.close();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const ensureDurableDirectoryChain = async (rootDir, segments, hooks) => {
|
|
66
73
|
let current = rootDir;
|
|
67
|
-
for (const segment of segments.
|
|
74
|
+
for (const [index, segment] of segments.entries()) {
|
|
75
|
+
const parent = current;
|
|
68
76
|
current = path.join(current, segment);
|
|
69
77
|
let metadata;
|
|
70
78
|
try {
|
|
71
79
|
metadata = await lstat(current);
|
|
72
80
|
}
|
|
73
81
|
catch (error) {
|
|
74
|
-
if (!
|
|
82
|
+
if (!isErrno(error, 'ENOENT'))
|
|
75
83
|
throw error;
|
|
76
|
-
|
|
84
|
+
const relativeDirectoryPath = segments.slice(0, index + 1).join('/');
|
|
85
|
+
await hooks.beforeDirectoryCreate?.(relativeDirectoryPath);
|
|
86
|
+
try {
|
|
87
|
+
await mkdir(current);
|
|
88
|
+
}
|
|
89
|
+
catch (mkdirError) {
|
|
90
|
+
if (!isErrno(mkdirError, 'EEXIST'))
|
|
91
|
+
throw mkdirError;
|
|
92
|
+
}
|
|
77
93
|
metadata = await lstat(current);
|
|
94
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
95
|
+
throw sourceCandidateInvariantError(`Local sync path traverses a symlink: ${relativeDirectoryPath}`);
|
|
96
|
+
}
|
|
97
|
+
await syncDirectory(parent);
|
|
98
|
+
await hooks.afterDirectoryEntrySync?.(relativeDirectoryPath);
|
|
99
|
+
continue;
|
|
78
100
|
}
|
|
79
101
|
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
80
|
-
throw sourceCandidateInvariantError(`Local sync path traverses a symlink: ${
|
|
102
|
+
throw sourceCandidateInvariantError(`Local sync path traverses a symlink: ${segments.join('/')}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const assertSafeExistingPath = async (rootDir, relativePath, createParents, allowMissingParents = false, hooks = {}) => {
|
|
107
|
+
const safePath = assertSafeSyncPath(relativePath);
|
|
108
|
+
const segments = safePath.split('/');
|
|
109
|
+
let parentIsMissing = false;
|
|
110
|
+
const parentSegments = segments.slice(0, -1);
|
|
111
|
+
if (createParents) {
|
|
112
|
+
await ensureDurableDirectoryChain(rootDir, parentSegments, hooks);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
let current = rootDir;
|
|
116
|
+
for (const segment of parentSegments) {
|
|
117
|
+
current = path.join(current, segment);
|
|
118
|
+
if (parentIsMissing)
|
|
119
|
+
continue;
|
|
120
|
+
let metadata;
|
|
121
|
+
try {
|
|
122
|
+
metadata = await lstat(current);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (allowMissingParents && isErrno(error, 'ENOENT')) {
|
|
126
|
+
parentIsMissing = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
132
|
+
throw sourceCandidateInvariantError(`Local sync path traverses a symlink: ${relativePath}`);
|
|
133
|
+
}
|
|
81
134
|
}
|
|
82
135
|
}
|
|
83
136
|
const absolutePath = path.join(rootDir, ...segments);
|
|
84
137
|
assertContainedPath(rootDir, absolutePath);
|
|
138
|
+
if (parentIsMissing)
|
|
139
|
+
return absolutePath;
|
|
85
140
|
try {
|
|
86
141
|
const metadata = await lstat(absolutePath);
|
|
87
142
|
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
@@ -94,6 +149,10 @@ const assertSafeExistingPath = async (rootDir, relativePath, createParents) => {
|
|
|
94
149
|
}
|
|
95
150
|
return absolutePath;
|
|
96
151
|
};
|
|
152
|
+
export const assertSafeRootFileWriteTarget = async (rootDir, relativePath) => {
|
|
153
|
+
const resolvedRoot = await resolveSafeRoot(rootDir, false);
|
|
154
|
+
await assertSafeExistingPath(resolvedRoot, relativePath, false, true);
|
|
155
|
+
};
|
|
97
156
|
const assertOpenFileContained = async (rootDir, absolutePath, opened) => {
|
|
98
157
|
const openedStat = await opened.stat();
|
|
99
158
|
if (!openedStat.isFile()) {
|
|
@@ -106,7 +165,7 @@ const assertOpenFileContained = async (rootDir, absolutePath, opened) => {
|
|
|
106
165
|
throw sourceCandidateInvariantError('Local source changed while opening it. Retry the sync.');
|
|
107
166
|
}
|
|
108
167
|
};
|
|
109
|
-
const readSafeRootFile = async (rootDir, relativePath) => {
|
|
168
|
+
export const readSafeRootFile = async (rootDir, relativePath) => {
|
|
110
169
|
const resolvedRoot = await resolveSafeRoot(rootDir, false);
|
|
111
170
|
const absolutePath = await assertSafeExistingPath(resolvedRoot, relativePath, false);
|
|
112
171
|
let handle;
|
|
@@ -127,9 +186,9 @@ const readSafeRootFile = async (rootDir, relativePath) => {
|
|
|
127
186
|
await handle.close();
|
|
128
187
|
}
|
|
129
188
|
};
|
|
130
|
-
const writeSafeRootFile = async (rootDir, relativePath, content) => {
|
|
189
|
+
export const writeSafeRootFile = async (rootDir, relativePath, content, hooks = {}) => {
|
|
131
190
|
const resolvedRoot = await resolveSafeRoot(rootDir, true);
|
|
132
|
-
const absolutePath = await assertSafeExistingPath(resolvedRoot, relativePath, true);
|
|
191
|
+
const absolutePath = await assertSafeExistingPath(resolvedRoot, relativePath, true, false, hooks);
|
|
133
192
|
const tempPath = path.join(path.dirname(absolutePath), `.${path.basename(absolutePath)}.fgrove-${randomUUID()}.tmp`);
|
|
134
193
|
let handle = null;
|
|
135
194
|
try {
|
|
@@ -141,6 +200,7 @@ const writeSafeRootFile = async (rootDir, relativePath, content) => {
|
|
|
141
200
|
handle = null;
|
|
142
201
|
await assertSafeExistingPath(resolvedRoot, relativePath, true);
|
|
143
202
|
await rename(tempPath, absolutePath);
|
|
203
|
+
await syncDirectory(path.dirname(absolutePath));
|
|
144
204
|
assertContainedPath(resolvedRoot, await realpath(absolutePath));
|
|
145
205
|
}
|
|
146
206
|
finally {
|
|
@@ -154,7 +214,7 @@ export function normalizeSyncPath(filePath) {
|
|
|
154
214
|
return normalized.replace(/^(\.\/|\/)+/, '');
|
|
155
215
|
}
|
|
156
216
|
const sourceCandidateInvariantError = (reason) => Object.assign(new Error(reason), { code: 'FG-CANDIDATE-001' });
|
|
157
|
-
function assertSafeSyncPath(filePath) {
|
|
217
|
+
export function assertSafeSyncPath(filePath) {
|
|
158
218
|
if (filePath.length === 0
|
|
159
219
|
|| Array.from(filePath).length > MAX_SOURCE_CANDIDATE_PATH_CHARACTERS
|
|
160
220
|
|| filePath !== filePath.trim()
|