@micropage-sh/cli 2.0.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.
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const { db, handleAuthError } = require('../supabase');
7
+ const { getProjectConfig } = require('../auth');
8
+ const { formatTable, formatDate } = require('../utils');
9
+
10
+ function formatCliFieldValue(value) {
11
+ if (value === null || value === undefined || value === '') return '—';
12
+ if (Array.isArray(value)) return value.join(', ');
13
+ return String(value);
14
+ }
15
+
16
+ async function list(options = {}) {
17
+ const cwd = process.cwd();
18
+ const config = getProjectConfig(cwd);
19
+ if (!config?.projectId) {
20
+ console.error('Not in a project folder. Run from a folder with .micropage/project.json');
21
+ process.exit(1);
22
+ }
23
+
24
+ let submissions;
25
+ try {
26
+ submissions = await db
27
+ .from('form_submissions')
28
+ .select('id,form_id,form_name,page_url,created_at,form_index')
29
+ .eq('project_id', config.projectId)
30
+ .order('created_at', 'desc')
31
+ .get();
32
+ } catch (err) {
33
+ handleAuthError(err);
34
+ console.error('Failed to list form submissions:', err.message);
35
+ process.exit(1);
36
+ }
37
+
38
+ if (!Array.isArray(submissions) || submissions.length === 0) {
39
+ console.log('No form submissions.');
40
+ return;
41
+ }
42
+
43
+ if (options.json) {
44
+ console.log(JSON.stringify(submissions, null, 2));
45
+ return;
46
+ }
47
+
48
+ const rows = submissions.map((s) => [
49
+ s.id.slice(0, 8) + '…',
50
+ s.form_id ? s.form_id.slice(0, 8) + '…' : '-',
51
+ s.form_name || '-',
52
+ s.page_url || '-',
53
+ formatDate(s.created_at),
54
+ ]);
55
+ formatTable(rows, ['ID (short)', 'Form ID', 'Form', 'Page URL', 'Submitted']);
56
+ }
57
+
58
+ async function show(id, options = {}) {
59
+ if (!id) {
60
+ console.error('Usage: micropage submissions show <submission-id>');
61
+ process.exit(1);
62
+ }
63
+
64
+ let submission;
65
+ try {
66
+ submission = await db
67
+ .from('form_submissions')
68
+ .select('*')
69
+ .eq('id', id)
70
+ .single();
71
+ } catch (err) {
72
+ handleAuthError(err);
73
+ console.error('Failed to fetch submission:', err.message);
74
+ process.exit(1);
75
+ }
76
+
77
+ if (!submission) {
78
+ console.error('Submission not found:', id);
79
+ process.exit(1);
80
+ }
81
+
82
+ if (options.json) {
83
+ console.log(JSON.stringify(submission, null, 2));
84
+ return;
85
+ }
86
+
87
+ console.log('ID: ', submission.id);
88
+ console.log('Form ID: ', submission.form_id || '-');
89
+ console.log('Form: ', submission.form_name || '-');
90
+ console.log('Page URL: ', submission.page_url || '-');
91
+ console.log('Form index: ', submission.form_index ?? '-');
92
+ console.log('Submitted: ', formatDate(submission.created_at));
93
+ console.log('Build ID: ', submission.build_id || '-');
94
+ const fields = submission.payload?.fields;
95
+ if (Array.isArray(fields) && fields.length > 0) {
96
+ console.log('');
97
+ console.log('Fields:');
98
+ for (const row of fields) {
99
+ console.log(` ${row.label}: ${formatCliFieldValue(row.value)}`);
100
+ }
101
+ }
102
+ console.log('');
103
+ console.log('Payload:');
104
+ console.log(JSON.stringify(submission.payload || {}, null, 2));
105
+ }
106
+
107
+ async function exportSubmissions(options = {}) {
108
+ const cwd = process.cwd();
109
+ const config = getProjectConfig(cwd);
110
+ if (!config?.projectId) {
111
+ console.error('Not in a project folder. Run from a folder with .micropage/project.json');
112
+ process.exit(1);
113
+ }
114
+
115
+ let submissions;
116
+ try {
117
+ submissions = await db
118
+ .from('form_submissions')
119
+ .select('*')
120
+ .eq('project_id', config.projectId)
121
+ .order('created_at', 'desc')
122
+ .get();
123
+ } catch (err) {
124
+ handleAuthError(err);
125
+ console.error('Failed to export form submissions:', err.message);
126
+ process.exit(1);
127
+ }
128
+
129
+ if (!Array.isArray(submissions) || submissions.length === 0) {
130
+ console.log('No form submissions to export.');
131
+ return;
132
+ }
133
+
134
+ const format = (options.format || 'csv').toLowerCase();
135
+ if (format !== 'csv' && format !== 'json') {
136
+ console.error('Invalid format. Use "csv" or "json".');
137
+ process.exit(1);
138
+ }
139
+
140
+ const defaultName = `submissions.${format}`;
141
+ const outFile = options.output
142
+ ? path.isAbsolute(options.output)
143
+ ? options.output
144
+ : path.join(cwd, options.output)
145
+ : path.join(cwd, defaultName);
146
+
147
+ try {
148
+ if (format === 'json') {
149
+ fs.writeFileSync(outFile, JSON.stringify(submissions, null, 2), 'utf8');
150
+ } else {
151
+ const header = [
152
+ 'id',
153
+ 'created_at',
154
+ 'page_url',
155
+ 'form_name',
156
+ 'form_id',
157
+ 'form_index',
158
+ 'build_id',
159
+ 'payload_json',
160
+ ];
161
+ const escapeCell = (value) => {
162
+ if (value === null || value === undefined) return '';
163
+ const str = String(value);
164
+ if (str.includes('"') || str.includes(',') || str.includes('\n')) {
165
+ return `"${str.replace(/"/g, '""')}"`;
166
+ }
167
+ return str;
168
+ };
169
+ const lines = [
170
+ header.join(','),
171
+ ...submissions.map((s) =>
172
+ [
173
+ s.id,
174
+ s.created_at,
175
+ s.page_url || '',
176
+ s.form_name || '',
177
+ s.form_id || '',
178
+ s.form_index ?? '',
179
+ s.build_id || '',
180
+ JSON.stringify(s.payload || {}),
181
+ ].map(escapeCell).join(','),
182
+ ),
183
+ ];
184
+ fs.writeFileSync(outFile, lines.join('\n'), 'utf8');
185
+ }
186
+ } catch (err) {
187
+ console.error('Failed to write export file:', err.message);
188
+ process.exit(1);
189
+ }
190
+
191
+ console.log(`Exported ${submissions.length} submission(s) → ${outFile}`);
192
+ }
193
+
194
+ module.exports = { list, show, exportSubmissions };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ const { getSession } = require('../auth');
4
+ const { db, getUserInfo, getValidAccessToken, handleAuthError } = require('../supabase');
5
+
6
+ async function run() {
7
+ const session = getSession();
8
+ if (!session) {
9
+ console.error('Not logged in. Run: micropage login');
10
+ process.exit(1);
11
+ }
12
+
13
+ let user;
14
+ try {
15
+ const accessToken = await getValidAccessToken();
16
+ user = await getUserInfo(accessToken);
17
+ } catch (err) {
18
+ handleAuthError(err);
19
+ console.error('Failed to get user info:', err.message);
20
+ process.exit(1);
21
+ }
22
+
23
+ if (!user) {
24
+ console.error('Session expired. Run: micropage login');
25
+ process.exit(1);
26
+ }
27
+
28
+ console.log('User ID: ', user.id);
29
+ console.log('Email: ', user.email || '-');
30
+ const name =
31
+ user.user_metadata?.full_name ||
32
+ user.user_metadata?.user_name ||
33
+ user.user_metadata?.name ||
34
+ '-';
35
+ console.log('Name: ', name);
36
+ if (user.app_metadata?.provider) {
37
+ console.log('Auth via: ', user.app_metadata.provider);
38
+ }
39
+
40
+ // Billing / subscription overview (best-effort; do not fail whoami if this breaks).
41
+ let planTier = 'free';
42
+ let subscription = null;
43
+ try {
44
+ const customer = await db
45
+ .from('customers')
46
+ .select('plan_tier')
47
+ .eq('user_id', user.id)
48
+ .single();
49
+ if (customer?.plan_tier) {
50
+ planTier = customer.plan_tier;
51
+ }
52
+ } catch (err) {
53
+ handleAuthError(err);
54
+ console.error('Warning: failed to load plan info:', err.message);
55
+ }
56
+
57
+ try {
58
+ const subs = await db
59
+ .from('subscriptions')
60
+ .select('status,plan_tier,provider,product_name,current_period_end')
61
+ .eq('user_id', user.id)
62
+ .in('status', ['active', 'trialing', 'paused'])
63
+ .order('updated_at', 'desc')
64
+ .limit(1)
65
+ .get();
66
+ subscription = Array.isArray(subs) ? subs[0] || null : subs || null;
67
+ } catch (err) {
68
+ handleAuthError(err);
69
+ console.error('Warning: failed to load subscription details:', err.message);
70
+ }
71
+
72
+ console.log('Plan tier: ', planTier);
73
+ if (subscription) {
74
+ console.log('Subscription status: ', subscription.status || '-');
75
+ if (subscription.provider) {
76
+ console.log('Billing provider: ', subscription.provider);
77
+ }
78
+ if (subscription.product_name) {
79
+ console.log('Product: ', subscription.product_name);
80
+ }
81
+ if (subscription.current_period_end) {
82
+ console.log('Current period end:', subscription.current_period_end);
83
+ }
84
+ }
85
+ }
86
+
87
+ module.exports = { run };
package/src/config.js ADDED
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Central configuration constants for the CLI.
5
+ * All values can be overridden with environment variables for local development.
6
+ */
7
+ const SUPABASE_URL =
8
+ process.env.MICROPAGE_SUPABASE_URL || 'https://vhlifcdslnmnvnvorluu.supabase.co';
9
+
10
+ // This is the public anon key – safe to embed in the CLI (same as the web app).
11
+ const SUPABASE_ANON_KEY =
12
+ process.env.MICROPAGE_SUPABASE_ANON_KEY ||
13
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZobGlmY2RzbG5tbnZudm9ybHV1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjUxMTUzMTMsImV4cCI6MjA0MDY5MTMxM30.IprHOpa397Wcv5iFwa_e9hkXhhmXYZFXC4BF_qGYkkk';
14
+
15
+ const APP_URL = process.env.MICROPAGE_APP_URL || 'https://app.micropage.sh';
16
+
17
+ const BUILD_COMPILER_URL =
18
+ process.env.MICROPAGE_BUILD_COMPILER_URL ||
19
+ process.env.MICROPAGE_PARSER_URL ||
20
+ 'https://build-compiler.micropage.sh';
21
+
22
+ module.exports = { SUPABASE_URL, SUPABASE_ANON_KEY, APP_URL, BUILD_COMPILER_URL };
package/src/index.js ADDED
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { Command } = require('commander');
5
+
6
+ const login = require('./commands/login');
7
+ const logout = require('./commands/logout');
8
+ const whoami = require('./commands/whoami');
9
+ const projects = require('./commands/projects');
10
+ const builds = require('./commands/builds');
11
+ const submissions = require('./commands/submissions');
12
+ const forms = require('./commands/forms');
13
+ const files = require('./commands/files');
14
+ const links = require('./commands/links');
15
+
16
+ const program = new Command();
17
+
18
+ program
19
+ .name('micropage')
20
+ .description('CLI for micropage.sh – create, sync, and publish microsites')
21
+ .version('2.0.0');
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Authentication
25
+ // ---------------------------------------------------------------------------
26
+
27
+ program
28
+ .command('login')
29
+ .description('Authenticate via the browser')
30
+ .option('-f, --force', 'Re-authenticate even if already logged in')
31
+ .action((opts) => login.run(opts));
32
+
33
+ program
34
+ .command('logout')
35
+ .description('Clear the stored session')
36
+ .action(() => logout.run());
37
+
38
+ program
39
+ .command('whoami')
40
+ .description('Show the currently logged-in user and subscription')
41
+ .action(() => whoami.run());
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Projects (micropage projects <subcommand>)
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const projectCmd = program.command('projects').description('Manage projects');
48
+
49
+ projectCmd
50
+ .command('list')
51
+ .description('List your projects')
52
+ .option('--json', 'Output as JSON')
53
+ .action((opts) => projects.list(opts));
54
+
55
+ projectCmd
56
+ .command('show [uuidOrDomain]')
57
+ .description('Show project details including latest build info (UUID or domain; omit from a project folder)')
58
+ .option('--json', 'Output as JSON')
59
+ .action((uuidOrDomain, opts) => projects.show(uuidOrDomain, opts));
60
+
61
+ projectCmd
62
+ .command('create <name>')
63
+ .description('Create a new project and init local folder')
64
+ .option(
65
+ '-d, --domain <domain>',
66
+ 'Override Cloudflare Pages project name / slug (default: server generates name-slug + 6 hex, max 58 chars)',
67
+ )
68
+ .action((name, opts) => projects.create(name, opts));
69
+
70
+ projectCmd
71
+ .command('fetch <uuidOrDomain>')
72
+ .description('Fetch an existing project by UUID or domain, init local folder')
73
+ .action((ref) => projects.fetch(ref));
74
+
75
+ projectCmd
76
+ .command('pull')
77
+ .description('Pull latest build raw content → local landing.page')
78
+ .action(() => projects.pull());
79
+
80
+ projectCmd
81
+ .command('delete')
82
+ .description('Delete project (remote + local .micropage/)')
83
+ .option('-y, --yes', 'Skip confirmation prompt')
84
+ .action((opts) => projects.deleteProject(opts));
85
+
86
+ projectCmd
87
+ .command('publish')
88
+ .description('Push local content and publish (deploy to Cloudflare Pages) — alias for `micropage publish`')
89
+ .option('-w, --watch', 'Stream build/deploy events after triggering publish')
90
+ .action((opts) => builds.publish(opts));
91
+
92
+ projectCmd
93
+ .command('deploy [projectUuid] [token]')
94
+ .description(
95
+ 'CI publish using a deploy token (Pro+). No login required. ' +
96
+ 'projectUuid is optional when run from a project directory (reads from .micropage/project.json). ' +
97
+ 'Without --build: reads .page files in the current directory, creates a build, and publishes. ' +
98
+ 'With --build <id>: publishes an existing build by id.',
99
+ )
100
+ .option('-b, --build <id>', 'Publish a specific existing build id instead of creating one from .page files')
101
+ .option('-w, --watch', 'Stream build/deploy events until finished or failed')
102
+ .option(
103
+ '--exchange-ttl <seconds>',
104
+ 'Exchanged access JWT lifetime in seconds (60–3600; omit for server default)',
105
+ (v) => parseInt(String(v), 10),
106
+ )
107
+ .option(
108
+ '--watch-with-deploy-token',
109
+ 'Use the deploy token for the event stream instead of the exchanged JWT',
110
+ )
111
+ .action((projectUuid, token, opts) => {
112
+ // If only one positional arg was given, it is the token; read UUID from project config.
113
+ if (token === undefined) {
114
+ return projects.deploy(null, projectUuid, opts);
115
+ }
116
+ return projects.deploy(projectUuid, token, opts);
117
+ });
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Builds (micropage builds <subcommand>)
121
+ // ---------------------------------------------------------------------------
122
+
123
+ const buildCmd = program.command('builds').description('Manage builds');
124
+
125
+ buildCmd
126
+ .command('list')
127
+ .description('List builds for the current project')
128
+ .option('--json', 'Output as JSON')
129
+ .action((opts) => builds.list(opts));
130
+
131
+ buildCmd
132
+ .command('redeploy [version]')
133
+ .description('Set active build to a version and republish (no new build row)')
134
+ .action((version) => builds.redeploy(version));
135
+
136
+ buildCmd
137
+ .command('download [version]')
138
+ .description('Download a build archive (zip) locally')
139
+ .option('-o, --output <file>', 'Output file path')
140
+ .action((version, opts) => builds.download(version, opts));
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Top-level push / publish (most common workflow commands)
144
+ // ---------------------------------------------------------------------------
145
+
146
+ program
147
+ .command('push')
148
+ .description('Merge local .page files and save as a draft build')
149
+ .action(() => builds.push());
150
+
151
+ program
152
+ .command('publish')
153
+ .description('Push local content and publish (deploy to Cloudflare Pages)')
154
+ .option('-w, --watch', 'Stream build/deploy events after triggering publish')
155
+ .action((opts) => builds.publish(opts));
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Links
159
+ // ---------------------------------------------------------------------------
160
+
161
+ program
162
+ .command('preview')
163
+ .description('Open the project live URL in the browser')
164
+ .action(() => links.preview());
165
+
166
+ program
167
+ .command('copy-link')
168
+ .description('Copy the project live URL to the clipboard')
169
+ .action(() => links.copyLink());
170
+
171
+ program
172
+ .command('open-pricing')
173
+ .description('Open the pricing page in the browser')
174
+ .action(() => links.openPricing());
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Files (micropage files <subcommand>)
178
+ // ---------------------------------------------------------------------------
179
+
180
+ const filesCmd = program.command('files').description('Manage project files');
181
+
182
+ filesCmd
183
+ .command('list')
184
+ .description('List uploaded project files')
185
+ .option('--json', 'Output as JSON')
186
+ .action((opts) => files.list(opts));
187
+
188
+ filesCmd
189
+ .command('url <filename>')
190
+ .description('Get the URL for an uploaded file')
191
+ .option('--copy', 'Copy the URL to clipboard')
192
+ .action((filename, opts) => files.url(filename, opts));
193
+
194
+ filesCmd
195
+ .command('sync')
196
+ .description('Download all project files from storage into ./assets (mirrors remote list)')
197
+ .option('-q, --quiet', 'No per-file output or summary (errors still print)')
198
+ .action((opts) => files.sync(opts));
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Forms (micropage forms <subcommand>)
202
+ // ---------------------------------------------------------------------------
203
+
204
+ const formCmd = program.command('forms').description('View forms for the current project');
205
+
206
+ formCmd
207
+ .command('list')
208
+ .description('List forms and their submission counts for the current project')
209
+ .option('--json', 'Output as JSON')
210
+ .action((opts) => forms.list(opts));
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Form submissions (micropage submissions <subcommand>)
214
+ // ---------------------------------------------------------------------------
215
+
216
+ const subCmd = program.command('submissions').description('View form submissions');
217
+
218
+ subCmd
219
+ .command('list')
220
+ .description('List form submissions for the current project')
221
+ .option('--json', 'Output as JSON')
222
+ .action((opts) => submissions.list(opts));
223
+
224
+ subCmd
225
+ .command('show <id>')
226
+ .description('Show a single form submission in detail')
227
+ .option('--json', 'Output as JSON')
228
+ .action((id, opts) => submissions.show(id, opts));
229
+
230
+ subCmd
231
+ .command('export')
232
+ .description('Export form submissions for the current project to a file')
233
+ .option(
234
+ '-f, --format <format>',
235
+ 'Output format: csv or json (default: csv)',
236
+ )
237
+ .option(
238
+ '-o, --output <file>',
239
+ 'Output file path (default: submissions.<format>)',
240
+ )
241
+ .action((opts) => submissions.exportSubmissions(opts));
242
+
243
+ program.parse();
package/src/mime.js ADDED
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const MAP = {
4
+ '.png': 'image/png',
5
+ '.jpg': 'image/jpeg',
6
+ '.jpeg': 'image/jpeg',
7
+ '.gif': 'image/gif',
8
+ '.webp': 'image/webp',
9
+ '.ico': 'image/x-icon',
10
+ '.svg': 'image/svg+xml',
11
+ };
12
+
13
+ function fromFilename(filename) {
14
+ const ext = require('path').extname(filename).toLowerCase();
15
+ return MAP[ext] || 'application/octet-stream';
16
+ }
17
+
18
+ module.exports = { fromFilename };
package/src/parser.js ADDED
@@ -0,0 +1,72 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Read and merge .page files from a directory.
6
+ *
7
+ * Merge order:
8
+ * 1. landing.page (canonical primary file, if present)
9
+ * 2. All other *.page files sorted lexicographically
10
+ *
11
+ * Files are joined with a single blank line separator.
12
+ */
13
+ function readPageFilesFromDir(dir) {
14
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
15
+ const primary = path.join(dir, 'landing.page');
16
+ const others = [];
17
+
18
+ for (const e of entries) {
19
+ if (!e.isFile() || !e.name.endsWith('.page')) continue;
20
+ if (e.name === 'landing.page') continue; // handled separately
21
+ others.push(path.join(dir, e.name));
22
+ }
23
+ others.sort();
24
+
25
+ const parts = [];
26
+ if (fs.existsSync(primary)) {
27
+ parts.push(fs.readFileSync(primary, 'utf8').trim());
28
+ }
29
+ for (const f of others) {
30
+ const content = fs.readFileSync(f, 'utf8').trim();
31
+ if (content) parts.push(content);
32
+ }
33
+
34
+ if (parts.length === 0) {
35
+ throw new Error('No .page files found');
36
+ }
37
+
38
+ return parts.join('\n\n');
39
+ }
40
+
41
+ /**
42
+ * Write raw content to landing.page in the given directory.
43
+ * Used by `micropage pull` to save the latest build content locally.
44
+ */
45
+ function writePageFile(dir, content) {
46
+ const dest = path.join(dir, 'landing.page');
47
+ fs.writeFileSync(dest, content, 'utf8');
48
+ return dest;
49
+ }
50
+
51
+ const ASSET_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg'];
52
+
53
+ function listAssetsFromDir(dir) {
54
+ const assetsDir = path.join(dir, 'assets');
55
+ if (!fs.existsSync(assetsDir)) return [];
56
+ const entries = fs.readdirSync(assetsDir, { withFileTypes: true });
57
+ const files = [];
58
+ for (const e of entries) {
59
+ if (e.isFile()) {
60
+ const ext = path.extname(e.name).toLowerCase();
61
+ if (ASSET_EXTENSIONS.includes(ext)) files.push(e.name);
62
+ }
63
+ }
64
+ return files.sort();
65
+ }
66
+
67
+ module.exports = {
68
+ readPageFilesFromDir,
69
+ writePageFile,
70
+ listAssetsFromDir,
71
+ ASSET_EXTENSIONS,
72
+ };
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ /** Match standard UUID strings (accepts any version nibble). */
4
+ const PROJECT_UUID_RE =
5
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
+
7
+ function isProjectUuid(s) {
8
+ return typeof s === 'string' && PROJECT_UUID_RE.test(s.trim());
9
+ }
10
+
11
+ function isLegacyNumericProjectId(s) {
12
+ return typeof s === 'string' && /^\d+$/.test(String(s).trim());
13
+ }
14
+
15
+ /**
16
+ * Load a project row from a CLI argument: UUID, legacy numeric id, or domain.
17
+ */
18
+ async function fetchProjectByUserRef(db, ref) {
19
+ const r = String(ref).trim();
20
+ if (!r) return null;
21
+ if (isProjectUuid(r)) {
22
+ return db.from('projects').select('*').eq('uuid', r).single();
23
+ }
24
+ if (isLegacyNumericProjectId(r)) {
25
+ return db.from('projects').select('*').eq('id', r).single();
26
+ }
27
+ return db.from('projects').select('*').eq('domain', r).single();
28
+ }
29
+
30
+ /**
31
+ * Load project from `.micropage/project.json` fields (prefers uuid when set).
32
+ */
33
+ async function fetchProjectFromConfig(db, config) {
34
+ if (!config) return null;
35
+ if (config.projectUuid) {
36
+ const row = await db
37
+ .from('projects')
38
+ .select('*')
39
+ .eq('uuid', String(config.projectUuid).trim())
40
+ .single();
41
+ if (row) return row;
42
+ }
43
+ if (config.projectId != null) {
44
+ return db.from('projects').select('*').eq('id', config.projectId).single();
45
+ }
46
+ return null;
47
+ }
48
+
49
+ module.exports = {
50
+ isProjectUuid,
51
+ isLegacyNumericProjectId,
52
+ fetchProjectByUserRef,
53
+ fetchProjectFromConfig,
54
+ };