statelyai 0.7.3 → 0.7.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 +322 -28
- package/dist/bin.mjs +1 -1
- package/dist/{cli-DR7_Qkzm.mjs → cli-DRKY520A.mjs} +212 -121
- package/dist/index.d.mts +55 -7
- package/dist/index.mjs +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,48 +1,126 @@
|
|
|
1
1
|
# statelyai
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`statelyai` connects local machine source files to Stately Studio. Use it to
|
|
4
|
+
open a file in the visual editor, compare local and remote machines, or sync a
|
|
5
|
+
project in both directions.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
## Run the CLI
|
|
6
8
|
|
|
7
|
-
<!--
|
|
8
|
-
## Happy Path
|
|
9
|
+
<!-- package name and binary from package.json -->
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
Run without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx statelyai --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or install it globally:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install --global statelyai
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Choose a workflow
|
|
24
|
+
|
|
25
|
+
<!-- command capabilities and side effects from src/cli.ts#COMMANDS -->
|
|
26
|
+
|
|
27
|
+
| Goal | Command | Writes |
|
|
28
|
+
| ---------------------------- | ---------------------------------------------------- | ------------------------------------------------------- |
|
|
29
|
+
| Edit one local file visually | `statelyai open <file>` | Local file, when you save in the editor |
|
|
30
|
+
| Set up project sync | `statelyai init [--scan]` | Remote project and local `statelyai.json` |
|
|
31
|
+
| Inspect project machines | `statelyai status` | Nothing |
|
|
32
|
+
| Preview project uploads | `statelyai push --dry-run` | Nothing |
|
|
33
|
+
| Upload local machines | `statelyai push [file]` | Remote machines and local `@statelyai` IDs |
|
|
34
|
+
| Download project machines | `statelyai pull` | Linked local files and new files under `newMachinesDir` |
|
|
35
|
+
| Compare two machines | `statelyai diff <source> <target>` | Nothing |
|
|
36
|
+
| Fail on differences in CI | `statelyai diff <source> <target> --fail-on-changes` | Nothing |
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
<!-- project initialization and sync flow from InitCommand, PushCommand, PullCommand, and projectConfig.ts -->
|
|
41
|
+
|
|
42
|
+
For hosted Stately, authenticate once:
|
|
11
43
|
|
|
12
44
|
```bash
|
|
13
45
|
statelyai login
|
|
14
46
|
```
|
|
15
47
|
|
|
16
|
-
|
|
17
|
-
API-key auth.
|
|
48
|
+
Initialize the current repository and scan for XState files:
|
|
18
49
|
|
|
19
|
-
|
|
50
|
+
```bash
|
|
51
|
+
statelyai init --scan
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`--scan` suggests `include` globs and asks before saving them. Without
|
|
55
|
+
`--scan`, `init` creates `statelyai.json` with an empty `include` list.
|
|
56
|
+
|
|
57
|
+
Preview the files and remote operations:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
statelyai push --dry-run
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then push and pull:
|
|
20
64
|
|
|
21
65
|
```bash
|
|
22
|
-
statelyai
|
|
66
|
+
statelyai push
|
|
67
|
+
statelyai pull
|
|
23
68
|
```
|
|
24
69
|
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
70
|
+
`push` creates remote machines for unlinked source machines, updates already
|
|
71
|
+
linked machines, and writes returned IDs into source comments. `pull` updates
|
|
72
|
+
linked files. If `newMachinesDir` is configured, it also creates local files
|
|
73
|
+
for machines that exist only in the Studio project.
|
|
74
|
+
|
|
75
|
+
## Authentication
|
|
30
76
|
|
|
31
|
-
|
|
77
|
+
<!-- authentication flags, environment resolution, and credential storage from src/cli.ts and src/credentials.ts -->
|
|
78
|
+
|
|
79
|
+
Browser OAuth is the default:
|
|
32
80
|
|
|
33
81
|
```bash
|
|
34
|
-
statelyai
|
|
82
|
+
statelyai login
|
|
35
83
|
```
|
|
36
84
|
|
|
37
|
-
|
|
85
|
+
Use an API key instead:
|
|
38
86
|
|
|
39
87
|
```bash
|
|
40
|
-
statelyai
|
|
88
|
+
statelyai login --api-key
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Create or manage keys in [Stately API key settings](https://stately.ai/registry/user/my-settings?tab=API+Key).
|
|
92
|
+
|
|
93
|
+
For noninteractive input:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
printf '%s\n' "$STATELY_API_KEY" | statelyai login --stdin
|
|
41
97
|
```
|
|
42
98
|
|
|
43
|
-
|
|
99
|
+
For automation, prefer an environment variable instead of storing a
|
|
100
|
+
credential. Resolution order is:
|
|
101
|
+
|
|
102
|
+
1. `STATELY_ACCESS_TOKEN`
|
|
103
|
+
2. `STATELY_API_KEY`
|
|
104
|
+
3. `NEXT_PUBLIC_STATELY_API_KEY`
|
|
105
|
+
4. the credential stored by `statelyai login`
|
|
106
|
+
|
|
107
|
+
The CLI loads `.env.local` from the current directory before resolving these
|
|
108
|
+
variables.
|
|
109
|
+
|
|
110
|
+
Use `statelyai status --auth` to report the selected source without printing
|
|
111
|
+
the credential. `statelyai logout` removes stored credentials but does not modify
|
|
112
|
+
environment variables.
|
|
44
113
|
|
|
45
|
-
|
|
114
|
+
Stored credentials use macOS Keychain or Linux Secret Service when available,
|
|
115
|
+
with a private config file as fallback. Set `STATELYAI_CREDENTIALS_BACKEND=file`
|
|
116
|
+
to force file storage, or `STATELYAI_CONFIG_DIR` to choose its directory.
|
|
117
|
+
|
|
118
|
+
## `statelyai.json`
|
|
119
|
+
|
|
120
|
+
<!-- config shape, defaults, and migration behavior from src/projectConfig.ts -->
|
|
121
|
+
|
|
122
|
+
`push` and `pull` use `statelyai.json` from the current directory unless
|
|
123
|
+
`--config` supplies another path.
|
|
46
124
|
|
|
47
125
|
```json
|
|
48
126
|
{
|
|
@@ -53,34 +131,250 @@ That writes a `statelyai.json` like:
|
|
|
53
131
|
"defaultXStateVersion": 5,
|
|
54
132
|
"include": ["src/**/*.ts"],
|
|
55
133
|
"exclude": ["**/*.test.*", "**/*.spec.*"],
|
|
56
|
-
"newMachinesDir": "src"
|
|
134
|
+
"newMachinesDir": "src/machines"
|
|
57
135
|
}
|
|
58
136
|
```
|
|
59
137
|
|
|
60
|
-
|
|
138
|
+
| Field | Purpose |
|
|
139
|
+
| ---------------------- | --------------------------------------------------------------- |
|
|
140
|
+
| `$schema` | Published JSON Schema URL. |
|
|
141
|
+
| `version` | Config format version. Currently `1.0.0`. |
|
|
142
|
+
| `projectId` | Remote Studio project ID. |
|
|
143
|
+
| `studioUrl` | Studio API origin. |
|
|
144
|
+
| `defaultXStateVersion` | XState version used when creating remote machines. Minimum `5`. |
|
|
145
|
+
| `include` | Source globs used by project-wide `push` and `pull`. |
|
|
146
|
+
| `exclude` | Globs removed from discovery. Defaults to tests and specs. |
|
|
147
|
+
| `newMachinesDir` | Destination for remote-only machines created by `pull`. |
|
|
148
|
+
|
|
149
|
+
Project-wide `push` currently discovers machine-bearing JavaScript and
|
|
150
|
+
TypeScript files configured as `xstate` or `auto`. The config schema accepts
|
|
151
|
+
other format names, but project discovery does not push them.
|
|
152
|
+
|
|
153
|
+
Mutating project commands rewrite legacy configs with one `sources` entry to
|
|
154
|
+
the current top-level shape. `status` and `push --dry-run` normalize legacy
|
|
155
|
+
config in memory without writing it. Multiple legacy entries cannot be merged
|
|
156
|
+
safely and require manual migration.
|
|
157
|
+
|
|
158
|
+
## Commands
|
|
159
|
+
|
|
160
|
+
<!-- command names, arguments, flags, defaults, and behavior from src/cli.ts#COMMANDS -->
|
|
161
|
+
|
|
162
|
+
### `login`
|
|
163
|
+
|
|
164
|
+
Store an OAuth credential or API key.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
statelyai login
|
|
168
|
+
statelyai login --api-key
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Flags: `--api-key`, `--stdin`, `--base-url`.
|
|
172
|
+
|
|
173
|
+
### `logout`
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
statelyai logout
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`logout` deletes stored credentials. Environment variables are unchanged.
|
|
180
|
+
|
|
181
|
+
### `status`
|
|
182
|
+
|
|
183
|
+
Inspect the configured project and classify local and remote machines:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
statelyai status
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Statuses are `linked`, `local-only`, `remote-only`, and `missing-remote`.
|
|
190
|
+
Use `--json` for automation or `--auth` to show only credential resolution.
|
|
191
|
+
`status` is read-only and never migrates `statelyai.json`.
|
|
192
|
+
|
|
193
|
+
Flags: `--config <path>`, `--base-url <url>`, `--json`, `--auth`.
|
|
194
|
+
|
|
195
|
+
### `init`
|
|
196
|
+
|
|
197
|
+
Create or reuse a Studio project and write `statelyai.json`.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
statelyai init --name "Checkout" --visibility Private --scan
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Flags:
|
|
204
|
+
|
|
205
|
+
- `--name <name>` sets the remote project name.
|
|
206
|
+
- `--visibility Private|Public|Unlisted` defaults to `Private`.
|
|
207
|
+
- `--scan` proposes source globs interactively.
|
|
208
|
+
- `--force` replaces an existing config.
|
|
209
|
+
- `--base-url <url>` overrides the Studio API origin.
|
|
210
|
+
|
|
211
|
+
### `open`
|
|
212
|
+
|
|
213
|
+
Start a local bridge and open one source file in the browser editor:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
statelyai open src/checkout.machine.ts
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Saved file changes refresh the editor. Saving visual edits writes them back to
|
|
220
|
+
the source file.
|
|
221
|
+
|
|
222
|
+
Flags:
|
|
223
|
+
|
|
224
|
+
- `--editor-url <url>` selects the editor origin. Default:
|
|
225
|
+
`https://editor.stately.ai`.
|
|
226
|
+
- `--host <host>` sets the local bridge host. Default: `127.0.0.1`.
|
|
227
|
+
- `--port <port>` selects a port. Default: a random available port.
|
|
228
|
+
- `--no-open` starts the bridge without launching a browser.
|
|
229
|
+
- `--debug` logs editor protocol messages.
|
|
230
|
+
|
|
231
|
+
### `diff`
|
|
232
|
+
|
|
233
|
+
Compare two locators after normalizing them to graph form:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
statelyai diff src/checkout.machine.ts machine_123 --fail-on-changes
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Locators may be:
|
|
240
|
+
|
|
241
|
+
- a local JavaScript or TypeScript machine file
|
|
242
|
+
- a local XState JSON, Stately graph JSON, or Studio digraph JSON file
|
|
243
|
+
- a Studio machine ID
|
|
244
|
+
- a Studio machine URL
|
|
245
|
+
|
|
246
|
+
`--fail-on-changes` exits with status `1` when the normalized graphs differ.
|
|
247
|
+
Use `--base-url` for remote IDs at another Studio origin. `plan` remains a
|
|
248
|
+
hidden compatibility alias for `diff`.
|
|
249
|
+
|
|
250
|
+
### `push`
|
|
251
|
+
|
|
252
|
+
Push every discovered machine in `statelyai.json`:
|
|
61
253
|
|
|
62
254
|
```bash
|
|
63
255
|
statelyai push
|
|
64
256
|
```
|
|
65
257
|
|
|
66
|
-
|
|
258
|
+
Push one file while still using the project and defaults from the config:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
statelyai push src/checkout.machine.ts
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Preview discovery and link/update decisions without a credential:
|
|
67
265
|
|
|
68
266
|
```bash
|
|
69
267
|
statelyai push --dry-run
|
|
70
268
|
```
|
|
71
269
|
|
|
72
|
-
|
|
270
|
+
`--dry-run` does not create or update machines, write `@statelyai` IDs, or
|
|
271
|
+
migrate legacy configuration.
|
|
272
|
+
|
|
273
|
+
Flags: `--config <path>`, `--base-url <url>`, `--dry-run`.
|
|
274
|
+
|
|
275
|
+
### `pull`
|
|
73
276
|
|
|
74
|
-
|
|
277
|
+
Pull all linked files from the configured project:
|
|
75
278
|
|
|
76
279
|
```bash
|
|
77
280
|
statelyai pull
|
|
78
281
|
```
|
|
79
282
|
|
|
80
|
-
|
|
283
|
+
Pull a linked file using its `@statelyai` ID:
|
|
81
284
|
|
|
82
|
-
|
|
285
|
+
```bash
|
|
286
|
+
statelyai pull src/checkout.machine.ts
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Pull a machine ID or URL to an explicit target:
|
|
83
290
|
|
|
84
291
|
```bash
|
|
85
|
-
|
|
292
|
+
statelyai pull machine_123 src/checkout.machine.ts
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
New targets support JavaScript/TypeScript, `.digraph.json`, and `.graph.json`.
|
|
296
|
+
For existing JSON targets, the current file shape determines the output format.
|
|
297
|
+
|
|
298
|
+
Project-wide pull skips linked files with uncommitted Git changes. Pass
|
|
299
|
+
`--force` to overwrite them. Remote-only machines are skipped until
|
|
300
|
+
`newMachinesDir` is set.
|
|
301
|
+
|
|
302
|
+
Flags: `--config <path>`, `--base-url <url>`, `--force`.
|
|
303
|
+
|
|
304
|
+
## CI examples
|
|
305
|
+
|
|
306
|
+
<!-- noninteractive and exit-code behavior from credential resolution, DiffCommand, and PushCommand -->
|
|
307
|
+
|
|
308
|
+
Fail when a local machine differs from Studio:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
npx statelyai diff src/checkout.machine.ts machine_123 --fail-on-changes
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Provide `STATELY_API_KEY` or `STATELY_ACCESS_TOKEN` through the CI runner's
|
|
315
|
+
secret environment.
|
|
316
|
+
|
|
317
|
+
Check project discovery without network credentials or writes:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
npx statelyai push --dry-run
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Set `NO_COLOR=1` or `CI=true` for plain output.
|
|
324
|
+
|
|
325
|
+
## Common problems
|
|
326
|
+
|
|
327
|
+
<!-- error and safeguard behavior from src/cli.ts and src/projectConfig.ts -->
|
|
328
|
+
|
|
329
|
+
- **The server returns `401`:** run `statelyai login`, or set
|
|
330
|
+
`STATELY_ACCESS_TOKEN` or `STATELY_API_KEY`.
|
|
331
|
+
- **`push` finds no files:** check `include` and `exclude`; `init` without
|
|
332
|
+
`--scan` intentionally leaves `include` empty.
|
|
333
|
+
- **`push` matches files but finds no machines:** project discovery currently
|
|
334
|
+
requires XState imports plus `createMachine(...)` or
|
|
335
|
+
`setup(...).createMachine(...)`.
|
|
336
|
+
- **Remote-only machines are skipped:** set `newMachinesDir`, then run
|
|
337
|
+
`statelyai pull` again.
|
|
338
|
+
- **`pull` refuses to overwrite a file:** commit or stash its changes, or pass
|
|
339
|
+
`--force` if overwriting is intentional.
|
|
340
|
+
- **A linked remote machine was deleted or is inaccessible:** interactive
|
|
341
|
+
`push` offers to relink it as a new remote machine and replaces the local ID.
|
|
342
|
+
|
|
343
|
+
Run `statelyai <command> --help` for generated command syntax and flags.
|
|
344
|
+
|
|
345
|
+
## Self-hosting
|
|
346
|
+
|
|
347
|
+
<!-- self-host auth and URL behavior from LoginCommand, baseUrlFlags, OpenCommand, credential resolution, and projectConfig.ts -->
|
|
348
|
+
|
|
349
|
+
Skip login when the server has authentication disabled. The CLI sends no
|
|
350
|
+
authorization header when no credential exists; the server decides whether
|
|
351
|
+
authentication is required. This also applies to `open` editor-sync requests
|
|
352
|
+
and CI commands.
|
|
353
|
+
|
|
354
|
+
For OAuth, point login at the deployment's protected resource:
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
statelyai login --base-url https://editor.example.com/api/mcp
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Passing only an origin uses its `/api/mcp` resource. The resource must
|
|
361
|
+
advertise an authorization server. If that server does not support dynamic
|
|
362
|
+
client registration, set `STATELY_OAUTH_CLIENT_ID`.
|
|
363
|
+
|
|
364
|
+
URL settings are intentionally scoped:
|
|
365
|
+
|
|
366
|
+
| Setting | Used by | Meaning |
|
|
367
|
+
| ------------------ | ---------------------------------------- | ---------------------------------------------------------- |
|
|
368
|
+
| `--base-url` | `init`, `status`, `diff`, `push`, `pull` | Studio API origin. |
|
|
369
|
+
| `studioUrl` | `statelyai.json` project sync | Default Studio API origin for that project. |
|
|
370
|
+
| `login --base-url` | `login` only | OAuth protected-resource URL used for discovery. |
|
|
371
|
+
| `--editor-url` | `open` only | Visual editor origin. Default `https://editor.stately.ai`. |
|
|
372
|
+
|
|
373
|
+
A complete setup may look like:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
statelyai login --base-url https://editor.example.com/api/mcp
|
|
377
|
+
statelyai init --base-url https://studio.example.com --scan
|
|
378
|
+
statelyai open src/checkout.machine.ts \
|
|
379
|
+
--editor-url https://editor.example.com
|
|
86
380
|
```
|
package/dist/bin.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
2
|
import fs, { promises, watch } from "node:fs";
|
|
3
3
|
import fsPromises from "node:fs/promises";
|
|
4
|
-
import * as http from "node:http";
|
|
5
4
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
6
5
|
import * as path$1 from "node:path";
|
|
7
6
|
import path from "node:path";
|
|
@@ -10,6 +9,7 @@ import { Writable } from "node:stream";
|
|
|
10
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
10
|
import { promisify } from "node:util";
|
|
12
11
|
import { Args, Command, Flags, flush, handle, run } from "@oclif/core";
|
|
12
|
+
import * as http from "node:http";
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import { StudioApiError, buildAuthorizeUrl, createPkcePair, createStatelyClient, discoverAuthorizationServer, discoverProtectedResource, exchangeCodeForToken, getStatelyPragma, registerOAuthClient } from "@statelyai/sdk";
|
|
15
15
|
import { planSync, pullSync, pushLocalMachineLinks } from "@statelyai/sdk/sync";
|
|
@@ -698,6 +698,51 @@ function escapeAttribute(value) {
|
|
|
698
698
|
return value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
+
//#endregion
|
|
702
|
+
//#region src/oauthCallbackServer.ts
|
|
703
|
+
async function createLocalOAuthCallbackServer(options) {
|
|
704
|
+
let resolveCode;
|
|
705
|
+
let rejectCode;
|
|
706
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
707
|
+
resolveCode = resolve;
|
|
708
|
+
rejectCode = reject;
|
|
709
|
+
});
|
|
710
|
+
const server = http.createServer((req, res) => {
|
|
711
|
+
const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
712
|
+
const code = requestUrl.searchParams.get("code");
|
|
713
|
+
const state = requestUrl.searchParams.get("state");
|
|
714
|
+
const error = requestUrl.searchParams.get("error");
|
|
715
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
716
|
+
res.setHeader("Connection", "close");
|
|
717
|
+
if (error) {
|
|
718
|
+
res.statusCode = 400;
|
|
719
|
+
res.end("<!doctype html><title>Sign in failed</title><p>Sign in failed. Return to the terminal.</p>");
|
|
720
|
+
rejectCode(new Error(error));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (!code || state !== options.state) {
|
|
724
|
+
res.statusCode = 400;
|
|
725
|
+
res.end("<!doctype html><title>Sign in failed</title><p>Invalid sign-in callback. Return to the terminal.</p>");
|
|
726
|
+
rejectCode(/* @__PURE__ */ new Error("Invalid OAuth callback."));
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
res.end("<!doctype html><title>Signed in</title><p>Signed in. You can close this window.</p>");
|
|
730
|
+
resolveCode(code);
|
|
731
|
+
});
|
|
732
|
+
await new Promise((resolve) => {
|
|
733
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
734
|
+
});
|
|
735
|
+
const address = server.address();
|
|
736
|
+
if (!address || typeof address === "string") throw new Error("Could not start OAuth callback server.");
|
|
737
|
+
return {
|
|
738
|
+
redirectUri: `http://127.0.0.1:${address.port}/callback`,
|
|
739
|
+
waitForCode: () => codePromise,
|
|
740
|
+
close: () => new Promise((resolve) => {
|
|
741
|
+
server.close(() => resolve());
|
|
742
|
+
})
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
701
746
|
//#endregion
|
|
702
747
|
//#region src/credentials.ts
|
|
703
748
|
const execFile$1 = promisify(execFile);
|
|
@@ -787,13 +832,10 @@ async function writeFileCredentials(apiKey) {
|
|
|
787
832
|
location: stored.location
|
|
788
833
|
};
|
|
789
834
|
}
|
|
790
|
-
async function writeFileStoredCredential(credential
|
|
835
|
+
async function writeFileStoredCredential(credential) {
|
|
791
836
|
const configDir = getConfigDir();
|
|
792
837
|
const credentialsPath = getCredentialsFilePath();
|
|
793
|
-
const payload =
|
|
794
|
-
version: 1,
|
|
795
|
-
authMode: "none"
|
|
796
|
-
} : {
|
|
838
|
+
const payload = {
|
|
797
839
|
version: 1,
|
|
798
840
|
credential
|
|
799
841
|
};
|
|
@@ -808,7 +850,6 @@ async function writeFileStoredCredential(credential, authMode) {
|
|
|
808
850
|
await promises.chmod(credentialsPath, 384);
|
|
809
851
|
return {
|
|
810
852
|
...credential ? { credential } : {},
|
|
811
|
-
...authMode ? { authMode } : {},
|
|
812
853
|
backend: "file",
|
|
813
854
|
location: credentialsPath
|
|
814
855
|
};
|
|
@@ -848,12 +889,9 @@ async function writeMacOSKeychain(apiKey) {
|
|
|
848
889
|
location: stored.location
|
|
849
890
|
} : void 0;
|
|
850
891
|
}
|
|
851
|
-
async function writeMacOSStoredCredential(credential
|
|
892
|
+
async function writeMacOSStoredCredential(credential) {
|
|
852
893
|
if (process.platform !== "darwin") return;
|
|
853
|
-
const payload =
|
|
854
|
-
version: 1,
|
|
855
|
-
authMode: "none"
|
|
856
|
-
} : {
|
|
894
|
+
const payload = {
|
|
857
895
|
version: 1,
|
|
858
896
|
credential
|
|
859
897
|
};
|
|
@@ -870,7 +908,6 @@ async function writeMacOSStoredCredential(credential, authMode) {
|
|
|
870
908
|
]);
|
|
871
909
|
return {
|
|
872
910
|
...credential ? { credential } : {},
|
|
873
|
-
...authMode ? { authMode } : {},
|
|
874
911
|
backend: "macos-keychain",
|
|
875
912
|
location: "macOS Keychain"
|
|
876
913
|
};
|
|
@@ -919,12 +956,9 @@ async function writeLinuxSecretTool(apiKey) {
|
|
|
919
956
|
location: stored.location
|
|
920
957
|
} : void 0;
|
|
921
958
|
}
|
|
922
|
-
async function writeLinuxStoredCredential(credential
|
|
959
|
+
async function writeLinuxStoredCredential(credential) {
|
|
923
960
|
if (process.platform !== "linux") return;
|
|
924
|
-
const payload =
|
|
925
|
-
version: 1,
|
|
926
|
-
authMode: "none"
|
|
927
|
-
} : {
|
|
961
|
+
const payload = {
|
|
928
962
|
version: 1,
|
|
929
963
|
credential
|
|
930
964
|
};
|
|
@@ -940,7 +974,6 @@ async function writeLinuxStoredCredential(credential, authMode) {
|
|
|
940
974
|
], { input: JSON.stringify(payload) });
|
|
941
975
|
return {
|
|
942
976
|
...credential ? { credential } : {},
|
|
943
|
-
...authMode ? { authMode } : {},
|
|
944
977
|
backend: "linux-secret-tool",
|
|
945
978
|
location: "Secret Service (secret-tool)"
|
|
946
979
|
};
|
|
@@ -975,10 +1008,6 @@ async function setStoredCredential(credential) {
|
|
|
975
1008
|
if (shouldUseFileBackendOnly()) return writeFileStoredCredential(credential);
|
|
976
1009
|
return await writeMacOSStoredCredential(credential) ?? await writeLinuxStoredCredential(credential) ?? await writeFileStoredCredential(credential);
|
|
977
1010
|
}
|
|
978
|
-
async function setStoredNoAuth() {
|
|
979
|
-
if (shouldUseFileBackendOnly()) return writeFileStoredCredential(void 0, "none");
|
|
980
|
-
return await writeMacOSStoredCredential(void 0, "none") ?? await writeLinuxStoredCredential(void 0, "none") ?? await writeFileStoredCredential(void 0, "none");
|
|
981
|
-
}
|
|
982
1011
|
async function deleteStoredApiKey() {
|
|
983
1012
|
if (shouldUseFileBackendOnly()) {
|
|
984
1013
|
const deleted = await deleteFileCredentials();
|
|
@@ -1511,47 +1540,6 @@ function createOAuthCredential(tokenResponse, now = Date.now()) {
|
|
|
1511
1540
|
...expiresAt ? { expiresAt } : {}
|
|
1512
1541
|
};
|
|
1513
1542
|
}
|
|
1514
|
-
async function createLocalOAuthCallbackServer(options) {
|
|
1515
|
-
let resolveCode;
|
|
1516
|
-
let rejectCode;
|
|
1517
|
-
const codePromise = new Promise((resolve, reject) => {
|
|
1518
|
-
resolveCode = resolve;
|
|
1519
|
-
rejectCode = reject;
|
|
1520
|
-
});
|
|
1521
|
-
const server = http.createServer((req, res) => {
|
|
1522
|
-
const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1523
|
-
const code = requestUrl.searchParams.get("code");
|
|
1524
|
-
const state = requestUrl.searchParams.get("state");
|
|
1525
|
-
const error = requestUrl.searchParams.get("error");
|
|
1526
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1527
|
-
if (error) {
|
|
1528
|
-
res.statusCode = 400;
|
|
1529
|
-
res.end("<!doctype html><title>Sign in failed</title><p>Sign in failed. Return to the terminal.</p>");
|
|
1530
|
-
rejectCode(new Error(error));
|
|
1531
|
-
return;
|
|
1532
|
-
}
|
|
1533
|
-
if (!code || state !== options.state) {
|
|
1534
|
-
res.statusCode = 400;
|
|
1535
|
-
res.end("<!doctype html><title>Sign in failed</title><p>Invalid sign-in callback. Return to the terminal.</p>");
|
|
1536
|
-
rejectCode(/* @__PURE__ */ new Error("Invalid OAuth callback."));
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
1539
|
-
res.end("<!doctype html><title>Signed in</title><p>Signed in. You can close this window.</p>");
|
|
1540
|
-
resolveCode(code);
|
|
1541
|
-
});
|
|
1542
|
-
await new Promise((resolve) => {
|
|
1543
|
-
server.listen(0, "127.0.0.1", resolve);
|
|
1544
|
-
});
|
|
1545
|
-
const address = server.address();
|
|
1546
|
-
if (!address || typeof address === "string") throw new Error("Could not start OAuth callback server.");
|
|
1547
|
-
return {
|
|
1548
|
-
redirectUri: `http://127.0.0.1:${address.port}/callback`,
|
|
1549
|
-
waitForCode: () => codePromise,
|
|
1550
|
-
close: () => new Promise((resolve) => {
|
|
1551
|
-
server.close(() => resolve());
|
|
1552
|
-
})
|
|
1553
|
-
};
|
|
1554
|
-
}
|
|
1555
1543
|
async function buildOAuthLoginStart(options) {
|
|
1556
1544
|
const [discovery, pkce] = await Promise.all([discoverOAuthLogin({
|
|
1557
1545
|
baseUrl: options.baseUrl,
|
|
@@ -1674,9 +1662,6 @@ function formatMissingNewMachineDirMessage(options) {
|
|
|
1674
1662
|
function toMachineFileStem(machineName) {
|
|
1675
1663
|
return (typeof machineName === "string" ? machineName : "").trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "machine";
|
|
1676
1664
|
}
|
|
1677
|
-
function getMissingCredentialMessage() {
|
|
1678
|
-
return `No Stately credential configured. Run \`statelyai login\`, set \`STATELY_ACCESS_TOKEN\`, or set \`STATELY_API_KEY\`.\nGet or create an API key at ${STATELY_API_KEY_SETTINGS_URL}`;
|
|
1679
|
-
}
|
|
1680
1665
|
function buildProjectEditorUrl(studioUrl, projectId) {
|
|
1681
1666
|
return `${studioUrl.replace(/\/+$/, "")}/registry/editor/${encodeURIComponent(projectId)}`;
|
|
1682
1667
|
}
|
|
@@ -1892,7 +1877,7 @@ async function resolveConfiguredProject(options) {
|
|
|
1892
1877
|
const { config, configPath, rootDir, rewriteStatus } = await readStatelyProjectConfig({
|
|
1893
1878
|
cwd: options.cwd,
|
|
1894
1879
|
configPath: options.configPath,
|
|
1895
|
-
rewriteIfNeeded: true
|
|
1880
|
+
rewriteIfNeeded: options.rewriteIfNeeded ?? true
|
|
1896
1881
|
});
|
|
1897
1882
|
const studioUrl = options.baseUrl ?? config.studioUrl;
|
|
1898
1883
|
return {
|
|
@@ -1938,13 +1923,102 @@ async function resolveConfiguredPullTargets(options) {
|
|
|
1938
1923
|
rewriteStatus
|
|
1939
1924
|
};
|
|
1940
1925
|
}
|
|
1941
|
-
|
|
1926
|
+
async function resolveCredentialStatus() {
|
|
1927
|
+
const envCredential = getEnvCredential();
|
|
1928
|
+
if (envCredential) return {
|
|
1929
|
+
detail: envCredential.variable,
|
|
1930
|
+
source: "environment",
|
|
1931
|
+
type: envCredential.credential.type
|
|
1932
|
+
};
|
|
1933
|
+
const storedCredential = await getStoredCredential();
|
|
1934
|
+
if (storedCredential?.credential) return {
|
|
1935
|
+
detail: describeCredentialBackend(storedCredential.backend, storedCredential.location),
|
|
1936
|
+
source: "stored",
|
|
1937
|
+
type: storedCredential.credential.type
|
|
1938
|
+
};
|
|
1939
|
+
return {
|
|
1940
|
+
detail: "server decides whether authentication is required",
|
|
1941
|
+
source: "none",
|
|
1942
|
+
type: "none"
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
function getProjectMachineId(machine) {
|
|
1946
|
+
const candidate = machine;
|
|
1947
|
+
return candidate.machineId || candidate.id;
|
|
1948
|
+
}
|
|
1949
|
+
function formatCredentialStatus(status) {
|
|
1950
|
+
return status.type === "none" ? `Auth: none (${status.detail})` : `Auth: ${status.type} from ${status.source} (${status.detail})`;
|
|
1951
|
+
}
|
|
1952
|
+
async function resolveProjectStatus(options = {}) {
|
|
1953
|
+
const { client, config, configPath, files } = await resolveConfiguredProject({
|
|
1954
|
+
...options,
|
|
1955
|
+
rewriteIfNeeded: false
|
|
1956
|
+
});
|
|
1957
|
+
const project = await client.projects.get(config.projectId);
|
|
1958
|
+
const { machineFiles } = await classifyPushCandidates(files.filter(supportsMachineDiscovery));
|
|
1959
|
+
const linkedTargets = await discoverLinkedPullTargets(machineFiles);
|
|
1960
|
+
const linkedByPath = new Map(linkedTargets.map((target) => [target.file.filePath, target]));
|
|
1961
|
+
const remoteById = new Map(project.machines.flatMap((machine) => {
|
|
1962
|
+
const id = getProjectMachineId(machine);
|
|
1963
|
+
return id ? [[id, machine]] : [];
|
|
1964
|
+
}));
|
|
1965
|
+
const linkedIds = new Set(linkedTargets.map((target) => target.machineId));
|
|
1966
|
+
const studioUrl = options.baseUrl ?? config.studioUrl;
|
|
1967
|
+
const machines = machineFiles.map((file) => {
|
|
1968
|
+
const linked = linkedByPath.get(file.filePath);
|
|
1969
|
+
if (!linked) return {
|
|
1970
|
+
name: path.basename(file.relativePath),
|
|
1971
|
+
path: file.relativePath,
|
|
1972
|
+
status: "local-only"
|
|
1973
|
+
};
|
|
1974
|
+
const remote = remoteById.get(linked.machineId);
|
|
1975
|
+
return {
|
|
1976
|
+
id: linked.machineId,
|
|
1977
|
+
name: remote?.name || path.basename(file.relativePath),
|
|
1978
|
+
path: file.relativePath,
|
|
1979
|
+
status: remote ? "linked" : "missing-remote",
|
|
1980
|
+
url: buildMachineEditorUrl(studioUrl, config.projectId, linked.machineId)
|
|
1981
|
+
};
|
|
1982
|
+
});
|
|
1983
|
+
for (const remote of project.machines) {
|
|
1984
|
+
const id = getProjectMachineId(remote);
|
|
1985
|
+
if (!id || linkedIds.has(id)) continue;
|
|
1986
|
+
machines.push({
|
|
1987
|
+
id,
|
|
1988
|
+
name: remote.name || id,
|
|
1989
|
+
status: "remote-only",
|
|
1990
|
+
url: buildMachineEditorUrl(studioUrl, config.projectId, id)
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
machines.sort((left, right) => left.status.localeCompare(right.status) || left.name.localeCompare(right.name));
|
|
1994
|
+
return {
|
|
1995
|
+
auth: await resolveCredentialStatus(),
|
|
1996
|
+
configPath,
|
|
1997
|
+
counts: {
|
|
1998
|
+
linked: machines.filter((machine) => machine.status === "linked").length,
|
|
1999
|
+
"local-only": machines.filter((machine) => machine.status === "local-only").length,
|
|
2000
|
+
"missing-remote": machines.filter((machine) => machine.status === "missing-remote").length,
|
|
2001
|
+
"remote-only": machines.filter((machine) => machine.status === "remote-only").length
|
|
2002
|
+
},
|
|
2003
|
+
machines,
|
|
2004
|
+
project: {
|
|
2005
|
+
id: config.projectId,
|
|
2006
|
+
...project.name ? { name: project.name } : {},
|
|
2007
|
+
url: buildProjectEditorUrl(studioUrl, config.projectId)
|
|
2008
|
+
},
|
|
2009
|
+
studioUrl
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
const baseUrlFlags = {
|
|
1942
2013
|
help: Flags.help({ char: "h" }),
|
|
2014
|
+
"base-url": Flags.string({ description: "Base URL for Stately Studio or a self-hosted deployment" })
|
|
2015
|
+
};
|
|
2016
|
+
const sharedFlags = {
|
|
2017
|
+
...baseUrlFlags,
|
|
1943
2018
|
"fail-on-changes": Flags.boolean({
|
|
1944
2019
|
default: false,
|
|
1945
2020
|
description: "Exit with a nonzero code when differences are found"
|
|
1946
|
-
})
|
|
1947
|
-
"base-url": Flags.string({ description: "Base URL for Stately Studio or a self-hosted deployment" })
|
|
2021
|
+
})
|
|
1948
2022
|
};
|
|
1949
2023
|
function formatChangeList(label, items) {
|
|
1950
2024
|
if (items.length === 0) return null;
|
|
@@ -1992,19 +2066,22 @@ var ParsedSyncCommand = class extends BaseSyncCommand {
|
|
|
1992
2066
|
return this.parse(command);
|
|
1993
2067
|
}
|
|
1994
2068
|
};
|
|
2069
|
+
async function createSemanticDiff(source, target, baseUrl) {
|
|
2070
|
+
return planSync({
|
|
2071
|
+
source,
|
|
2072
|
+
target,
|
|
2073
|
+
apiKey: (await resolveApiKey()).apiKey,
|
|
2074
|
+
baseUrl: baseUrl ?? getDefaultBaseUrl()
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
1995
2077
|
var PlanCommand = class PlanCommand extends ParsedSyncCommand {
|
|
2078
|
+
static hidden = true;
|
|
1996
2079
|
static summary = "Plan semantic sync changes between a source and target.";
|
|
1997
2080
|
static description = "Resolves the source and target, normalizes both into graph form, and prints a semantic change summary.";
|
|
1998
2081
|
static args = sharedArgs;
|
|
1999
2082
|
async run() {
|
|
2000
2083
|
const { args, flags } = await this.parseSync(PlanCommand);
|
|
2001
|
-
const
|
|
2002
|
-
const plan = await planSync({
|
|
2003
|
-
source: args.source,
|
|
2004
|
-
target: args.target,
|
|
2005
|
-
apiKey,
|
|
2006
|
-
baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
|
|
2007
|
-
});
|
|
2084
|
+
const plan = await createSemanticDiff(args.source, args.target, flags["base-url"]);
|
|
2008
2085
|
this.log(formatPlanSummary(plan));
|
|
2009
2086
|
this.exit(flags["fail-on-changes"] && plan.summary.hasChanges ? 1 : 0);
|
|
2010
2087
|
}
|
|
@@ -2015,13 +2092,7 @@ var DiffCommand = class DiffCommand extends ParsedSyncCommand {
|
|
|
2015
2092
|
static args = sharedArgs;
|
|
2016
2093
|
async run() {
|
|
2017
2094
|
const { args, flags } = await this.parseSync(DiffCommand);
|
|
2018
|
-
const
|
|
2019
|
-
const plan = await planSync({
|
|
2020
|
-
source: args.source,
|
|
2021
|
-
target: args.target,
|
|
2022
|
-
apiKey,
|
|
2023
|
-
baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
|
|
2024
|
-
});
|
|
2095
|
+
const plan = await createSemanticDiff(args.source, args.target, flags["base-url"]);
|
|
2025
2096
|
this.log(formatPlanSummary(plan));
|
|
2026
2097
|
this.exit(flags["fail-on-changes"] && plan.summary.hasChanges ? 1 : 0);
|
|
2027
2098
|
}
|
|
@@ -2030,7 +2101,7 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
|
|
|
2030
2101
|
static summary = "Pull a source locator into a local target file.";
|
|
2031
2102
|
static description = "Resolves the source, materializes it in the target format inferred from the target file, and writes the result locally. With no arguments, pulls all linked files discovered from statelyai.json.";
|
|
2032
2103
|
static flags = {
|
|
2033
|
-
...
|
|
2104
|
+
...baseUrlFlags,
|
|
2034
2105
|
config: Flags.string({ description: "Path to statelyai.json" }),
|
|
2035
2106
|
force: Flags.boolean({
|
|
2036
2107
|
description: "Overwrite linked files even when they have local git changes",
|
|
@@ -2051,7 +2122,6 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
|
|
|
2051
2122
|
const { args, flags } = await this.parseSync(PullCommand);
|
|
2052
2123
|
const apiKey = (await resolveApiKey()).apiKey;
|
|
2053
2124
|
if (!args.source) {
|
|
2054
|
-
if (!apiKey) this.error(getMissingCredentialMessage());
|
|
2055
2125
|
this.log(colorize("Resolving configured project and linked source files...", ANSI_CYAN));
|
|
2056
2126
|
const { config, linkedTargets, project, remoteOnlyTargets, skipped, rewriteStatus } = await resolveConfiguredPullTargets({
|
|
2057
2127
|
apiKey,
|
|
@@ -2130,7 +2200,6 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
|
|
|
2130
2200
|
target = localCandidate;
|
|
2131
2201
|
}
|
|
2132
2202
|
if (!target) this.error("Missing target path. Pass `statelyai pull <machine-id|url> <file>` or `statelyai pull <linked-file>`.");
|
|
2133
|
-
if (!apiKey) this.error(getMissingCredentialMessage());
|
|
2134
2203
|
if (!flags.force && await fileExists(target) && await isFileGitDirty(target)) this.error(`Refusing to overwrite locally modified file ${target}. Pass --force to overwrite it.`);
|
|
2135
2204
|
this.log(colorize(`Pulling ${source} into ${target}...`, ANSI_CYAN));
|
|
2136
2205
|
const result = await pullSync({
|
|
@@ -2162,12 +2231,11 @@ var PushCommand = class PushCommand extends Command {
|
|
|
2162
2231
|
async run() {
|
|
2163
2232
|
const { args, flags } = await this.parse(PushCommand);
|
|
2164
2233
|
const resolvedApiKey = flags["dry-run"] ? { source: "missing" } : await resolveApiKey();
|
|
2165
|
-
if (!flags["dry-run"] && !resolvedApiKey.apiKey) this.error(getMissingCredentialMessage());
|
|
2166
2234
|
this.log(colorize("Resolving configured project and source files...", ANSI_CYAN));
|
|
2167
2235
|
const { client, config, files, rewriteStatus } = flags["dry-run"] ? await (async () => {
|
|
2168
2236
|
const { config, rootDir, rewriteStatus } = await readStatelyProjectConfig({
|
|
2169
2237
|
configPath: flags.config,
|
|
2170
|
-
rewriteIfNeeded:
|
|
2238
|
+
rewriteIfNeeded: false
|
|
2171
2239
|
});
|
|
2172
2240
|
return {
|
|
2173
2241
|
client: void 0,
|
|
@@ -2300,7 +2368,6 @@ var OpenCommand = class OpenCommand extends Command {
|
|
|
2300
2368
|
async run() {
|
|
2301
2369
|
const { args, flags } = await this.parse(OpenCommand);
|
|
2302
2370
|
const apiKey = (await resolveApiKey()).apiKey;
|
|
2303
|
-
if (!apiKey) throw new Error("No Stately credential configured. Run `statelyai login` and retry.");
|
|
2304
2371
|
await openEditor({
|
|
2305
2372
|
fileName: path.resolve(args.file),
|
|
2306
2373
|
editorUrl: flags["editor-url"] ?? getDefaultEditorUrl(),
|
|
@@ -2341,7 +2408,6 @@ var InitCommand = class InitCommand extends Command {
|
|
|
2341
2408
|
async run() {
|
|
2342
2409
|
const { flags } = await this.parse(InitCommand);
|
|
2343
2410
|
const resolvedApiKey = await resolveApiKey();
|
|
2344
|
-
if (!resolvedApiKey.apiKey) this.error(getMissingCredentialMessage());
|
|
2345
2411
|
this.log("Creating or reusing remote project...");
|
|
2346
2412
|
const result = await initProject({
|
|
2347
2413
|
apiKey: resolvedApiKey.apiKey,
|
|
@@ -2393,21 +2459,10 @@ var LoginCommand = class LoginCommand extends Command {
|
|
|
2393
2459
|
stdin: Flags.boolean({
|
|
2394
2460
|
description: "Read the API key from standard input",
|
|
2395
2461
|
default: false
|
|
2396
|
-
}),
|
|
2397
|
-
auth: Flags.string({
|
|
2398
|
-
description: "Authentication mode for the target server",
|
|
2399
|
-
options: ["none", "bearer"]
|
|
2400
2462
|
})
|
|
2401
2463
|
};
|
|
2402
2464
|
async run() {
|
|
2403
2465
|
const { flags } = await this.parse(LoginCommand);
|
|
2404
|
-
if ((flags["api-key"] || flags.stdin) && flags.auth === "bearer") this.error("Pass either --auth bearer or API-key login flags, not both.");
|
|
2405
|
-
if (flags.auth === "none") {
|
|
2406
|
-
if (flags.stdin || flags["api-key"]) this.error("Pass either --auth none or an API key, not both.");
|
|
2407
|
-
const stored = await setStoredNoAuth();
|
|
2408
|
-
this.log(`Stored no-auth mode in ${describeCredentialBackend(stored.backend, stored.location)}.`);
|
|
2409
|
-
return;
|
|
2410
|
-
}
|
|
2411
2466
|
if (!flags["api-key"] && !flags.stdin) {
|
|
2412
2467
|
const stored = await runOAuthBrowserLogin({
|
|
2413
2468
|
baseUrl: flags["base-url"],
|
|
@@ -2437,29 +2492,64 @@ var LogoutCommand = class extends Command {
|
|
|
2437
2492
|
this.log(`Removed stored credential from ${result.locations.join(", ")}.`);
|
|
2438
2493
|
}
|
|
2439
2494
|
};
|
|
2495
|
+
function formatProjectStatus(result) {
|
|
2496
|
+
const lines = [
|
|
2497
|
+
formatCredentialStatus(result.auth),
|
|
2498
|
+
`Project: ${result.project.name ? `${result.project.name} ` : ""}(${result.project.id})`,
|
|
2499
|
+
`Studio: ${result.project.url}`,
|
|
2500
|
+
`Config: ${result.configPath}`,
|
|
2501
|
+
`Machines: ${result.machines.length} (${result.counts.linked} linked, ${result.counts["local-only"]} local-only, ${result.counts["remote-only"]} remote-only, ${result.counts["missing-remote"]} missing-remote)`
|
|
2502
|
+
];
|
|
2503
|
+
for (const machine of result.machines) {
|
|
2504
|
+
const details = [machine.id, machine.path].filter(Boolean).join(" ");
|
|
2505
|
+
lines.push(` [${machine.status}] ${machine.name}${details ? ` ${details}` : ""}`);
|
|
2506
|
+
}
|
|
2507
|
+
const next = [];
|
|
2508
|
+
if (result.counts["local-only"] > 0 || result.counts["missing-remote"] > 0) next.push("statelyai push");
|
|
2509
|
+
if (result.counts["remote-only"] > 0) next.push("statelyai pull");
|
|
2510
|
+
if (next.length > 0) lines.push(`Next: ${next.join(", ")}`);
|
|
2511
|
+
return lines.join("\n");
|
|
2512
|
+
}
|
|
2513
|
+
var StatusCommand = class StatusCommand extends Command {
|
|
2514
|
+
static enableJsonFlag = false;
|
|
2515
|
+
static summary = "Show project, machine, and authentication status.";
|
|
2516
|
+
static description = "Reads statelyai.json and Studio project metadata, then classifies configured machine files as linked, local-only, remote-only, or missing remotely.";
|
|
2517
|
+
static flags = {
|
|
2518
|
+
...baseUrlFlags,
|
|
2519
|
+
auth: Flags.boolean({
|
|
2520
|
+
description: "Show only credential resolution",
|
|
2521
|
+
default: false
|
|
2522
|
+
}),
|
|
2523
|
+
config: Flags.string({ description: "Path to statelyai.json" }),
|
|
2524
|
+
json: Flags.boolean({
|
|
2525
|
+
description: "Print machine-readable JSON",
|
|
2526
|
+
default: false
|
|
2527
|
+
})
|
|
2528
|
+
};
|
|
2529
|
+
async run() {
|
|
2530
|
+
const { flags } = await this.parse(StatusCommand);
|
|
2531
|
+
if (flags.auth) {
|
|
2532
|
+
const auth = await resolveCredentialStatus();
|
|
2533
|
+
this.log(flags.json ? JSON.stringify({ auth }, null, 2) : formatCredentialStatus(auth));
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
const apiKey = (await resolveApiKey()).apiKey;
|
|
2537
|
+
const result = await resolveProjectStatus({
|
|
2538
|
+
apiKey,
|
|
2539
|
+
baseUrl: flags["base-url"],
|
|
2540
|
+
configPath: flags.config
|
|
2541
|
+
});
|
|
2542
|
+
this.log(flags.json ? JSON.stringify(result, null, 2) : formatProjectStatus(result));
|
|
2543
|
+
}
|
|
2544
|
+
};
|
|
2440
2545
|
var WhoamiCommand = class extends Command {
|
|
2546
|
+
static hidden = true;
|
|
2441
2547
|
static enableJsonFlag = false;
|
|
2442
2548
|
static summary = "Show how the CLI would resolve credentials.";
|
|
2443
2549
|
static description = "Reports whether the CLI would use an environment variable or stored credential.";
|
|
2444
2550
|
static flags = { help: Flags.help({ char: "h" }) };
|
|
2445
2551
|
async run() {
|
|
2446
|
-
|
|
2447
|
-
const storedCredential = await getStoredCredential();
|
|
2448
|
-
const storedApiKey = storedCredential?.credential?.type === "api_key" ? {
|
|
2449
|
-
apiKey: storedCredential.credential.token,
|
|
2450
|
-
backend: storedCredential.backend,
|
|
2451
|
-
location: storedCredential.location
|
|
2452
|
-
} : void 0;
|
|
2453
|
-
if (envCredential) {
|
|
2454
|
-
this.log(`Credential source: environment ${envCredential.credential.type} (${envCredential.variable}).`);
|
|
2455
|
-
if (storedApiKey) this.log(`Stored credential also available in ${describeCredentialBackend(storedApiKey.backend, storedApiKey.location)}.`);
|
|
2456
|
-
return;
|
|
2457
|
-
}
|
|
2458
|
-
if (storedCredential) {
|
|
2459
|
-
this.log(formatStoredCredentialSource(storedCredential));
|
|
2460
|
-
return;
|
|
2461
|
-
}
|
|
2462
|
-
this.log(getMissingCredentialMessage());
|
|
2552
|
+
this.log(formatCredentialStatus(await resolveCredentialStatus()));
|
|
2463
2553
|
}
|
|
2464
2554
|
};
|
|
2465
2555
|
const COMMANDS = {
|
|
@@ -2468,6 +2558,7 @@ const COMMANDS = {
|
|
|
2468
2558
|
pull: PullCommand,
|
|
2469
2559
|
push: PushCommand,
|
|
2470
2560
|
open: OpenCommand,
|
|
2561
|
+
status: StatusCommand,
|
|
2471
2562
|
init: InitCommand,
|
|
2472
2563
|
login: LoginCommand,
|
|
2473
2564
|
logout: LogoutCommand,
|
|
@@ -2495,4 +2586,4 @@ function isDirectExecution() {
|
|
|
2495
2586
|
if (isDirectExecution()) run$1();
|
|
2496
2587
|
|
|
2497
2588
|
//#endregion
|
|
2498
|
-
export {
|
|
2589
|
+
export { createStatelyProjectConfig as S, resolveConfiguredPullTargets as _, discoverOAuthLogin as a, run$1 as b, formatPlanSummary as c, getEnvApiKey as d, getEnvCredential as f, resolveApiKey as g, isFileGitDirty as h, discoverLinkedPullTargets as i, formatProjectStatus as l, initProject as m, buildOAuthLoginStart as n, discoverRemoteProjectMachineTargets as o, inferInitProjectName as p, classifyPushCandidates as r, formatMissingNewMachineDirMessage as s, COMMANDS as t, formatStoredCredentialSource as u, resolveCredentialStatus as v, scanProjectSources as x, resolveProjectStatus as y };
|
package/dist/index.d.mts
CHANGED
|
@@ -51,7 +51,7 @@ declare function createStatelyProjectConfig(options: {
|
|
|
51
51
|
//#endregion
|
|
52
52
|
//#region src/cli.d.ts
|
|
53
53
|
interface InitProjectOptions {
|
|
54
|
-
apiKey
|
|
54
|
+
apiKey?: string;
|
|
55
55
|
baseUrl?: string;
|
|
56
56
|
cwd?: string;
|
|
57
57
|
client?: StudioClient;
|
|
@@ -97,6 +97,31 @@ interface ResolvedConfiguredPullTargets {
|
|
|
97
97
|
reason?: string;
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
|
+
type ProjectMachineStatus = 'linked' | 'local-only' | 'missing-remote' | 'remote-only';
|
|
101
|
+
interface ProjectStatusMachine {
|
|
102
|
+
id?: string;
|
|
103
|
+
name: string;
|
|
104
|
+
path?: string;
|
|
105
|
+
status: ProjectMachineStatus;
|
|
106
|
+
url?: string;
|
|
107
|
+
}
|
|
108
|
+
interface CredentialStatus {
|
|
109
|
+
detail: string;
|
|
110
|
+
source: 'environment' | 'none' | 'stored';
|
|
111
|
+
type: 'api_key' | 'none' | 'oauth';
|
|
112
|
+
}
|
|
113
|
+
interface ProjectStatusResult {
|
|
114
|
+
auth: CredentialStatus;
|
|
115
|
+
configPath: string;
|
|
116
|
+
counts: Record<ProjectMachineStatus, number>;
|
|
117
|
+
machines: ProjectStatusMachine[];
|
|
118
|
+
project: {
|
|
119
|
+
id: string;
|
|
120
|
+
name?: string;
|
|
121
|
+
url: string;
|
|
122
|
+
};
|
|
123
|
+
studioUrl: string;
|
|
124
|
+
}
|
|
100
125
|
type ApiKeyResolution = {
|
|
101
126
|
apiKey: string;
|
|
102
127
|
detail: string;
|
|
@@ -162,23 +187,31 @@ declare function classifyPushCandidates(files: DiscoveredSourceFile[]): Promise<
|
|
|
162
187
|
declare function resolveConfiguredPullTargets(options: {
|
|
163
188
|
cwd?: string;
|
|
164
189
|
configPath?: string;
|
|
165
|
-
apiKey
|
|
190
|
+
apiKey?: string;
|
|
166
191
|
baseUrl?: string;
|
|
167
192
|
client?: StudioClient;
|
|
168
193
|
}): Promise<ResolvedConfiguredPullTargets>;
|
|
194
|
+
declare function resolveCredentialStatus(): Promise<CredentialStatus>;
|
|
195
|
+
declare function resolveProjectStatus(options?: {
|
|
196
|
+
apiKey?: string;
|
|
197
|
+
baseUrl?: string;
|
|
198
|
+
client?: StudioClient;
|
|
199
|
+
configPath?: string;
|
|
200
|
+
cwd?: string;
|
|
201
|
+
}): Promise<ProjectStatusResult>;
|
|
169
202
|
declare function formatPlanSummary(plan: SyncPlan): string;
|
|
170
203
|
declare abstract class BaseSyncCommand extends Command {
|
|
171
204
|
static enableJsonFlag: boolean;
|
|
172
205
|
static flags: {
|
|
173
|
-
help: _oclif_core_interfaces0.BooleanFlag<void>;
|
|
174
206
|
'fail-on-changes': _oclif_core_interfaces0.BooleanFlag<boolean>;
|
|
207
|
+
help: _oclif_core_interfaces0.BooleanFlag<void>;
|
|
175
208
|
'base-url': _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
|
|
176
209
|
};
|
|
177
210
|
}
|
|
178
211
|
declare abstract class ParsedSyncCommand extends BaseSyncCommand {
|
|
179
212
|
protected parseSync<T extends typeof BaseSyncCommand>(command: T): Promise<_oclif_core_interfaces0.ParserOutput<{
|
|
180
|
-
help: void;
|
|
181
213
|
'fail-on-changes': boolean;
|
|
214
|
+
help: void;
|
|
182
215
|
'base-url': string | undefined;
|
|
183
216
|
}, {
|
|
184
217
|
[flag: string]: any;
|
|
@@ -187,6 +220,7 @@ declare abstract class ParsedSyncCommand extends BaseSyncCommand {
|
|
|
187
220
|
}>>;
|
|
188
221
|
}
|
|
189
222
|
declare class PlanCommand extends ParsedSyncCommand {
|
|
223
|
+
static hidden: boolean;
|
|
190
224
|
static summary: string;
|
|
191
225
|
static description: string;
|
|
192
226
|
static args: {
|
|
@@ -211,7 +245,6 @@ declare class PullCommand extends ParsedSyncCommand {
|
|
|
211
245
|
config: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
|
|
212
246
|
force: _oclif_core_interfaces0.BooleanFlag<boolean>;
|
|
213
247
|
help: _oclif_core_interfaces0.BooleanFlag<void>;
|
|
214
|
-
'fail-on-changes': _oclif_core_interfaces0.BooleanFlag<boolean>;
|
|
215
248
|
'base-url': _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
|
|
216
249
|
};
|
|
217
250
|
static args: {
|
|
@@ -275,7 +308,6 @@ declare class LoginCommand extends Command {
|
|
|
275
308
|
'api-key': _oclif_core_interfaces0.BooleanFlag<boolean>;
|
|
276
309
|
'base-url': _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
|
|
277
310
|
stdin: _oclif_core_interfaces0.BooleanFlag<boolean>;
|
|
278
|
-
auth: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
|
|
279
311
|
};
|
|
280
312
|
run(): Promise<void>;
|
|
281
313
|
}
|
|
@@ -288,7 +320,22 @@ declare class LogoutCommand extends Command {
|
|
|
288
320
|
};
|
|
289
321
|
run(): Promise<void>;
|
|
290
322
|
}
|
|
323
|
+
declare function formatProjectStatus(result: ProjectStatusResult): string;
|
|
324
|
+
declare class StatusCommand extends Command {
|
|
325
|
+
static enableJsonFlag: boolean;
|
|
326
|
+
static summary: string;
|
|
327
|
+
static description: string;
|
|
328
|
+
static flags: {
|
|
329
|
+
auth: _oclif_core_interfaces0.BooleanFlag<boolean>;
|
|
330
|
+
config: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
|
|
331
|
+
json: _oclif_core_interfaces0.BooleanFlag<boolean>;
|
|
332
|
+
help: _oclif_core_interfaces0.BooleanFlag<void>;
|
|
333
|
+
'base-url': _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
|
|
334
|
+
};
|
|
335
|
+
run(): Promise<void>;
|
|
336
|
+
}
|
|
291
337
|
declare class WhoamiCommand extends Command {
|
|
338
|
+
static hidden: boolean;
|
|
292
339
|
static enableJsonFlag: boolean;
|
|
293
340
|
static summary: string;
|
|
294
341
|
static description: string;
|
|
@@ -303,6 +350,7 @@ declare const COMMANDS: {
|
|
|
303
350
|
pull: typeof PullCommand;
|
|
304
351
|
push: typeof PushCommand;
|
|
305
352
|
open: typeof OpenCommand;
|
|
353
|
+
status: typeof StatusCommand;
|
|
306
354
|
init: typeof InitCommand;
|
|
307
355
|
login: typeof LoginCommand;
|
|
308
356
|
logout: typeof LogoutCommand;
|
|
@@ -310,4 +358,4 @@ declare const COMMANDS: {
|
|
|
310
358
|
};
|
|
311
359
|
declare function run(argv?: any, entryUrl?: string): Promise<void>;
|
|
312
360
|
//#endregion
|
|
313
|
-
export { ApiKeyResolution, COMMANDS, InitProjectOptions, InitProjectResult, LinkedPullTarget, OAuthLoginDiscovery, PushCandidateClassification, RemoteProjectMachineTarget, ResolvedConfiguredPullTargets, ScanProjectSourcesOptions, buildOAuthLoginStart, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverOAuthLogin, discoverRemoteProjectMachineTargets, formatMissingNewMachineDirMessage, formatPlanSummary, formatStoredCredentialSource, getEnvApiKey, getEnvCredential, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, resolveConfiguredPullTargets, run, scanProjectSources };
|
|
361
|
+
export { ApiKeyResolution, COMMANDS, CredentialStatus, InitProjectOptions, InitProjectResult, LinkedPullTarget, OAuthLoginDiscovery, ProjectMachineStatus, ProjectStatusMachine, ProjectStatusResult, PushCandidateClassification, RemoteProjectMachineTarget, ResolvedConfiguredPullTargets, ScanProjectSourcesOptions, buildOAuthLoginStart, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverOAuthLogin, discoverRemoteProjectMachineTargets, formatMissingNewMachineDirMessage, formatPlanSummary, formatProjectStatus, formatStoredCredentialSource, getEnvApiKey, getEnvCredential, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, resolveConfiguredPullTargets, resolveCredentialStatus, resolveProjectStatus, run, scanProjectSources };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { S as createStatelyProjectConfig, _ as resolveConfiguredPullTargets, a as discoverOAuthLogin, b as run, c as formatPlanSummary, d as getEnvApiKey, f as getEnvCredential, g as resolveApiKey, h as isFileGitDirty, i as discoverLinkedPullTargets, l as formatProjectStatus, m as initProject, n as buildOAuthLoginStart, o as discoverRemoteProjectMachineTargets, p as inferInitProjectName, r as classifyPushCandidates, s as formatMissingNewMachineDirMessage, t as COMMANDS, u as formatStoredCredentialSource, v as resolveCredentialStatus, x as scanProjectSources, y as resolveProjectStatus } from "./cli-DRKY520A.mjs";
|
|
2
2
|
|
|
3
|
-
export { COMMANDS, buildOAuthLoginStart, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverOAuthLogin, discoverRemoteProjectMachineTargets, formatMissingNewMachineDirMessage, formatPlanSummary, formatStoredCredentialSource, getEnvApiKey, getEnvCredential, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, resolveConfiguredPullTargets, run, scanProjectSources };
|
|
3
|
+
export { COMMANDS, buildOAuthLoginStart, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverOAuthLogin, discoverRemoteProjectMachineTargets, formatMissingNewMachineDirMessage, formatPlanSummary, formatProjectStatus, formatStoredCredentialSource, getEnvApiKey, getEnvCredential, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, resolveConfiguredPullTargets, resolveCredentialStatus, resolveProjectStatus, run, scanProjectSources };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "statelyai",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "Command-line tools for Stately",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@oclif/core": "^4.10.3",
|
|
24
|
-
"@statelyai/sdk": "0.13.
|
|
24
|
+
"@statelyai/sdk": "0.13.2"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"tsdown": "0.21.0-beta.2",
|