@micropage-sh/cli 2.5.0 → 2.6.2

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
@@ -19,7 +19,7 @@ brew install micropage
19
19
  ### npm
20
20
 
21
21
  ```bash
22
- npm install -g micropage
22
+ npm install -g @micropage-sh/cli
23
23
  ```
24
24
 
25
25
  ## Authentication
@@ -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
@@ -175,9 +177,9 @@ When you create a new project, the CLI also scaffolds:
175
177
 
176
178
  | Command | Description |
177
179
  |---|---|
178
- | `micropage submissions list [--json]` | List form submissions for the project |
179
- | `micropage submissions show <id> [--json]` | Show a single submission in detail |
180
- | `micropage submissions export [--format csv|json] [-o file]` | Export all form submissions for the current project |
180
+ | `micropage submissions list [--json] [--spam]` | List form submissions for the project. Spam is excluded; `--spam` lists only submissions flagged as spam, with the reason |
181
+ | `micropage submissions show <id> [--json]` | Show a single submission in detail, including a `Spam:` line with the reason when it was flagged |
182
+ | `micropage submissions export [--format csv|json] [-o file] [--spam]` | Export form submissions for the current project. Spam is excluded; `--spam` exports only flagged submissions (default file `submissions-spam.<format>`, CSV gets a trailing `spam_reason` column) |
181
183
 
182
184
  **Examples:**
183
185
 
@@ -199,6 +201,12 @@ When you create a new project, the CLI also scaffolds:
199
201
  micropage submissions export --format json --output submissions.json
200
202
  ```
201
203
 
204
+ - Export only submissions flagged as spam:
205
+
206
+ ```bash
207
+ micropage submissions export --spam
208
+ ```
209
+
202
210
  ## Local content model
203
211
 
204
212
  The CLI uses one or more `*.page` files in the working directory as the source of truth for project content.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micropage-sh/cli",
3
- "version": "2.5.0",
3
+ "version": "2.6.2",
4
4
  "description": "CLI for micropage.sh - create, sync, and publish microsites",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "README.md"
14
14
  ],
15
15
  "scripts": {
16
- "test": "node -e \"require('./src/index.js')\"; node --test test/",
16
+ "test": "node -e \"require('./src/index.js')\"; node --test test/*.test.js test/*_test.mjs",
17
17
  "build:bin": "pkg . --targets node18-linux-x64,node18-macos-x64,node18-macos-arm64 --output dist/micropage",
18
18
  "build:linux": "pkg . --target node18-linux-x64 --output dist/micropage-linux-amd64",
19
19
  "build:macos-amd64": "pkg . --target node18-macos-x64 --output dist/micropage-darwin-amd64",
@@ -52,13 +52,14 @@ async function list(options = {}) {
52
52
  return;
53
53
  }
54
54
 
55
- // Count submissions per form_id
55
+ // Count non-spam submissions per form_id
56
56
  let submissions;
57
57
  try {
58
58
  submissions = await db
59
59
  .from('form_submissions')
60
60
  .select('form_id')
61
61
  .eq('project_id', config.projectId)
62
+ .is('flagged_at', 'null')
62
63
  .get();
63
64
  } catch (err) {
64
65
  handleAuthError(err);
@@ -9,7 +9,7 @@ const {
9
9
  fn,
10
10
  handleAuthError,
11
11
  getValidAccessToken,
12
- getMaxDeployEventIdForBuild,
12
+ getMaxDeployEventIdForProject,
13
13
  streamDeployEventsUntilDone,
14
14
  } = require('../supabase');
15
15
  const { getProjectConfig } = require('../auth');
@@ -478,6 +478,18 @@ function localSlugsFromPostsDir(postsDir) {
478
478
  return slugs;
479
479
  }
480
480
 
481
+ /**
482
+ * The build the publish-post rebuilds redeploy. Servers that report
483
+ * rebuild_build_id rebuild the live build, never a page draft; older ones
484
+ * leave it out and rebuild the project's active build.
485
+ */
486
+ function rebuildTarget(responses, activeBuildId) {
487
+ const reported = responses.filter((r) => r && Object.prototype.hasOwnProperty.call(r, 'rebuild_build_id'));
488
+ if (reported.length === 0) return { buildId: activeBuildId || null, serverReported: false };
489
+ const ids = reported.map((r) => r.rebuild_build_id).filter((id) => id != null);
490
+ return { buildId: ids.length > 0 ? ids[ids.length - 1] : null, serverReported: true };
491
+ }
492
+
481
493
  async function publish(slugArg, options = {}) {
482
494
  const cwd = process.cwd();
483
495
  const config = requireProjectConfig(cwd);
@@ -498,10 +510,11 @@ async function publish(slugArg, options = {}) {
498
510
  'Publishing sends (or re-sends) email to the active subscriber list for any email-configured post.',
499
511
  );
500
512
 
501
- // Publishing a post makes the publisher auto-rebuild the site's active build so
502
- // the /content archive picks it up — no separate `micropage publish` needed.
503
- // Resolve that build up front so we can report accurately and (with --watch)
504
- // capture an event cursor before the rebuild is queued.
513
+ // Publishing a post makes the publisher auto-rebuild the live site so the
514
+ // /content archive picks it up — no separate `micropage publish` needed.
515
+ // publish-post reports which build it rebuilds; active_build_id is only the
516
+ // fallback for servers that don't. The --watch cursor is project-wide and
517
+ // taken before the rebuild is queued, since the build isn't known yet.
505
518
  let activeBuildId = null;
506
519
  try {
507
520
  const proj = await db
@@ -515,9 +528,9 @@ async function publish(slugArg, options = {}) {
515
528
  }
516
529
 
517
530
  let eventCursor = 0;
518
- if (options.watch && activeBuildId) {
531
+ if (options.watch) {
519
532
  try {
520
- eventCursor = await getMaxDeployEventIdForBuild(activeBuildId);
533
+ eventCursor = await getMaxDeployEventIdForProject(config.projectId);
521
534
  } catch {
522
535
  eventCursor = 0;
523
536
  }
@@ -525,10 +538,12 @@ async function publish(slugArg, options = {}) {
525
538
 
526
539
  let hadError = false;
527
540
  let publishedCount = 0;
541
+ const responses = [];
528
542
 
529
543
  for (const slug of targetSlugs) {
530
544
  try {
531
545
  const result = await fn.invoke('publish-post', { project_id: config.projectId, slug });
546
+ responses.push(result);
532
547
  const bits = [`published_at ${result.published_at}`];
533
548
  bits.push(result.emailed ? `emailed ${result.recipient_count} recipient(s)` : 'no email');
534
549
  console.log(`"${slug}": ${bits.join(', ')}`);
@@ -545,17 +560,22 @@ async function publish(slugArg, options = {}) {
545
560
  console.log(`Published ${publishedCount}/${targetSlugs.length} post(s).`);
546
561
 
547
562
  if (publishedCount > 0) {
548
- if (activeBuildId) {
563
+ const { buildId: rebuildBuildId, serverReported } = rebuildTarget(responses, activeBuildId);
564
+ if (rebuildBuildId) {
549
565
  console.log(
550
566
  'A site rebuild was queued automatically; the /content archive updates once it deploys (usually a minute or two).',
551
567
  );
568
+ } else if (serverReported) {
569
+ console.log(
570
+ "No site rebuild was queued: the project has no deployed build yet (run 'micropage publish'), or the post is email-only.",
571
+ );
552
572
  } else {
553
573
  console.log(
554
574
  "Note: this project hasn't been published yet, so the post won't appear until you run 'micropage publish'.",
555
575
  );
556
576
  }
557
577
 
558
- if (options.watch && activeBuildId) {
578
+ if (options.watch && rebuildBuildId) {
559
579
  console.log('');
560
580
  console.log('Build / deploy events:');
561
581
  try {
@@ -563,7 +583,7 @@ async function publish(slugArg, options = {}) {
563
583
  const { terminalEvent } = await streamDeployEventsUntilDone(
564
584
  accessToken,
565
585
  config.projectId,
566
- activeBuildId,
586
+ rebuildBuildId,
567
587
  { afterId: eventCursor },
568
588
  );
569
589
  if (terminalEvent?.event_type === 'build.failed') {
@@ -619,6 +639,7 @@ module.exports = {
619
639
  rm,
620
640
  publish,
621
641
  unpublish,
642
+ rebuildTarget,
622
643
  slugify,
623
644
  defaultSlugFromFilename,
624
645
  frontMatterFromPost,
@@ -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
  }
@@ -13,6 +13,11 @@ function formatCliFieldValue(value) {
13
13
  return String(value);
14
14
  }
15
15
 
16
+ /** Spam is `flagged_at IS NOT NULL`; the inbox is everything else. */
17
+ function applySpamFilter(query, spam) {
18
+ return spam ? query.not('flagged_at', 'is', 'null') : query.is('flagged_at', 'null');
19
+ }
20
+
16
21
  async function list(options = {}) {
17
22
  const cwd = process.cwd();
18
23
  const config = getProjectConfig(cwd);
@@ -21,12 +26,17 @@ async function list(options = {}) {
21
26
  process.exit(1);
22
27
  }
23
28
 
29
+ const spam = Boolean(options.spam);
30
+ const columns = spam
31
+ ? 'id,form_id,form_name,page_url,created_at,form_index,spam_reason,flagged_at,flagged_by'
32
+ : 'id,form_id,form_name,page_url,created_at,form_index';
33
+
24
34
  let submissions;
25
35
  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)
36
+ submissions = await applySpamFilter(
37
+ db.from('form_submissions').select(columns).eq('project_id', config.projectId),
38
+ spam,
39
+ )
30
40
  .order('created_at', 'desc')
31
41
  .get();
32
42
  } catch (err) {
@@ -36,7 +46,7 @@ async function list(options = {}) {
36
46
  }
37
47
 
38
48
  if (!Array.isArray(submissions) || submissions.length === 0) {
39
- console.log('No form submissions.');
49
+ console.log(spam ? 'No spam submissions.' : 'No form submissions.');
40
50
  return;
41
51
  }
42
52
 
@@ -45,14 +55,20 @@ async function list(options = {}) {
45
55
  return;
46
56
  }
47
57
 
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']);
58
+ const rows = submissions.map((s) => {
59
+ const row = [
60
+ s.id.slice(0, 8) + '…',
61
+ s.form_id ? s.form_id.slice(0, 8) + '…' : '-',
62
+ s.form_name || '-',
63
+ s.page_url || '-',
64
+ formatDate(s.created_at),
65
+ ];
66
+ if (spam) row.push(s.spam_reason || '-');
67
+ return row;
68
+ });
69
+ const headers = ['ID (short)', 'Form ID', 'Form', 'Page URL', 'Submitted'];
70
+ if (spam) headers.push('Spam reason');
71
+ formatTable(rows, headers);
56
72
  }
57
73
 
58
74
  async function show(id, options = {}) {
@@ -85,6 +101,9 @@ async function show(id, options = {}) {
85
101
  }
86
102
 
87
103
  console.log('ID: ', submission.id);
104
+ if (submission.flagged_at) {
105
+ console.log('Spam: ', submission.spam_reason || 'yes');
106
+ }
88
107
  console.log('Form ID: ', submission.form_id || '-');
89
108
  console.log('Form: ', submission.form_name || '-');
90
109
  console.log('Page URL: ', submission.page_url || '-');
@@ -112,12 +131,14 @@ async function exportSubmissions(options = {}) {
112
131
  process.exit(1);
113
132
  }
114
133
 
134
+ const spam = Boolean(options.spam);
135
+
115
136
  let submissions;
116
137
  try {
117
- submissions = await db
118
- .from('form_submissions')
119
- .select('*')
120
- .eq('project_id', config.projectId)
138
+ submissions = await applySpamFilter(
139
+ db.from('form_submissions').select('*').eq('project_id', config.projectId),
140
+ spam,
141
+ )
121
142
  .order('created_at', 'desc')
122
143
  .get();
123
144
  } catch (err) {
@@ -127,7 +148,7 @@ async function exportSubmissions(options = {}) {
127
148
  }
128
149
 
129
150
  if (!Array.isArray(submissions) || submissions.length === 0) {
130
- console.log('No form submissions to export.');
151
+ console.log(spam ? 'No spam submissions to export.' : 'No form submissions to export.');
131
152
  return;
132
153
  }
133
154
 
@@ -137,7 +158,7 @@ async function exportSubmissions(options = {}) {
137
158
  process.exit(1);
138
159
  }
139
160
 
140
- const defaultName = `submissions.${format}`;
161
+ const defaultName = spam ? `submissions-spam.${format}` : `submissions.${format}`;
141
162
  const outFile = options.output
142
163
  ? path.isAbsolute(options.output)
143
164
  ? options.output
@@ -158,6 +179,7 @@ async function exportSubmissions(options = {}) {
158
179
  'build_id',
159
180
  'payload_json',
160
181
  ];
182
+ if (spam) header.push('spam_reason');
161
183
  const escapeCell = (value) => {
162
184
  if (value === null || value === undefined) return '';
163
185
  const str = String(value);
@@ -168,8 +190,8 @@ async function exportSubmissions(options = {}) {
168
190
  };
169
191
  const lines = [
170
192
  header.join(','),
171
- ...submissions.map((s) =>
172
- [
193
+ ...submissions.map((s) => {
194
+ const cells = [
173
195
  s.id,
174
196
  s.created_at,
175
197
  s.page_url || '',
@@ -178,8 +200,10 @@ async function exportSubmissions(options = {}) {
178
200
  s.form_index ?? '',
179
201
  s.build_id || '',
180
202
  JSON.stringify(s.payload || {}),
181
- ].map(escapeCell).join(','),
182
- ),
203
+ ];
204
+ if (spam) cells.push(s.spam_reason || '');
205
+ return cells.map(escapeCell).join(',');
206
+ }),
183
207
  ];
184
208
  fs.writeFileSync(outFile, lines.join('\n'), 'utf8');
185
209
  }
@@ -188,7 +212,8 @@ async function exportSubmissions(options = {}) {
188
212
  process.exit(1);
189
213
  }
190
214
 
191
- console.log(`Exported ${submissions.length} submission(s) → ${outFile}`);
215
+ const noun = spam ? 'spam submission(s)' : 'submission(s)';
216
+ console.log(`Exported ${submissions.length} ${noun} → ${outFile}`);
192
217
  }
193
218
 
194
- module.exports = { list, show, exportSubmissions };
219
+ module.exports = { list, show, exportSubmissions, applySpamFilter };
package/src/index.js CHANGED
@@ -219,8 +219,9 @@ const subCmd = program.command('submissions').description('View form submissions
219
219
 
220
220
  subCmd
221
221
  .command('list')
222
- .description('List form submissions for the current project')
222
+ .description('List form submissions for the current project (spam excluded)')
223
223
  .option('--json', 'Output as JSON')
224
+ .option('--spam', 'List only submissions flagged as spam')
224
225
  .action((opts) => submissions.list(opts));
225
226
 
226
227
  subCmd
@@ -231,14 +232,15 @@ subCmd
231
232
 
232
233
  subCmd
233
234
  .command('export')
234
- .description('Export form submissions for the current project to a file')
235
+ .description('Export form submissions for the current project to a file (spam excluded)')
236
+ .option('--spam', 'Export only submissions flagged as spam')
235
237
  .option(
236
238
  '-f, --format <format>',
237
239
  'Output format: csv or json (default: csv)',
238
240
  )
239
241
  .option(
240
242
  '-o, --output <file>',
241
- 'Output file path (default: submissions.<format>)',
243
+ 'Output file path (default: submissions.<format>, or submissions-spam.<format> with --spam)',
242
244
  )
243
245
  .action((opts) => submissions.exportSubmissions(opts));
244
246
 
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
 
@@ -207,6 +222,8 @@ function buildQuery(table) {
207
222
  select(cols) { _select = cols; return q; },
208
223
  eq(col, val) { filters.push(`${col}=eq.${val}`); return q; },
209
224
  in(col, vals) { filters.push(`${col}=in.(${vals.join(',')})`); return q; },
225
+ is(col, val) { filters.push(`${col}=is.${val}`); return q; },
226
+ not(col, op, val) { filters.push(`${col}=not.${op}.${val}`); return q; },
210
227
  order(col, dir = 'asc') { _order = `${col}.${dir}`; return q; },
211
228
  limit(n) { _limit = n; return q; },
212
229
 
@@ -286,6 +303,10 @@ function handleAuthError(err) {
286
303
  console.error(err.message);
287
304
  process.exit(1);
288
305
  }
306
+ if (err.code === 'PLAN_REQUIRED') {
307
+ console.error(formatPlanRequiredMessage(err));
308
+ process.exit(1);
309
+ }
289
310
  }
290
311
 
291
312
  // ---------------------------------------------------------------------------
@@ -363,6 +384,20 @@ async function getMaxDeployEventIdForBuild(buildId, accessToken = null) {
363
384
  return Number.isFinite(n) ? n : 0;
364
385
  }
365
386
 
387
+ /**
388
+ * Highest build_deploy_events.id across the project, 0 when none. A cursor
389
+ * taken before a post change, when the build the server will rebuild is not
390
+ * known yet: event ids are global and the stream filters by build.
391
+ */
392
+ async function getMaxDeployEventIdForProject(projectId) {
393
+ const url = `${SUPABASE_URL}/rest/v1/build_deploy_events?select=id&project_id=eq.${encodeURIComponent(String(projectId))}&order=id.desc&limit=1`;
394
+ const data = await request('GET', url, null);
395
+ const row = Array.isArray(data) ? data[0] : null;
396
+ if (!row || row.id == null) return 0;
397
+ const n = Number(row.id);
398
+ return Number.isFinite(n) ? n : 0;
399
+ }
400
+
366
401
  /**
367
402
  * Stream build/deploy/archive events until a terminal event or the connection closes.
368
403
  * @param {string} bearerToken
@@ -654,6 +689,7 @@ async function exchangeDeployTokenForAccessToken(deployTokenPlaintext, projectUu
654
689
  );
655
690
  err.status = res.status;
656
691
  err.data = data;
692
+ attachPlanRequired(err, data);
657
693
  throw err;
658
694
  }
659
695
  if (!data?.access_token) {
@@ -693,6 +729,7 @@ async function invokePublishBuild(accessToken, projectId, buildId) {
693
729
  );
694
730
  err.status = res.status;
695
731
  err.data = data;
732
+ attachPlanRequired(err, data);
696
733
  throw err;
697
734
  }
698
735
  return data;
@@ -703,8 +740,10 @@ module.exports = {
703
740
  fn,
704
741
  getUserInfo,
705
742
  handleAuthError,
743
+ attachPlanRequired,
706
744
  getValidAccessToken,
707
745
  getMaxDeployEventIdForBuild,
746
+ getMaxDeployEventIdForProject,
708
747
  streamDeployEventsUntilDone,
709
748
  exchangeDeployTokenForAccessToken,
710
749
  pushWithToken,