@somewhere-tech/cli 0.30.2 → 0.31.1
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 +31 -15
- package/dist/commands/dev.js +261 -78
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/promote.js +111 -3
- package/dist/commands/promote.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/promote-outcome.js +116 -0
- package/dist/lib/promote-outcome.js.map +1 -0
- package/dist/local/compiler-core.js +29 -2
- package/dist/local/compiler-core.js.map +1 -1
- package/dist/local/compiler.js +137 -18
- package/dist/local/compiler.js.map +1 -1
- package/dist/local/dev-server.js +5 -1
- package/dist/local/dev-server.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/runtime/VENDOR.json +4 -4
- package/runtime/browser-probes.mjs +1 -1
- package/runtime/compiler/VENDOR.json +2 -2
- package/runtime/compiler/compile-core.cjs +86 -24
- package/runtime/platform-context.mjs +1 -1
- package/runtime/sw-init.mjs +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ After `somewhere init`, Claude Code and Codex auto-connect via the `.mcp.json` i
|
|
|
46
46
|
| `somewhere project view [name]` | Show project details |
|
|
47
47
|
| `somewhere project delete <name>` | Delete with email confirmation |
|
|
48
48
|
| `somewhere dev` | Serve your app on localhost, compiled by the platform's own compiler; save a file and the page updates in milliseconds |
|
|
49
|
-
| `somewhere
|
|
49
|
+
| `somewhere preview` | Run your app on the platform instead of your machine — every save goes to a private URL, and production is untouched until you promote |
|
|
50
50
|
| `somewhere dev <cmd...>` | Run your own command locally with the project's env vars injected |
|
|
51
51
|
| `somewhere deploy` | Deploy current directory to linked project |
|
|
52
52
|
| `somewhere deploy --dry-run` | Preview the deploy diff without shipping |
|
|
@@ -234,24 +234,40 @@ Environment variables come from a `.env` in the project directory. Run
|
|
|
234
234
|
on the platform), then fill in the ones you want locally. `somewhere dev`
|
|
235
235
|
names any key the project expects that has no local value.
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
## `somewhere preview`
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
send someone, or when the agent doing the work reaches the platform only over
|
|
243
|
-
MCP and has no local machine to serve from. It needs the development
|
|
244
|
-
environment, which is on the Pro and Scale plans and is enabled per account;
|
|
245
|
-
without it the command returns `CLOUD_DEV_NOT_ENABLED`. Serving locally has no
|
|
246
|
-
such requirement, on any plan.
|
|
239
|
+
There are two loops, and they are named after where the app runs.
|
|
240
|
+
`somewhere dev` runs it on your machine. `somewhere preview` runs it on the
|
|
241
|
+
platform.
|
|
247
242
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
243
|
+
`somewhere preview` sends every save to a private URL, reachable only by you
|
|
244
|
+
until you share the link. The build is the one production would get. The
|
|
245
|
+
database is a separate copy of your schema with none of your production rows,
|
|
246
|
+
so nothing you try in a preview can touch real data. After each update the
|
|
247
|
+
command prints that URL and the `somewhere promote` command that makes those
|
|
248
|
+
exact bytes live. Reach for it when you want a URL to send someone, or when the
|
|
249
|
+
agent doing the work reaches the platform over MCP and has no local machine to
|
|
250
|
+
serve from.
|
|
251
|
+
|
|
252
|
+
Nothing your users see changes while you preview. Production keeps serving what
|
|
253
|
+
you last promoted.
|
|
254
|
+
|
|
255
|
+
The URL is single-use and short-lived by design: opening it exchanges it for a
|
|
256
|
+
private session in that browser, and saving a file replaces it with a new one.
|
|
257
|
+
Use the newest URL the command printed. Anyone without a live URL — signed out,
|
|
258
|
+
or signed in as someone else — gets a 404.
|
|
259
|
+
|
|
260
|
+
A preview is built against your live version, so a project that has never been
|
|
261
|
+
published has nothing for the first one to build on. On such a project
|
|
262
|
+
`somewhere preview` publishes once — it says so before it does — and every
|
|
251
263
|
preview after that stays private to you and never changes what is live.
|
|
252
264
|
|
|
253
|
-
`somewhere
|
|
254
|
-
|
|
265
|
+
`somewhere preview` is on the Pro and Scale plans. `somewhere dev` runs the same
|
|
266
|
+
app on your machine on every plan, and deploying is unaffected on every plan.
|
|
267
|
+
|
|
268
|
+
`somewhere dev --cloud` still starts the same loop and points you at the new
|
|
269
|
+
name. `somewhere dev --local` is accepted and does what bare `somewhere dev`
|
|
270
|
+
does.
|
|
255
271
|
|
|
256
272
|
### Running your own command
|
|
257
273
|
|
package/dist/commands/dev.js
CHANGED
|
@@ -5,6 +5,7 @@ import { join, relative } from 'node:path';
|
|
|
5
5
|
import { reportTypecheck } from './typecheck.js';
|
|
6
6
|
import { runTypecheck } from '../lib/typecheck.js';
|
|
7
7
|
import chokidar from 'chokidar';
|
|
8
|
+
import prompts from 'prompts';
|
|
8
9
|
import open from '../lib/open.js';
|
|
9
10
|
import ora from '../lib/spinner.js';
|
|
10
11
|
import { ApiClient, CliApiError, LONG_CALL_TIMEOUT_MS } from '../lib/client.js';
|
|
@@ -26,6 +27,29 @@ import { isRecord, unwrapPlatformData } from '../lib/platform-command.js';
|
|
|
26
27
|
const WATCH_EXTS = /\.(ts|tsx|js|jsx|mjs|html|css|json|svg|md|txt|png|jpe?g|gif|webp|ico|woff2?|ttf|otf)$/i;
|
|
27
28
|
const DEBOUNCE_MS = 500;
|
|
28
29
|
const RETRYABLE_DRAFT_CODES = new Set(['TIMEOUT', 'SERVER_SLOW', 'NETWORK_ERROR']);
|
|
30
|
+
/**
|
|
31
|
+
* The preview this loop was watching has finished — it was promoted, or closed.
|
|
32
|
+
*
|
|
33
|
+
* A promote ends the preview session, but the watcher used to keep running and
|
|
34
|
+
* keep failing, once per save, with the platform's API-client wording
|
|
35
|
+
* (tsk_74375b3c). The loop knows perfectly well what happened; it should stop
|
|
36
|
+
* and say so, not relay a refusal written for a machine.
|
|
37
|
+
*/
|
|
38
|
+
class PreviewFinishedError extends Error {
|
|
39
|
+
reason;
|
|
40
|
+
constructor(reason) {
|
|
41
|
+
super(reason);
|
|
42
|
+
this.reason = reason;
|
|
43
|
+
this.name = 'PreviewFinishedError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Did the platform just tell us this preview is over? */
|
|
47
|
+
function previewFinishedReason(err) {
|
|
48
|
+
if (!(err instanceof CliApiError) || err.code !== 'DRAFT_SESSION_TERMINAL')
|
|
49
|
+
return null;
|
|
50
|
+
const status = err.data?.terminal_status;
|
|
51
|
+
return status === 'promoted' ? 'promoted' : 'closed';
|
|
52
|
+
}
|
|
29
53
|
export async function mintPreviewHandoff(client, projectId, draftId, candidateReleaseId) {
|
|
30
54
|
const cap = await client.call('POST', `/projects/${encodeURIComponent(projectId)}/preview/mint`, { draft_id: draftId, candidate_release_id: candidateReleaseId });
|
|
31
55
|
if (!cap || typeof cap.preview_url !== 'string' || !cap.preview_url.includes('/__sw_cap?t=')) {
|
|
@@ -65,6 +89,22 @@ export async function callDraftCandidate(client, path, body) {
|
|
|
65
89
|
});
|
|
66
90
|
}
|
|
67
91
|
}
|
|
92
|
+
export function registerPreview(program) {
|
|
93
|
+
program
|
|
94
|
+
.command('preview')
|
|
95
|
+
.description('Run your app on the platform instead of your machine. Every save goes to a private URL, '
|
|
96
|
+
+ 'reachable only by you until you share the link. The build is the one production would get; '
|
|
97
|
+
+ 'the database is a separate copy of your schema, so nothing you try here can touch production '
|
|
98
|
+
+ 'rows. Nothing your users see changes — production keeps serving what you last promoted, until '
|
|
99
|
+
+ 'you run `somewhere promote`. Reach for this when you want the real hosted app in front of you, '
|
|
100
|
+
+ 'or when your agent reaches the platform over MCP and cannot serve on localhost. '
|
|
101
|
+
+ 'Available on the Pro and Scale plans; `somewhere dev` runs the app on your machine on every plan.')
|
|
102
|
+
.option('--project <id>', 'Override project ID')
|
|
103
|
+
.option('--publish-first', 'For a project that has never been published: publish this directory to production first, so the preview has a live version to build on. Without it you are asked, and a script that cannot be asked is refused.')
|
|
104
|
+
.action(async (opts) => {
|
|
105
|
+
await runHotDeploy(opts);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
68
108
|
export function registerDev(program) {
|
|
69
109
|
program
|
|
70
110
|
.command('dev [cmd...]')
|
|
@@ -75,10 +115,11 @@ export function registerDev(program) {
|
|
|
75
115
|
'production app, and this is a faster window onto it. Same app, same build. ' +
|
|
76
116
|
'Reaching the project DATABASE from the local loop is a plan feature and the command says so ' +
|
|
77
117
|
'once at startup when your plan does not include it; deploying is unaffected on every plan. ' +
|
|
78
|
-
'
|
|
118
|
+
'To see the same app running on the platform instead of your machine, use `somewhere preview`. ' +
|
|
79
119
|
'Pass a command (e.g. `somewhere dev npm run dev`) to run it locally with platform env vars.')
|
|
80
120
|
.option('--project <id>', 'Override project ID')
|
|
81
|
-
.option('--cloud', '
|
|
121
|
+
.option('--cloud', 'Alias for `somewhere preview`')
|
|
122
|
+
.option('--publish-first', 'Only with `--cloud`: see `somewhere preview --help`')
|
|
82
123
|
.option('--port <port>', 'Port to serve on (default 8787)')
|
|
83
124
|
.option('--open', 'Open the app in your browser once it is serving')
|
|
84
125
|
.option('--check', 'Typecheck (tsc --noEmit) before starting and EXIT on type errors instead of warning. Needs `npm install` in this directory — tsc reads package types out of node_modules, which the CLI\'s own dependency cache does not stand in for.')
|
|
@@ -90,6 +131,9 @@ export function registerDev(program) {
|
|
|
90
131
|
return runLegacyExec(cmdParts);
|
|
91
132
|
}
|
|
92
133
|
if (opts.cloud) {
|
|
134
|
+
// Pre-launch alias. One line, then the identical loop — no ceremony,
|
|
135
|
+
// no grandfathering. `preview` is the name.
|
|
136
|
+
info('This is `somewhere preview`. Use that name — `--cloud` still works for now.');
|
|
93
137
|
return runHotDeploy(opts);
|
|
94
138
|
}
|
|
95
139
|
return runLocalDev(opts);
|
|
@@ -369,16 +413,22 @@ export function localDevDbNotice(allowed, plans = []) {
|
|
|
369
413
|
'Deploying is unaffected on every plan: `somewhere deploy` publishes to production and the deployed app reads and writes the database normally.',
|
|
370
414
|
];
|
|
371
415
|
}
|
|
372
|
-
export const CLOUD_DEV_UNAVAILABLE_MESSAGE = '
|
|
373
|
-
+ 'This account is on a plan that does not include
|
|
416
|
+
export const CLOUD_DEV_UNAVAILABLE_MESSAGE = '`somewhere preview` is available on the Pro and Scale plans. '
|
|
417
|
+
+ 'This account is on a plan that does not include it.';
|
|
374
418
|
/**
|
|
375
419
|
* Does this account have private previews?
|
|
376
420
|
*
|
|
377
421
|
* Returns `true`/`false` when the platform states it, and `null` when it does
|
|
378
422
|
* not — a read that fails, or a platform that stopped reporting the field.
|
|
379
|
-
* `null` must NEVER refuse: an unknown answer is not a denial, and
|
|
380
|
-
* someone whose account works today would be a worse bug than the
|
|
381
|
-
* check exists to fix. Only an explicit `false` stops the command.
|
|
423
|
+
* `null` must NEVER refuse: an unknown answer is not a denial, and telling
|
|
424
|
+
* someone whose account works today to upgrade would be a worse bug than the
|
|
425
|
+
* one this check exists to fix. Only an explicit `false` stops the command.
|
|
426
|
+
*
|
|
427
|
+
* That is safe HERE, and only here, because of where the answer is used: an
|
|
428
|
+
* unknown entitlement is read on the path that already requires explicit
|
|
429
|
+
* consent to publish (see resolveBaseRelease), so it can no longer wave through
|
|
430
|
+
* a production release nobody asked for. It is the RELEASE read, not this one,
|
|
431
|
+
* that must refuse when it cannot answer.
|
|
382
432
|
*
|
|
383
433
|
* On a never-published project the platform answers this with the plain plan
|
|
384
434
|
* entitlement (there is no release to bind a preview to yet), which is exactly
|
|
@@ -403,45 +453,142 @@ export class CloudDevUnavailableError extends Error {
|
|
|
403
453
|
this.name = 'CloudDevUnavailableError';
|
|
404
454
|
}
|
|
405
455
|
}
|
|
406
|
-
/**
|
|
407
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Read the project's live version, distinguishing "nothing is live" from
|
|
458
|
+
* "could not tell". Every failure mode — a refusal, a server error, a dropped
|
|
459
|
+
* connection, a 200 whose shape this CLI does not recognise — is `unknown`.
|
|
460
|
+
*
|
|
461
|
+
* `call` is injected so a fixture can drive each of those answers.
|
|
462
|
+
*/
|
|
463
|
+
export async function readBaseReleaseState(projectId, call = callPlatformTool) {
|
|
464
|
+
let status;
|
|
408
465
|
try {
|
|
409
|
-
|
|
410
|
-
if (isRecord(status) && typeof status.active_release_id === 'string') {
|
|
411
|
-
return status.active_release_id;
|
|
412
|
-
}
|
|
466
|
+
status = unwrapPlatformData(await call('deploy_status', { project_id: projectId }, { allTools: true }));
|
|
413
467
|
}
|
|
414
|
-
catch {
|
|
415
|
-
|
|
468
|
+
catch (err) {
|
|
469
|
+
return { known: false, reason: err instanceof Error ? err.message : String(err) };
|
|
470
|
+
}
|
|
471
|
+
if (!isRecord(status)) {
|
|
472
|
+
return { known: false, reason: 'the platform did not describe this project.' };
|
|
473
|
+
}
|
|
474
|
+
if (typeof status.active_release_id === 'string') {
|
|
475
|
+
return { known: true, activeReleaseId: status.active_release_id };
|
|
476
|
+
}
|
|
477
|
+
// The ONLY answer that may lead to a publish: the platform states this
|
|
478
|
+
// project is not published. Note the deliberate asymmetry — a project that IS
|
|
479
|
+
// published but does not name a version behind it falls through to unknown
|
|
480
|
+
// below rather than being treated as never-published, because the cost of
|
|
481
|
+
// being wrong in that direction is publishing over a live app.
|
|
482
|
+
if (status.published === false)
|
|
483
|
+
return { known: true, activeReleaseId: null };
|
|
484
|
+
return {
|
|
485
|
+
known: false,
|
|
486
|
+
reason: 'the platform did not say which version of this project is live.',
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/** The live version could not be read, so nothing may be published over it. */
|
|
490
|
+
export class BaseReleaseUnknownError extends Error {
|
|
491
|
+
reason;
|
|
492
|
+
code = 'BASE_RELEASE_UNKNOWN';
|
|
493
|
+
constructor(reason) {
|
|
494
|
+
super('Could not tell whether this project already has a live version, so nothing was published. '
|
|
495
|
+
+ `The platform said: ${reason}`);
|
|
496
|
+
this.reason = reason;
|
|
497
|
+
this.name = 'BaseReleaseUnknownError';
|
|
416
498
|
}
|
|
417
|
-
return null;
|
|
418
499
|
}
|
|
419
500
|
/**
|
|
420
|
-
*
|
|
421
|
-
* publishing once if there is none.
|
|
501
|
+
* Publishing this directory to production was never agreed to.
|
|
422
502
|
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
503
|
+
* `declined` — asked, and the answer was no.
|
|
504
|
+
* `not-asked` — nothing here could ask (a script, an agent, a piped shell), so
|
|
505
|
+
* the answer is no by default. Consent that cannot be given is not consent.
|
|
506
|
+
*/
|
|
507
|
+
export class PublishConsentRequiredError extends Error {
|
|
508
|
+
why;
|
|
509
|
+
code = 'PUBLISH_CONSENT_REQUIRED';
|
|
510
|
+
constructor(why) {
|
|
511
|
+
super('Nothing was published.');
|
|
512
|
+
this.why = why;
|
|
513
|
+
this.name = 'PublishConsentRequiredError';
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Find the live version a private preview will build on — and, on a project
|
|
518
|
+
* that has none, publish one ONLY after the person running the command says so.
|
|
519
|
+
*
|
|
520
|
+
* THE ORDER IS THE CONTRACT (tsk_cf48f4ab, tsk_5504e045). Every step below is
|
|
521
|
+
* placed so that the customer's live site cannot change as a side effect of
|
|
522
|
+
* asking for a preview:
|
|
523
|
+
*
|
|
524
|
+
* 1. The plan entitlement is read FIRST, before any write and before any
|
|
525
|
+
* question. An account without private previews is refused having created
|
|
526
|
+
* nothing, rather than handed a production release and then told the
|
|
527
|
+
* command is unavailable.
|
|
528
|
+
* 2. The live version is read, and an unreadable answer STOPS the command.
|
|
529
|
+
* This is the reversal: `null` used to mean both "nothing is live" and
|
|
530
|
+
* "I could not tell", and the second one published over live projects.
|
|
531
|
+
* 3. A project that already has a live version returns it and publishes
|
|
532
|
+
* NOTHING. This is the overwhelmingly common path.
|
|
533
|
+
* 4. Only a positively-confirmed "never published" reaches the publish, and
|
|
534
|
+
* only after explicit consent — a confirmation, or `--publish-first`.
|
|
432
535
|
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
536
|
+
* Why an unknown ENTITLEMENT does not refuse here, when an unknown RELEASE
|
|
537
|
+
* does: the entitlement question is only ever asked on the path that already
|
|
538
|
+
* requires the customer's explicit consent to publish, and consent settles it.
|
|
539
|
+
* A working account is never blocked by a read that failed; it is only ever
|
|
540
|
+
* asked. Only a stated `false` refuses, so nobody is told to upgrade on the
|
|
541
|
+
* strength of a read that did not answer.
|
|
542
|
+
*
|
|
543
|
+
* `publish` and `confirmPublish` are injected so a fixture can prove `publish`
|
|
544
|
+
* was never called on each refusal path — the ordering is the behaviour under
|
|
545
|
+
* test, not the call shape.
|
|
435
546
|
*/
|
|
436
|
-
export async function
|
|
437
|
-
// Only an explicit `false` refuses; `null` means the platform did not say,
|
|
438
|
-
// and an unknown answer must never block an account that works today.
|
|
547
|
+
export async function resolveBaseRelease(args) {
|
|
439
548
|
if ((await args.cloudDevAllowed()) === false)
|
|
440
549
|
throw new CloudDevUnavailableError();
|
|
550
|
+
const state = await args.readBaseReleaseState();
|
|
551
|
+
if (!state.known)
|
|
552
|
+
throw new BaseReleaseUnknownError(state.reason);
|
|
553
|
+
if (state.activeReleaseId)
|
|
554
|
+
return { baseReleaseId: state.activeReleaseId, published: false };
|
|
441
555
|
args.announce?.('This project has never been published, so there is no live version for a private preview to build on.');
|
|
442
|
-
args.announce?.('Publishing it once now
|
|
556
|
+
args.announce?.('Publishing it once now would put the files in this directory in front of your users. '
|
|
557
|
+
+ 'After that, every preview stays private to you and production only changes when you promote.');
|
|
558
|
+
const consent = await args.confirmPublish();
|
|
559
|
+
if (consent !== 'granted')
|
|
560
|
+
throw new PublishConsentRequiredError(consent);
|
|
443
561
|
await args.publish();
|
|
444
|
-
|
|
562
|
+
const after = await args.readBaseReleaseState();
|
|
563
|
+
if (!after.known || !after.activeReleaseId) {
|
|
564
|
+
throw new BaseReleaseUnknownError('the first version was published, but the live version could not be read back. '
|
|
565
|
+
+ 'Run `somewhere preview` again.');
|
|
566
|
+
}
|
|
567
|
+
return { baseReleaseId: after.activeReleaseId, published: true };
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Ask, once, before the one thing `somewhere preview` can do to a live site.
|
|
571
|
+
*
|
|
572
|
+
* A prompt is the primary mechanism rather than a bare flag because the publish
|
|
573
|
+
* happens on a FIRST run, when nobody knows a flag is needed — a flag-only
|
|
574
|
+
* design would either block every interactive first run or, worse, be added
|
|
575
|
+
* blindly and re-open the hole. A prompt nobody can answer is not consent
|
|
576
|
+
* either, so a non-interactive shell (a script, an agent, a piped terminal)
|
|
577
|
+
* gets `not-asked` and the refusal names `--publish-first`, which is the same
|
|
578
|
+
* consent given up front.
|
|
579
|
+
*/
|
|
580
|
+
export async function readPublishConsent(publishFirst) {
|
|
581
|
+
if (publishFirst)
|
|
582
|
+
return 'granted';
|
|
583
|
+
if (!process.stdin.isTTY)
|
|
584
|
+
return 'not-asked';
|
|
585
|
+
const { ok } = await prompts({
|
|
586
|
+
type: 'confirm',
|
|
587
|
+
name: 'ok',
|
|
588
|
+
message: 'Publish this directory to production now, so the preview has a live version to build on?',
|
|
589
|
+
initial: false,
|
|
590
|
+
});
|
|
591
|
+
return ok === true ? 'granted' : 'declined';
|
|
445
592
|
}
|
|
446
593
|
async function runHotDeploy(opts) {
|
|
447
594
|
const token = getToken();
|
|
@@ -459,60 +606,73 @@ async function runHotDeploy(opts) {
|
|
|
459
606
|
subdomain = config.subdomain;
|
|
460
607
|
}
|
|
461
608
|
await showProjectNotices(client, projectId);
|
|
462
|
-
// Initial full sync to the PREVIEW slot (preview: true). Writes only the
|
|
463
|
-
// owner-gated dev slot — never prod, never a version bump or history entry.
|
|
464
|
-
// /deploy/patch rejects projects with no prior deploy, so a full (preview)
|
|
465
|
-
// deploy first establishes the sandbox AND returns the {slug}-dev URL.
|
|
466
|
-
const spinner = ora('Syncing to preview...').start();
|
|
467
609
|
const { files, binaryFiles, functions } = collectFiles(cwd);
|
|
468
610
|
const draftId = `draft_${randomUUID()}`;
|
|
469
611
|
const firstOperationId = `previewop_${randomUUID()}`;
|
|
470
|
-
// The first preview snapshot must name the production release it was read
|
|
471
|
-
// (base_release_id) — the platform binds the
|
|
472
|
-
// release.
|
|
473
|
-
//
|
|
474
|
-
let baseReleaseId
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
replace_functions: true,
|
|
495
|
-
});
|
|
496
|
-
},
|
|
497
|
-
readActiveReleaseId: () => readActiveReleaseId(projectId),
|
|
498
|
-
});
|
|
612
|
+
// The first preview snapshot must name the production release it was read
|
|
613
|
+
// from (base_release_id) — the platform binds the preview to that exact
|
|
614
|
+
// production release. Everything about how that id is obtained, including
|
|
615
|
+
// the refusal to invent one, lives in resolveBaseRelease.
|
|
616
|
+
let baseReleaseId;
|
|
617
|
+
try {
|
|
618
|
+
const resolved = await resolveBaseRelease({
|
|
619
|
+
cloudDevAllowed: () => readCloudDevAllowed(projectId),
|
|
620
|
+
readBaseReleaseState: () => readBaseReleaseState(projectId),
|
|
621
|
+
confirmPublish: () => readPublishConsent(opts.publishFirst === true),
|
|
622
|
+
announce: info,
|
|
623
|
+
publish: async () => {
|
|
624
|
+
await callDraftCandidate(client, '/deploy', {
|
|
625
|
+
project_id: projectId,
|
|
626
|
+
scope: 'all',
|
|
627
|
+
files,
|
|
628
|
+
binary_files: binaryFiles,
|
|
629
|
+
functions,
|
|
630
|
+
replace_functions: true,
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
baseReleaseId = resolved.baseReleaseId;
|
|
635
|
+
if (resolved.published)
|
|
499
636
|
success('Published — this project now has a live version.');
|
|
637
|
+
}
|
|
638
|
+
catch (err) {
|
|
639
|
+
// Every branch here says the same thing in different words: your live site
|
|
640
|
+
// is exactly as you left it. Nothing below may claim anything about whether
|
|
641
|
+
// this project is published — that is precisely the read that failed.
|
|
642
|
+
if (err instanceof CloudDevUnavailableError) {
|
|
643
|
+
error(err.message);
|
|
644
|
+
info('Nothing was created or changed — whatever is live stays live.');
|
|
645
|
+
info('`somewhere deploy` publishes to production on any plan, and `somewhere dev` runs the same app on your machine.');
|
|
646
|
+
process.exit(1);
|
|
500
647
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
648
|
+
if (err instanceof BaseReleaseUnknownError) {
|
|
649
|
+
error(err.message);
|
|
650
|
+
info('Nothing was created or changed — whatever is live stays live.');
|
|
651
|
+
info('Try `somewhere preview` again. To publish this directory to production deliberately, run `somewhere deploy`.');
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
if (err instanceof PublishConsentRequiredError) {
|
|
655
|
+
if (err.why === 'declined') {
|
|
656
|
+
warn('Nothing was published — whatever is live stays live.');
|
|
507
657
|
}
|
|
508
|
-
|
|
509
|
-
error(
|
|
658
|
+
else {
|
|
659
|
+
error('This project has never been published, so a preview has no live version to build on.');
|
|
660
|
+
info('Re-run as `somewhere preview --publish-first` to publish this directory to production first, or run `somewhere deploy` yourself.');
|
|
661
|
+
info('Nothing was created or changed — whatever is live stays live.');
|
|
510
662
|
}
|
|
511
|
-
error('Could not publish the first version, so the private preview has nothing to build on.');
|
|
512
663
|
process.exit(1);
|
|
513
664
|
}
|
|
514
|
-
|
|
665
|
+
if (!(isBuildError(err) && renderBuildError(err, cwd))) {
|
|
666
|
+
error(err instanceof Error ? err.message : String(err));
|
|
667
|
+
}
|
|
668
|
+
error('Could not publish the first version, so the private preview has nothing to build on.');
|
|
669
|
+
process.exit(1);
|
|
515
670
|
}
|
|
671
|
+
// Initial full sync to the PREVIEW slot (preview: true). Writes only the
|
|
672
|
+
// owner-gated dev slot — never prod, never a version bump or history entry.
|
|
673
|
+
// /deploy/patch rejects projects with no prior deploy, so a full (preview)
|
|
674
|
+
// deploy first establishes the sandbox AND returns the {slug}-dev URL.
|
|
675
|
+
const spinner = ora('Syncing to preview...').start();
|
|
516
676
|
let candidateReleaseId = null;
|
|
517
677
|
let initialHandoff;
|
|
518
678
|
let initialAutoOpenUrl;
|
|
@@ -603,6 +763,24 @@ async function runHotDeploy(opts) {
|
|
|
603
763
|
if (nextCandidate)
|
|
604
764
|
candidateReleaseId = nextCandidate;
|
|
605
765
|
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
if (!(err instanceof PreviewFinishedError))
|
|
768
|
+
throw err;
|
|
769
|
+
// One line, in the two words the product uses, naming the next command.
|
|
770
|
+
// Nothing about how a preview is built reaches this terminal.
|
|
771
|
+
console.log('');
|
|
772
|
+
if (err.reason === 'promoted') {
|
|
773
|
+
success('Promoted — this preview is now your live app, and the preview has finished.');
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
info('This preview has finished.');
|
|
777
|
+
}
|
|
778
|
+
info(`Run ${teal('somewhere preview')} to keep previewing.`);
|
|
779
|
+
if (timer)
|
|
780
|
+
clearTimeout(timer);
|
|
781
|
+
await watcher.close().catch(() => { });
|
|
782
|
+
process.exit(0);
|
|
783
|
+
}
|
|
606
784
|
finally {
|
|
607
785
|
deploying = false;
|
|
608
786
|
if (pendingChanged.size || pendingDeleted.size)
|
|
@@ -737,6 +915,11 @@ async function deployBatch(client, projectId, cwd, changed, deleted, draftId, ex
|
|
|
737
915
|
info(dim('Your last working preview is still up. Fix and save again.'));
|
|
738
916
|
return null;
|
|
739
917
|
}
|
|
918
|
+
const finished = previewFinishedReason(err);
|
|
919
|
+
if (finished) {
|
|
920
|
+
// Not a failed save — the preview itself is over. The loop stops on it.
|
|
921
|
+
throw new PreviewFinishedError(finished);
|
|
922
|
+
}
|
|
740
923
|
console.log(`${dim(stamp())} ${label} ${red('✗ failed')} ${dim(`(${secs}s)`)}`);
|
|
741
924
|
error(err instanceof Error ? err.message : String(err));
|
|
742
925
|
return null;
|