@micropage-sh/cli 2.0.0 → 2.0.7
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/package.json +1 -1
- package/src/commands/builds.js +18 -0
- package/src/commands/links.js +4 -10
- package/src/commands/login.js +38 -7
- package/src/commands/logout.js +10 -2
- package/src/commands/projects.js +57 -16
- package/src/commands/whoami.js +24 -17
- package/src/config.js +27 -1
- package/src/index.js +2 -1
- package/src/parser.js +3 -7
- package/src/plan.js +40 -0
- package/src/supabase.js +18 -7
- package/templates/assets/favicons/favicon-a.svg +7 -0
- package/templates/assets/favicons/favicon-b.svg +12 -0
- package/templates/assets/favicons/favicon-c.svg +8 -0
- package/templates/assets/favicons/favicon-d.svg +6 -0
- package/templates/assets/logos/logo-a.svg +13 -0
- package/templates/assets/logos/logo-b.svg +16 -0
- package/templates/assets/logos/logo-c.svg +9 -0
- package/templates/assets/logos/logo-d.svg +7 -0
- package/templates/examples/agency-portfolio.page +130 -0
- package/templates/examples/blog-publication.page +158 -0
- package/templates/examples/devtool-docs.page +181 -0
- package/templates/examples/event-conference.page +171 -0
- package/templates/examples/restaurant-landing.page +124 -0
package/package.json
CHANGED
package/src/commands/builds.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
getValidAccessToken,
|
|
12
12
|
getMaxDeployEventIdForBuild,
|
|
13
13
|
streamDeployEventsUntilDone,
|
|
14
|
+
uploadAssetsWithToken,
|
|
14
15
|
} = require('../supabase');
|
|
15
16
|
const { getProjectConfig, setProjectConfig } = require('../auth');
|
|
16
17
|
const { readPageFilesFromDir } = require('../parser');
|
|
@@ -139,6 +140,23 @@ async function push(options = {}) {
|
|
|
139
140
|
process.exit(1);
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
// Upload new files in assets/ to project Files before parsing,
|
|
144
|
+
// so `img: <- name` lookups can resolve against just-uploaded files.
|
|
145
|
+
try {
|
|
146
|
+
const accessToken = await getValidAccessToken();
|
|
147
|
+
const uploaded = await uploadAssetsWithToken(
|
|
148
|
+
accessToken,
|
|
149
|
+
config.projectId,
|
|
150
|
+
cwd,
|
|
151
|
+
(filename) => console.log(`Uploaded asset: ${filename}`),
|
|
152
|
+
);
|
|
153
|
+
if (uploaded > 0) console.log(`Uploaded ${uploaded} asset(s).`);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
handleAuthError(err);
|
|
156
|
+
console.error('Asset upload failed:', err.message);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
142
160
|
// Parse content via parser API
|
|
143
161
|
console.log('Parsing content…');
|
|
144
162
|
let jsonContent;
|
package/src/commands/links.js
CHANGED
|
@@ -3,13 +3,7 @@
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const { db, handleAuthError } = require('../supabase');
|
|
5
5
|
const { getProjectConfig } = require('../auth');
|
|
6
|
-
const { APP_URL } = require('../config');
|
|
7
|
-
|
|
8
|
-
function projectUrl(domain) {
|
|
9
|
-
if (!domain) return null;
|
|
10
|
-
if (domain.startsWith('http')) return domain;
|
|
11
|
-
return `https://${domain}.pages.dev`;
|
|
12
|
-
}
|
|
6
|
+
const { APP_URL, projectUrl } = require('../config');
|
|
13
7
|
|
|
14
8
|
function copyToClipboard(text) {
|
|
15
9
|
const cmds = [
|
|
@@ -41,7 +35,7 @@ async function getProject(cwd) {
|
|
|
41
35
|
try {
|
|
42
36
|
const project = await db
|
|
43
37
|
.from('projects')
|
|
44
|
-
.select('id,name,domain')
|
|
38
|
+
.select('id,name,domain,custom_domain')
|
|
45
39
|
.eq('id', config.projectId)
|
|
46
40
|
.single();
|
|
47
41
|
return project || config;
|
|
@@ -56,7 +50,7 @@ async function getProject(cwd) {
|
|
|
56
50
|
async function preview() {
|
|
57
51
|
const cwd = process.cwd();
|
|
58
52
|
const project = await getProject(cwd);
|
|
59
|
-
const url = projectUrl(project.domain);
|
|
53
|
+
const url = projectUrl(project.domain, project.custom_domain);
|
|
60
54
|
|
|
61
55
|
if (!url) {
|
|
62
56
|
console.error('No domain set for this project. Publish first.');
|
|
@@ -76,7 +70,7 @@ async function preview() {
|
|
|
76
70
|
async function copyLink() {
|
|
77
71
|
const cwd = process.cwd();
|
|
78
72
|
const project = await getProject(cwd);
|
|
79
|
-
const url = projectUrl(project.domain);
|
|
73
|
+
const url = projectUrl(project.domain, project.custom_domain);
|
|
80
74
|
|
|
81
75
|
if (!url) {
|
|
82
76
|
console.error('No domain set for this project. Publish first.');
|
package/src/commands/login.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const http = require('http');
|
|
4
4
|
const { randomBytes } = require('crypto');
|
|
5
|
-
const { setSession, getSession } = require('../auth');
|
|
5
|
+
const { setSession, getSession, clearSession } = require('../auth');
|
|
6
6
|
const { getUserInfo, getValidAccessToken } = require('../supabase');
|
|
7
7
|
const { APP_URL, SUPABASE_URL, SUPABASE_ANON_KEY } = require('../config');
|
|
8
|
+
const { UPGRADE_MESSAGE, isPaidTier, getPlanTierWithToken } = require('../plan');
|
|
8
9
|
|
|
9
10
|
// Find an available ephemeral port by briefly binding to port 0.
|
|
10
11
|
function getFreePort() {
|
|
@@ -164,11 +165,24 @@ async function run(options = {}) {
|
|
|
164
165
|
const existing = getSession();
|
|
165
166
|
if (existing) {
|
|
166
167
|
try {
|
|
167
|
-
await getValidAccessToken();
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
const accessToken = await getValidAccessToken();
|
|
169
|
+
const planTier = await getPlanTierWithToken(existing.user?.id, accessToken);
|
|
170
|
+
if (!isPaidTier(planTier)) {
|
|
171
|
+
clearSession();
|
|
172
|
+
console.error(UPGRADE_MESSAGE);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
const displayName = existing.user?.email
|
|
176
|
+
|| existing.user?.user_metadata?.full_name
|
|
177
|
+
|| existing.user?.user_metadata?.user_name
|
|
178
|
+
|| existing.user?.user_metadata?.name
|
|
179
|
+
|| existing.user?.id;
|
|
180
|
+
if (displayName) {
|
|
181
|
+
console.log(`Already logged in as ${displayName}.`);
|
|
182
|
+
console.log('Use --force to re-authenticate.');
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
// No identifiable display name on the cached user — fall through to a fresh login.
|
|
172
186
|
} catch {
|
|
173
187
|
// Token expired or refresh failed — fall through to re-login
|
|
174
188
|
}
|
|
@@ -240,10 +254,27 @@ async function run(options = {}) {
|
|
|
240
254
|
server.close();
|
|
241
255
|
|
|
242
256
|
const user = await getUserInfo(access_token);
|
|
257
|
+
if (!user?.id) {
|
|
258
|
+
console.error('Login failed: could not load user profile.');
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const planTier = await getPlanTierWithToken(user.id, access_token);
|
|
263
|
+
if (!isPaidTier(planTier)) {
|
|
264
|
+
// Free-tier (or unconfirmed) users get no session at all.
|
|
265
|
+
console.error(UPGRADE_MESSAGE);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
|
|
243
269
|
setSession({ access_token, refresh_token, user });
|
|
244
270
|
|
|
245
|
-
const displayName = user
|
|
271
|
+
const displayName = user.email
|
|
272
|
+
|| user.user_metadata?.full_name
|
|
273
|
+
|| user.user_metadata?.user_name
|
|
274
|
+
|| user.user_metadata?.name
|
|
275
|
+
|| user.id;
|
|
246
276
|
console.log(`Logged in as ${displayName}`);
|
|
277
|
+
process.exit(0);
|
|
247
278
|
} catch (err) {
|
|
248
279
|
clearTimeout(timeout);
|
|
249
280
|
server.close();
|
package/src/commands/logout.js
CHANGED
|
@@ -9,8 +9,16 @@ function run() {
|
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
clearSession();
|
|
12
|
-
const
|
|
13
|
-
|
|
12
|
+
const displayName = session.user?.email
|
|
13
|
+
|| session.user?.user_metadata?.full_name
|
|
14
|
+
|| session.user?.user_metadata?.user_name
|
|
15
|
+
|| session.user?.user_metadata?.name
|
|
16
|
+
|| session.user?.id;
|
|
17
|
+
if (displayName) {
|
|
18
|
+
console.log(`Logged out (was: ${displayName}).`);
|
|
19
|
+
} else {
|
|
20
|
+
console.log('Logged out.');
|
|
21
|
+
}
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
module.exports = { run };
|
package/src/commands/projects.js
CHANGED
|
@@ -23,11 +23,9 @@ const { syncAssets } = require('./files');
|
|
|
23
23
|
// Helpers
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return `https://${host}`;
|
|
30
|
-
}
|
|
26
|
+
const { projectUrl: buildProjectUrl } = require('../config');
|
|
27
|
+
const projectUrl = (slug, customDomain) =>
|
|
28
|
+
buildProjectUrl(slug, customDomain) || '-';
|
|
31
29
|
|
|
32
30
|
function copyIfExists(src, dest) {
|
|
33
31
|
try {
|
|
@@ -49,26 +47,69 @@ function copyExamplesAndAssetsIntoProject(dir) {
|
|
|
49
47
|
fs.mkdirSync(examplesDest, { recursive: true });
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
//
|
|
50
|
+
// Collect full-page templates (exclude components-* demo files) for the starter pick.
|
|
51
|
+
// All examples are still copied into examples/ for the user to browse.
|
|
52
|
+
const allExampleFiles = [];
|
|
53
|
+
const starterCandidates = [];
|
|
53
54
|
try {
|
|
54
55
|
const files = fs.readdirSync(examplesSrc);
|
|
55
56
|
for (const file of files) {
|
|
56
57
|
if (!file.endsWith('.page')) continue;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
allExampleFiles.push(file);
|
|
59
|
+
if (!file.startsWith('components-')) {
|
|
60
|
+
starterCandidates.push(file);
|
|
61
|
+
}
|
|
60
62
|
}
|
|
61
63
|
} catch {
|
|
62
64
|
// ignore if templates/examples missing in this build
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
for (const file of allExampleFiles) {
|
|
68
|
+
const src = path.join(examplesSrc, file);
|
|
69
|
+
const dest = path.join(examplesDest, file);
|
|
70
|
+
copyIfExists(src, dest);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Write a randomized starter landing.page at the project root.
|
|
74
|
+
if (starterCandidates.length > 0) {
|
|
75
|
+
const pick = starterCandidates[Math.floor(Math.random() * starterCandidates.length)];
|
|
76
|
+
copyIfExists(path.join(examplesSrc, pick), path.join(dir, 'landing.page'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Logo / favicon in assets/ — pick a random variant pair from logos/ and favicons/.
|
|
80
|
+
// Falls back to the root logo.svg / favicon.svg if the variant directories are absent or empty.
|
|
66
81
|
const destAssets = path.join(dir, 'assets');
|
|
67
82
|
if (!fs.existsSync(destAssets)) {
|
|
68
83
|
fs.mkdirSync(destAssets, { recursive: true });
|
|
69
84
|
}
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
|
|
86
|
+
const logosDir = path.join(assetsSrc, 'logos');
|
|
87
|
+
const faviconsDir = path.join(assetsSrc, 'favicons');
|
|
88
|
+
let usedVariant = false;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
if (fs.existsSync(logosDir)) {
|
|
92
|
+
const logoFiles = fs.readdirSync(logosDir).filter((f) => /^logo-[^.]+\.svg$/.test(f));
|
|
93
|
+
if (logoFiles.length > 0) {
|
|
94
|
+
const chosen = logoFiles[Math.floor(Math.random() * logoFiles.length)];
|
|
95
|
+
// logo-x.svg -> favicon-x.svg
|
|
96
|
+
const variantId = chosen.replace(/^logo-/, '').replace(/\.svg$/, '');
|
|
97
|
+
const matchingFavicon = path.join(faviconsDir, `favicon-${variantId}.svg`);
|
|
98
|
+
if (fs.existsSync(matchingFavicon)) {
|
|
99
|
+
copyIfExists(path.join(logosDir, chosen), path.join(destAssets, 'logo.svg'));
|
|
100
|
+
copyIfExists(matchingFavicon, path.join(destAssets, 'favicon.svg'));
|
|
101
|
+
usedVariant = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// fall through to default below
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!usedVariant) {
|
|
110
|
+
copyIfExists(path.join(assetsSrc, 'logo.svg'), path.join(destAssets, 'logo.svg'));
|
|
111
|
+
copyIfExists(path.join(assetsSrc, 'favicon.svg'), path.join(destAssets, 'favicon.svg'));
|
|
112
|
+
}
|
|
72
113
|
}
|
|
73
114
|
|
|
74
115
|
async function getLatestBuild(projectId) {
|
|
@@ -145,7 +186,7 @@ async function list(options = {}) {
|
|
|
145
186
|
|
|
146
187
|
const rows = projects.map((p) => {
|
|
147
188
|
const build = latestBuild[p.id];
|
|
148
|
-
const url = projectUrl(p.
|
|
189
|
+
const url = projectUrl(p.domain, p.custom_domain);
|
|
149
190
|
return [
|
|
150
191
|
p.uuid || '-',
|
|
151
192
|
p.name || '-',
|
|
@@ -195,7 +236,7 @@ async function show(uuidOrDomain, options = {}) {
|
|
|
195
236
|
return;
|
|
196
237
|
}
|
|
197
238
|
|
|
198
|
-
const url = projectUrl(project.
|
|
239
|
+
const url = projectUrl(project.domain, project.custom_domain);
|
|
199
240
|
|
|
200
241
|
console.log('UUID: ', project.uuid || '-');
|
|
201
242
|
console.log('Name: ', project.name || '-');
|
|
@@ -261,9 +302,9 @@ async function create(name, options = {}) {
|
|
|
261
302
|
|
|
262
303
|
console.log('Created project:', project.name || name);
|
|
263
304
|
if (project.uuid) console.log('UUID:', project.uuid);
|
|
264
|
-
console.log('URL:', projectUrl(project.domain));
|
|
305
|
+
console.log('URL:', projectUrl(project.domain, project.custom_domain));
|
|
265
306
|
console.log('Folder:', dir);
|
|
266
|
-
console.log(`\nNext: cd ${name} &&
|
|
307
|
+
console.log(`\nNext: cd ${name} && micropage push`);
|
|
267
308
|
}
|
|
268
309
|
|
|
269
310
|
// ---------------------------------------------------------------------------
|
package/src/commands/whoami.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { getSession } = require('../auth');
|
|
3
|
+
const { getSession, clearSession } = require('../auth');
|
|
4
4
|
const { db, getUserInfo, getValidAccessToken, handleAuthError } = require('../supabase');
|
|
5
|
+
const { UPGRADE_MESSAGE, isPaidTier } = require('../plan');
|
|
5
6
|
|
|
6
7
|
async function run() {
|
|
7
8
|
const session = getSession();
|
|
@@ -25,35 +26,41 @@ async function run() {
|
|
|
25
26
|
process.exit(1);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
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).
|
|
29
|
+
// Gate access on plan first — free-tier accounts get no session and no detail dump.
|
|
41
30
|
let planTier = 'free';
|
|
42
|
-
let subscription = null;
|
|
43
31
|
try {
|
|
44
32
|
const customer = await db
|
|
45
33
|
.from('customers')
|
|
46
34
|
.select('plan_tier')
|
|
47
35
|
.eq('user_id', user.id)
|
|
48
36
|
.single();
|
|
49
|
-
if (customer?.plan_tier) {
|
|
37
|
+
if (customer?.plan_tier === 'pro' || customer?.plan_tier === 'pro_plus') {
|
|
50
38
|
planTier = customer.plan_tier;
|
|
51
39
|
}
|
|
52
40
|
} catch (err) {
|
|
53
41
|
handleAuthError(err);
|
|
54
|
-
|
|
42
|
+
// If we cannot determine plan, default to free → deny CLI access below.
|
|
55
43
|
}
|
|
56
44
|
|
|
45
|
+
if (!isPaidTier(planTier)) {
|
|
46
|
+
clearSession();
|
|
47
|
+
console.error(UPGRADE_MESSAGE);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log('User ID: ', user.id);
|
|
52
|
+
console.log('Email: ', user.email || '-');
|
|
53
|
+
const name =
|
|
54
|
+
user.user_metadata?.full_name ||
|
|
55
|
+
user.user_metadata?.user_name ||
|
|
56
|
+
user.user_metadata?.name ||
|
|
57
|
+
'-';
|
|
58
|
+
console.log('Name: ', name);
|
|
59
|
+
if (user.app_metadata?.provider) {
|
|
60
|
+
console.log('Auth via: ', user.app_metadata.provider);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let subscription = null;
|
|
57
64
|
try {
|
|
58
65
|
const subs = await db
|
|
59
66
|
.from('subscriptions')
|
package/src/config.js
CHANGED
|
@@ -19,4 +19,30 @@ const BUILD_COMPILER_URL =
|
|
|
19
19
|
process.env.MICROPAGE_PARSER_URL ||
|
|
20
20
|
'https://build-compiler.micropage.sh';
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const BASE_DOMAIN = process.env.MICROPAGE_BASE_DOMAIN || 'micropage.sh';
|
|
23
|
+
|
|
24
|
+
// Live URL for a project. Prefers the customer's custom domain (a full FQDN
|
|
25
|
+
// like `www.example.com`) when set; falls back to `https://<slug>.<BASE_DOMAIN>`.
|
|
26
|
+
// Returns null when there's no usable identifier.
|
|
27
|
+
function projectUrl(slug, customDomain) {
|
|
28
|
+
const cd = customDomain && String(customDomain).trim();
|
|
29
|
+
if (cd) {
|
|
30
|
+
return cd.startsWith('http://') || cd.startsWith('https://')
|
|
31
|
+
? cd
|
|
32
|
+
: `https://${cd}`;
|
|
33
|
+
}
|
|
34
|
+
if (!slug) return null;
|
|
35
|
+
const s = String(slug).trim();
|
|
36
|
+
if (!s) return null;
|
|
37
|
+
if (s.startsWith('http://') || s.startsWith('https://')) return s;
|
|
38
|
+
return `https://${s}.${BASE_DOMAIN}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
SUPABASE_URL,
|
|
43
|
+
SUPABASE_ANON_KEY,
|
|
44
|
+
APP_URL,
|
|
45
|
+
BUILD_COMPILER_URL,
|
|
46
|
+
BASE_DOMAIN,
|
|
47
|
+
projectUrl,
|
|
48
|
+
};
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const { Command } = require('commander');
|
|
5
|
+
const { version } = require('../package.json');
|
|
5
6
|
|
|
6
7
|
const login = require('./commands/login');
|
|
7
8
|
const logout = require('./commands/logout');
|
|
@@ -18,7 +19,7 @@ const program = new Command();
|
|
|
18
19
|
program
|
|
19
20
|
.name('micropage')
|
|
20
21
|
.description('CLI for micropage.sh – create, sync, and publish microsites')
|
|
21
|
-
.version(
|
|
22
|
+
.version(version);
|
|
22
23
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Authentication
|
package/src/parser.js
CHANGED
|
@@ -48,18 +48,15 @@ function writePageFile(dir, content) {
|
|
|
48
48
|
return dest;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const ASSET_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg'];
|
|
52
|
-
|
|
53
51
|
function listAssetsFromDir(dir) {
|
|
54
52
|
const assetsDir = path.join(dir, 'assets');
|
|
55
53
|
if (!fs.existsSync(assetsDir)) return [];
|
|
56
54
|
const entries = fs.readdirSync(assetsDir, { withFileTypes: true });
|
|
57
55
|
const files = [];
|
|
58
56
|
for (const e of entries) {
|
|
59
|
-
if (e.isFile())
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
57
|
+
if (!e.isFile()) continue;
|
|
58
|
+
if (e.name.startsWith('.')) continue;
|
|
59
|
+
files.push(e.name);
|
|
63
60
|
}
|
|
64
61
|
return files.sort();
|
|
65
62
|
}
|
|
@@ -68,5 +65,4 @@ module.exports = {
|
|
|
68
65
|
readPageFilesFromDir,
|
|
69
66
|
writePageFile,
|
|
70
67
|
listAssetsFromDir,
|
|
71
|
-
ASSET_EXTENSIONS,
|
|
72
68
|
};
|
package/src/plan.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { SUPABASE_URL, SUPABASE_ANON_KEY } = require('./config');
|
|
4
|
+
|
|
5
|
+
const UPGRADE_MESSAGE = [
|
|
6
|
+
'The micropage CLI is available on paid plans only.',
|
|
7
|
+
'Your account is currently on the free plan.',
|
|
8
|
+
'',
|
|
9
|
+
'Upgrade at: https://micropage.sh/pricing',
|
|
10
|
+
].join('\n');
|
|
11
|
+
|
|
12
|
+
function isPaidTier(tier) {
|
|
13
|
+
return tier === 'pro' || tier === 'pro_plus';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Look up plan_tier using a bearer token directly so this can be called inside
|
|
17
|
+
// `login` before a session is persisted. Defaults to 'free' on any failure —
|
|
18
|
+
// CLI access is denied when we cannot confirm a paid tier.
|
|
19
|
+
async function getPlanTierWithToken(userId, accessToken) {
|
|
20
|
+
try {
|
|
21
|
+
const url = `${SUPABASE_URL}/rest/v1/customers?select=plan_tier&user_id=eq.${encodeURIComponent(userId)}&limit=1`;
|
|
22
|
+
const res = await fetch(url, {
|
|
23
|
+
headers: {
|
|
24
|
+
apikey: SUPABASE_ANON_KEY,
|
|
25
|
+
Authorization: `Bearer ${accessToken}`,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) return 'free';
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
const row = Array.isArray(data) ? data[0] : null;
|
|
31
|
+
if (row?.plan_tier === 'pro' || row?.plan_tier === 'pro_plus') {
|
|
32
|
+
return row.plan_tier;
|
|
33
|
+
}
|
|
34
|
+
return 'free';
|
|
35
|
+
} catch {
|
|
36
|
+
return 'free';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { UPGRADE_MESSAGE, isPaidTier, getPlanTierWithToken };
|
package/src/supabase.js
CHANGED
|
@@ -257,13 +257,23 @@ const db = {
|
|
|
257
257
|
// ---------------------------------------------------------------------------
|
|
258
258
|
|
|
259
259
|
async function getUserInfo(accessToken) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
260
|
+
let res;
|
|
261
|
+
try {
|
|
262
|
+
res = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
|
|
263
|
+
headers: {
|
|
264
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
265
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error(`[micropage] getUserInfo network error: ${err.message}`);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
if (!res.ok) {
|
|
273
|
+
const body = await res.text().catch(() => '');
|
|
274
|
+
console.error(`[micropage] getUserInfo failed (HTTP ${res.status} from ${SUPABASE_URL}/auth/v1/user): ${body}`);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
267
277
|
return res.json();
|
|
268
278
|
}
|
|
269
279
|
|
|
@@ -634,4 +644,5 @@ module.exports = {
|
|
|
634
644
|
exchangeDeployTokenForAccessToken,
|
|
635
645
|
pushWithToken,
|
|
636
646
|
invokePublishBuild,
|
|
647
|
+
uploadAssetsWithToken,
|
|
637
648
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage favicon — variant A</title>
|
|
3
|
+
<desc>Minimal stacked bar favicon for starter projects</desc>
|
|
4
|
+
<rect x="4" y="7" width="24" height="4" rx="2" fill="#4f46e5"/>
|
|
5
|
+
<rect x="6" y="14" width="20" height="4" rx="2" fill="#0ea5e9"/>
|
|
6
|
+
<rect x="8" y="21" width="16" height="4" rx="2" fill="#22c55e"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage favicon — variant B</title>
|
|
3
|
+
<desc>Minimal sunrise arc favicon</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="g" x1="0%" y1="100%" x2="100%" y2="0%">
|
|
6
|
+
<stop offset="0%" stop-color="#dc2626"/>
|
|
7
|
+
<stop offset="100%" stop-color="#f59e0b"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
<path d="M 4 22 A 12 12 0 0 1 28 22" stroke="url(#g)" stroke-width="5" fill="none" stroke-linecap="round"/>
|
|
11
|
+
<circle cx="16" cy="22" r="3" fill="#dc2626"/>
|
|
12
|
+
</svg>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage favicon — variant C</title>
|
|
3
|
+
<desc>Minimal concentric circles favicon in forest green</desc>
|
|
4
|
+
<circle cx="16" cy="14" r="10" fill="#166534"/>
|
|
5
|
+
<circle cx="16" cy="14" r="5" fill="#14532d"/>
|
|
6
|
+
<circle cx="16" cy="14" r="2" fill="#bbf7d0"/>
|
|
7
|
+
<rect x="14" y="24" width="4" height="6" rx="2" fill="#713f12"/>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage favicon — variant D</title>
|
|
3
|
+
<desc>Minimal diamond favicon in charcoal with magenta center</desc>
|
|
4
|
+
<polygon points="16,4 28,16 16,28 4,16" fill="#1c1c1c"/>
|
|
5
|
+
<polygon points="16,11 21,16 16,21 11,16" fill="#e91e8c"/>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage starter logo — variant A</title>
|
|
3
|
+
<desc>Abstract stacked rectangles suggesting pages</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
6
|
+
<stop offset="0%" stop-color="#4f46e5"/>
|
|
7
|
+
<stop offset="100%" stop-color="#06b6d4"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
<rect x="10" y="14" width="44" height="12" rx="3" fill="url(#g)"/>
|
|
11
|
+
<rect x="14" y="28" width="36" height="10" rx="3" fill="#111827" opacity="0.9"/>
|
|
12
|
+
<rect x="18" y="40" width="28" height="8" rx="3" fill="#111827" opacity="0.7"/>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage starter logo — variant B</title>
|
|
3
|
+
<desc>Abstract sunrise arc suggesting warmth and momentum</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="g" x1="0%" y1="100%" x2="100%" y2="0%">
|
|
6
|
+
<stop offset="0%" stop-color="#dc2626"/>
|
|
7
|
+
<stop offset="50%" stop-color="#ea580c"/>
|
|
8
|
+
<stop offset="100%" stop-color="#f59e0b"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
</defs>
|
|
11
|
+
<path d="M 8 44 A 24 24 0 0 1 56 44" stroke="url(#g)" stroke-width="8" fill="none" stroke-linecap="round"/>
|
|
12
|
+
<circle cx="32" cy="44" r="5" fill="#dc2626"/>
|
|
13
|
+
<line x1="32" y1="14" x2="32" y2="22" stroke="#f59e0b" stroke-width="4" stroke-linecap="round"/>
|
|
14
|
+
<line x1="14" y1="26" x2="20" y2="30" stroke="#ea580c" stroke-width="3" stroke-linecap="round"/>
|
|
15
|
+
<line x1="50" y1="26" x2="44" y2="30" stroke="#ea580c" stroke-width="3" stroke-linecap="round"/>
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage starter logo — variant C</title>
|
|
3
|
+
<desc>Abstract leaf and root mark suggesting growth and earth</desc>
|
|
4
|
+
<circle cx="32" cy="20" r="14" fill="#166534"/>
|
|
5
|
+
<circle cx="32" cy="20" r="8" fill="#14532d"/>
|
|
6
|
+
<circle cx="32" cy="20" r="4" fill="#bbf7d0"/>
|
|
7
|
+
<rect x="30" y="34" width="4" height="14" rx="2" fill="#713f12"/>
|
|
8
|
+
<path d="M 20 48 Q 32 38 44 48" stroke="#713f12" stroke-width="3" fill="none" stroke-linecap="round"/>
|
|
9
|
+
</svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage starter logo — variant D</title>
|
|
3
|
+
<desc>Bold geometric diamond mark in charcoal with magenta accent</desc>
|
|
4
|
+
<polygon points="32,8 52,32 32,56 12,32" fill="#1c1c1c"/>
|
|
5
|
+
<polygon points="32,18 44,32 32,46 20,32" fill="#2e2e2e"/>
|
|
6
|
+
<polygon points="32,26 38,32 32,38 26,32" fill="#e91e8c"/>
|
|
7
|
+
</svg>
|