@funnelsgrove/cli 0.1.1 → 0.1.3
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 +7 -7
- package/dist/apiClient.js +11 -1
- package/dist/cli.js +101 -52
- package/dist/localSync.d.ts +12 -0
- package/dist/localSync.js +53 -1
- package/package.json +3 -3
- package/template_docs/agent.md +3 -3
package/README.md
CHANGED
|
@@ -4,24 +4,24 @@ Install:
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @funnelsgrove/cli
|
|
7
|
-
|
|
7
|
+
fgrove login
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
Set the active project and funnel:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
fgrove use --project claimbee --funnel claimbee-ios
|
|
14
|
+
fgrove status
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Common workflow:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
|
|
20
|
+
fgrove sync down --dir ./claimbee-ios
|
|
21
21
|
cd ./claimbee-ios
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
fgrove docs
|
|
23
|
+
fgrove sync up --message 'Update funnel copy'
|
|
24
|
+
fgrove publish --env preview
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
The package also keeps the longer `funnelsgrove` command as a compatibility alias.
|
package/dist/apiClient.js
CHANGED
|
@@ -46,6 +46,16 @@ export async function callTrpcProcedure(input) {
|
|
|
46
46
|
? `${baseUrl}?input=${encodeURIComponent(JSON.stringify(input.input))}`
|
|
47
47
|
: baseUrl;
|
|
48
48
|
const response = await fetchFn(url, init);
|
|
49
|
-
const
|
|
49
|
+
const responseText = await response.text();
|
|
50
|
+
let json;
|
|
51
|
+
try {
|
|
52
|
+
json = JSON.parse(responseText);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
const contentType = response.headers.get('content-type') || 'unknown content type';
|
|
56
|
+
const snippet = responseText.replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
57
|
+
const details = snippet ? `: ${snippet}` : '';
|
|
58
|
+
throw new Error(`FunnelsGrove API returned ${response.status} ${response.statusText || 'Unknown status'} with ${contentType}${details}`);
|
|
59
|
+
}
|
|
50
60
|
return parseTrpcJsonResponse(json);
|
|
51
61
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { cp, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { createInterface } from 'node:readline/promises';
|
|
4
5
|
import path from 'node:path';
|
|
@@ -6,10 +7,22 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
7
|
import { Command } from 'commander';
|
|
7
8
|
import { callTrpcProcedure } from './apiClient.js';
|
|
8
9
|
import { clearActiveContext, getDefaultAuthConfigPath, loadActiveContext, loadAuthToken, saveActiveContext, saveAuthToken, } from './authStore.js';
|
|
9
|
-
import { buildSyncManifest, collectSourceFiles, readSyncManifest, writeSourceFiles, writeSyncManifest, } from './localSync.js';
|
|
10
|
+
import { buildSyncManifest, chunkChangedSourceFiles, collectChangedSourceFiles, collectSourceFiles, readSyncManifest, writeSourceFiles, writeSyncManifest, } from './localSync.js';
|
|
10
11
|
import { isKnownTemplate, KNOWN_TEMPLATE_SLUGS, reskinFunnel } from './reskin.js';
|
|
11
12
|
import { syncTemplateDocs, TEMPLATE_DOCS_DIR } from './templateDocs.js';
|
|
12
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const readCliVersion = () => {
|
|
15
|
+
try {
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
17
|
+
if (typeof packageJson.version === 'string' && packageJson.version.trim()) {
|
|
18
|
+
return packageJson.version;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Fall through to the packaged fallback below.
|
|
23
|
+
}
|
|
24
|
+
return '0.1.3';
|
|
25
|
+
};
|
|
13
26
|
const toKebabCase = (value) => value
|
|
14
27
|
.trim()
|
|
15
28
|
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
@@ -30,7 +43,7 @@ const getConfigPath = () => {
|
|
|
30
43
|
const readAuthToken = async () => {
|
|
31
44
|
const token = await loadAuthToken(getConfigPath());
|
|
32
45
|
if (!token) {
|
|
33
|
-
throw new Error('Not logged in. Run `
|
|
46
|
+
throw new Error('Not logged in. Run `fgrove login` first.');
|
|
34
47
|
}
|
|
35
48
|
return token;
|
|
36
49
|
};
|
|
@@ -173,7 +186,7 @@ const resolveSyncTarget = async (input) => {
|
|
|
173
186
|
? await resolveFunnelId(input.token, workspaceId, input.funnel)
|
|
174
187
|
: manifest?.funnelId || active?.funnelId;
|
|
175
188
|
if (!funnelId) {
|
|
176
|
-
throw new Error('No active funnel. Run `
|
|
189
|
+
throw new Error('No active funnel. Run `fgrove use --funnel <id-or-slug>` or pass `--funnel`.');
|
|
177
190
|
}
|
|
178
191
|
return {
|
|
179
192
|
workspaceId,
|
|
@@ -203,23 +216,23 @@ const printRows = (rows, columns) => {
|
|
|
203
216
|
const addExamples = (command, examples) => command.addHelpText('after', `\nExamples:\n${examples.map((example) => ` $ ${example}`).join('\n')}`);
|
|
204
217
|
const program = new Command();
|
|
205
218
|
program
|
|
206
|
-
.name('
|
|
219
|
+
.name('fgrove')
|
|
207
220
|
.description('FunnelsGrove CLI for editing, syncing, and publishing funnels')
|
|
208
|
-
.version(
|
|
221
|
+
.version(readCliVersion())
|
|
209
222
|
.option('--api-url <url>', 'FunnelsGrove tRPC API URL', process.env.FUNNELSGROVE_API_URL || DEFAULT_API_URL)
|
|
210
223
|
.option('--config <path>', 'Auth config path', process.env.FUNNELSGROVE_CONFIG || getDefaultAuthConfigPath());
|
|
211
224
|
addExamples(program, [
|
|
212
|
-
'
|
|
213
|
-
'
|
|
214
|
-
'
|
|
215
|
-
'
|
|
225
|
+
'fgrove login',
|
|
226
|
+
'fgrove use --project claimbee --funnel claimbee-ios',
|
|
227
|
+
'fgrove sync down --dir ./claimbee-ios',
|
|
228
|
+
'fgrove publish --env preview',
|
|
216
229
|
]);
|
|
217
230
|
addExamples(program
|
|
218
231
|
.command('login')
|
|
219
232
|
.description('Authorize this CLI with your FunnelsGrove account')
|
|
220
233
|
.option('--code <code>', '8 digit code from the authorization page'), [
|
|
221
|
-
'
|
|
222
|
-
'
|
|
234
|
+
'fgrove login',
|
|
235
|
+
'fgrove login --api-url http://localhost:3001/trpc',
|
|
223
236
|
])
|
|
224
237
|
.action(async (options) => {
|
|
225
238
|
const request = await callApi({
|
|
@@ -244,7 +257,7 @@ addExamples(program
|
|
|
244
257
|
addExamples(program
|
|
245
258
|
.command('whoami')
|
|
246
259
|
.description('Show the current authenticated user'), [
|
|
247
|
-
'
|
|
260
|
+
'fgrove whoami',
|
|
248
261
|
])
|
|
249
262
|
.action(async () => {
|
|
250
263
|
const token = await readAuthToken();
|
|
@@ -258,9 +271,9 @@ addExamples(program
|
|
|
258
271
|
.option('--project <id-or-slug-or-name>', 'Project id, slug, or name')
|
|
259
272
|
.option('--funnel <id-or-slug-or-name>', 'Funnel id, slug, or name')
|
|
260
273
|
.option('--clear', 'Clear the active CLI context'), [
|
|
261
|
-
'
|
|
262
|
-
'
|
|
263
|
-
'
|
|
274
|
+
'fgrove use --project claimbee --funnel claimbee-ios',
|
|
275
|
+
'fgrove use --workspace acme --project claimbee',
|
|
276
|
+
'fgrove use --clear',
|
|
264
277
|
])
|
|
265
278
|
.action(async (options) => {
|
|
266
279
|
if (options.clear) {
|
|
@@ -292,8 +305,8 @@ addExamples(program
|
|
|
292
305
|
.command('status')
|
|
293
306
|
.description('Show the authenticated user and active CLI context')
|
|
294
307
|
.option('--dir <path>', 'Local source directory for reading sync manifest', '.'), [
|
|
295
|
-
'
|
|
296
|
-
'
|
|
308
|
+
'fgrove status',
|
|
309
|
+
'fgrove status --dir ./claimbee-ios',
|
|
297
310
|
])
|
|
298
311
|
.action(async (options) => {
|
|
299
312
|
const token = await readAuthToken();
|
|
@@ -315,15 +328,15 @@ addExamples(program
|
|
|
315
328
|
}
|
|
316
329
|
});
|
|
317
330
|
const projectsCommand = addExamples(program.command('projects').description('Manage projects'), [
|
|
318
|
-
'
|
|
319
|
-
'
|
|
331
|
+
'fgrove projects list',
|
|
332
|
+
'fgrove projects list --workspace acme',
|
|
320
333
|
]);
|
|
321
334
|
addExamples(projectsCommand
|
|
322
335
|
.command('list')
|
|
323
336
|
.description('List projects in the active or provided workspace')
|
|
324
337
|
.option('--workspace <id-or-slug-or-name>', 'Workspace id, slug, or name'), [
|
|
325
|
-
'
|
|
326
|
-
'
|
|
338
|
+
'fgrove projects list',
|
|
339
|
+
'fgrove projects list --workspace acme',
|
|
327
340
|
])
|
|
328
341
|
.action(async (options) => {
|
|
329
342
|
const token = await readAuthToken();
|
|
@@ -332,15 +345,15 @@ addExamples(projectsCommand
|
|
|
332
345
|
printRows(projects, ['id', 'name', 'slug']);
|
|
333
346
|
});
|
|
334
347
|
const funnelsCommand = addExamples(program.command('funnels').description('Manage funnels'), [
|
|
335
|
-
'
|
|
336
|
-
'
|
|
348
|
+
'fgrove funnels list',
|
|
349
|
+
'fgrove funnels clone --funnel claimbee --name claimbee-ios',
|
|
337
350
|
]);
|
|
338
351
|
addExamples(funnelsCommand
|
|
339
352
|
.command('list')
|
|
340
353
|
.description('List funnels in a workspace')
|
|
341
354
|
.option('--workspace <id-or-slug-or-name>', 'Workspace id, slug, or name'), [
|
|
342
|
-
'
|
|
343
|
-
'
|
|
355
|
+
'fgrove funnels list',
|
|
356
|
+
'fgrove funnels list --workspace acme',
|
|
344
357
|
])
|
|
345
358
|
.action(async (options) => {
|
|
346
359
|
const token = await readAuthToken();
|
|
@@ -354,8 +367,8 @@ addExamples(funnelsCommand
|
|
|
354
367
|
.option('--workspace <id-or-slug-or-name>', 'Workspace id, slug, or name')
|
|
355
368
|
.requiredOption('--funnel <id-or-slug>', 'Source funnel id or slug')
|
|
356
369
|
.requiredOption('--name <name>', 'New funnel name'), [
|
|
357
|
-
'
|
|
358
|
-
'
|
|
370
|
+
'fgrove funnels clone --funnel claimbee --name claimbee-ios',
|
|
371
|
+
'fgrove funnels clone --workspace acme --funnel claimbee --name claimbee-ios',
|
|
359
372
|
])
|
|
360
373
|
.action(async (options) => {
|
|
361
374
|
const token = await readAuthToken();
|
|
@@ -374,8 +387,8 @@ addExamples(funnelsCommand
|
|
|
374
387
|
console.log(`${result.funnel.id}\t${result.funnel.name}\t${result.funnel.slug}`);
|
|
375
388
|
});
|
|
376
389
|
const syncCommand = addExamples(program.command('sync').description('Sync funnel source'), [
|
|
377
|
-
'
|
|
378
|
-
'
|
|
390
|
+
'fgrove sync down --dir ./claimbee-ios',
|
|
391
|
+
'fgrove sync up --message "Update copy"',
|
|
379
392
|
]);
|
|
380
393
|
addExamples(syncCommand
|
|
381
394
|
.command('down')
|
|
@@ -383,8 +396,8 @@ addExamples(syncCommand
|
|
|
383
396
|
.option('--workspace <id-or-slug-or-name>', 'Workspace id, slug, or name')
|
|
384
397
|
.option('--funnel <id-or-slug>', 'Funnel id or slug')
|
|
385
398
|
.requiredOption('--dir <path>', 'Local target directory'), [
|
|
386
|
-
'
|
|
387
|
-
'
|
|
399
|
+
'fgrove sync down --dir ./claimbee-ios',
|
|
400
|
+
'fgrove sync down --funnel claimbee-ios --dir ./claimbee-ios',
|
|
388
401
|
])
|
|
389
402
|
.action(async (options) => {
|
|
390
403
|
const token = await readAuthToken();
|
|
@@ -423,8 +436,8 @@ addExamples(syncCommand
|
|
|
423
436
|
.option('--funnel <id-or-slug>', 'Funnel id or slug')
|
|
424
437
|
.option('--dir <path>', 'Local source directory', '.')
|
|
425
438
|
.option('--message <message>', 'Draft version message'), [
|
|
426
|
-
'
|
|
427
|
-
'
|
|
439
|
+
'fgrove sync up --message "Update hero copy"',
|
|
440
|
+
'fgrove sync up --dir ./claimbee-ios --message "Update paywall"',
|
|
428
441
|
])
|
|
429
442
|
.action(async (options) => {
|
|
430
443
|
const token = await readAuthToken();
|
|
@@ -434,24 +447,60 @@ addExamples(syncCommand
|
|
|
434
447
|
funnel: options.funnel,
|
|
435
448
|
dir: options.dir,
|
|
436
449
|
});
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
450
|
+
const changes = target.manifest
|
|
451
|
+
? await collectChangedSourceFiles(target.sourceDir, target.manifest)
|
|
452
|
+
: null;
|
|
453
|
+
if (changes && changes.files.length === 0 && changes.deletedPaths.length === 0) {
|
|
454
|
+
console.log('No local changes to sync.');
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
let result = null;
|
|
458
|
+
let syncedFileCount = 0;
|
|
459
|
+
let deletedFileCount = 0;
|
|
460
|
+
if (changes) {
|
|
461
|
+
const batches = chunkChangedSourceFiles(changes);
|
|
462
|
+
for (const batch of batches) {
|
|
463
|
+
result = await callApi({
|
|
464
|
+
path: 'funnels.patchSource',
|
|
465
|
+
type: 'mutation',
|
|
466
|
+
token,
|
|
467
|
+
data: {
|
|
468
|
+
workspaceId: target.workspaceId,
|
|
469
|
+
funnelId: target.funnelId,
|
|
470
|
+
message: options.message,
|
|
471
|
+
files: batch.files,
|
|
472
|
+
deletedPaths: batch.deletedPaths,
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
syncedFileCount += result.syncedFiles.length;
|
|
476
|
+
deletedFileCount += result.deletedFiles?.length || 0;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
result = await callApi({
|
|
481
|
+
path: 'funnels.importSource',
|
|
482
|
+
type: 'mutation',
|
|
483
|
+
token,
|
|
484
|
+
data: {
|
|
485
|
+
workspaceId: target.workspaceId,
|
|
486
|
+
funnelId: target.funnelId,
|
|
487
|
+
message: options.message,
|
|
488
|
+
files: await collectSourceFiles(target.sourceDir),
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
syncedFileCount = result.syncedFiles.length;
|
|
492
|
+
deletedFileCount = result.deletedFiles?.length || 0;
|
|
493
|
+
}
|
|
494
|
+
if (!result) {
|
|
495
|
+
throw new Error('No sync result returned.');
|
|
496
|
+
}
|
|
449
497
|
await writeSyncManifest(target.sourceDir, await buildSyncManifest(target.sourceDir, {
|
|
450
498
|
workspaceId: target.workspaceId,
|
|
451
499
|
funnelId: target.funnelId,
|
|
452
500
|
draftVersionId: result.versionId,
|
|
453
501
|
}));
|
|
454
|
-
|
|
502
|
+
const deletedSummary = deletedFileCount > 0 ? ` and removed ${deletedFileCount} files` : '';
|
|
503
|
+
console.log(`Synced ${syncedFileCount} files${deletedSummary} to draft v${result.versionSeq} (${result.versionId})`);
|
|
455
504
|
});
|
|
456
505
|
addExamples(program
|
|
457
506
|
.command('publish')
|
|
@@ -462,8 +511,8 @@ addExamples(program
|
|
|
462
511
|
.option('--env <preview-or-production>', 'Publish environment', 'preview')
|
|
463
512
|
.option('--domain <domain>', 'Production custom domain')
|
|
464
513
|
.option('--message <message>', 'Publish version message'), [
|
|
465
|
-
'
|
|
466
|
-
'
|
|
514
|
+
'fgrove publish --env preview --message "Preview copy updates"',
|
|
515
|
+
'fgrove publish --env production --domain claimbee.example.com --message "Launch"',
|
|
467
516
|
])
|
|
468
517
|
.action(async (options) => {
|
|
469
518
|
const token = await readAuthToken();
|
|
@@ -497,8 +546,8 @@ addExamples(program
|
|
|
497
546
|
.command('docs')
|
|
498
547
|
.description('Install or refresh local funnel editing docs in a funnel directory')
|
|
499
548
|
.option('--dir <path>', 'Local funnel directory', '.'), [
|
|
500
|
-
'
|
|
501
|
-
'
|
|
549
|
+
'fgrove docs',
|
|
550
|
+
'fgrove docs --dir ./claimbee-ios',
|
|
502
551
|
])
|
|
503
552
|
.action(async (options) => {
|
|
504
553
|
const targetDir = path.resolve(process.cwd(), options.dir);
|
|
@@ -511,8 +560,8 @@ addExamples(program
|
|
|
511
560
|
.requiredOption('--from <template>', 'Template slug from rag-catalog')
|
|
512
561
|
.requiredOption('--app <name>', 'Your app name')
|
|
513
562
|
.option('--output <dir>', 'Output directory'), [
|
|
514
|
-
'
|
|
515
|
-
'
|
|
563
|
+
'fgrove create --from headway-funnel --app ClaimBee',
|
|
564
|
+
'fgrove create --from promova --app ClaimBee --output ./claimbee-funnel',
|
|
516
565
|
])
|
|
517
566
|
.action(async (options) => {
|
|
518
567
|
const { from: templateSlug, app: appName, output } = options;
|
package/dist/localSync.d.ts
CHANGED
|
@@ -16,10 +16,22 @@ export type SourceFile = {
|
|
|
16
16
|
content: string;
|
|
17
17
|
contentType?: string;
|
|
18
18
|
};
|
|
19
|
+
export type ChangedSourceFiles = {
|
|
20
|
+
currentManifest: SyncManifest;
|
|
21
|
+
deletedPaths: string[];
|
|
22
|
+
files: SourceFile[];
|
|
23
|
+
};
|
|
24
|
+
export type SourceFilePatchBatch = {
|
|
25
|
+
deletedPaths: string[];
|
|
26
|
+
files: SourceFile[];
|
|
27
|
+
};
|
|
28
|
+
export declare const DEFAULT_PATCH_BATCH_CONTENT_CHARS = 12000000;
|
|
19
29
|
export declare function normalizeSyncPath(filePath: string): string;
|
|
20
30
|
export declare function shouldSyncFile(filePath: string): boolean;
|
|
21
31
|
export declare function buildSyncManifest(rootDir: string, input: SyncManifestInput): Promise<SyncManifest>;
|
|
22
32
|
export declare function writeSyncManifest(rootDir: string, manifest: SyncManifest): Promise<void>;
|
|
23
33
|
export declare function readSyncManifest(rootDir: string): Promise<SyncManifest | null>;
|
|
24
34
|
export declare function collectSourceFiles(rootDir: string): Promise<SourceFile[]>;
|
|
35
|
+
export declare function collectChangedSourceFiles(rootDir: string, previousManifest: SyncManifest): Promise<ChangedSourceFiles>;
|
|
36
|
+
export declare function chunkChangedSourceFiles(changes: Pick<ChangedSourceFiles, 'deletedPaths' | 'files'>, maxContentChars?: number): SourceFilePatchBatch[];
|
|
25
37
|
export declare function writeSourceFiles(rootDir: string, files: SourceFile[]): Promise<void>;
|
package/dist/localSync.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
export const SYNC_MANIFEST_FILE = '.funnelsgrove-sync.json';
|
|
5
|
+
export const DEFAULT_PATCH_BATCH_CONTENT_CHARS = 12_000_000;
|
|
5
6
|
const EXCLUDED_PATH_PARTS = new Set(['node_modules', '.next', 'out']);
|
|
6
7
|
export function normalizeSyncPath(filePath) {
|
|
7
8
|
const normalized = path.posix.normalize(filePath.replaceAll('\\', '/'));
|
|
@@ -76,7 +77,58 @@ export async function collectSourceFiles(rootDir) {
|
|
|
76
77
|
funnelId: '',
|
|
77
78
|
draftVersionId: '',
|
|
78
79
|
});
|
|
79
|
-
|
|
80
|
+
return readSourceFiles(rootDir, manifest.files);
|
|
81
|
+
}
|
|
82
|
+
export async function collectChangedSourceFiles(rootDir, previousManifest) {
|
|
83
|
+
const currentManifest = await buildSyncManifest(rootDir, {
|
|
84
|
+
workspaceId: previousManifest.workspaceId,
|
|
85
|
+
funnelId: previousManifest.funnelId,
|
|
86
|
+
draftVersionId: previousManifest.draftVersionId,
|
|
87
|
+
});
|
|
88
|
+
const previousHashByPath = new Map(previousManifest.files.map((file) => [file.path, file.hash]));
|
|
89
|
+
const currentHashByPath = new Map(currentManifest.files.map((file) => [file.path, file.hash]));
|
|
90
|
+
const changedManifestFiles = currentManifest.files.filter((file) => previousHashByPath.get(file.path) !== file.hash);
|
|
91
|
+
const deletedPaths = previousManifest.files
|
|
92
|
+
.filter((file) => !currentHashByPath.has(file.path))
|
|
93
|
+
.map((file) => file.path)
|
|
94
|
+
.sort((left, right) => left.localeCompare(right));
|
|
95
|
+
return {
|
|
96
|
+
currentManifest,
|
|
97
|
+
deletedPaths,
|
|
98
|
+
files: await readSourceFiles(rootDir, changedManifestFiles),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function chunkChangedSourceFiles(changes, maxContentChars = DEFAULT_PATCH_BATCH_CONTENT_CHARS) {
|
|
102
|
+
const batches = [];
|
|
103
|
+
let currentBatch = {
|
|
104
|
+
deletedPaths: changes.deletedPaths,
|
|
105
|
+
files: [],
|
|
106
|
+
};
|
|
107
|
+
let currentContentChars = 0;
|
|
108
|
+
const pushCurrentBatch = () => {
|
|
109
|
+
if (currentBatch.files.length === 0 && currentBatch.deletedPaths.length === 0) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
batches.push(currentBatch);
|
|
113
|
+
currentBatch = {
|
|
114
|
+
deletedPaths: [],
|
|
115
|
+
files: [],
|
|
116
|
+
};
|
|
117
|
+
currentContentChars = 0;
|
|
118
|
+
};
|
|
119
|
+
for (const file of changes.files) {
|
|
120
|
+
if (currentBatch.files.length > 0 &&
|
|
121
|
+
currentContentChars + file.content.length > maxContentChars) {
|
|
122
|
+
pushCurrentBatch();
|
|
123
|
+
}
|
|
124
|
+
currentBatch.files.push(file);
|
|
125
|
+
currentContentChars += file.content.length;
|
|
126
|
+
}
|
|
127
|
+
pushCurrentBatch();
|
|
128
|
+
return batches;
|
|
129
|
+
}
|
|
130
|
+
async function readSourceFiles(rootDir, manifestFiles) {
|
|
131
|
+
const files = await Promise.all(manifestFiles.map(async (file) => {
|
|
80
132
|
const absolutePath = path.join(rootDir, assertSafeSyncPath(file.path));
|
|
81
133
|
const imageContentType = inferImageContentType(file.path);
|
|
82
134
|
const buffer = await readFile(absolutePath);
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnelsgrove/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "FunnelsGrove command-line tools for editing, syncing, and publishing funnels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
7
|
+
"fgrove": "dist/cli.js",
|
|
8
8
|
"funnelsgrove": "dist/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "rm -rf dist && tsc",
|
|
19
|
+
"build": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
|
20
20
|
"prepack": "npm run build",
|
|
21
21
|
"test": "vitest run"
|
|
22
22
|
},
|
package/template_docs/agent.md
CHANGED
|
@@ -4,11 +4,11 @@ Use this folder as the local source-of-truth guide when editing a synced Funnels
|
|
|
4
4
|
|
|
5
5
|
## Workflow
|
|
6
6
|
|
|
7
|
-
1. Run `
|
|
7
|
+
1. Run `fgrove status` and confirm the active project and funnel.
|
|
8
8
|
2. Keep edits inside the synced funnel tree.
|
|
9
9
|
3. Run the funnel's local checks before syncing.
|
|
10
|
-
4. Run `
|
|
11
|
-
5. Run `
|
|
10
|
+
4. Run `fgrove sync up --message '<summary>'`.
|
|
11
|
+
5. Run `fgrove publish --env preview --message '<summary>'` and verify the preview.
|
|
12
12
|
6. Publish production only when explicitly requested.
|
|
13
13
|
|
|
14
14
|
## Topics
|