zoodealio 0.2.1 → 0.3.0-dev.1607
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 +4 -41
- package/package.json +1 -1
- package/src/cli.js +13 -6
- package/src/commands.js +11 -4
- package/src/http.js +14 -2
- package/src/login.js +2 -2
package/README.md
CHANGED
|
@@ -17,8 +17,7 @@ zoodealio whoami
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
or run it without installing: `npx zoodealio whoami` (after a `zoodealio login` elsewhere, or with
|
|
20
|
-
`ZOODEALIO_API_KEY` set).
|
|
21
|
-
runs the working copy.
|
|
20
|
+
`ZOODEALIO_API_KEY` set).
|
|
22
21
|
|
|
23
22
|
## Authenticate
|
|
24
23
|
|
|
@@ -43,9 +42,7 @@ It prints `{ok, email, name, keyPrefix, origin}` on stdout and never the key.
|
|
|
43
42
|
|
|
44
43
|
The key is stored per API origin in `$XDG_CONFIG_HOME/zoodealio/credentials.json`
|
|
45
44
|
(default `~/.config/zoodealio/credentials.json`; `%APPDATA%\zoodealio\credentials.json` on Windows),
|
|
46
|
-
written atomically with mode `0600` in a `0700` directory.
|
|
47
|
-
`zoodealio login --env dev` stores a separate login. `ZOODEALIO_WEB_URL` overrides the sign-in
|
|
48
|
-
page origin the way `ZOODEALIO_API_URL` overrides the API origin.
|
|
45
|
+
written atomically with mode `0600` in a `0700` directory.
|
|
49
46
|
|
|
50
47
|
### API key for CI and servers
|
|
51
48
|
|
|
@@ -96,17 +93,6 @@ Cursor (`.cursor/mcp.json`) or VS Code (`.vscode/mcp.json`, under `servers`):
|
|
|
96
93
|
Run `zoodealio login` first (or put `ZOODEALIO_API_KEY` in the client's `env`). Stdout carries only
|
|
97
94
|
JSON-RPC; logs go to stderr.
|
|
98
95
|
|
|
99
|
-
## Environments
|
|
100
|
-
|
|
101
|
-
| Selection | API origin |
|
|
102
|
-
| --- | --- |
|
|
103
|
-
| default | `https://api.zoodealio.ai` |
|
|
104
|
-
| `--env dev` | `https://dev.api.zoodealio.ai` |
|
|
105
|
-
| `ZOODEALIO_API_URL=http://localhost:5256` | that origin (overrides both) |
|
|
106
|
-
|
|
107
|
-
Keys are per environment. Use a local API URL only for a local API; the port above is an
|
|
108
|
-
example, use whichever yours runs on.
|
|
109
|
-
|
|
110
96
|
## Output and errors
|
|
111
97
|
|
|
112
98
|
stdout carries JSON and nothing else: pretty-printed on a terminal, compact when piped
|
|
@@ -138,7 +124,6 @@ Route IDs are GUIDs and are checked before any request is made.
|
|
|
138
124
|
|
|
139
125
|
| Flag | Effect |
|
|
140
126
|
| --- | --- |
|
|
141
|
-
| `--env prod\|dev` | Pick the environment (default `prod`) |
|
|
142
127
|
| `--pretty` | Pretty-print JSON even when piped |
|
|
143
128
|
| `--dry-run` | Print `{method, url, headers, body}` with the key redacted and send nothing; works without a key |
|
|
144
129
|
| `--yes` | Confirm a destructive command (not needed with `--dry-run`) |
|
|
@@ -157,7 +142,7 @@ concurrency on `properties update`, `offers accept` and `offers request-details`
|
|
|
157
142
|
| --- | --- | --- |
|
|
158
143
|
| `properties list` | GET Properties | `--search`, `--state` |
|
|
159
144
|
| `properties get <id>` | GET Properties/{id} | |
|
|
160
|
-
| `properties create` | POST Properties | `--body
|
|
145
|
+
| `properties create` | POST Properties | `--body`; `--agent <id>` assigns a team member or a pending invite |
|
|
161
146
|
| `properties update <id>` | PUT Properties/{id} | `--body`, `--if-match`; replaces wholesale |
|
|
162
147
|
| `properties photos list\|add\|delete` | photos | `add`: `--url` (repeatable) or `--body`; `delete <id> <photoId>` needs `--yes` |
|
|
163
148
|
| `properties documents list\|add\|delete` | documents | same shape as photos |
|
|
@@ -169,7 +154,7 @@ concurrency on `properties update`, `offers accept` and `offers request-details`
|
|
|
169
154
|
| `transactions get <id>` | GET Transactions/{id} | |
|
|
170
155
|
| `transactions active` | GET Transactions/active | `--property` required |
|
|
171
156
|
| `comping status <transactionId>` | GET Transactions/{id}/comping | |
|
|
172
|
-
| `team members`, `team get <id>`, `teams list` | GET Team | `--search` |
|
|
157
|
+
| `team members`, `team get <id>`, `teams list` | GET Team | `--search`; `team members` also lists pending invites (`kind: "invite"`) |
|
|
173
158
|
| `team invite` | POST Team/members | `--email` (+ `--first-name`, `--last-name`, `--team`, `--admin-team`) or `--body` |
|
|
174
159
|
| `team remove <id>` | DELETE Team/members/{id} | `--yes` required |
|
|
175
160
|
| `webhooks event-types\|list\|deliveries` | GET Webhooks | `deliveries`: `--subscription`, `--status` |
|
|
@@ -241,25 +226,3 @@ zoodealio api GET Properties --query pageSize=5
|
|
|
241
226
|
- No keychain or interactive prompts. Credentials are `ZOODEALIO_API_KEY` or the
|
|
242
227
|
`credentials.json` written by `zoodealio login`.
|
|
243
228
|
|
|
244
|
-
## Development
|
|
245
|
-
|
|
246
|
-
```sh
|
|
247
|
-
cd integrations/cli
|
|
248
|
-
npm install
|
|
249
|
-
npm run check # eslint, tsc --noEmit (checkJs), node --test
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
## Releasing
|
|
253
|
-
|
|
254
|
-
npm no longer lets a token that bypasses 2FA publish directly, so releases are staged and then
|
|
255
|
-
approved by a maintainer with 2FA:
|
|
256
|
-
|
|
257
|
-
```sh
|
|
258
|
-
cd integrations/cli
|
|
259
|
-
npm version <patch|minor|major> --no-git-tag-version # in a PR, then merge
|
|
260
|
-
npm run check
|
|
261
|
-
npm stage publish --userconfig ~/.config/zoodealio/npm-publish.npmrc
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
Then approve it on npmjs.com (avatar → Staged Packages → Approve). A version with a `-` suffix
|
|
265
|
-
(`0.2.0-beta.1`) is staged with `--tag next` so `latest` stays stable.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -2,7 +2,7 @@ const { CliError, usage } = require('./errors');
|
|
|
2
2
|
const { parse, normalizeApiPath, METHODS } = require('./args');
|
|
3
3
|
const { catalogue } = require('./commands');
|
|
4
4
|
const { resolveBody } = require('./input');
|
|
5
|
-
const { USER_AGENT, redactKey, scrubKeys, resolveOrigin, buildUrl, send } = require('./http');
|
|
5
|
+
const { DEFAULT_ENV, USER_AGENT, redactKey, scrubKeys, resolveOrigin, buildUrl, send } = require('./http');
|
|
6
6
|
const { readCredential, saveCredential, removeCredential } = require('./credentials');
|
|
7
7
|
const { browserLogin, resolveWebOrigin, defaultDevice } = require('./login');
|
|
8
8
|
const { bridge } = require('./mcp');
|
|
@@ -57,7 +57,8 @@ async function execute(argv, io, envKey, secrets) {
|
|
|
57
57
|
|
|
58
58
|
if (command.name === 'commands') return emit(catalogue());
|
|
59
59
|
|
|
60
|
-
const
|
|
60
|
+
const envName = flags.env || DEFAULT_ENV;
|
|
61
|
+
const origin = resolveOrigin(io.env, envName);
|
|
61
62
|
const stored = readCredential(io.env, origin);
|
|
62
63
|
if (stored) secrets.add(stored.key);
|
|
63
64
|
const source = envKey ? 'env' : stored ? 'login' : undefined;
|
|
@@ -68,13 +69,14 @@ async function execute(argv, io, envKey, secrets) {
|
|
|
68
69
|
if (command.name === 'login') {
|
|
69
70
|
const ui = createUi(io.stderr, { fancy: !!io.interactive && !io.env.NO_COLOR });
|
|
70
71
|
const result = await browserLogin({
|
|
71
|
-
apiOrigin: origin, webOrigin: resolveWebOrigin(io.env,
|
|
72
|
-
envName
|
|
72
|
+
apiOrigin: origin, webOrigin: resolveWebOrigin(io.env, envName), device: (flags.name?.trim() || defaultDevice()).slice(0, 64),
|
|
73
|
+
envName, noBrowser: !!flags['no-browser'], openBrowser: io.openBrowser, copy: io.copy,
|
|
73
74
|
interactive: io.interactive, onKey: io.onKey, timeoutMs: io.loginTimeoutMs, ui, fetch: io.fetch,
|
|
74
75
|
});
|
|
75
76
|
secrets.add(result.key);
|
|
76
77
|
const file = saveCredential(io.env, origin, { key: result.key, keyId: result.keyId, email: result.email, name: result.name });
|
|
77
|
-
|
|
78
|
+
// The hints run without --env only where that reaches the environment just signed in to.
|
|
79
|
+
const dev = envName === DEFAULT_ENV ? '' : ` --env ${envName}`;
|
|
78
80
|
const home = io.env.HOME || io.env.USERPROFILE;
|
|
79
81
|
const where = home && file.startsWith(home) ? `~${file.slice(home.length)}` : file;
|
|
80
82
|
const { style } = ui;
|
|
@@ -86,7 +88,7 @@ async function execute(argv, io, envKey, secrets) {
|
|
|
86
88
|
];
|
|
87
89
|
const width = Math.max(...next.map(([c]) => c.length)) + 3;
|
|
88
90
|
ui.say();
|
|
89
|
-
ui.say(`${style.green('✓')} Signed in as ${style.bold(result.email)}${
|
|
91
|
+
ui.say(`${style.green('✓')} Signed in as ${style.bold(result.email)}${envName === 'prod' ? '' : ` ${style.dim(`(${envName})`)}`}`);
|
|
90
92
|
ui.say(` ${style.dim(`Key "${result.name}" saved to ${where}`)}`);
|
|
91
93
|
ui.say();
|
|
92
94
|
ui.say(` ${style.bold('Next')}`);
|
|
@@ -148,6 +150,11 @@ async function execute(argv, io, envKey, secrets) {
|
|
|
148
150
|
if (flags.body !== undefined) {
|
|
149
151
|
if ((command.convenience || []).some((n) => flags[n] !== undefined)) throw usage('use either --body or the convenience flags, not both');
|
|
150
152
|
body = await resolveBody(flags.body, io.stdin);
|
|
153
|
+
const extra = command.overlay && command.overlay(flags);
|
|
154
|
+
if (extra && Object.keys(extra).length) {
|
|
155
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) throw usage('--body must be a JSON object when combined with convenience flags');
|
|
156
|
+
body = { ...body, ...extra };
|
|
157
|
+
}
|
|
151
158
|
} else if (command.build) {
|
|
152
159
|
body = command.build(flags);
|
|
153
160
|
if (body === undefined && command.body === 'required') throw usage('provide --body or the convenience flags');
|
package/src/commands.js
CHANGED
|
@@ -7,7 +7,7 @@ const { parseJsonArg } = require('./input');
|
|
|
7
7
|
* @typedef {{name: string, description: string, guid?: boolean}} Arg
|
|
8
8
|
* @typedef {{name: string, summary: string, method: string | null, path: string | null, args: Arg[],
|
|
9
9
|
* flags: Flag[], body?: 'required' | 'optional', convenience?: string[], destructive?: boolean,
|
|
10
|
-
* auth?: boolean, build?: (flags: Record<string, any>) => unknown, notes?: string[]}} Command
|
|
10
|
+
* auth?: boolean, build?: (flags: Record<string, any>) => unknown, overlay?: (flags: Record<string, any>) => Record<string, unknown>, notes?: string[]}} Command
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -50,7 +50,14 @@ const COMMANDS = [
|
|
|
50
50
|
flags: [str('search', 'Free-text search', { query: 'search' }), str('state', 'Two-letter state code', { query: 'stateCode' }), ...PAGING],
|
|
51
51
|
}),
|
|
52
52
|
cmd('properties get', 'Get one property', 'GET', '/openapi/Properties/{id}', { args: [id('id', 'Property id')] }),
|
|
53
|
-
cmd('properties create', 'Create a property', 'POST', '/openapi/Properties', {
|
|
53
|
+
cmd('properties create', 'Create a property', 'POST', '/openapi/Properties', {
|
|
54
|
+
body: 'required',
|
|
55
|
+
flags: [str('agent', 'Agent to assign: a team member id, or a pending invite id from `team members`'), BODY],
|
|
56
|
+
overlay: (f) => {
|
|
57
|
+
if (f.agent !== undefined && !GUID.test(f.agent)) throw usage(`agent id is not a GUID: ${f.agent}`);
|
|
58
|
+
return compact({ agentId: f.agent });
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
54
61
|
cmd('properties update', 'Replace a property (wholesale)', 'PUT', '/openapi/Properties/{id}', {
|
|
55
62
|
args: [id('id', 'Property id')], body: 'required', flags: [BODY, IF_MATCH],
|
|
56
63
|
}),
|
|
@@ -95,7 +102,7 @@ const COMMANDS = [
|
|
|
95
102
|
cmd('comping status', 'Get AI comping status for a transaction', 'GET', '/openapi/Transactions/{transactionId}/comping', {
|
|
96
103
|
args: [id('transactionId', 'Transaction id')],
|
|
97
104
|
}),
|
|
98
|
-
cmd('team members', 'List team members', 'GET', '/openapi/Team/members', {
|
|
105
|
+
cmd('team members', 'List team members, including pending invites', 'GET', '/openapi/Team/members', {
|
|
99
106
|
flags: [str('search', 'Free-text search', { query: 'search' }), ...PAGING],
|
|
100
107
|
}),
|
|
101
108
|
cmd('team get', 'Get one team member', 'GET', '/openapi/Team/members/{id}', { args: [id('id', 'Member id')] }),
|
|
@@ -179,7 +186,7 @@ const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
179
186
|
|
|
180
187
|
/** Flags accepted by every command. */
|
|
181
188
|
const GLOBAL_FLAGS = /** @type {Flag[]} */ ([
|
|
182
|
-
str('env', 'prod (default
|
|
189
|
+
str('env', 'prod or dev (default prod; dev on a dev build); ZOODEALIO_API_URL overrides'),
|
|
183
190
|
{ name: 'pretty', type: 'boolean', description: 'Pretty-print JSON even when piped' },
|
|
184
191
|
{ name: 'dry-run', type: 'boolean', description: 'Print the request (key redacted) without sending it' },
|
|
185
192
|
{ name: 'yes', type: 'boolean', description: 'Confirm a destructive command (not needed with --dry-run)' },
|
package/src/http.js
CHANGED
|
@@ -4,6 +4,18 @@ const pkg = require('../package.json');
|
|
|
4
4
|
const USER_AGENT = `zoodealio-cli/${pkg.version}`;
|
|
5
5
|
const ENVS = { prod: 'https://api.zoodealio.ai', dev: 'https://dev.api.zoodealio.ai' };
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* CI publishes `<version>-dev.<run>` under the `dev` tag after the dev deployment, so that build
|
|
9
|
+
* talks to dev unless told otherwise. Every other version is a release and talks to prod.
|
|
10
|
+
* @param {string} version
|
|
11
|
+
* @returns {'prod'|'dev'}
|
|
12
|
+
*/
|
|
13
|
+
function defaultEnv(version) {
|
|
14
|
+
return version.includes('-dev.') ? 'dev' : 'prod';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_ENV = defaultEnv(pkg.version);
|
|
18
|
+
|
|
7
19
|
/** @type {Record<number, [number, string]>} status -> [exit code, error code slug] */
|
|
8
20
|
const STATUS_MAP = {
|
|
9
21
|
400: [2, 'bad_request'],
|
|
@@ -47,7 +59,7 @@ function scrubKeys(text, keys) {
|
|
|
47
59
|
function resolveOrigin(env, envName) {
|
|
48
60
|
const override = env.ZOODEALIO_API_URL;
|
|
49
61
|
if (override) return override.replace(/\/+$/, '');
|
|
50
|
-
const name = envName ||
|
|
62
|
+
const name = envName || DEFAULT_ENV;
|
|
51
63
|
if (!Object.hasOwn(ENVS, name)) throw usage('--env must be prod or dev');
|
|
52
64
|
return ENVS[/** @type {'prod'|'dev'} */ (name)];
|
|
53
65
|
}
|
|
@@ -123,4 +135,4 @@ async function send(doFetch, req) {
|
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
|
|
126
|
-
module.exports = { ENVS, USER_AGENT, redactKey, scrubKeys, resolveOrigin, buildUrl, send };
|
|
138
|
+
module.exports = { ENVS, DEFAULT_ENV, USER_AGENT, defaultEnv, redactKey, scrubKeys, resolveOrigin, buildUrl, send };
|
package/src/login.js
CHANGED
|
@@ -3,7 +3,7 @@ const http = require('node:http');
|
|
|
3
3
|
const os = require('node:os');
|
|
4
4
|
const { spawn } = require('node:child_process');
|
|
5
5
|
const { CliError } = require('./errors');
|
|
6
|
-
const { USER_AGENT, send } = require('./http');
|
|
6
|
+
const { DEFAULT_ENV, USER_AGENT, send } = require('./http');
|
|
7
7
|
|
|
8
8
|
const WEB_ENVS = { prod: 'https://zoodealio.ai', dev: 'https://dev.zoodealio.ai' };
|
|
9
9
|
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -15,7 +15,7 @@ const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
15
15
|
*/
|
|
16
16
|
function resolveWebOrigin(env, envName) {
|
|
17
17
|
if (env.ZOODEALIO_WEB_URL) return env.ZOODEALIO_WEB_URL.replace(/\/+$/, '');
|
|
18
|
-
return WEB_ENVS[/** @type {'prod'|'dev'} */ (envName === 'dev' ? 'dev' : 'prod')];
|
|
18
|
+
return WEB_ENVS[/** @type {'prod'|'dev'} */ ((envName || DEFAULT_ENV) === 'dev' ? 'dev' : 'prod')];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|