@micropage-sh/cli 2.6.1 → 2.6.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # micropage CLI
2
2
 
3
- Command-line interface for [micropage.sh](https://app.micropage.sh) — create, edit, publish, and manage microsites from your terminal.
3
+ Command-line interface for [micropage.sh](https://micropage.sh): keep your `.page` files in git and create, edit, publish and manage your sites from the terminal.
4
4
 
5
5
  ## Requirements
6
6
 
@@ -32,6 +32,8 @@ micropage whoami # show the currently logged-in user and subscription
32
32
  micropage logout # clear the stored session
33
33
  ```
34
34
 
35
+ The CLI requires a paid plan (Pro or Pro+). Some features need a specific tier, such as deploy tokens (Pro+). The server enforces these limits. When a command needs a higher plan, the CLI prints the server's reason and an upgrade link.
36
+
35
37
  ## Project workflow
36
38
 
37
39
  ```bash
@@ -46,7 +48,7 @@ nano landing.page
46
48
  # Push local content as a draft build
47
49
  micropage push
48
50
 
49
- # Publish the current draft (deploys to Cloudflare Pages)
51
+ # Publish the current draft (deploys it live)
50
52
  micropage publish
51
53
 
52
54
  # Pull the latest remote build content to landing.page
@@ -113,7 +115,7 @@ See [`micropage posts`](#posts) below for the full command set.
113
115
  |---|---|
114
116
  | `micropage projects list [--json]` | List your projects |
115
117
  | `micropage projects show [id] [--json]` | Show project + latest build details |
116
- | `micropage projects create <name> [-d domain]` | Create a project and init local folder. Omit `-d` to let the API assign `{slugified-name}-{6 hex}` (unique Pages/DNS label, max 58 chars). Use `-d` only to override the slug. |
118
+ | `micropage projects create <name> [-d domain]` | Create a project and init local folder. Omit `-d` to let the API assign `{slugified-name}-{6 hex}` (unique `<slug>.micropage.sh` label, max 58 chars). Use `-d` only to override the slug. |
117
119
  | `micropage projects fetch <uuid\|domain>` | Fetch an existing project and init local folder (includes storage → `./assets` sync) |
118
120
  | `micropage projects pull` | Pull latest build raw content → `landing.page`, then sync `./assets` with project storage (1:1) |
119
121
  | `micropage projects delete [-y]` | Delete project (remote + local `.micropage/`) |
@@ -130,7 +132,7 @@ When you create a new project, the CLI also scaffolds:
130
132
  | Command | Description |
131
133
  |---|---|
132
134
  | `micropage push` | Merge local `.page` files and save as a draft build |
133
- | `micropage publish` | Push then deploy to Cloudflare Pages |
135
+ | `micropage publish` | Push then deploy the site live |
134
136
  | `micropage builds list [--json]` | List builds for the current project |
135
137
  | `micropage builds redeploy [version]` | Re-publish an older build as a new deploy |
136
138
  | `micropage builds download [version] [-o file]` | Download a deployed build archive as a `.zip` file |
@@ -225,7 +227,7 @@ If the project folder contains a root-level `llms.txt`, its contents are publish
225
227
  Each project folder has a `.micropage/project.json` file that stores:
226
228
  - `projectId` — the Supabase project ID
227
229
  - `buildId` — the ID of the last draft/deployed build (auto-updated by push/pull)
228
- - `domain` — the Cloudflare Pages domain
230
+ - `domain` — the project slug; the site lives at `<domain>.micropage.sh`
229
231
  - `name` — the project name
230
232
 
231
233
  Add `.micropage/` to your `.gitignore` if you share the content folder.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@micropage-sh/cli",
3
- "version": "2.6.1",
4
- "description": "CLI for micropage.sh - create, sync, and publish microsites",
3
+ "version": "2.6.3",
4
+ "description": "Micropage CLI: keep your .page files in git and publish static sites from the terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "micropage": "bin/micropage.js"
@@ -607,6 +607,7 @@ async function deploy(projectUuid, deployToken, options = {}) {
607
607
  }
608
608
  }
609
609
  } catch (err) {
610
+ handleAuthError(err);
610
611
  console.error('Deploy failed:', err.message);
611
612
  process.exit(1);
612
613
  }
package/src/index.js CHANGED
@@ -19,7 +19,7 @@ const program = new Command();
19
19
 
20
20
  program
21
21
  .name('micropage')
22
- .description('CLI for micropage.sh – create, sync, and publish microsites')
22
+ .description('Micropage CLI: keep your .page files in git and publish static sites from the terminal')
23
23
  .version(version);
24
24
 
25
25
  // ---------------------------------------------------------------------------
@@ -65,7 +65,7 @@ projectCmd
65
65
  .description('Create a new project and init local folder')
66
66
  .option(
67
67
  '-d, --domain <domain>',
68
- 'Override Cloudflare Pages project name / slug (default: server generates name-slug + 6 hex, max 58 chars)',
68
+ 'Choose the project slug, which sets its address <domain>.micropage.sh (default: server generates name-slug + 6 hex, max 58 chars)',
69
69
  )
70
70
  .action((name, opts) => projects.create(name, opts));
71
71
 
@@ -87,7 +87,7 @@ projectCmd
87
87
 
88
88
  projectCmd
89
89
  .command('publish')
90
- .description('Push local content and publish (deploy to Cloudflare Pages) — alias for `micropage publish`')
90
+ .description('Push local content and publish it live — alias for `micropage publish`')
91
91
  .option('-w, --watch', 'Stream build/deploy events after triggering publish')
92
92
  .action((opts) => builds.publish(opts));
93
93
 
@@ -152,7 +152,7 @@ program
152
152
 
153
153
  program
154
154
  .command('publish')
155
- .description('Push local content and publish (deploy to Cloudflare Pages)')
155
+ .description('Push local content and publish it live')
156
156
  .option('-w, --watch', 'Stream build/deploy events after triggering publish')
157
157
  .action((opts) => builds.publish(opts));
158
158
 
package/src/plan.js CHANGED
@@ -2,13 +2,37 @@
2
2
 
3
3
  const { SUPABASE_URL, SUPABASE_ANON_KEY } = require('./config');
4
4
 
5
+ const PRICING_URL = 'https://micropage.sh/pricing';
6
+
5
7
  const UPGRADE_MESSAGE = [
6
8
  'The micropage CLI is available on paid plans only.',
7
9
  'Your account is currently on the free plan.',
8
10
  '',
9
- 'Upgrade at: https://micropage.sh/pricing',
11
+ `Upgrade at: ${PRICING_URL}`,
10
12
  ].join('\n');
11
13
 
14
+ const TIER_LABELS = { pro: 'Pro', pro_plus: 'Pro+' };
15
+
16
+ // Message for a server refusal tagged PLAN_REQUIRED (see attachPlanRequired in
17
+ // supabase.js). Leads with the server's own explanation, then the upgrade hint.
18
+ function formatPlanRequiredMessage(err) {
19
+ const label = TIER_LABELS[err?.requiredTier];
20
+ const lines = [];
21
+ // Server messages written for the editor may end in a relative "Upgrade at /pricing.";
22
+ // drop it so the absolute link below is the only one.
23
+ const reason = (err?.message || '').replace(/\s*Upgrade at \S+$/i, '').trim();
24
+ if (reason) lines.push(reason, '');
25
+ lines.push(`This requires ${label ? `the ${label} plan` : 'a paid plan'}.`);
26
+ lines.push('');
27
+ lines.push(`Upgrade at: ${err?.upgradeUrl || PRICING_URL}`);
28
+ return lines.join('\n');
29
+ }
30
+
31
+ // The login gate below (and in whoami) is a product gate, not a security
32
+ // boundary: anyone can call the Supabase API directly with an editor session.
33
+ // The server enforces the per-feature gates (project limits via RLS, storage
34
+ // quota, custom domains, archives, posts, deploy-token creation and exchange)
35
+ // and answers with `code: "plan_required"` when a tier is missing.
12
36
  function isPaidTier(tier) {
13
37
  return tier === 'pro' || tier === 'pro_plus';
14
38
  }
@@ -37,4 +61,4 @@ async function getPlanTierWithToken(userId, accessToken) {
37
61
  }
38
62
  }
39
63
 
40
- module.exports = { UPGRADE_MESSAGE, isPaidTier, getPlanTierWithToken };
64
+ module.exports = { UPGRADE_MESSAGE, isPaidTier, getPlanTierWithToken, formatPlanRequiredMessage };
package/src/supabase.js CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  const { getSession, setSession, clearSession } = require('./auth');
15
15
  const { SUPABASE_URL, SUPABASE_ANON_KEY } = require('./config');
16
+ const { formatPlanRequiredMessage } = require('./plan');
16
17
 
17
18
  // ---------------------------------------------------------------------------
18
19
  // JWT helpers
@@ -97,6 +98,18 @@ async function getValidAccessToken() {
97
98
  // Base request
98
99
  // ---------------------------------------------------------------------------
99
100
 
101
+ // Server tier refusals carry `code: "plan_required"` in the JSON body, reusing
102
+ // 402/403. Tag the error so callers can tell "upgrade needed" apart from
103
+ // "not yours" / "invalid token", which share those statuses.
104
+ function attachPlanRequired(err, data) {
105
+ if (data && typeof data === 'object' && data.code === 'plan_required') {
106
+ err.code = 'PLAN_REQUIRED';
107
+ err.requiredTier = data.required_tier || null;
108
+ err.upgradeUrl = data.upgrade_url || null;
109
+ }
110
+ return err;
111
+ }
112
+
100
113
  async function requestWithToken(method, url, body, accessToken, extraHeaders = {}) {
101
114
  const headers = {
102
115
  'apikey': SUPABASE_ANON_KEY,
@@ -123,6 +136,7 @@ async function requestWithToken(method, url, body, accessToken, extraHeaders = {
123
136
  const err = new Error(msg);
124
137
  err.status = res.status;
125
138
  err.data = data;
139
+ attachPlanRequired(err, data);
126
140
  throw err;
127
141
  }
128
142
 
@@ -172,6 +186,7 @@ async function request(method, url, body, extraHeaders = {}) {
172
186
  err.status = res.status;
173
187
  err.data = data;
174
188
  if (res.status === 401) err.code = 'SESSION_EXPIRED';
189
+ attachPlanRequired(err, data);
175
190
  throw err;
176
191
  }
177
192
 
@@ -288,6 +303,10 @@ function handleAuthError(err) {
288
303
  console.error(err.message);
289
304
  process.exit(1);
290
305
  }
306
+ if (err.code === 'PLAN_REQUIRED') {
307
+ console.error(formatPlanRequiredMessage(err));
308
+ process.exit(1);
309
+ }
291
310
  }
292
311
 
293
312
  // ---------------------------------------------------------------------------
@@ -670,6 +689,7 @@ async function exchangeDeployTokenForAccessToken(deployTokenPlaintext, projectUu
670
689
  );
671
690
  err.status = res.status;
672
691
  err.data = data;
692
+ attachPlanRequired(err, data);
673
693
  throw err;
674
694
  }
675
695
  if (!data?.access_token) {
@@ -709,6 +729,7 @@ async function invokePublishBuild(accessToken, projectId, buildId) {
709
729
  );
710
730
  err.status = res.status;
711
731
  err.data = data;
732
+ attachPlanRequired(err, data);
712
733
  throw err;
713
734
  }
714
735
  return data;
@@ -719,6 +740,7 @@ module.exports = {
719
740
  fn,
720
741
  getUserInfo,
721
742
  handleAuthError,
743
+ attachPlanRequired,
722
744
  getValidAccessToken,
723
745
  getMaxDeployEventIdForBuild,
724
746
  getMaxDeployEventIdForProject,