@embeddables/cli 0.4.0 → 0.4.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 +46 -124
- package/dist/cli.js +19 -19
- package/dist/commands/save.d.ts.map +1 -1
- package/dist/commands/save.js +149 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,146 +1,68 @@
|
|
|
1
|
-
# Embeddables CLI
|
|
1
|
+
# Embeddables CLI (development)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This repo is the source for **@embeddables/cli** — the CLI for authoring and managing Embeddables locally with TypeScript/TSX. If you only want to _use_ the CLI, see the [package on npm](https://www.npmjs.com/package/@embeddables/cli) or the [user-facing README](./package-readme.md) (same content as the npm package page).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Prerequisites
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- **Build** TSX pages into the canonical Embeddable JSON format
|
|
9
|
-
- **Dev** mode with hot reload and proxy to a live Engine instance
|
|
10
|
-
- Full **TypeScript support** with autocomplete for all component primitives
|
|
7
|
+
- Node.js >= 18.0.0
|
|
11
8
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
Install the CLI globally:
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npm install -g @embeddables/cli
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Quick Start
|
|
9
|
+
## Setup
|
|
21
10
|
|
|
22
11
|
```bash
|
|
23
|
-
|
|
24
|
-
embeddables
|
|
25
|
-
|
|
26
|
-
# Install dependencies
|
|
12
|
+
git clone <this-repo>
|
|
13
|
+
cd embeddables-cli
|
|
27
14
|
npm install
|
|
28
|
-
|
|
29
|
-
# Login to your Embeddables account
|
|
30
|
-
embeddables login
|
|
31
|
-
|
|
32
|
-
# Pull an embeddable (shows a list to choose from)
|
|
33
|
-
embeddables pull
|
|
34
|
-
|
|
35
|
-
# Start dev server with hot reload
|
|
36
|
-
embeddables dev
|
|
37
15
|
```
|
|
38
16
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
embeddables/
|
|
43
|
-
<embeddable-id>/
|
|
44
|
-
pages/ # TSX page files
|
|
45
|
-
styles/ # CSS styles
|
|
46
|
-
computed-fields/ # Custom computed field logic
|
|
47
|
-
actions/ # Data output actions
|
|
48
|
-
config.json # Embeddable configuration
|
|
49
|
-
.generated/ # Compiled JSON output
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Commands
|
|
53
|
-
|
|
54
|
-
### `embeddables init`
|
|
55
|
-
Initialize a new Embeddables project. Creates `package.json`, `embeddables.json`, `.gitignore`, and an `embeddables/` directory.
|
|
56
|
-
|
|
57
|
-
If you're logged in, you'll see an interactive list of your projects to choose from. Otherwise, you can enter a project ID manually or skip.
|
|
58
|
-
|
|
59
|
-
Options:
|
|
60
|
-
- `--name <name>`: Project name (prompts if not provided)
|
|
61
|
-
- `--project-id <id>`: Embeddables project ID (shows list if logged in)
|
|
62
|
-
- `-y, --yes`: Skip prompts and use defaults
|
|
63
|
-
|
|
64
|
-
The project ID is stored in `embeddables.json` and enables interactive embeddable selection when running `embeddables pull`.
|
|
65
|
-
|
|
66
|
-
### `embeddables login`
|
|
67
|
-
Authenticate with your Embeddables account.
|
|
17
|
+
## Scripts
|
|
68
18
|
|
|
69
|
-
|
|
70
|
-
|
|
19
|
+
| Script | Description |
|
|
20
|
+
| ----------------------- | ----------------------------- |
|
|
21
|
+
| `npm run build` | Compile TypeScript to `dist/` |
|
|
22
|
+
| `npm run build:watch` | Watch and recompile on change |
|
|
23
|
+
| `npm test` | Run tests (Vitest) |
|
|
24
|
+
| `npm run test:watch` | Run tests in watch mode |
|
|
25
|
+
| `npm run test:coverage` | Coverage report |
|
|
26
|
+
| `npm run lint` | ESLint |
|
|
27
|
+
| `npm run format` | Prettier (write) |
|
|
28
|
+
| `npm run format:check` | Prettier (check only) |
|
|
71
29
|
|
|
72
|
-
|
|
73
|
-
Fetch an embeddable from the cloud and reverse-compile it into local TSX files.
|
|
30
|
+
## Local testing
|
|
74
31
|
|
|
75
|
-
|
|
76
|
-
1. If no project is configured, you'll be prompted to select one (saved to `embeddables.json`)
|
|
77
|
-
2. Then you'll see an interactive list of embeddables to choose from
|
|
32
|
+
After building, link the CLI so you can run `embeddables` from another project:
|
|
78
33
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
### `embeddables branch`
|
|
85
|
-
Switch to a different branch of a local embeddable. Shows an interactive list of branches to choose from.
|
|
86
|
-
|
|
87
|
-
Options:
|
|
88
|
-
- `--id <id>`: Embeddable ID (will prompt from local embeddables if not provided)
|
|
89
|
-
|
|
90
|
-
### `embeddables build --id <id>`
|
|
91
|
-
Compile TSX pages into the canonical JSON format.
|
|
92
|
-
|
|
93
|
-
Options:
|
|
94
|
-
- `--id <id>` (required): Embeddable ID
|
|
95
|
-
- `--pages <glob>`: Custom pages glob pattern
|
|
96
|
-
- `--out <path>`: Custom output path
|
|
97
|
-
|
|
98
|
-
### `embeddables dev`
|
|
99
|
-
Start a dev server with hot reload.
|
|
100
|
-
|
|
101
|
-
Options:
|
|
102
|
-
- `--id <id>`: Embeddable ID (will prompt if not provided)
|
|
103
|
-
- `--engine <url>`: Engine origin (default: `http://localhost:8787`)
|
|
104
|
-
- `--remote`: Use production engine (`https://engine.embeddables.com`)
|
|
105
|
-
- `--port <n>`: Dev proxy port (default: `3000`)
|
|
106
|
-
|
|
107
|
-
## Authoring with TypeScript
|
|
34
|
+
```bash
|
|
35
|
+
npm run build
|
|
36
|
+
npm link
|
|
37
|
+
```
|
|
108
38
|
|
|
109
|
-
|
|
39
|
+
Then in a project that uses the CLI (e.g. an embeddables app), run `embeddables <command>` — it will use your linked build. Unlink with `npm unlink -g @embeddables/cli` when done.
|
|
110
40
|
|
|
111
|
-
|
|
112
|
-
import { Container, InputBox, PlainText, CustomButton } from '@embeddables/cli/components'
|
|
41
|
+
You can also run the CLI without installing, from this repo:
|
|
113
42
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<InputBox
|
|
119
|
-
id="email"
|
|
120
|
-
key="email"
|
|
121
|
-
input_type="email"
|
|
122
|
-
label="Email"
|
|
123
|
-
placeholder="you@example.com"
|
|
124
|
-
isRequired
|
|
125
|
-
/>
|
|
126
|
-
<CustomButton id="submit" key="submit" text="Continue" action="next-page" />
|
|
127
|
-
</Container>
|
|
128
|
-
)
|
|
129
|
-
}
|
|
43
|
+
```bash
|
|
44
|
+
npx tsx src/cli.ts dev
|
|
45
|
+
# or
|
|
46
|
+
npx tsx src/cli.ts build -i <embeddable-id>
|
|
130
47
|
```
|
|
131
48
|
|
|
132
|
-
##
|
|
49
|
+
## Project structure
|
|
133
50
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
51
|
+
- **`src/cli.ts`** — Commander setup and command registration
|
|
52
|
+
- **`src/commands/`** — Implementations for `init`, `login`, `logout`, `pull`, `branch`, `build`, `save`, `dev`, `build-workbench`
|
|
53
|
+
- **`src/compiler/`** — TSX → JSON compile and JSON → TSX reverse-compile
|
|
54
|
+
- **`src/config/`** — `embeddables.json` and project config
|
|
55
|
+
- **`src/auth/`** — Login/logout and token storage
|
|
56
|
+
- **`src/prompts/`** — Interactive prompts (projects, embeddables, branches)
|
|
57
|
+
- **`src/proxy/`** — Dev server and engine proxy
|
|
58
|
+
- **`src/components/`** — React primitives exported as `@embeddables/cli/components`
|
|
59
|
+
- **`bin/embeddables.mjs`** — Entry script that runs `dist/cli.js`
|
|
60
|
+
- **`tests/`** — Unit and integration tests (Vitest)
|
|
137
61
|
|
|
138
|
-
|
|
139
|
-
npm run build
|
|
62
|
+
## Publishing
|
|
140
63
|
|
|
141
|
-
|
|
142
|
-
npm link
|
|
64
|
+
The user-facing README is in **`package-readme.md`**. It is swapped in as `README.md` during `npm pack` / `npm publish` via `prepack`/`postpack` scripts so the npm package shows the right docs. Do not remove those scripts.
|
|
143
65
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
66
|
+
## User documentation
|
|
67
|
+
|
|
68
|
+
Full command reference, installation, and usage for people who install the package: **[package-readme.md](./package-readme.md)**.
|
package/dist/cli.js
CHANGED
|
@@ -23,27 +23,27 @@ program.name('embeddables').description('Embeddables CLI').version('0.1.0');
|
|
|
23
23
|
program
|
|
24
24
|
.command('init')
|
|
25
25
|
.description('Initialize a new Embeddables project')
|
|
26
|
-
.option('--project-id <id>', 'Embeddables project ID')
|
|
26
|
+
.option('-p, --project-id <id>', 'Embeddables project ID')
|
|
27
27
|
.option('-y, --yes', 'Skip prompts and use defaults')
|
|
28
28
|
.action(async (opts) => {
|
|
29
29
|
await runInit({ projectId: opts.projectId, yes: opts.yes });
|
|
30
30
|
});
|
|
31
31
|
program
|
|
32
32
|
.command('build')
|
|
33
|
-
.requiredOption('--id <id>', 'Embeddable ID')
|
|
34
|
-
.option('--pages <glob>', 'Pages glob')
|
|
35
|
-
.option('--out <path>', 'Output json path')
|
|
33
|
+
.requiredOption('-i, --id <id>', 'Embeddable ID')
|
|
34
|
+
.option('-p, --pages <glob>', 'Pages glob')
|
|
35
|
+
.option('-o, --out <path>', 'Output json path')
|
|
36
36
|
.option('--pageKeyFrom <mode>', 'filename|export', 'filename')
|
|
37
37
|
.action(async (opts) => {
|
|
38
38
|
await runBuild(opts);
|
|
39
39
|
});
|
|
40
40
|
program
|
|
41
41
|
.command('dev')
|
|
42
|
-
.option('--id <id>', 'Embeddable ID (will prompt if not provided)')
|
|
43
|
-
.option('--pages <glob>', 'Pages glob')
|
|
44
|
-
.option('--out <path>', 'Output json path')
|
|
45
|
-
.option('--local', 'Use local engine (http://localhost:8787)')
|
|
46
|
-
.option('--engine <url>', 'Engine origin', 'https://engine.embeddables.com')
|
|
42
|
+
.option('-i, --id <id>', 'Embeddable ID (will prompt if not provided)')
|
|
43
|
+
.option('-p, --pages <glob>', 'Pages glob')
|
|
44
|
+
.option('-o, --out <path>', 'Output json path')
|
|
45
|
+
.option('-L, --local', 'Use local engine (http://localhost:8787)')
|
|
46
|
+
.option('-e, --engine <url>', 'Engine origin', 'https://engine.embeddables.com')
|
|
47
47
|
.option('--port <n>', 'Dev proxy port', '3000')
|
|
48
48
|
.option('--overrideRoute <path>', 'Route to override in proxy (exact match, no wildcards yet)', '/init')
|
|
49
49
|
.option('--pageKeyFrom <mode>', 'filename|export', 'filename')
|
|
@@ -69,20 +69,20 @@ program
|
|
|
69
69
|
program
|
|
70
70
|
.command('pull')
|
|
71
71
|
.description('Pull an embeddable from the cloud')
|
|
72
|
-
.option('--id <id>', 'Embeddable ID to pull (interactive selection if not provided)')
|
|
73
|
-
.option('--out <path>', 'Output json path')
|
|
74
|
-
.option('--branch <branch_id>', 'Embeddable branch ID')
|
|
75
|
-
.option('--fix', 'Fix by removing components missing required props (warn instead of error)')
|
|
72
|
+
.option('-i, --id <id>', 'Embeddable ID to pull (interactive selection if not provided)')
|
|
73
|
+
.option('-o, --out <path>', 'Output json path')
|
|
74
|
+
.option('-b, --branch <branch_id>', 'Embeddable branch ID')
|
|
75
|
+
.option('-f, --fix', 'Fix by removing components missing required props (warn instead of error)')
|
|
76
76
|
.action(async (opts) => {
|
|
77
77
|
await runPull(opts);
|
|
78
78
|
});
|
|
79
79
|
program
|
|
80
80
|
.command('save')
|
|
81
81
|
.description('Build and save an embeddable to the cloud')
|
|
82
|
-
.option('--id <id>', 'Embeddable ID (will prompt if not provided)')
|
|
83
|
-
.option('--label <label>', 'Human-readable label for this version')
|
|
84
|
-
.option('--branch <branch_id>', 'Branch ID to save to')
|
|
85
|
-
.option('--skip-build', 'Skip the build step and use existing compiled JSON')
|
|
82
|
+
.option('-i, --id <id>', 'Embeddable ID (will prompt if not provided)')
|
|
83
|
+
.option('-l, --label <label>', 'Human-readable label for this version')
|
|
84
|
+
.option('-b, --branch <branch_id>', 'Branch ID to save to')
|
|
85
|
+
.option('-s, --skip-build', 'Skip the build step and use existing compiled JSON')
|
|
86
86
|
.option('--from-version <number>', 'Base version number (auto-detected from local files if not provided)')
|
|
87
87
|
.action(async (opts) => {
|
|
88
88
|
await runSave({
|
|
@@ -96,14 +96,14 @@ program
|
|
|
96
96
|
program
|
|
97
97
|
.command('branch')
|
|
98
98
|
.description('Switch to a different branch of an embeddable')
|
|
99
|
-
.option('--id <id>', 'Embeddable ID (will prompt if not provided)')
|
|
99
|
+
.option('-i, --id <id>', 'Embeddable ID (will prompt if not provided)')
|
|
100
100
|
.action(async (opts) => {
|
|
101
101
|
await runBranch(opts);
|
|
102
102
|
});
|
|
103
103
|
program
|
|
104
104
|
.command('build-workbench')
|
|
105
105
|
.description('Build Workbench for CDN deployment')
|
|
106
|
-
.option('--out <path>', 'Output directory', 'dist/workbench')
|
|
106
|
+
.option('-o, --out <path>', 'Output directory', 'dist/workbench')
|
|
107
107
|
.option('--no-minify', 'Disable minification (for debugging)')
|
|
108
108
|
.action(async (opts) => {
|
|
109
109
|
await runBuildWorkbench(opts);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/commands/save.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/commands/save.ts"],"names":[],"mappings":"AAoJA,wBAAsB,OAAO,CAAC,IAAI,EAAE;IAClC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,iBAoBA"}
|
package/dist/commands/save.js
CHANGED
|
@@ -8,6 +8,48 @@ import { compileAllPages } from '../compiler/index.js';
|
|
|
8
8
|
import { formatError } from '../compiler/errors.js';
|
|
9
9
|
import { promptForLocalEmbeddable, promptForProject } from '../prompts/index.js';
|
|
10
10
|
import { WEB_APP_BASE_URL } from '../constants.js';
|
|
11
|
+
/** Error with optional gray detail line (hint/next step) for the user. */
|
|
12
|
+
class SaveError extends Error {
|
|
13
|
+
detail;
|
|
14
|
+
constructor(message, detail) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.detail = detail;
|
|
17
|
+
this.name = 'SaveError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse response body as JSON. Returns null if body is not valid JSON (e.g. HTML error page).
|
|
22
|
+
*/
|
|
23
|
+
async function safeParseJson(response) {
|
|
24
|
+
try {
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function isSaveErrorResponse(value) {
|
|
33
|
+
return (typeof value === 'object' &&
|
|
34
|
+
value !== null &&
|
|
35
|
+
'error' in value &&
|
|
36
|
+
typeof value.error === 'string');
|
|
37
|
+
}
|
|
38
|
+
function isSaveConflictResponse(value) {
|
|
39
|
+
return (typeof value === 'object' &&
|
|
40
|
+
value !== null &&
|
|
41
|
+
'latestVersionNumber' in value &&
|
|
42
|
+
'yourVersionNumber' in value &&
|
|
43
|
+
typeof value.latestVersionNumber === 'number' &&
|
|
44
|
+
typeof value.yourVersionNumber === 'number');
|
|
45
|
+
}
|
|
46
|
+
function isSaveResponse(value) {
|
|
47
|
+
return (typeof value === 'object' &&
|
|
48
|
+
value !== null &&
|
|
49
|
+
value.success === true &&
|
|
50
|
+
typeof value.data === 'object' &&
|
|
51
|
+
typeof value.data?.newVersionNumber === 'number');
|
|
52
|
+
}
|
|
11
53
|
/**
|
|
12
54
|
* Read `_version` from config.json for the given embeddable.
|
|
13
55
|
*/
|
|
@@ -67,6 +109,30 @@ function getLatestVersionFromFiles(generatedDir) {
|
|
|
67
109
|
return maxVersion;
|
|
68
110
|
}
|
|
69
111
|
export async function runSave(opts) {
|
|
112
|
+
try {
|
|
113
|
+
await runSaveInner(opts);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof SaveError) {
|
|
117
|
+
console.error(pc.red(`Save failed: ${error.message}`));
|
|
118
|
+
if (error.detail) {
|
|
119
|
+
console.log(pc.gray(error.detail));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (error instanceof Error) {
|
|
123
|
+
console.error(pc.red(`Save failed: ${error.message}`));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
console.error(pc.red('Save failed with an unexpected error.'));
|
|
127
|
+
}
|
|
128
|
+
process.exit(1);
|
|
129
|
+
// Rethrow only when exit was mocked to throw (e.g. in tests expecting rejection)
|
|
130
|
+
if (error instanceof Error && error.message === 'exit') {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function runSaveInner(opts) {
|
|
70
136
|
// 1. Check login
|
|
71
137
|
if (!isLoggedIn()) {
|
|
72
138
|
console.error(pc.red('Not logged in.'));
|
|
@@ -195,75 +261,106 @@ export async function runSave(opts) {
|
|
|
195
261
|
Authorization: `Bearer ${accessToken}`,
|
|
196
262
|
'Content-Type': 'application/json',
|
|
197
263
|
};
|
|
264
|
+
let response;
|
|
198
265
|
try {
|
|
199
|
-
|
|
266
|
+
response = await fetch(url, {
|
|
200
267
|
method: 'POST',
|
|
201
268
|
headers,
|
|
202
269
|
body: JSON.stringify(body),
|
|
203
270
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
271
|
+
}
|
|
272
|
+
catch (networkError) {
|
|
273
|
+
const message = networkError instanceof Error ? networkError.message : 'Network request failed';
|
|
274
|
+
if (message.includes('fetch') ||
|
|
275
|
+
message.includes('ECONNREFUSED') ||
|
|
276
|
+
message.includes('ETIMEDOUT') ||
|
|
277
|
+
message.includes('ENOTFOUND') ||
|
|
278
|
+
message.includes('network')) {
|
|
279
|
+
throw new SaveError(`Could not reach the server (${message}).`, `Check your connection and that the base URL is correct (currently ${WEB_APP_BASE_URL}).`);
|
|
280
|
+
}
|
|
281
|
+
throw new SaveError(`Network error: ${message}`, 'Check your network and firewall settings.');
|
|
282
|
+
}
|
|
283
|
+
if (response.status === 404) {
|
|
284
|
+
throw new SaveError('Save endpoint not found. The server may not support this feature or the URL may be incorrect.', `The request was sent to ${WEB_APP_BASE_URL}. If you use a custom deployment, ensure the save-version API is available there.`);
|
|
285
|
+
}
|
|
286
|
+
if (response.status === 401 || response.status === 403) {
|
|
287
|
+
throw new SaveError('Not authorized.', 'Run "embeddables login" to re-authenticate.');
|
|
288
|
+
}
|
|
289
|
+
if (response.status >= 500) {
|
|
290
|
+
throw new SaveError(`Server error (HTTP ${response.status}). Please try again later.`, 'If this keeps happening, try again later or contact support.');
|
|
291
|
+
}
|
|
292
|
+
if (response.status === 409) {
|
|
293
|
+
const conflictResult = await safeParseJson(response);
|
|
294
|
+
if (!conflictResult || !isSaveConflictResponse(conflictResult)) {
|
|
295
|
+
throw new SaveError(`Version conflict but invalid response (HTTP ${response.status}).`, 'Try saving again; if it persists, the server may be misconfigured.');
|
|
296
|
+
}
|
|
297
|
+
console.log('');
|
|
298
|
+
console.warn(pc.yellow(`⚠ Version conflict: the server has version ${conflictResult.latestVersionNumber}, but you are saving from version ${conflictResult.yourVersionNumber}.`));
|
|
299
|
+
const { forceSave } = await prompts({
|
|
300
|
+
type: 'confirm',
|
|
301
|
+
name: 'forceSave',
|
|
302
|
+
message: 'A newer version exists on the server. Save anyway?',
|
|
303
|
+
initial: false,
|
|
304
|
+
}, {
|
|
305
|
+
onCancel: () => {
|
|
306
|
+
process.exit(1);
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
if (!forceSave) {
|
|
310
|
+
console.log(pc.gray('Save cancelled.'));
|
|
311
|
+
process.exit(0);
|
|
312
|
+
}
|
|
313
|
+
// Retry with force flag
|
|
314
|
+
console.log(pc.cyan('Retrying save with force...'));
|
|
315
|
+
let forceResponse;
|
|
316
|
+
try {
|
|
317
|
+
forceResponse = await fetch(url, {
|
|
225
318
|
method: 'POST',
|
|
226
319
|
headers,
|
|
227
320
|
body: JSON.stringify({ ...body, force: true }),
|
|
228
321
|
});
|
|
229
|
-
const forceResult = (await forceResponse.json());
|
|
230
|
-
if (!forceResponse.ok) {
|
|
231
|
-
const errorResult = forceResult;
|
|
232
|
-
throw new Error(errorResult.error || `HTTP ${forceResponse.status}`);
|
|
233
|
-
}
|
|
234
|
-
const successResult = forceResult;
|
|
235
|
-
const { newVersionNumber } = successResult.data;
|
|
236
|
-
console.log(pc.green(`✓ Saved as version ${newVersionNumber}`));
|
|
237
|
-
setVersionInConfig(embeddableId, newVersionNumber);
|
|
238
|
-
const versionedPath = path.join(generatedDir, `embeddable-v${newVersionNumber}.json`);
|
|
239
|
-
fs.mkdirSync(generatedDir, { recursive: true });
|
|
240
|
-
fs.writeFileSync(versionedPath, jsonContent, 'utf8');
|
|
241
|
-
console.log(pc.cyan(`✓ Saved version file to ${versionedPath}`));
|
|
242
|
-
return;
|
|
243
322
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
323
|
+
catch (forceNetworkError) {
|
|
324
|
+
const message = forceNetworkError instanceof Error ? forceNetworkError.message : 'Network request failed';
|
|
325
|
+
throw new SaveError(`Retry failed: ${message}`, 'The initial save hit a version conflict; the force-save retry could not reach the server.');
|
|
326
|
+
}
|
|
327
|
+
if (forceResponse.status === 404) {
|
|
328
|
+
throw new SaveError('Save endpoint not found. The server may not support this feature or the URL may be incorrect.', `The request was sent to ${WEB_APP_BASE_URL}. If you use a custom deployment, ensure the save-version API is available there.`);
|
|
329
|
+
}
|
|
330
|
+
const forceResult = await safeParseJson(forceResponse);
|
|
331
|
+
if (!forceResponse.ok) {
|
|
332
|
+
const errorMessage = forceResult && isSaveErrorResponse(forceResult)
|
|
333
|
+
? forceResult.error
|
|
334
|
+
: `HTTP ${forceResponse.status}`;
|
|
335
|
+
throw new SaveError(errorMessage, 'If the problem persists, run "embeddables login" or try again later.');
|
|
336
|
+
}
|
|
337
|
+
if (!forceResult || !isSaveResponse(forceResult)) {
|
|
338
|
+
throw new SaveError(`Invalid response from server (HTTP ${forceResponse.status}).`, 'The server returned an unexpected format. Try again or contact support if it persists.');
|
|
248
339
|
}
|
|
249
|
-
const
|
|
250
|
-
const { newVersionNumber } = successResult.data;
|
|
340
|
+
const { newVersionNumber } = forceResult.data;
|
|
251
341
|
console.log(pc.green(`✓ Saved as version ${newVersionNumber}`));
|
|
252
|
-
// Update _version in config.json so future saves know the base version
|
|
253
342
|
setVersionInConfig(embeddableId, newVersionNumber);
|
|
254
|
-
// Also save the versioned file to .generated/ as a snapshot
|
|
255
343
|
const versionedPath = path.join(generatedDir, `embeddable-v${newVersionNumber}.json`);
|
|
256
344
|
fs.mkdirSync(generatedDir, { recursive: true });
|
|
257
345
|
fs.writeFileSync(versionedPath, jsonContent, 'utf8');
|
|
258
346
|
console.log(pc.cyan(`✓ Saved version file to ${versionedPath}`));
|
|
347
|
+
return;
|
|
259
348
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
process.exit(1);
|
|
349
|
+
const result = await safeParseJson(response);
|
|
350
|
+
if (!response.ok) {
|
|
351
|
+
const errorMessage = result && isSaveErrorResponse(result) ? result.error : `HTTP ${response.status}`;
|
|
352
|
+
throw new SaveError(errorMessage, 'If the problem persists, run "embeddables login" or try again later.');
|
|
353
|
+
}
|
|
354
|
+
if (!result || !isSaveResponse(result)) {
|
|
355
|
+
throw new SaveError(`Invalid response from server (HTTP ${response.status}).`, 'The server returned an unexpected format. Try again or contact support if it persists.');
|
|
268
356
|
}
|
|
357
|
+
const { newVersionNumber } = result.data;
|
|
358
|
+
console.log(pc.green(`✓ Saved as version ${newVersionNumber}`));
|
|
359
|
+
// Update _version in config.json so future saves know the base version
|
|
360
|
+
setVersionInConfig(embeddableId, newVersionNumber);
|
|
361
|
+
// Also save the versioned file to .generated/ as a snapshot
|
|
362
|
+
const versionedPath = path.join(generatedDir, `embeddable-v${newVersionNumber}.json`);
|
|
363
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
364
|
+
fs.writeFileSync(versionedPath, jsonContent, 'utf8');
|
|
365
|
+
console.log(pc.cyan(`✓ Saved version file to ${versionedPath}`));
|
|
269
366
|
}
|