primo-cli 0.1.3 → 0.1.5
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 +111 -39
- package/dist/commands/build.js +488 -272
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.js +293 -141
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +2007 -150
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +65 -43
- package/dist/commands/login.d.ts +1 -2
- package/dist/commands/login.js +24 -6
- package/dist/commands/new.js +161 -274
- package/dist/commands/pull-library.d.ts +7 -0
- package/dist/commands/pull-library.js +92 -0
- package/dist/commands/pull.d.ts +0 -1
- package/dist/commands/pull.js +160 -165
- package/dist/commands/push-library.d.ts +7 -0
- package/dist/commands/push-library.js +88 -0
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +358 -51
- package/dist/commands/validate.d.ts +1 -1
- package/dist/commands/validate.js +379 -161
- package/dist/index.js +110 -20
- package/dist/utils/binary.js +1 -1
- package/dist/utils/format.d.ts +12 -0
- package/dist/utils/format.js +98 -0
- package/dist/utils/head-svelte.d.ts +2 -0
- package/dist/utils/head-svelte.js +53 -0
- package/dist/utils/server-config.d.ts +19 -0
- package/dist/utils/server-config.js +49 -0
- package/dist/utils/site-config.d.ts +11 -0
- package/dist/utils/site-config.js +14 -0
- package/package.json +8 -4
- package/dist/commands/export.d.ts +0 -8
- package/dist/commands/export.js +0 -163
- package/dist/commands/import.d.ts +0 -9
- package/dist/commands/import.js +0 -118
- package/dist/commands/publish.d.ts +0 -6
- package/dist/commands/publish.js +0 -239
package/dist/commands/deploy.js
CHANGED
|
@@ -2,66 +2,144 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import ora from 'ora';
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const spinner = ora('Preparing deployment...').start();
|
|
5
|
+
import { select } from '@inquirer/prompts';
|
|
6
|
+
import { execSync, spawn } from 'child_process';
|
|
7
|
+
import { read_server_config, write_server_config, get_server_config_path, SERVER_CONFIG_FILE } from '../utils/server-config.js';
|
|
8
|
+
import { read_site_config, get_site_config_path } from '../utils/site-config.js';
|
|
9
|
+
async function path_exists(p) {
|
|
11
10
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
await fs.access(p);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function inventory_workspace(root_dir) {
|
|
19
|
+
const server_config_path = get_server_config_path(root_dir);
|
|
20
|
+
if (!await path_exists(server_config_path)) {
|
|
21
|
+
console.log('');
|
|
22
|
+
console.log(chalk.red(`No ${SERVER_CONFIG_FILE} found in ${root_dir}.`));
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log(chalk.dim(' primo deploy must be run from a workspace root.'));
|
|
25
|
+
console.log(chalk.dim(' A workspace root is the directory containing server.yaml.'));
|
|
26
|
+
console.log(chalk.dim(' Run `primo init` to create one, or `cd` into an existing workspace.'));
|
|
27
|
+
console.log('');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
// Confirm parseable; we don't need the contents but failing fast is helpful.
|
|
31
|
+
await read_server_config(root_dir);
|
|
32
|
+
const sites = [];
|
|
33
|
+
const sites_root = path.join(root_dir, 'sites');
|
|
34
|
+
if (await path_exists(sites_root)) {
|
|
35
|
+
const entries = await fs.readdir(sites_root, { withFileTypes: true });
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
38
|
+
continue;
|
|
39
|
+
const site_dir = path.join(sites_root, entry.name);
|
|
40
|
+
if (!await path_exists(get_site_config_path(site_dir)))
|
|
41
|
+
continue;
|
|
42
|
+
try {
|
|
43
|
+
const config = await read_site_config(site_dir);
|
|
44
|
+
sites.push({ dir: site_dir, config });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Skip sites with invalid config
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const has_library = await path_exists(path.join(root_dir, 'library'));
|
|
52
|
+
return {
|
|
53
|
+
root_dir,
|
|
54
|
+
server_config_path,
|
|
55
|
+
sites,
|
|
56
|
+
has_library
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function normalize_provider(raw) {
|
|
60
|
+
if (typeof raw !== 'string')
|
|
61
|
+
return undefined;
|
|
62
|
+
const v = raw.trim().toLowerCase();
|
|
63
|
+
if (v === 'railway')
|
|
64
|
+
return 'railway';
|
|
65
|
+
if (v === 'fly' || v === 'fly.io' || v === 'flyio')
|
|
66
|
+
return 'fly';
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
function workspace_app_name(root_dir) {
|
|
70
|
+
return path.basename(root_dir).toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-') || 'primo-workspace';
|
|
71
|
+
}
|
|
72
|
+
export async function deploy(options) {
|
|
73
|
+
const root_dir = process.cwd();
|
|
74
|
+
const inventory = await inventory_workspace(root_dir);
|
|
75
|
+
if (options.dryRun) {
|
|
76
|
+
print_dry_run(inventory, options.provider);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
let provider = normalize_provider(options.provider);
|
|
80
|
+
if (!provider) {
|
|
16
81
|
try {
|
|
17
|
-
const
|
|
18
|
-
|
|
82
|
+
const choice = await select({
|
|
83
|
+
message: 'Where do you want to deploy?',
|
|
84
|
+
choices: [
|
|
85
|
+
{ name: 'Railway', value: 'railway' },
|
|
86
|
+
{ name: 'Fly.io', value: 'fly' }
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
provider = normalize_provider(choice);
|
|
19
90
|
}
|
|
20
91
|
catch {
|
|
21
|
-
|
|
92
|
+
// User cancelled (Ctrl+C / Esc) — exit quietly.
|
|
93
|
+
console.log('');
|
|
22
94
|
process.exit(1);
|
|
23
95
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
96
|
+
}
|
|
97
|
+
if (!provider) {
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(chalk.red('No provider selected.'));
|
|
100
|
+
console.log(chalk.dim(' Pass -p railway or -p fly, or pick one from the prompt.'));
|
|
101
|
+
console.log('');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
const cli_installed = await check_provider_cli(provider);
|
|
105
|
+
if (!cli_installed) {
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(chalk.yellow(`${provider} CLI not found. Install it first:`));
|
|
108
|
+
if (provider === 'railway') {
|
|
109
|
+
console.log(chalk.dim(' npm install -g @railway/cli'));
|
|
110
|
+
console.log(chalk.dim(' railway login'));
|
|
38
111
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
112
|
+
else {
|
|
113
|
+
console.log(chalk.dim(' curl -L https://fly.io/install.sh | sh'));
|
|
114
|
+
console.log(chalk.dim(' fly auth login'));
|
|
115
|
+
}
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
const provider_logged_in = await check_provider_auth(provider);
|
|
119
|
+
if (!provider_logged_in) {
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log(chalk.red(`Not logged in to ${provider}.`));
|
|
122
|
+
if (provider === 'railway') {
|
|
123
|
+
console.log(chalk.dim(' Run `railway login` and try again.'));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
console.log(chalk.dim(' Run `fly auth login` and try again.'));
|
|
127
|
+
}
|
|
128
|
+
console.log('');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const spinner = ora('Generating deployment files...').start();
|
|
132
|
+
try {
|
|
133
|
+
await generate_dockerfile(inventory);
|
|
134
|
+
if (provider === 'fly') {
|
|
135
|
+
await generate_fly_toml(inventory);
|
|
53
136
|
}
|
|
54
|
-
// Generate deployment files
|
|
55
|
-
spinner.start('Generating deployment files...');
|
|
56
|
-
await generate_dockerfile(site_dir, config);
|
|
57
|
-
await generate_fly_toml(site_dir, config);
|
|
58
137
|
spinner.succeed('Deployment files generated');
|
|
59
|
-
// Deploy based on provider
|
|
60
138
|
if (provider === 'railway') {
|
|
61
|
-
await deploy_to_railway(
|
|
139
|
+
await deploy_to_railway(inventory);
|
|
62
140
|
}
|
|
63
141
|
else {
|
|
64
|
-
await deploy_to_fly(
|
|
142
|
+
await deploy_to_fly(inventory);
|
|
65
143
|
}
|
|
66
144
|
}
|
|
67
145
|
catch (error) {
|
|
@@ -69,6 +147,52 @@ export async function deploy(options) {
|
|
|
69
147
|
process.exit(1);
|
|
70
148
|
}
|
|
71
149
|
}
|
|
150
|
+
function print_dry_run(inventory, provider) {
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(chalk.bold('Deploy preview (dry-run)'));
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(` Workspace: ${chalk.cyan(inventory.root_dir)}`);
|
|
155
|
+
console.log(` Provider: ${chalk.cyan(provider || '(prompted at deploy time)')}`);
|
|
156
|
+
console.log(` Image: ${chalk.cyan(PRIMO_SERVER_IMAGE)}`);
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log(chalk.bold(' Will create on the provider:'));
|
|
159
|
+
console.log(` ${chalk.green('+')} A service running ${PRIMO_SERVER_IMAGE}`);
|
|
160
|
+
console.log(` ${chalk.green('+')} A persistent volume mounted at /app/pb_data`);
|
|
161
|
+
if (provider === 'fly') {
|
|
162
|
+
console.log(` ${chalk.green('+')} fly.toml in this workspace`);
|
|
163
|
+
}
|
|
164
|
+
console.log(` ${chalk.green('+')} Dockerfile in this workspace`);
|
|
165
|
+
console.log('');
|
|
166
|
+
console.log(chalk.bold(' Will be uploaded after first boot via primo push:'));
|
|
167
|
+
console.log(` ${chalk.dim('—')} ${SERVER_CONFIG_FILE}`);
|
|
168
|
+
if (inventory.has_library) {
|
|
169
|
+
console.log(` ${chalk.dim('—')} library/`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log(chalk.dim(' (no library/ directory found)'));
|
|
173
|
+
}
|
|
174
|
+
if (inventory.sites.length > 0) {
|
|
175
|
+
console.log(` ${chalk.dim('—')} sites/ (${inventory.sites.length} site${inventory.sites.length === 1 ? '' : 's'})`);
|
|
176
|
+
for (const site of inventory.sites) {
|
|
177
|
+
const slug = path.basename(site.dir);
|
|
178
|
+
console.log(chalk.dim(` - ${slug} (${site.config.name})`));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
console.log(chalk.yellow(' (no sites found under sites/)'));
|
|
183
|
+
}
|
|
184
|
+
console.log(chalk.dim(' (first push bootstraps; subsequent pushes update incrementally)'));
|
|
185
|
+
console.log('');
|
|
186
|
+
if (provider) {
|
|
187
|
+
console.log(chalk.dim(` Auth: run \`${provider === 'railway' ? 'railway whoami' : 'fly auth whoami'}\` to confirm provider login.`));
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
console.log(chalk.dim(' Auth: provider not selected; pass -p railway|fly to dry-run with auth context.'));
|
|
191
|
+
}
|
|
192
|
+
console.log('');
|
|
193
|
+
console.log(chalk.dim(' No files were written. Run without --dry-run to deploy.'));
|
|
194
|
+
console.log('');
|
|
195
|
+
}
|
|
72
196
|
async function check_provider_cli(provider) {
|
|
73
197
|
try {
|
|
74
198
|
if (provider === 'railway') {
|
|
@@ -83,70 +207,59 @@ async function check_provider_cli(provider) {
|
|
|
83
207
|
return false;
|
|
84
208
|
}
|
|
85
209
|
}
|
|
86
|
-
async function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
# Set permissions
|
|
117
|
-
RUN chmod +x /app/palacms
|
|
118
|
-
|
|
119
|
-
# Environment variables
|
|
120
|
-
ENV PB_DATA_DIR=/app/pb_data
|
|
121
|
-
|
|
122
|
-
# Expose port
|
|
210
|
+
async function check_provider_auth(provider) {
|
|
211
|
+
try {
|
|
212
|
+
if (provider === 'railway') {
|
|
213
|
+
execSync('railway whoami', { stdio: 'ignore' });
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
execSync('fly auth whoami', { stdio: 'ignore' });
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Pinned to the upstream-published image. palacms's main.yml workflow
|
|
225
|
+
// publishes branch tags for whitelisted prefixes (main, feature/**, rc/**),
|
|
226
|
+
// with slashes slugified to dashes (feature/local-dev-cli → :feature-local-dev-cli).
|
|
227
|
+
// Bump to a release tag (:v3.0.0) when palacms cuts a stable release.
|
|
228
|
+
const PRIMO_SERVER_IMAGE = 'ghcr.io/palacms/palacms:feature-local-dev-cli';
|
|
229
|
+
async function generate_dockerfile(inventory) {
|
|
230
|
+
// One-line Dockerfile: pull the published palacms image and run it
|
|
231
|
+
// unchanged. Workspace data (server.yaml, sites/, library/) is uploaded
|
|
232
|
+
// after deploy via `primo push`, which calls /api/palacms/bootstrap on
|
|
233
|
+
// first push (server with no sites) and /api/palacms/import/<id> on
|
|
234
|
+
// subsequent pushes. The volume mounted at /app/pb_data persists the
|
|
235
|
+
// SQLite database between restarts.
|
|
236
|
+
void inventory;
|
|
237
|
+
const dockerfile = `# Primo CMS deployment (generated by primo deploy)
|
|
238
|
+
FROM ${PRIMO_SERVER_IMAGE}
|
|
123
239
|
EXPOSE 8080
|
|
124
|
-
|
|
125
|
-
# Start server
|
|
126
|
-
CMD ["/app/palacms", "serve", "--http", "0.0.0.0:8080"]
|
|
127
240
|
`;
|
|
128
|
-
await fs.writeFile(path.join(
|
|
129
|
-
//
|
|
241
|
+
await fs.writeFile(path.join(inventory.root_dir, 'Dockerfile'), dockerfile);
|
|
242
|
+
// .dockerignore keeps the build context small (Railway uploads cwd to its
|
|
243
|
+
// builder). Nothing in the workspace gets baked into the image — we just
|
|
244
|
+
// FROM the upstream image directly.
|
|
130
245
|
const dockerignore = `node_modules/
|
|
131
246
|
.git/
|
|
132
|
-
.
|
|
133
|
-
.vite-cache/
|
|
247
|
+
.primo/
|
|
134
248
|
*.log
|
|
135
249
|
.DS_Store
|
|
136
250
|
`;
|
|
137
|
-
await fs.writeFile(path.join(
|
|
251
|
+
await fs.writeFile(path.join(inventory.root_dir, '.dockerignore'), dockerignore);
|
|
138
252
|
}
|
|
139
|
-
async function generate_fly_toml(
|
|
140
|
-
const app_name =
|
|
141
|
-
|
|
142
|
-
|
|
253
|
+
async function generate_fly_toml(inventory) {
|
|
254
|
+
const app_name = workspace_app_name(inventory.root_dir);
|
|
255
|
+
// palacms binds 0.0.0.0:8080 in its CMD; mount /app/pb_data on a persistent
|
|
256
|
+
// volume so the SQLite db + uploads survive restarts. Auto-start/stop keeps
|
|
257
|
+
// the small instance free-tier-friendly.
|
|
258
|
+
const fly_toml = `app = "${app_name}"
|
|
143
259
|
primary_region = "sjc"
|
|
144
260
|
|
|
145
261
|
[build]
|
|
146
262
|
|
|
147
|
-
[env]
|
|
148
|
-
PB_DATA_DIR = "/app/pb_data"
|
|
149
|
-
|
|
150
263
|
[http_service]
|
|
151
264
|
internal_port = 8080
|
|
152
265
|
force_https = true
|
|
@@ -163,24 +276,21 @@ primary_region = "sjc"
|
|
|
163
276
|
source = "pb_data"
|
|
164
277
|
destination = "/app/pb_data"
|
|
165
278
|
`;
|
|
166
|
-
await fs.writeFile(path.join(
|
|
279
|
+
await fs.writeFile(path.join(inventory.root_dir, 'fly.toml'), fly_toml);
|
|
167
280
|
}
|
|
168
|
-
async function deploy_to_railway(
|
|
281
|
+
async function deploy_to_railway(inventory) {
|
|
169
282
|
console.log('');
|
|
170
283
|
console.log(chalk.cyan('Deploying to Railway...'));
|
|
171
|
-
console.log('');
|
|
172
|
-
// Check if project exists or create new one
|
|
173
284
|
const spinner = ora('Setting up Railway project...').start();
|
|
285
|
+
const project_name = workspace_app_name(inventory.root_dir);
|
|
174
286
|
try {
|
|
175
|
-
// Try to link existing project
|
|
176
287
|
try {
|
|
177
|
-
execSync('railway status', { cwd:
|
|
288
|
+
execSync('railway status', { cwd: inventory.root_dir, stdio: 'ignore' });
|
|
178
289
|
spinner.succeed('Linked to existing Railway project');
|
|
179
290
|
}
|
|
180
291
|
catch {
|
|
181
|
-
// No project linked, create new one
|
|
182
292
|
spinner.text = 'Creating new Railway project...';
|
|
183
|
-
execSync(`railway init --name "${
|
|
293
|
+
execSync(`railway init --name "${project_name}"`, { cwd: inventory.root_dir, stdio: 'inherit' });
|
|
184
294
|
spinner.succeed('Created new Railway project');
|
|
185
295
|
}
|
|
186
296
|
}
|
|
@@ -188,74 +298,116 @@ async function deploy_to_railway(site_dir, config) {
|
|
|
188
298
|
spinner.fail('Failed to set up Railway project');
|
|
189
299
|
throw error;
|
|
190
300
|
}
|
|
191
|
-
// Deploy
|
|
192
301
|
console.log('');
|
|
193
302
|
console.log(chalk.dim('Building and deploying...'));
|
|
194
303
|
console.log('');
|
|
195
304
|
const deploy_process = spawn('railway', ['up', '--detach'], {
|
|
196
|
-
cwd:
|
|
305
|
+
cwd: inventory.root_dir,
|
|
197
306
|
stdio: 'inherit'
|
|
198
307
|
});
|
|
199
308
|
return new Promise((resolve, reject) => {
|
|
200
|
-
deploy_process.on('close', (code) => {
|
|
201
|
-
if (code
|
|
202
|
-
console.log('');
|
|
203
|
-
console.log(chalk.green('✓ Deployment started!'));
|
|
204
|
-
console.log('');
|
|
205
|
-
console.log(chalk.dim(' Run `railway open` to view your deployment'));
|
|
206
|
-
console.log(chalk.dim(' Run `railway logs` to view logs'));
|
|
207
|
-
resolve();
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
309
|
+
deploy_process.on('close', async (code) => {
|
|
310
|
+
if (code !== 0) {
|
|
210
311
|
reject(new Error(`Railway deployment failed with code ${code}`));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// Generate (or fetch existing) public domain so the user can hit
|
|
315
|
+
// the server immediately. railway domain is idempotent — calling
|
|
316
|
+
// it on a service that already has a generated domain returns the
|
|
317
|
+
// same URL.
|
|
318
|
+
const domain = await try_railway_domain(inventory.root_dir);
|
|
319
|
+
const url = domain ? `https://${domain}` : undefined;
|
|
320
|
+
if (url) {
|
|
321
|
+
await record_workspace_server(inventory.root_dir, url);
|
|
211
322
|
}
|
|
323
|
+
print_post_deploy_next_steps('railway', url);
|
|
324
|
+
resolve();
|
|
212
325
|
});
|
|
213
326
|
});
|
|
214
327
|
}
|
|
215
|
-
async function
|
|
328
|
+
async function try_railway_domain(cwd) {
|
|
329
|
+
try {
|
|
330
|
+
const out = execSync('railway domain --json', { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
331
|
+
const parsed = JSON.parse(out);
|
|
332
|
+
const raw = parsed.domain || parsed.url;
|
|
333
|
+
if (!raw)
|
|
334
|
+
return undefined;
|
|
335
|
+
// railway sometimes returns bare hostname, sometimes full URL
|
|
336
|
+
return raw.replace(/^https?:\/\//, '').replace(/\/+$/, '');
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async function record_workspace_server(root_dir, url) {
|
|
343
|
+
try {
|
|
344
|
+
const config = await read_server_config(root_dir);
|
|
345
|
+
if (config.server === url)
|
|
346
|
+
return;
|
|
347
|
+
await write_server_config(root_dir, { ...config, server: url });
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// Don't fail the deploy if we can't write the file — user can add
|
|
351
|
+
// the server URL manually.
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function print_post_deploy_next_steps(provider, url) {
|
|
216
355
|
console.log('');
|
|
217
|
-
console.log(chalk.
|
|
356
|
+
console.log(chalk.green('✓ Deployment started'));
|
|
357
|
+
console.log('');
|
|
358
|
+
if (url) {
|
|
359
|
+
console.log(chalk.bold(' URL: ') + chalk.cyan(url));
|
|
360
|
+
console.log(chalk.dim(' (saved as `server:` in server.yaml — primo push/login pick it up automatically)'));
|
|
361
|
+
console.log('');
|
|
362
|
+
}
|
|
363
|
+
console.log(chalk.bold('Next steps'));
|
|
218
364
|
console.log('');
|
|
219
|
-
|
|
220
|
-
|
|
365
|
+
if (provider === 'railway') {
|
|
366
|
+
console.log(chalk.dim(' Railway needs one manual setting the CLI can\'t set for you:'));
|
|
367
|
+
console.log(chalk.dim(' Settings → Volumes → mount on /app/pb_data (size 1GB+)'));
|
|
368
|
+
console.log('');
|
|
369
|
+
}
|
|
370
|
+
console.log(chalk.dim(' Then upload your workspace into the deployed server:'));
|
|
371
|
+
console.log(chalk.dim(` primo push${url ? '' : ' -s <url>'}`));
|
|
372
|
+
console.log(chalk.dim(' (first push bootstraps the sites; later pushes update them incrementally)'));
|
|
373
|
+
console.log('');
|
|
374
|
+
}
|
|
375
|
+
async function deploy_to_fly(inventory) {
|
|
376
|
+
console.log('');
|
|
377
|
+
console.log(chalk.cyan('Deploying to Fly.io...'));
|
|
378
|
+
const app_name = workspace_app_name(inventory.root_dir);
|
|
221
379
|
const spinner = ora('Checking Fly.io app...').start();
|
|
222
380
|
try {
|
|
223
|
-
execSync(`fly status --app ${app_name}`, { cwd:
|
|
381
|
+
execSync(`fly status --app ${app_name}`, { cwd: inventory.root_dir, stdio: 'ignore' });
|
|
224
382
|
spinner.succeed(`Found existing app: ${app_name}`);
|
|
225
383
|
}
|
|
226
384
|
catch {
|
|
227
|
-
// App doesn't exist, create it
|
|
228
385
|
spinner.text = 'Creating Fly.io app...';
|
|
229
|
-
execSync(`fly apps create ${app_name}`, { cwd:
|
|
230
|
-
// Create volume for persistent data
|
|
386
|
+
execSync(`fly apps create ${app_name}`, { cwd: inventory.root_dir, stdio: 'inherit' });
|
|
231
387
|
spinner.text = 'Creating persistent volume...';
|
|
232
388
|
execSync(`fly volumes create pb_data --size 1 --region sjc --app ${app_name}`, {
|
|
233
|
-
cwd:
|
|
389
|
+
cwd: inventory.root_dir,
|
|
234
390
|
stdio: 'inherit'
|
|
235
391
|
});
|
|
236
392
|
spinner.succeed(`Created app: ${app_name}`);
|
|
237
393
|
}
|
|
238
|
-
// Deploy
|
|
239
394
|
console.log('');
|
|
240
395
|
console.log(chalk.dim('Building and deploying...'));
|
|
241
396
|
console.log('');
|
|
242
397
|
const deploy_process = spawn('fly', ['deploy'], {
|
|
243
|
-
cwd:
|
|
398
|
+
cwd: inventory.root_dir,
|
|
244
399
|
stdio: 'inherit'
|
|
245
400
|
});
|
|
246
401
|
return new Promise((resolve, reject) => {
|
|
247
|
-
deploy_process.on('close', (code) => {
|
|
248
|
-
if (code
|
|
249
|
-
console.log('');
|
|
250
|
-
console.log(chalk.green('✓ Deployment complete!'));
|
|
251
|
-
console.log('');
|
|
252
|
-
console.log(chalk.cyan(` https://${app_name}.fly.dev`));
|
|
253
|
-
console.log(chalk.cyan(` https://${app_name}.fly.dev/_/ (admin)`));
|
|
254
|
-
resolve();
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
402
|
+
deploy_process.on('close', async (code) => {
|
|
403
|
+
if (code !== 0) {
|
|
257
404
|
reject(new Error(`Fly deployment failed with code ${code}`));
|
|
405
|
+
return;
|
|
258
406
|
}
|
|
407
|
+
const url = `https://${app_name}.fly.dev`;
|
|
408
|
+
await record_workspace_server(inventory.root_dir, url);
|
|
409
|
+
print_post_deploy_next_steps('fly', url);
|
|
410
|
+
resolve();
|
|
259
411
|
});
|
|
260
412
|
});
|
|
261
413
|
}
|