@popoverai/dotrequirements 0.27.4 → 0.29.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 (65) hide show
  1. package/README.md +24 -20
  2. package/dist/cli.js +34 -12
  3. package/dist/codebase-to-spec/compose.d.ts +3 -2
  4. package/dist/codebase-to-spec/compose.js +3 -3
  5. package/dist/commands/aliases.d.ts +26 -0
  6. package/dist/commands/aliases.js +31 -0
  7. package/dist/commands/diff.d.ts +14 -0
  8. package/dist/commands/diff.js +62 -0
  9. package/dist/commands/init.js +5 -5
  10. package/dist/commands/link.d.ts +1 -1
  11. package/dist/commands/link.js +10 -7
  12. package/dist/commands/sync-common.d.ts +21 -0
  13. package/dist/commands/sync-common.js +24 -0
  14. package/dist/commands/sync.d.ts +26 -0
  15. package/dist/commands/sync.js +187 -0
  16. package/dist/convex.d.ts +1 -1
  17. package/dist/convex.js +2 -2
  18. package/dist/push/core.d.ts +18 -116
  19. package/dist/push/core.js +23 -267
  20. package/dist/push/index.d.ts +3 -3
  21. package/dist/push/index.js +4 -4
  22. package/dist/requirements/index.d.ts +2 -0
  23. package/dist/requirements/index.js +6 -1
  24. package/dist/requirements/style-guide.js +4 -4
  25. package/dist/schema/browser.d.ts +1 -0
  26. package/dist/schema/browser.js +3 -0
  27. package/dist/schema/index.d.ts +1 -0
  28. package/dist/schema/index.js +1 -0
  29. package/dist/schema/parser.d.ts +3 -0
  30. package/dist/schema/parser.js +1 -1
  31. package/dist/schema/schemas.d.ts +24 -22
  32. package/dist/schema/schemas.js +9 -12
  33. package/dist/schema/title-markdown.d.ts +30 -0
  34. package/dist/schema/title-markdown.js +38 -0
  35. package/dist/sync/compare.d.ts +21 -0
  36. package/dist/sync/compare.js +285 -0
  37. package/dist/sync/execute.d.ts +53 -0
  38. package/dist/sync/execute.js +225 -0
  39. package/dist/sync/index.d.ts +21 -0
  40. package/dist/sync/index.js +52 -0
  41. package/dist/sync/local-files.d.ts +26 -0
  42. package/dist/sync/local-files.js +93 -0
  43. package/dist/sync/plan.d.ts +39 -0
  44. package/dist/sync/plan.js +90 -0
  45. package/dist/sync/render.d.ts +17 -0
  46. package/dist/sync/render.js +46 -0
  47. package/dist/sync/segment.d.ts +40 -0
  48. package/dist/sync/segment.js +76 -0
  49. package/dist/sync/snapshot.d.ts +30 -0
  50. package/dist/sync/snapshot.js +122 -0
  51. package/dist/sync/types.d.ts +82 -0
  52. package/dist/sync/types.js +12 -0
  53. package/dist/templates/context-file-section.md +2 -1
  54. package/dist/templates/example-requirements.js +0 -2
  55. package/dist/templates/example-requirements.ts +0 -2
  56. package/dist/templates/requirements-readme.js +3 -4
  57. package/dist/templates/requirements-readme.ts +3 -4
  58. package/dist/templates/skills/codebase-to-spec/SKILL.md +2 -2
  59. package/dist/utils/project-settings.d.ts +8 -2
  60. package/dist/utils/project-settings.js +47 -24
  61. package/package.json +1 -1
  62. package/dist/commands/pull.d.ts +0 -8
  63. package/dist/commands/pull.js +0 -230
  64. package/dist/commands/push.d.ts +0 -6
  65. package/dist/commands/push.js +0 -244
@@ -1,230 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { ConvexHttpClient } from "convex/browser";
4
- import { getConvexUrl } from "../config.js";
5
- import { api } from "../convex.js";
6
- import { findRequirementsFiles } from "../requirements/index.js";
7
- import { buildRequirementsFile } from "../schema/index.js";
8
- import { extractFrontmatterBlock } from "../schema/parser-core.js";
9
- import { brand } from "../utils/brand.js";
10
- import { getProjectCredentials } from "../utils/project-settings.js";
11
- export async function pullCommand(options) {
12
- // Connect to Convex
13
- const convexUrl = getConvexUrl();
14
- const client = new ConvexHttpClient(convexUrl);
15
- let documents;
16
- let projectName;
17
- let isShareTokenFlow = false;
18
- // SHARE-TOKEN-CLI-1: Handle share token flow (no project config needed)
19
- if (options.share) {
20
- isShareTokenFlow = true;
21
- console.log(`Pulling requirements with share token...`);
22
- try {
23
- // SHARE-TOKEN-CLI-1.1: Pass only projectSecret, no projectSlug
24
- // The authzQueryReadOnly wrapper will resolve project from the token
25
- const result = await client.query(api.documents.queries.exportProjectForCli, {
26
- projectAuth: {
27
- projectSecret: options.share,
28
- },
29
- });
30
- documents = result.documents;
31
- projectName = result.projectName;
32
- // SHARE-TOKEN-CLI-1.2: Display project name in output
33
- console.log(`\nProject: "${projectName}"`);
34
- }
35
- catch (error) {
36
- // SHARE-TOKEN-CLI-2: Handle invalid/revoked tokens gracefully.
37
- // Production redacts ConvexError messages to "Server Error" but
38
- // preserves error.data — check both, or the friendly message only
39
- // ever appears against dev deployments.
40
- const data = error.data;
41
- const errorMessage = error instanceof Error ? error.message : String(error);
42
- if (data?.message === "Invalid share token" ||
43
- errorMessage.includes("Invalid share token")) {
44
- throw new Error("Invalid share token. The token may be incorrect or has been revoked.\n" +
45
- "Please request a new share token from your team.");
46
- }
47
- throw error;
48
- }
49
- }
50
- else {
51
- console.log(`Syncing requirements from ${brand} cloud...`);
52
- // Get project credentials from .requirements/project-settings.json
53
- // This throws helpful errors if project not found or not connected to cloud
54
- const credentials = getProjectCredentials();
55
- // Allow --project flag to override, but still need secret from settings
56
- const projectId = options.project ?? credentials.projectId;
57
- const projectSecret = credentials.projectSecret;
58
- // Fetch documents
59
- if (options.document) {
60
- // Fetch specific document
61
- const doc = await client.query(api.documents.queries.exportForCli, {
62
- projectAuth: {
63
- projectSlug: projectId,
64
- projectSecret,
65
- },
66
- target: { type: "document", id: options.document },
67
- documentId: options.document,
68
- });
69
- documents = doc ? [doc] : [];
70
- }
71
- else if (projectId) {
72
- // Fetch all documents for project
73
- // SYNC-ARCH-3.1: Use projectSlug (not projectId) since env stores the slug
74
- const result = await client.query(api.documents.queries.exportProjectForCli, {
75
- projectAuth: {
76
- projectSlug: projectId,
77
- projectSecret,
78
- },
79
- });
80
- documents = result.documents;
81
- projectName = result.projectName;
82
- }
83
- else {
84
- throw new Error("Either --project or --document must be specified");
85
- }
86
- }
87
- if (documents.length === 0) {
88
- console.log("No documents found.");
89
- return;
90
- }
91
- // Ensure .requirements/ directory exists (for new documents)
92
- const requirementsDir = path.join(process.cwd(), ".requirements");
93
- if (!fs.existsSync(requirementsDir)) {
94
- fs.mkdirSync(requirementsDir, { recursive: true });
95
- }
96
- // Build index of existing files by document ID (search entire workspace)
97
- const existingFilesByDocId = await buildDocumentIdIndex(process.cwd());
98
- // SYNC-WEB-CREATE-2.0: track paths claimed during this pull so colliding
99
- // titles don't silently overwrite each other within one operation
100
- const usedPaths = new Set(existingFilesByDocId.values());
101
- // Write each document as a Markdown file
102
- for (const doc of documents) {
103
- // Check if an existing file has this document ID
104
- const existingFilePath = existingFilesByDocId.get(doc.documentId);
105
- let filePath;
106
- if (existingFilePath) {
107
- filePath = existingFilePath;
108
- }
109
- else {
110
- // SYNC-WEB-CREATE-2.1: an all-symbols title sanitizes to nothing —
111
- // fall back to the document ID rather than a hidden ".requirements.md"
112
- const baseName = sanitizeFileName(doc.title) || doc.documentId;
113
- let candidate = path.join(requirementsDir, `${baseName}.requirements.md`);
114
- // SYNC-WEB-CREATE-2.0: disambiguate later collisions with a numeric
115
- // suffix; also avoid clobbering an on-disk file that belongs to a
116
- // different (or no) document
117
- let suffix = 2;
118
- while (usedPaths.has(candidate) || fs.existsSync(candidate)) {
119
- candidate = path.join(requirementsDir, `${baseName}-${suffix}.requirements.md`);
120
- suffix++;
121
- }
122
- filePath = candidate;
123
- }
124
- usedPaths.add(filePath);
125
- const fileName = path.basename(filePath);
126
- // IMPORT-1: carry the CTS run marker forward from the existing local
127
- // file — pull rebuilds frontmatter from cloud data, and silently dropping
128
- // the marker would strip a committed spec's import credential before a
129
- // teammate ever pushes it
130
- let existingCtsRun;
131
- if (existingFilePath && fs.existsSync(existingFilePath)) {
132
- const match = fs
133
- .readFileSync(existingFilePath, "utf-8")
134
- .match(/^ctsRun: (.+)$/m);
135
- existingCtsRun = match?.[1].trim();
136
- }
137
- // Build metadata with pulledAt for conflict detection
138
- // SYNC-CLI-EDIT-2: pulledAt is compared with cloud updatedAt during push
139
- const metadata = {
140
- pulledAt: new Date().toISOString(),
141
- version: doc.version,
142
- ...(existingCtsRun ? { ctsRun: existingCtsRun } : {}),
143
- document: {
144
- id: doc.documentId,
145
- title: doc.title,
146
- defaultPrefix: doc.defaultPrefix,
147
- },
148
- };
149
- // SYNC-ARCH-1: Use markdownContent directly from cloud (source of truth)
150
- // No need to rebuild from requirements - just combine with frontmatter
151
- const fileContent = buildRequirementsFile(metadata, doc.markdownContent);
152
- fs.writeFileSync(filePath, fileContent, "utf-8");
153
- console.log(`✓ Synced: ${fileName} (version ${doc.version})`);
154
- }
155
- console.log(`\nSuccessfully synced ${documents.length} document(s) to .requirements/`);
156
- // SHARE-TOKEN-CLI-3: Show next steps for share token users
157
- if (isShareTokenFlow) {
158
- console.log("\n─────────────────────────────────────────────────────────");
159
- console.log("Next steps to get full access (push, coverage):");
160
- console.log(" Run: dotrequirements link");
161
- console.log("─────────────────────────────────────────────────────────");
162
- }
163
- }
164
- /**
165
- * Sanitize document title for use as filename
166
- */
167
- function sanitizeFileName(title) {
168
- return title
169
- .toLowerCase()
170
- .replace(/[^a-z0-9]+/g, "-")
171
- .replace(/^-+|-+$/g, "");
172
- }
173
- /**
174
- * Build an index mapping document IDs to their file paths.
175
- * Scans existing .requirements.md files across the entire workspace and extracts document.id from frontmatter.
176
- * SYNC-DISCOVERY-2.0: Uses the same discovery logic as push and MCP.
177
- */
178
- async function buildDocumentIdIndex(workspaceRoot) {
179
- const index = new Map();
180
- // Use the same file discovery as MCP and push
181
- const allFiles = await findRequirementsFiles(workspaceRoot);
182
- for (const filePath of allFiles) {
183
- const docId = extractDocumentIdFromFile(filePath);
184
- if (docId) {
185
- index.set(docId, filePath);
186
- }
187
- }
188
- return index;
189
- }
190
- /**
191
- * Extract document.id from a requirements file's frontmatter.
192
- * Returns undefined if the file can't be read or doesn't have a document ID.
193
- * SYNC-DISCOVERY-3: only the leading YAML frontmatter block is consulted —
194
- * a document id quoted in body prose or a fenced example must never mark
195
- * the file as owning that document.
196
- */
197
- function extractDocumentIdFromFile(filePath) {
198
- try {
199
- const fileContent = fs.readFileSync(filePath, "utf-8");
200
- // Isolate the leading ---...--- frontmatter block; no frontmatter means
201
- // the file is unlinked to any cloud document (SYNC-DISCOVERY-3.2).
202
- // Shared helper normalizes CRLF (SYNC-FORMAT-1) so a Windows-saved file
203
- // keeps matching its document and is updated in place (SYNC-DISCOVERY-2.1).
204
- const content = extractFrontmatterBlock(fileContent);
205
- if (content === undefined) {
206
- return undefined;
207
- }
208
- // Match document.id in YAML frontmatter - handles both inline and nested formats
209
- // Inline: document: { id: "abc123", ... }
210
- // Nested (id: can appear at any position within the indented document block):
211
- // document:
212
- // title: Some Title
213
- // defaultPrefix: REQ
214
- // id: abc123
215
- const inlineMatch = content.match(/document:\s*\{[^}]*id:\s*["']?([^"',}\s]+)/);
216
- if (inlineMatch) {
217
- return inlineMatch[1];
218
- }
219
- // Match id: anywhere within the document block (allows other properties before it)
220
- const nestedMatch = content.match(/document:\s*\n(?:[ \t]+[^\n]*\n)*?[ \t]+id:\s*["']?([^"'\s\n]+)/);
221
- if (nestedMatch) {
222
- return nestedMatch[1];
223
- }
224
- return undefined;
225
- }
226
- catch {
227
- return undefined;
228
- }
229
- }
230
- //# sourceMappingURL=pull.js.map
@@ -1,6 +0,0 @@
1
- interface PushOptions {
2
- yes?: boolean;
3
- }
4
- export declare function pushCommand(file: string | undefined, options: PushOptions): Promise<void>;
5
- export {};
6
- //# sourceMappingURL=push.d.ts.map
@@ -1,244 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import * as readline from "node:readline";
4
- import { getConvexUrl } from "../config.js";
5
- import { dryRunPush, executePush, parseFilesForPushIndividually, } from "../push/index.js";
6
- import { findRequirementsFiles } from "../requirements/index.js";
7
- import { brand } from "../utils/brand.js";
8
- import { getProjectCredentials } from "../utils/project-settings.js";
9
- export async function pushCommand(file, options) {
10
- console.log(`Preparing to push requirements to ${brand} cloud...\n`);
11
- // Get project credentials from .requirements/project-settings.json
12
- // This throws helpful errors if project not found or not connected to cloud
13
- const { projectId, projectSecret } = getProjectCredentials();
14
- // Determine which files to push
15
- let filesToPush;
16
- if (file) {
17
- // Push specific file
18
- const filePath = path.resolve(file);
19
- if (!fs.existsSync(filePath)) {
20
- throw new Error(`File not found: ${file}`);
21
- }
22
- filesToPush = [filePath];
23
- }
24
- else {
25
- // SYNC-DISCOVERY-1.0: Push all *.requirements.md files in workspace (not just .requirements/)
26
- const workspaceRoot = process.cwd();
27
- filesToPush = await findRequirementsFiles(workspaceRoot);
28
- if (filesToPush.length === 0) {
29
- console.log("No *.requirements.md files found. Nothing to push.");
30
- return;
31
- }
32
- }
33
- // Parse local files. SYNC-FAIL-4: one invalid file is skipped while the
34
- // rest of the batch still pushes — same helper as the MCP push handler.
35
- console.log("Parsing local requirements...");
36
- const { parsedFiles, totalRequirements, parseFailures } = parseFilesForPushIndividually(filesToPush);
37
- // Log file parsing progress
38
- for (const file of parsedFiles) {
39
- const fileName = path.basename(file.filePath);
40
- const doc = file.metadata.document;
41
- console.log(` Reading ${fileName}...`);
42
- // DOC-HEADER-11.4: parseFilesForPush mutates the metadata when it infers
43
- // the prefix, so the flag it returns is the only record of the inference
44
- if (doc?.defaultPrefix && file.inferredDefaultPrefix) {
45
- console.log(` Inferred defaultPrefix "${doc.defaultPrefix}" from first requirement`);
46
- }
47
- }
48
- // SYNC-FAIL-4.1: every file in the push failed to parse — report each
49
- // skipped file, then fail honestly instead of dry-running nothing
50
- if (parsedFiles.length === 0) {
51
- for (const failure of parseFailures) {
52
- console.log(` ✗ Skipped ${path.basename(failure.filePath)}: ${failure.error}`);
53
- }
54
- console.log("\nNo valid documents to push.");
55
- process.exitCode = 1;
56
- return;
57
- }
58
- console.log(`\nFound ${totalRequirements} requirement(s) in ${parsedFiles.length} document(s).\n`);
59
- // Build credentials
60
- const credentials = {
61
- projectId,
62
- projectSecret,
63
- convexUrl: getConvexUrl(),
64
- };
65
- // Phase 1: Dry run
66
- console.log("Validating documents...");
67
- const dryRunResult = await dryRunPush(parsedFiles, credentials);
68
- // Display unified summary
69
- displayDryRunSummary(dryRunResult, parseFailures);
70
- // Check if there's anything to push
71
- const pushableCount = dryRunResult.updates.length +
72
- dryRunResult.creates.length +
73
- dryRunResult.notFound.length;
74
- if (pushableCount === 0) {
75
- console.log("No valid documents to push.");
76
- // SYNC-FAIL-4.1: every file in the push was invalid — whether it failed
77
- // local parse or dry-run validation — so the command must fail honestly,
78
- // matching the MCP handler's merged rule
79
- if (parseFailures.length > 0 || dryRunResult.invalid.length > 0) {
80
- process.exitCode = 1;
81
- }
82
- return;
83
- }
84
- // Single confirmation prompt
85
- if (!options.yes) {
86
- const hasConflicts = dryRunResult.conflicts.length > 0;
87
- const prompt = hasConflicts
88
- ? "Continue with push? (will overwrite cloud changes) [y/N] "
89
- : "Continue with push? [y/N] ";
90
- const confirmed = await confirm(prompt);
91
- if (!confirmed) {
92
- console.log("Push cancelled.");
93
- return;
94
- }
95
- }
96
- // Phase 2: Execute push
97
- console.log("\nPushing documents to cloud...");
98
- const result = await executePush(dryRunResult, credentials);
99
- // Display results
100
- for (const { file, result: dryResult } of [
101
- ...dryRunResult.updates,
102
- ...dryRunResult.creates,
103
- ...dryRunResult.notFound,
104
- ]) {
105
- const fileName = path.basename(file.filePath);
106
- const isCreate = dryResult.action === "create" || dryResult.action === "not_found";
107
- // Check if this file had an error — match by full path, since two pushed
108
- // files can share a basename
109
- const error = result.errors.find((e) => e.filePath === file.filePath);
110
- if (error) {
111
- console.log(` ✗ Failed: ${fileName} - ${error.error}`);
112
- }
113
- else if (isCreate) {
114
- console.log(` ✓ Created: ${fileName} → ${file.metadata.document?.id}`);
115
- }
116
- else {
117
- console.log(` ✓ Updated: ${fileName}`);
118
- }
119
- }
120
- // SYNC-FAIL-2.1: the cloud save succeeded but the local file couldn't be
121
- // updated — name the file, say the cloud is fine, and give the recovery
122
- // step that re-links the file instead of minting a duplicate on retry
123
- if (result.writeBackWarnings?.length > 0) {
124
- console.log();
125
- for (const warning of result.writeBackWarnings) {
126
- console.log(`⚠ ${warning.fileName}: saved to the cloud, but the local file could not be updated (${warning.error}). ` +
127
- `To avoid creating a duplicate, add "id: ${warning.documentId}" under "document:" in the frontmatter of ${warning.filePath}, then push again.`);
128
- }
129
- }
130
- // IMPORT-3: marker failures are loud but never fatal
131
- if (result.importWarnings.length > 0) {
132
- console.log();
133
- for (const { fileName, status } of result.importWarnings) {
134
- if (status === "already_used") {
135
- console.log(`⚠ ${fileName}: this file's import marker was already used, so its requirements were saved as natively authored (they count toward your plan's requirement limit). Re-running codebase-to-spec produces a fresh marker.`);
136
- }
137
- else {
138
- console.log(`⚠ ${fileName}: this file's import marker was not recognized — your CLI may need updating. Its requirements were saved as natively authored (they count toward your plan's requirement limit).`);
139
- }
140
- }
141
- }
142
- // SYNC-FAIL-1: a failed push must not masquerade as success — agents and
143
- // scripts drive this command and rely on the exit code to tell the truth
144
- const failed = result.errors.length;
145
- const syncedCount = result.created + result.updated;
146
- if (failed > 0) {
147
- process.exitCode = 1;
148
- if (syncedCount === 0) {
149
- console.log(`\n✗ Push failed: ${failed} document(s) could not be saved.`);
150
- }
151
- else {
152
- console.log(`\n⚠ Push incomplete: ${syncedCount} document(s) synced, ${failed} failed.`);
153
- }
154
- }
155
- else {
156
- console.log(`\n✓ Push complete!`);
157
- }
158
- if (result.created > 0) {
159
- console.log(` Created: ${result.created} document(s)`);
160
- }
161
- if (result.updated > 0) {
162
- console.log(` Updated: ${result.updated} document(s)`);
163
- }
164
- if (failed === 0 && syncedCount === 0) {
165
- console.log(" No documents were pushed.");
166
- }
167
- // SYNC-LAND-1: land the user on their spec, not at a dead end
168
- if (result.synced.length > 0) {
169
- console.log("\nReview in dot•requirements:");
170
- for (const doc of result.synced) {
171
- console.log(` ${doc.fileName} → ${doc.url}`);
172
- }
173
- }
174
- }
175
- /**
176
- * Display the dry run summary.
177
- */
178
- function displayDryRunSummary(dryRunResult, parseFailures = []) {
179
- const { updates, creates, notFound, invalid, conflicts } = dryRunResult;
180
- console.log("\n=== Push Summary ===\n");
181
- if (updates.length > 0) {
182
- console.log(`Updated documents (${updates.length}):`);
183
- for (const { file, result } of updates) {
184
- const fileName = path.basename(file.filePath);
185
- const hasConflict = conflicts.some((c) => c.item.file === file);
186
- const conflictNote = hasConflict ? " ⚠️ [cloud changed since pull]" : "";
187
- const warningNote = result.warning ? ` (${result.warning})` : "";
188
- console.log(` ~ ${fileName}${conflictNote}${warningNote}`);
189
- }
190
- console.log();
191
- }
192
- if (creates.length > 0) {
193
- console.log(`New documents (${creates.length}):`);
194
- for (const { file, result } of creates) {
195
- const fileName = path.basename(file.filePath);
196
- const warningNote = result.warning ? ` (${result.warning})` : "";
197
- console.log(` + ${fileName}${warningNote}`);
198
- }
199
- console.log();
200
- }
201
- if (notFound.length > 0) {
202
- console.log(`Documents not found in cloud (${notFound.length}) - will create new:`);
203
- for (const { file, result } of notFound) {
204
- const fileName = path.basename(file.filePath);
205
- console.log(` ! ${fileName} (ID: ${result.documentId})`);
206
- }
207
- console.log();
208
- }
209
- // SYNC-FAIL-4.0: local parse failures surface alongside dry-run invalids,
210
- // each named by file, while the rest of the batch proceeds
211
- const skipped = [
212
- ...parseFailures.map((failure) => ({
213
- fileName: path.basename(failure.filePath),
214
- error: failure.error,
215
- })),
216
- ...invalid.map(({ file, result }) => ({
217
- fileName: path.basename(file.filePath),
218
- error: result.error,
219
- })),
220
- ];
221
- if (skipped.length > 0) {
222
- console.log(`Skipped - invalid files (${skipped.length}):`);
223
- for (const { fileName, error } of skipped) {
224
- console.log(` ✗ ${fileName}: ${error}`);
225
- }
226
- console.log();
227
- }
228
- }
229
- /**
230
- * Prompt user for confirmation
231
- */
232
- function confirm(question) {
233
- const rl = readline.createInterface({
234
- input: process.stdin,
235
- output: process.stdout,
236
- });
237
- return new Promise((resolve) => {
238
- rl.question(question, (answer) => {
239
- rl.close();
240
- resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
241
- });
242
- });
243
- }
244
- //# sourceMappingURL=push.js.map