@funnelsgrove/cli 0.1.2 → 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/dist/apiClient.js +11 -1
- package/dist/cli.js +64 -15
- package/dist/localSync.d.ts +12 -0
- package/dist/localSync.js +53 -1
- package/package.json +2 -2
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')
|
|
@@ -205,7 +218,7 @@ const program = new Command();
|
|
|
205
218
|
program
|
|
206
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, [
|
|
@@ -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')
|
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,6 +1,6 @@
|
|
|
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": {
|
|
@@ -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
|
},
|