flipstream 0.4.0 → 0.5.0
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 +189 -15
- package/dist/commands/connections/list.js +4 -3
- package/dist/commands/log/add.d.ts +16 -0
- package/dist/commands/log/add.js +44 -0
- package/dist/commands/log/list.d.ts +19 -0
- package/dist/commands/log/list.js +43 -0
- package/dist/commands/workspaces/connections.js +3 -2
- package/dist/lib/api/admin-client.d.ts +5 -0
- package/dist/lib/api/admin-client.js +19 -0
- package/dist/lib/api/connections.d.ts +0 -1
- package/dist/lib/api/connections.js +0 -25
- package/dist/lib/api/hydrate.d.ts +10 -0
- package/dist/lib/api/hydrate.js +46 -0
- package/dist/lib/api/log.d.ts +22 -0
- package/dist/lib/api/log.js +56 -0
- package/dist/lib/api/projections.d.ts +1 -0
- package/dist/lib/api/projections.js +23 -0
- package/dist/lib/auth/claims.js +3 -3
- package/dist/lib/command/admin.d.ts +1 -0
- package/dist/lib/command/admin.js +21 -0
- package/dist/lib/errors.js +12 -1
- package/oclif.manifest.json +215 -1
- package/package.json +16 -1
- package/dist/lib/auth/register.d.ts +0 -4
- package/dist/lib/auth/register.js +0 -43
package/README.md
CHANGED
|
@@ -6,25 +6,20 @@ authenticates once and prints clean **structured JSON** for AI coding agents.
|
|
|
6
6
|
> **Architecture:** this supersedes `flipstream-cli-kickoff.md`. See [`CLAUDE.md`](./CLAUDE.md)
|
|
7
7
|
> for the corrected model (thin client, two hosts, **no natural-language "ask"**).
|
|
8
8
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
- [Bun](https://bun.sh) (development toolchain)
|
|
12
|
-
- Node.js **20+** (the published CLI runs under Node)
|
|
13
|
-
|
|
14
|
-
## Install (development)
|
|
9
|
+
## Install
|
|
15
10
|
|
|
16
11
|
```sh
|
|
17
|
-
|
|
18
|
-
bun run build
|
|
12
|
+
npm i -g flipstream # requires Node.js 20+
|
|
19
13
|
```
|
|
20
14
|
|
|
21
15
|
## Usage
|
|
22
16
|
|
|
23
17
|
```sh
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
flipstream --help
|
|
19
|
+
flipstream --version
|
|
20
|
+
flipstream version --json # {version,node,platform,contractVersion}
|
|
21
|
+
flipstream auth login # opens the browser to sign in (OAuth2 + PKCE)
|
|
22
|
+
flipstream ws list # list your organization's workspaces
|
|
28
23
|
```
|
|
29
24
|
|
|
30
25
|
## For agents (Claude Code / Codex / MCP)
|
|
@@ -46,19 +41,30 @@ taxonomy, per-command shapes, `CONTRACT_VERSION`) lives in
|
|
|
46
41
|
[`docs/AGENT-CONTRACT.md`](./docs/AGENT-CONTRACT.md) and is enforced by a
|
|
47
42
|
conformance suite.
|
|
48
43
|
|
|
49
|
-
##
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
Built with [Bun](https://bun.sh); the published CLI runs under **Node.js 20+**.
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
bun install
|
|
50
|
+
bun run build
|
|
51
|
+
./bin/run.js --help # run from source (or during dev: bun bin/dev.js --help)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Scripts:
|
|
50
55
|
|
|
51
56
|
- `bun run build` — compile TypeScript to `dist/`
|
|
52
57
|
- `bun run lint` — eslint (flat config)
|
|
53
58
|
- `bun run check:secrets` — static deny-list gate over `src/lib/auth` (CI-enforced)
|
|
54
59
|
- `bun run format` — prettier
|
|
55
60
|
- `bun test` — run the test suite
|
|
61
|
+
- `bun run smoke` — pack the tarball + run the packaged binary (E5-4)
|
|
56
62
|
- `bun run dev` — run the CLI from source (`./bin/dev.js`)
|
|
57
63
|
|
|
58
64
|
## Security
|
|
59
65
|
|
|
60
|
-
The CLI is a **public OAuth2 client** (Authorization Code + PKCE
|
|
61
|
-
|
|
66
|
+
The CLI is a **public OAuth2 client** (Authorization Code + PKCE, a fixed
|
|
67
|
+
first-party `client_id`) — it holds **no client secret**.
|
|
62
68
|
|
|
63
69
|
- **Tokens live only in the OS keychain** (`@napi-rs/keyring`, service id
|
|
64
70
|
`io.flipstream.cli`); nothing is written to disk in plaintext, and the
|
|
@@ -104,6 +110,10 @@ If you already removed the binary, clean up manually:
|
|
|
104
110
|
* [`flipstream commands`](#flipstream-commands)
|
|
105
111
|
* [`flipstream conn list`](#flipstream-conn-list)
|
|
106
112
|
* [`flipstream connections list`](#flipstream-connections-list)
|
|
113
|
+
* [`flipstream log add MESSAGE`](#flipstream-log-add-message)
|
|
114
|
+
* [`flipstream log list`](#flipstream-log-list)
|
|
115
|
+
* [`flipstream logs add MESSAGE`](#flipstream-logs-add-message)
|
|
116
|
+
* [`flipstream logs list`](#flipstream-logs-list)
|
|
107
117
|
* [`flipstream query`](#flipstream-query)
|
|
108
118
|
* [`flipstream version`](#flipstream-version)
|
|
109
119
|
* [`flipstream workspaces connections ID`](#flipstream-workspaces-connections-id)
|
|
@@ -341,6 +351,170 @@ EXAMPLES
|
|
|
341
351
|
$ flipstream conn list --all
|
|
342
352
|
```
|
|
343
353
|
|
|
354
|
+
## `flipstream log add MESSAGE`
|
|
355
|
+
|
|
356
|
+
Add an entry to a workspace's log.
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
USAGE
|
|
360
|
+
$ flipstream log add MESSAGE --workspace <value> [--json] [-H <value>] [--ndjson] [--timeout <value>]
|
|
361
|
+
[--verbose] [--end <value>] [--start <value>]
|
|
362
|
+
|
|
363
|
+
ARGUMENTS
|
|
364
|
+
MESSAGE The log entry text.
|
|
365
|
+
|
|
366
|
+
FLAGS
|
|
367
|
+
-H, --host=<value> OAuth provider host (defaults to the prod issuer).
|
|
368
|
+
--end=<value> End date (ISO 8601). Defaults to now.
|
|
369
|
+
--ndjson Stream output as NDJSON (one compact JSON object per line).
|
|
370
|
+
--start=<value> Start date (ISO 8601). Defaults to now.
|
|
371
|
+
--timeout=<value> Network timeout in milliseconds.
|
|
372
|
+
--verbose Print redacted diagnostics to stderr.
|
|
373
|
+
--workspace=<value> (required) Workspace id (UUID) the entry belongs to.
|
|
374
|
+
|
|
375
|
+
GLOBAL FLAGS
|
|
376
|
+
--json Format output as json.
|
|
377
|
+
|
|
378
|
+
DESCRIPTION
|
|
379
|
+
Add an entry to a workspace's log.
|
|
380
|
+
|
|
381
|
+
Add an entry to a workspace's log (the logbook). You give the workspace (--workspace/--ws) and the message; the
|
|
382
|
+
organization is derived from the workspace, and the dates default to now (override with --start/--end). Returns the
|
|
383
|
+
created entry. Targets the OAuth/admin host.
|
|
384
|
+
|
|
385
|
+
ALIASES
|
|
386
|
+
$ flipstream logs add
|
|
387
|
+
|
|
388
|
+
EXAMPLES
|
|
389
|
+
$ flipstream log add "shipped the new pipeline" --workspace <id>
|
|
390
|
+
|
|
391
|
+
$ flipstream log add "kickoff call done" --ws <id> --json
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## `flipstream log list`
|
|
395
|
+
|
|
396
|
+
Read a workspace's log entries.
|
|
397
|
+
|
|
398
|
+
```
|
|
399
|
+
USAGE
|
|
400
|
+
$ flipstream log list --workspace <value> [--json] [-H <value>] [--ndjson] [--timeout <value>] [--verbose]
|
|
401
|
+
[--all] [--limit <value>] [--offset <value>] [--q <value>] [--sort <value>]
|
|
402
|
+
|
|
403
|
+
FLAGS
|
|
404
|
+
-H, --host=<value> OAuth provider host (defaults to the prod issuer).
|
|
405
|
+
--all Fetch every page (drains pagination).
|
|
406
|
+
--limit=<value> [default: 50] Page size.
|
|
407
|
+
--ndjson Stream output as NDJSON (one compact JSON object per line).
|
|
408
|
+
--offset=<value> Starting offset.
|
|
409
|
+
--q=<value> Free-text filter.
|
|
410
|
+
--sort=<value> Sort field.
|
|
411
|
+
--timeout=<value> Network timeout in milliseconds.
|
|
412
|
+
--verbose Print redacted diagnostics to stderr.
|
|
413
|
+
--workspace=<value> (required) The workspace whose log to read (UUID).
|
|
414
|
+
|
|
415
|
+
GLOBAL FLAGS
|
|
416
|
+
--json Format output as json.
|
|
417
|
+
|
|
418
|
+
DESCRIPTION
|
|
419
|
+
Read a workspace's log entries.
|
|
420
|
+
|
|
421
|
+
Read a workspace's log entries (the logbook). --workspace is required — a log entry only makes sense in a workspace's
|
|
422
|
+
context, so there is no account-wide read. Shows each entry's description, dates, and author; the full record
|
|
423
|
+
(including any new fields like tags) is under --json. Targets the OAuth/admin host.
|
|
424
|
+
|
|
425
|
+
ALIASES
|
|
426
|
+
$ flipstream logs list
|
|
427
|
+
|
|
428
|
+
EXAMPLES
|
|
429
|
+
$ flipstream log list --workspace <id>
|
|
430
|
+
|
|
431
|
+
$ flipstream logs list --ws <id> --json
|
|
432
|
+
|
|
433
|
+
$ flipstream log list --ws <id> --all
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
## `flipstream logs add MESSAGE`
|
|
437
|
+
|
|
438
|
+
Add an entry to a workspace's log.
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
USAGE
|
|
442
|
+
$ flipstream logs add MESSAGE --workspace <value> [--json] [-H <value>] [--ndjson] [--timeout <value>]
|
|
443
|
+
[--verbose] [--end <value>] [--start <value>]
|
|
444
|
+
|
|
445
|
+
ARGUMENTS
|
|
446
|
+
MESSAGE The log entry text.
|
|
447
|
+
|
|
448
|
+
FLAGS
|
|
449
|
+
-H, --host=<value> OAuth provider host (defaults to the prod issuer).
|
|
450
|
+
--end=<value> End date (ISO 8601). Defaults to now.
|
|
451
|
+
--ndjson Stream output as NDJSON (one compact JSON object per line).
|
|
452
|
+
--start=<value> Start date (ISO 8601). Defaults to now.
|
|
453
|
+
--timeout=<value> Network timeout in milliseconds.
|
|
454
|
+
--verbose Print redacted diagnostics to stderr.
|
|
455
|
+
--workspace=<value> (required) Workspace id (UUID) the entry belongs to.
|
|
456
|
+
|
|
457
|
+
GLOBAL FLAGS
|
|
458
|
+
--json Format output as json.
|
|
459
|
+
|
|
460
|
+
DESCRIPTION
|
|
461
|
+
Add an entry to a workspace's log.
|
|
462
|
+
|
|
463
|
+
Add an entry to a workspace's log (the logbook). You give the workspace (--workspace/--ws) and the message; the
|
|
464
|
+
organization is derived from the workspace, and the dates default to now (override with --start/--end). Returns the
|
|
465
|
+
created entry. Targets the OAuth/admin host.
|
|
466
|
+
|
|
467
|
+
ALIASES
|
|
468
|
+
$ flipstream logs add
|
|
469
|
+
|
|
470
|
+
EXAMPLES
|
|
471
|
+
$ flipstream log add "shipped the new pipeline" --workspace <id>
|
|
472
|
+
|
|
473
|
+
$ flipstream log add "kickoff call done" --ws <id> --json
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## `flipstream logs list`
|
|
477
|
+
|
|
478
|
+
Read a workspace's log entries.
|
|
479
|
+
|
|
480
|
+
```
|
|
481
|
+
USAGE
|
|
482
|
+
$ flipstream logs list --workspace <value> [--json] [-H <value>] [--ndjson] [--timeout <value>] [--verbose]
|
|
483
|
+
[--all] [--limit <value>] [--offset <value>] [--q <value>] [--sort <value>]
|
|
484
|
+
|
|
485
|
+
FLAGS
|
|
486
|
+
-H, --host=<value> OAuth provider host (defaults to the prod issuer).
|
|
487
|
+
--all Fetch every page (drains pagination).
|
|
488
|
+
--limit=<value> [default: 50] Page size.
|
|
489
|
+
--ndjson Stream output as NDJSON (one compact JSON object per line).
|
|
490
|
+
--offset=<value> Starting offset.
|
|
491
|
+
--q=<value> Free-text filter.
|
|
492
|
+
--sort=<value> Sort field.
|
|
493
|
+
--timeout=<value> Network timeout in milliseconds.
|
|
494
|
+
--verbose Print redacted diagnostics to stderr.
|
|
495
|
+
--workspace=<value> (required) The workspace whose log to read (UUID).
|
|
496
|
+
|
|
497
|
+
GLOBAL FLAGS
|
|
498
|
+
--json Format output as json.
|
|
499
|
+
|
|
500
|
+
DESCRIPTION
|
|
501
|
+
Read a workspace's log entries.
|
|
502
|
+
|
|
503
|
+
Read a workspace's log entries (the logbook). --workspace is required — a log entry only makes sense in a workspace's
|
|
504
|
+
context, so there is no account-wide read. Shows each entry's description, dates, and author; the full record
|
|
505
|
+
(including any new fields like tags) is under --json. Targets the OAuth/admin host.
|
|
506
|
+
|
|
507
|
+
ALIASES
|
|
508
|
+
$ flipstream logs list
|
|
509
|
+
|
|
510
|
+
EXAMPLES
|
|
511
|
+
$ flipstream log list --workspace <id>
|
|
512
|
+
|
|
513
|
+
$ flipstream logs list --ws <id> --json
|
|
514
|
+
|
|
515
|
+
$ flipstream log list --ws <id> --all
|
|
516
|
+
```
|
|
517
|
+
|
|
344
518
|
## `flipstream query`
|
|
345
519
|
|
|
346
520
|
Run an opaque query against the Flipstream data API.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import { fetchConnections
|
|
2
|
+
import { fetchConnections } from '../../lib/api/connections.js';
|
|
3
|
+
import { hydrate, WORKSPACE_NAME } from '../../lib/api/hydrate.js';
|
|
3
4
|
import { assertUuid } from '../../lib/api/ids.js';
|
|
4
5
|
import { authedAdminClient, renderConnectionsTable } from '../../lib/command/admin.js';
|
|
5
6
|
import { BaseCommand } from '../../lib/command/base.js';
|
|
@@ -17,7 +18,7 @@ export default class ConnectionsList extends BaseCommand {
|
|
|
17
18
|
];
|
|
18
19
|
static flags = {
|
|
19
20
|
...paginationFlags,
|
|
20
|
-
workspace: Flags.string({ description: "List only this workspace's connections (UUID)." }),
|
|
21
|
+
workspace: Flags.string({ aliases: ['ws'], description: "List only this workspace's connections (UUID)." }),
|
|
21
22
|
};
|
|
22
23
|
static summary = 'List connections, org-wide or for one workspace.';
|
|
23
24
|
async run() {
|
|
@@ -37,7 +38,7 @@ export default class ConnectionsList extends BaseCommand {
|
|
|
37
38
|
sort: this.flags.sort,
|
|
38
39
|
workspace,
|
|
39
40
|
});
|
|
40
|
-
await
|
|
41
|
+
await hydrate(client, projected.records, [WORKSPACE_NAME]);
|
|
41
42
|
return this.respondList(projected, (records) => renderConnectionsTable(records));
|
|
42
43
|
}
|
|
43
44
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/command/base.js';
|
|
2
|
+
export default class LogAdd extends BaseCommand<typeof LogAdd> {
|
|
3
|
+
static aliases: string[];
|
|
4
|
+
static args: {
|
|
5
|
+
message: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static flags: {
|
|
10
|
+
end: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
start: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
static summary: string;
|
|
15
|
+
run(): Promise<unknown>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { assertUuid } from '../../lib/api/ids.js';
|
|
3
|
+
import { addLogEntry, resolveWorkspaceOrg } from '../../lib/api/log.js';
|
|
4
|
+
import { authedAdminClient } from '../../lib/command/admin.js';
|
|
5
|
+
import { BaseCommand } from '../../lib/command/base.js';
|
|
6
|
+
import { AuthRequiredError } from '../../lib/errors.js';
|
|
7
|
+
import { createStore } from '../../lib/store/index.js';
|
|
8
|
+
export default class LogAdd extends BaseCommand {
|
|
9
|
+
static aliases = ['logs:add'];
|
|
10
|
+
static args = {
|
|
11
|
+
message: Args.string({ description: 'The log entry text.', required: true }),
|
|
12
|
+
};
|
|
13
|
+
static description = "Add an entry to a workspace's log (the logbook). You give the workspace (--workspace/--ws) and the message; the " +
|
|
14
|
+
'organization is derived from the workspace, and the dates default to now (override with --start/--end). ' +
|
|
15
|
+
'Returns the created entry. Targets the OAuth/admin host.';
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> log add "shipped the new pipeline" --workspace <id>',
|
|
18
|
+
'<%= config.bin %> log add "kickoff call done" --ws <id> --json',
|
|
19
|
+
];
|
|
20
|
+
static flags = {
|
|
21
|
+
end: Flags.string({ description: 'End date (ISO 8601). Defaults to now.' }),
|
|
22
|
+
start: Flags.string({ description: 'Start date (ISO 8601). Defaults to now.' }),
|
|
23
|
+
workspace: Flags.string({ aliases: ['ws'], description: 'Workspace id (UUID) the entry belongs to.', required: true }),
|
|
24
|
+
};
|
|
25
|
+
static summary = "Add an entry to a workspace's log.";
|
|
26
|
+
async run() {
|
|
27
|
+
assertUuid(this.flags.workspace, 'workspace id'); // exit 2, no request
|
|
28
|
+
const host = this.resolvedHost();
|
|
29
|
+
const store = createStore();
|
|
30
|
+
if (!store.load(host))
|
|
31
|
+
throw AuthRequiredError.notLoggedIn();
|
|
32
|
+
const client = authedAdminClient(host, store, this.flags.timeout);
|
|
33
|
+
const organizationId = await resolveWorkspaceOrg(client, this.flags.workspace);
|
|
34
|
+
const now = new Date().toISOString();
|
|
35
|
+
const created = await addLogEntry(client, {
|
|
36
|
+
clientId: this.flags.workspace,
|
|
37
|
+
description: this.args.message,
|
|
38
|
+
endDate: this.flags.end ?? now,
|
|
39
|
+
organizationId,
|
|
40
|
+
startDate: this.flags.start ?? now,
|
|
41
|
+
});
|
|
42
|
+
return this.respond(created, (entry) => this.log(`Logged entry ${String(entry.id)} to workspace ${this.flags.workspace}.`));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/command/base.js';
|
|
2
|
+
export default class LogList extends BaseCommand<typeof LogList> {
|
|
3
|
+
static aliases: string[];
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
offset: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
q: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
sort: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
static summary: string;
|
|
15
|
+
run(): Promise<{
|
|
16
|
+
count: number;
|
|
17
|
+
records: unknown[];
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { CREATOR_EMAIL, hydrate, WORKSPACE_NAME } from '../../lib/api/hydrate.js';
|
|
3
|
+
import { assertUuid } from '../../lib/api/ids.js';
|
|
4
|
+
import { fetchLog } from '../../lib/api/log.js';
|
|
5
|
+
import { authedAdminClient, renderLogEntriesTable } from '../../lib/command/admin.js';
|
|
6
|
+
import { BaseCommand } from '../../lib/command/base.js';
|
|
7
|
+
import { paginationFlags } from '../../lib/command/flags.js';
|
|
8
|
+
import { AuthRequiredError } from '../../lib/errors.js';
|
|
9
|
+
import { createStore } from '../../lib/store/index.js';
|
|
10
|
+
export default class LogList extends BaseCommand {
|
|
11
|
+
static aliases = ['logs:list'];
|
|
12
|
+
static description = "Read a workspace's log entries (the logbook). --workspace is required — a log entry only makes sense in a " +
|
|
13
|
+
"workspace's context, so there is no account-wide read. Shows each entry's description, dates, and author; the " +
|
|
14
|
+
'full record (including any new fields like tags) is under --json. Targets the OAuth/admin host.';
|
|
15
|
+
static examples = [
|
|
16
|
+
'<%= config.bin %> log list --workspace <id>',
|
|
17
|
+
'<%= config.bin %> logs list --ws <id> --json',
|
|
18
|
+
'<%= config.bin %> log list --ws <id> --all',
|
|
19
|
+
];
|
|
20
|
+
static flags = {
|
|
21
|
+
...paginationFlags,
|
|
22
|
+
workspace: Flags.string({ aliases: ['ws'], description: 'The workspace whose log to read (UUID).', required: true }),
|
|
23
|
+
};
|
|
24
|
+
static summary = "Read a workspace's log entries.";
|
|
25
|
+
async run() {
|
|
26
|
+
assertUuid(this.flags.workspace, 'workspace id'); // exit 2, no request
|
|
27
|
+
const host = this.resolvedHost();
|
|
28
|
+
const store = createStore();
|
|
29
|
+
if (!store.load(host))
|
|
30
|
+
throw AuthRequiredError.notLoggedIn();
|
|
31
|
+
const client = authedAdminClient(host, store, this.flags.timeout);
|
|
32
|
+
const projected = await fetchLog(client, {
|
|
33
|
+
all: this.flags.all,
|
|
34
|
+
limit: this.flags.limit,
|
|
35
|
+
offset: this.flags.offset,
|
|
36
|
+
q: this.flags.q,
|
|
37
|
+
sort: this.flags.sort,
|
|
38
|
+
workspace: this.flags.workspace,
|
|
39
|
+
});
|
|
40
|
+
await hydrate(client, projected.records, [WORKSPACE_NAME, CREATOR_EMAIL]);
|
|
41
|
+
return this.respondList(projected, (records) => renderLogEntriesTable(records));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
|
-
import { fetchConnections
|
|
2
|
+
import { fetchConnections } from '../../lib/api/connections.js';
|
|
3
|
+
import { hydrate, WORKSPACE_NAME } from '../../lib/api/hydrate.js';
|
|
3
4
|
import { assertUuid } from '../../lib/api/ids.js';
|
|
4
5
|
import { authedAdminClient, renderConnectionsTable } from '../../lib/command/admin.js';
|
|
5
6
|
import { BaseCommand } from '../../lib/command/base.js';
|
|
@@ -35,7 +36,7 @@ export default class WorkspacesConnections extends BaseCommand {
|
|
|
35
36
|
sort: this.flags.sort,
|
|
36
37
|
workspace: this.args.id,
|
|
37
38
|
});
|
|
38
|
-
await
|
|
39
|
+
await hydrate(client, projected.records, [WORKSPACE_NAME]);
|
|
39
40
|
return this.respondList(projected, (records) => renderConnectionsTable(records));
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -3,8 +3,13 @@ export interface AdminGetOptions {
|
|
|
3
3
|
query?: Record<string, boolean | number | string | undefined>;
|
|
4
4
|
timeoutMs?: number;
|
|
5
5
|
}
|
|
6
|
+
export interface AdminPostOptions {
|
|
7
|
+
body?: unknown;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
6
10
|
export interface AdminClient {
|
|
7
11
|
get(path: string, options?: AdminGetOptions): Promise<unknown>;
|
|
12
|
+
post(path: string, options?: AdminPostOptions): Promise<unknown>;
|
|
8
13
|
}
|
|
9
14
|
export interface AdminClientOptions {
|
|
10
15
|
getToken: () => null | Promise<null | string> | string;
|
|
@@ -22,6 +22,16 @@ export function createAdminClient(options) {
|
|
|
22
22
|
url: `${base}${path}`,
|
|
23
23
|
});
|
|
24
24
|
},
|
|
25
|
+
async post(path, postOptions = {}) {
|
|
26
|
+
const token = await options.getToken();
|
|
27
|
+
return requestJson({
|
|
28
|
+
body: postOptions.body,
|
|
29
|
+
method: 'POST',
|
|
30
|
+
timeoutMs: postOptions.timeoutMs ?? defaultTimeout,
|
|
31
|
+
token,
|
|
32
|
+
url: `${base}${path}`,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
25
35
|
};
|
|
26
36
|
}
|
|
27
37
|
// The authed admin client: the SAME withFreshToken 401->refresh->retry-once
|
|
@@ -40,5 +50,14 @@ export function createAuthedAdminClient(deps) {
|
|
|
40
50
|
url: `${base}${path}`,
|
|
41
51
|
}));
|
|
42
52
|
},
|
|
53
|
+
post(path, postOptions = {}) {
|
|
54
|
+
return withFreshToken(deps, (token) => requestJson({
|
|
55
|
+
body: postOptions.body,
|
|
56
|
+
method: 'POST',
|
|
57
|
+
timeoutMs: postOptions.timeoutMs ?? defaultTimeout,
|
|
58
|
+
token,
|
|
59
|
+
url: `${base}${path}`,
|
|
60
|
+
}));
|
|
61
|
+
},
|
|
43
62
|
};
|
|
44
63
|
}
|
|
@@ -11,4 +11,3 @@ export declare function fetchConnections(client: AdminClient, query: Connections
|
|
|
11
11
|
count: number;
|
|
12
12
|
records: Array<Record<string, unknown>>;
|
|
13
13
|
}>;
|
|
14
|
-
export declare function resolveWorkspaceNames(client: AdminClient, records: Array<Record<string, unknown>>): Promise<void>;
|
|
@@ -17,28 +17,3 @@ export async function fetchConnections(client, query) {
|
|
|
17
17
|
const envelope = query.all ? await drainPages(fetchPage, start) : await fetchPage(start);
|
|
18
18
|
return { count: envelope.count, records: envelope.records.map((record) => projectConnection(record)) };
|
|
19
19
|
}
|
|
20
|
-
// Fill `client_name` for connections that lack it (the API didn't nest the
|
|
21
|
-
// client): one drained workspace lookup, keyed by id. Skipped when all already
|
|
22
|
-
// have it; best-effort (leaves names unresolved rather than failing the list).
|
|
23
|
-
export async function resolveWorkspaceNames(client, records) {
|
|
24
|
-
const missing = records.some((record) => record.client_name === undefined && record.client_id !== undefined);
|
|
25
|
-
if (!missing)
|
|
26
|
-
return;
|
|
27
|
-
const names = new Map();
|
|
28
|
-
try {
|
|
29
|
-
const workspaces = await drainPages(async (page) => normalizeList(await client.get('/clients', { query: { limit: page.limit, offset: page.offset } })), { limit: 100, offset: 0 });
|
|
30
|
-
for (const workspace of workspaces.records) {
|
|
31
|
-
const ws = workspace;
|
|
32
|
-
if (ws.id)
|
|
33
|
-
names.set(ws.id, ws.name ?? '');
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
for (const record of records) {
|
|
40
|
-
if (record.client_name === undefined && typeof record.client_id === 'string') {
|
|
41
|
-
record.client_name = names.get(record.client_id) ?? null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type AdminClient } from './admin-client.js';
|
|
2
|
+
export interface HydrateSpec {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
idField: string;
|
|
5
|
+
label: (entity: Record<string, unknown>) => unknown;
|
|
6
|
+
labelField: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const WORKSPACE_NAME: HydrateSpec;
|
|
9
|
+
export declare const CREATOR_EMAIL: HydrateSpec;
|
|
10
|
+
export declare function hydrate(client: AdminClient, records: Array<Record<string, unknown>>, specs: HydrateSpec[]): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { drainPages, normalizeList } from './list.js';
|
|
2
|
+
// The shared workspace-name and author-email specs (used across list commands).
|
|
3
|
+
export const WORKSPACE_NAME = {
|
|
4
|
+
endpoint: '/clients',
|
|
5
|
+
idField: 'client_id',
|
|
6
|
+
label: (entity) => entity.name,
|
|
7
|
+
labelField: 'client_name',
|
|
8
|
+
};
|
|
9
|
+
export const CREATOR_EMAIL = {
|
|
10
|
+
endpoint: '/users',
|
|
11
|
+
idField: 'created_by_id',
|
|
12
|
+
label: (entity) => entity.email ?? entity.name,
|
|
13
|
+
labelField: 'created_by_email',
|
|
14
|
+
};
|
|
15
|
+
async function hydrateOne(client, records, spec) {
|
|
16
|
+
const needs = (record) => record[spec.labelField] === undefined &&
|
|
17
|
+
typeof record[spec.idField] === 'string' &&
|
|
18
|
+
record[spec.idField].length > 0;
|
|
19
|
+
if (!records.some((record) => needs(record)))
|
|
20
|
+
return;
|
|
21
|
+
const byId = new Map();
|
|
22
|
+
try {
|
|
23
|
+
const entities = await drainPages(async (page) => normalizeList(await client.get(spec.endpoint, { query: { limit: page.limit, offset: page.offset } })), { limit: 100, offset: 0 });
|
|
24
|
+
for (const raw of entities.records) {
|
|
25
|
+
const entity = (raw ?? {});
|
|
26
|
+
if (typeof entity.id === 'string') {
|
|
27
|
+
const label = spec.label(entity);
|
|
28
|
+
byId.set(entity.id, typeof label === 'string' ? label : '');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return; // best-effort: leave the label unresolved rather than fail the list
|
|
34
|
+
}
|
|
35
|
+
for (const record of records) {
|
|
36
|
+
if (needs(record))
|
|
37
|
+
record[spec.labelField] = byId.get(record[spec.idField]) ?? null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Hydrate a record set against one or more specs, running each lookup CONCURRENTLY
|
|
41
|
+
// and ONCE. Each spec is skipped when no record needs it, and is best-effort: a
|
|
42
|
+
// forbidden/unavailable lookup leaves its label unresolved rather than failing.
|
|
43
|
+
// Mutates `records` in place.
|
|
44
|
+
export async function hydrate(client, records, specs) {
|
|
45
|
+
await Promise.all(specs.map((spec) => hydrateOne(client, records, spec)));
|
|
46
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type AdminClient } from './admin-client.js';
|
|
2
|
+
export interface LogQuery {
|
|
3
|
+
all?: boolean;
|
|
4
|
+
limit: number;
|
|
5
|
+
offset: number;
|
|
6
|
+
q?: string;
|
|
7
|
+
sort?: string;
|
|
8
|
+
workspace: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function fetchLog(client: AdminClient, query: LogQuery): Promise<{
|
|
11
|
+
count: number;
|
|
12
|
+
records: Array<Record<string, unknown>>;
|
|
13
|
+
}>;
|
|
14
|
+
export interface NewLogEntry {
|
|
15
|
+
clientId: string;
|
|
16
|
+
description: string;
|
|
17
|
+
endDate: string;
|
|
18
|
+
organizationId: string;
|
|
19
|
+
startDate: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function addLogEntry(client: AdminClient, entry: NewLogEntry): Promise<Record<string, unknown>>;
|
|
22
|
+
export declare function resolveWorkspaceOrg(client: AdminClient, workspaceId: string): Promise<string>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DataHttpError } from '../errors.js';
|
|
2
|
+
import { mapDataError, notFoundError } from './errors.js';
|
|
3
|
+
import { drainPages, normalizeList } from './list.js';
|
|
4
|
+
import { projectLogEntry, projectWorkspace } from './projections.js';
|
|
5
|
+
// Fetch + project a workspace's log (event-log) entries via the per-client
|
|
6
|
+
// endpoint, draining pages when requested. Errors are mapped to the E3 model.
|
|
7
|
+
export async function fetchLog(client, query) {
|
|
8
|
+
const path = `/clients/${query.workspace}/event-logs`;
|
|
9
|
+
const fetchPage = async (page) => {
|
|
10
|
+
try {
|
|
11
|
+
return normalizeList(await client.get(path, { query: { limit: page.limit, offset: page.offset, q: query.q, sort: query.sort } }));
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
throw mapDataError(error);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const start = { limit: query.limit, offset: query.offset };
|
|
18
|
+
const envelope = query.all ? await drainPages(fetchPage, start) : await fetchPage(start);
|
|
19
|
+
return { count: envelope.count, records: envelope.records.map((record) => projectLogEntry(record)) };
|
|
20
|
+
}
|
|
21
|
+
// Add a log entry: POST /event-logs (the workspace + org go in the body). The
|
|
22
|
+
// body is a structured builder, so future fields (e.g. tags) drop in here.
|
|
23
|
+
// Returns the projected created entry. Errors are mapped to the E3 model.
|
|
24
|
+
export async function addLogEntry(client, entry) {
|
|
25
|
+
const body = {
|
|
26
|
+
client_id: entry.clientId,
|
|
27
|
+
description: entry.description,
|
|
28
|
+
end_date: entry.endDate,
|
|
29
|
+
organization_id: entry.organizationId,
|
|
30
|
+
start_date: entry.startDate,
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
return projectLogEntry(await client.post('/event-logs', { body }));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw mapDataError(error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Derive a workspace's organization_id (required to create an entry) via one
|
|
40
|
+
// workspace lookup. A foreign or missing id collapses to a single not-found
|
|
41
|
+
// message (no existence leak), mirroring `ws get`.
|
|
42
|
+
export async function resolveWorkspaceOrg(client, workspaceId) {
|
|
43
|
+
let raw;
|
|
44
|
+
try {
|
|
45
|
+
raw = await client.get(`/clients/${workspaceId}`);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof DataHttpError && (error.status === 403 || error.status === 404))
|
|
49
|
+
throw notFoundError();
|
|
50
|
+
throw mapDataError(error);
|
|
51
|
+
}
|
|
52
|
+
const org = projectWorkspace(raw).organization_id;
|
|
53
|
+
if (typeof org !== 'string' || org.length === 0)
|
|
54
|
+
throw notFoundError();
|
|
55
|
+
return org;
|
|
56
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { redact } from '../output/redact.js';
|
|
2
2
|
const WORKSPACE_CORE = ['organization_id', 'slug', 'business_type', 'client_main_goal', 'created_on'];
|
|
3
3
|
const CONNECTION_CORE = ['name', 'client_id', 'type', 'active', 'backfill', 'data_refresh_status', 'created_on'];
|
|
4
|
+
const LOG_CORE = ['description', 'start_date', 'end_date', 'client_id', 'organization_id', 'created_on', 'created_by_id'];
|
|
4
5
|
// Project a raw admin Client into a stable "workspace" shape: id + name + present
|
|
5
6
|
// core fields + owner_email (from the nested owner) + a redacted `raw` passthrough
|
|
6
7
|
// so it survives schema drift. Adds/invents no fields.
|
|
@@ -32,3 +33,25 @@ export function projectConnection(raw) {
|
|
|
32
33
|
out.raw = redact(raw);
|
|
33
34
|
return out;
|
|
34
35
|
}
|
|
36
|
+
// Project a raw EventLog (the "log" / logbook entry) into a stable shape: id +
|
|
37
|
+
// present core fields + the workspace name (nested `client`) and author (nested
|
|
38
|
+
// `created_by`) + a redacted `raw` passthrough. New API fields (e.g. `tags`)
|
|
39
|
+
// flow through `raw` automatically under --json; surface them by adding to
|
|
40
|
+
// LOG_CORE + a table column when they ship.
|
|
41
|
+
export function projectLogEntry(raw) {
|
|
42
|
+
const entry = (raw ?? {});
|
|
43
|
+
const out = { id: entry.id };
|
|
44
|
+
for (const key of LOG_CORE)
|
|
45
|
+
if (entry[key] !== undefined)
|
|
46
|
+
out[key] = entry[key];
|
|
47
|
+
const client = entry.client;
|
|
48
|
+
if (client?.name)
|
|
49
|
+
out.client_name = client.name;
|
|
50
|
+
const by = entry.created_by;
|
|
51
|
+
if (by?.name)
|
|
52
|
+
out.created_by_name = by.name;
|
|
53
|
+
if (by?.email)
|
|
54
|
+
out.created_by_email = by.email;
|
|
55
|
+
out.raw = redact(raw);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
package/dist/lib/auth/claims.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
// MUST NOT be used for any authorization / request-gating decision (the CLI is a
|
|
3
3
|
// thin client; the server validates tokens). A convention test guards this.
|
|
4
4
|
export function decodeJwtClaimsUnverified(accessToken) {
|
|
5
|
-
const
|
|
6
|
-
if (
|
|
5
|
+
const [, payload] = accessToken.split('.');
|
|
6
|
+
if (payload === undefined)
|
|
7
7
|
return null;
|
|
8
8
|
try {
|
|
9
|
-
const padded =
|
|
9
|
+
const padded = payload + '='.repeat((4 - (payload.length % 4)) % 4);
|
|
10
10
|
return JSON.parse(Buffer.from(padded, 'base64url').toString('utf8'));
|
|
11
11
|
}
|
|
12
12
|
catch {
|
|
@@ -2,3 +2,4 @@ import { type AdminClient } from '../api/admin-client.js';
|
|
|
2
2
|
import { type TokenStore } from '../store/keyring.js';
|
|
3
3
|
export declare function authedAdminClient(host: string, store: TokenStore, timeoutMs?: number): AdminClient;
|
|
4
4
|
export declare function renderConnectionsTable(records: Array<Record<string, unknown>>): void;
|
|
5
|
+
export declare function renderLogEntriesTable(records: Array<Record<string, unknown>>): void;
|
|
@@ -28,3 +28,24 @@ export function renderConnectionsTable(records) {
|
|
|
28
28
|
{ key: 'id', name: 'ID' },
|
|
29
29
|
], 'No connections.');
|
|
30
30
|
}
|
|
31
|
+
// The log (event-log) human table — shared by `log list`. Date prefers
|
|
32
|
+
// created_on (falls back to start_date); By prefers the author name then email;
|
|
33
|
+
// Workspace falls back to the id when its name couldn't be resolved.
|
|
34
|
+
// First non-empty string among the candidates, else an em-dash placeholder.
|
|
35
|
+
function firstLabel(...values) {
|
|
36
|
+
return values.find((value) => typeof value === 'string' && value.length > 0) ?? '—';
|
|
37
|
+
}
|
|
38
|
+
export function renderLogEntriesTable(records) {
|
|
39
|
+
const rows = records.map((record) => ({
|
|
40
|
+
...record,
|
|
41
|
+
by: firstLabel(record.created_by_name, record.created_by_email, record.created_by_id),
|
|
42
|
+
client_name: firstLabel(record.client_name, record.client_id),
|
|
43
|
+
when: firstLabel(record.created_on, record.start_date),
|
|
44
|
+
}));
|
|
45
|
+
renderTable(rows, [
|
|
46
|
+
{ key: 'when', name: 'Date' },
|
|
47
|
+
{ key: 'client_name', name: 'Workspace' },
|
|
48
|
+
{ key: 'description', name: 'Entry' },
|
|
49
|
+
{ key: 'by', name: 'By' },
|
|
50
|
+
], 'No log entries.');
|
|
51
|
+
}
|
package/dist/lib/errors.js
CHANGED
|
@@ -98,6 +98,11 @@ export function toExitCode(error) {
|
|
|
98
98
|
if (error.name === 'LoopbackError') {
|
|
99
99
|
return /tim(e|ed) ?out/i.test(error.message) ? ExitCode.TIMEOUT : ExitCode.AUTH_FAILED;
|
|
100
100
|
}
|
|
101
|
+
// oclif parse/usage errors (missing or unknown flag, bad arg) carry their own
|
|
102
|
+
// exit — honor it so a missing required flag is exit 2 ("bad invocation").
|
|
103
|
+
const oclifExit = error.oclif?.exit;
|
|
104
|
+
if (typeof oclifExit === 'number' && oclifExit > 0)
|
|
105
|
+
return oclifExit;
|
|
101
106
|
}
|
|
102
107
|
return ExitCode.GENERIC;
|
|
103
108
|
}
|
|
@@ -109,7 +114,13 @@ export function toExitCode(error) {
|
|
|
109
114
|
// tokens never leak.
|
|
110
115
|
export function renderError(error, options = {}) {
|
|
111
116
|
const exitCode = toExitCode(error);
|
|
112
|
-
|
|
117
|
+
let code = 'error';
|
|
118
|
+
if (error instanceof CliError)
|
|
119
|
+
code = error.code;
|
|
120
|
+
else if (exitCode === ExitCode.USAGE)
|
|
121
|
+
code = 'usage'; // oclif parse/usage errors
|
|
122
|
+
else if (error instanceof Error)
|
|
123
|
+
code = error.name;
|
|
113
124
|
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
114
125
|
const message = redact(rawMessage);
|
|
115
126
|
if (options.json) {
|
package/oclif.manifest.json
CHANGED
|
@@ -518,6 +518,9 @@
|
|
|
518
518
|
"type": "option"
|
|
519
519
|
},
|
|
520
520
|
"workspace": {
|
|
521
|
+
"aliases": [
|
|
522
|
+
"ws"
|
|
523
|
+
],
|
|
521
524
|
"description": "List only this workspace's connections (UUID).",
|
|
522
525
|
"name": "workspace",
|
|
523
526
|
"hasDynamicHelp": false,
|
|
@@ -542,6 +545,217 @@
|
|
|
542
545
|
"list.js"
|
|
543
546
|
]
|
|
544
547
|
},
|
|
548
|
+
"log:add": {
|
|
549
|
+
"aliases": [
|
|
550
|
+
"logs:add"
|
|
551
|
+
],
|
|
552
|
+
"args": {
|
|
553
|
+
"message": {
|
|
554
|
+
"description": "The log entry text.",
|
|
555
|
+
"name": "message",
|
|
556
|
+
"required": true
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
"description": "Add an entry to a workspace's log (the logbook). You give the workspace (--workspace/--ws) and the message; the organization is derived from the workspace, and the dates default to now (override with --start/--end). Returns the created entry. Targets the OAuth/admin host.",
|
|
560
|
+
"examples": [
|
|
561
|
+
"<%= config.bin %> log add \"shipped the new pipeline\" --workspace <id>",
|
|
562
|
+
"<%= config.bin %> log add \"kickoff call done\" --ws <id> --json"
|
|
563
|
+
],
|
|
564
|
+
"flags": {
|
|
565
|
+
"json": {
|
|
566
|
+
"description": "Format output as json.",
|
|
567
|
+
"helpGroup": "GLOBAL",
|
|
568
|
+
"name": "json",
|
|
569
|
+
"allowNo": false,
|
|
570
|
+
"type": "boolean"
|
|
571
|
+
},
|
|
572
|
+
"host": {
|
|
573
|
+
"char": "H",
|
|
574
|
+
"description": "OAuth provider host (defaults to the prod issuer).",
|
|
575
|
+
"name": "host",
|
|
576
|
+
"hasDynamicHelp": false,
|
|
577
|
+
"multiple": false,
|
|
578
|
+
"type": "option"
|
|
579
|
+
},
|
|
580
|
+
"ndjson": {
|
|
581
|
+
"description": "Stream output as NDJSON (one compact JSON object per line).",
|
|
582
|
+
"name": "ndjson",
|
|
583
|
+
"allowNo": false,
|
|
584
|
+
"type": "boolean"
|
|
585
|
+
},
|
|
586
|
+
"timeout": {
|
|
587
|
+
"description": "Network timeout in milliseconds.",
|
|
588
|
+
"name": "timeout",
|
|
589
|
+
"hasDynamicHelp": false,
|
|
590
|
+
"multiple": false,
|
|
591
|
+
"type": "option"
|
|
592
|
+
},
|
|
593
|
+
"verbose": {
|
|
594
|
+
"aliases": [
|
|
595
|
+
"debug"
|
|
596
|
+
],
|
|
597
|
+
"description": "Print redacted diagnostics to stderr.",
|
|
598
|
+
"name": "verbose",
|
|
599
|
+
"allowNo": false,
|
|
600
|
+
"type": "boolean"
|
|
601
|
+
},
|
|
602
|
+
"end": {
|
|
603
|
+
"description": "End date (ISO 8601). Defaults to now.",
|
|
604
|
+
"name": "end",
|
|
605
|
+
"hasDynamicHelp": false,
|
|
606
|
+
"multiple": false,
|
|
607
|
+
"type": "option"
|
|
608
|
+
},
|
|
609
|
+
"start": {
|
|
610
|
+
"description": "Start date (ISO 8601). Defaults to now.",
|
|
611
|
+
"name": "start",
|
|
612
|
+
"hasDynamicHelp": false,
|
|
613
|
+
"multiple": false,
|
|
614
|
+
"type": "option"
|
|
615
|
+
},
|
|
616
|
+
"workspace": {
|
|
617
|
+
"aliases": [
|
|
618
|
+
"ws"
|
|
619
|
+
],
|
|
620
|
+
"description": "Workspace id (UUID) the entry belongs to.",
|
|
621
|
+
"name": "workspace",
|
|
622
|
+
"required": true,
|
|
623
|
+
"hasDynamicHelp": false,
|
|
624
|
+
"multiple": false,
|
|
625
|
+
"type": "option"
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
"hasDynamicHelp": false,
|
|
629
|
+
"hiddenAliases": [],
|
|
630
|
+
"id": "log:add",
|
|
631
|
+
"pluginAlias": "flipstream",
|
|
632
|
+
"pluginName": "flipstream",
|
|
633
|
+
"pluginType": "core",
|
|
634
|
+
"strict": true,
|
|
635
|
+
"summary": "Add an entry to a workspace's log.",
|
|
636
|
+
"enableJsonFlag": true,
|
|
637
|
+
"isESM": true,
|
|
638
|
+
"relativePath": [
|
|
639
|
+
"dist",
|
|
640
|
+
"commands",
|
|
641
|
+
"log",
|
|
642
|
+
"add.js"
|
|
643
|
+
]
|
|
644
|
+
},
|
|
645
|
+
"log:list": {
|
|
646
|
+
"aliases": [
|
|
647
|
+
"logs:list"
|
|
648
|
+
],
|
|
649
|
+
"args": {},
|
|
650
|
+
"description": "Read a workspace's log entries (the logbook). --workspace is required — a log entry only makes sense in a workspace's context, so there is no account-wide read. Shows each entry's description, dates, and author; the full record (including any new fields like tags) is under --json. Targets the OAuth/admin host.",
|
|
651
|
+
"examples": [
|
|
652
|
+
"<%= config.bin %> log list --workspace <id>",
|
|
653
|
+
"<%= config.bin %> logs list --ws <id> --json",
|
|
654
|
+
"<%= config.bin %> log list --ws <id> --all"
|
|
655
|
+
],
|
|
656
|
+
"flags": {
|
|
657
|
+
"json": {
|
|
658
|
+
"description": "Format output as json.",
|
|
659
|
+
"helpGroup": "GLOBAL",
|
|
660
|
+
"name": "json",
|
|
661
|
+
"allowNo": false,
|
|
662
|
+
"type": "boolean"
|
|
663
|
+
},
|
|
664
|
+
"host": {
|
|
665
|
+
"char": "H",
|
|
666
|
+
"description": "OAuth provider host (defaults to the prod issuer).",
|
|
667
|
+
"name": "host",
|
|
668
|
+
"hasDynamicHelp": false,
|
|
669
|
+
"multiple": false,
|
|
670
|
+
"type": "option"
|
|
671
|
+
},
|
|
672
|
+
"ndjson": {
|
|
673
|
+
"description": "Stream output as NDJSON (one compact JSON object per line).",
|
|
674
|
+
"name": "ndjson",
|
|
675
|
+
"allowNo": false,
|
|
676
|
+
"type": "boolean"
|
|
677
|
+
},
|
|
678
|
+
"timeout": {
|
|
679
|
+
"description": "Network timeout in milliseconds.",
|
|
680
|
+
"name": "timeout",
|
|
681
|
+
"hasDynamicHelp": false,
|
|
682
|
+
"multiple": false,
|
|
683
|
+
"type": "option"
|
|
684
|
+
},
|
|
685
|
+
"verbose": {
|
|
686
|
+
"aliases": [
|
|
687
|
+
"debug"
|
|
688
|
+
],
|
|
689
|
+
"description": "Print redacted diagnostics to stderr.",
|
|
690
|
+
"name": "verbose",
|
|
691
|
+
"allowNo": false,
|
|
692
|
+
"type": "boolean"
|
|
693
|
+
},
|
|
694
|
+
"all": {
|
|
695
|
+
"description": "Fetch every page (drains pagination).",
|
|
696
|
+
"name": "all",
|
|
697
|
+
"allowNo": false,
|
|
698
|
+
"type": "boolean"
|
|
699
|
+
},
|
|
700
|
+
"limit": {
|
|
701
|
+
"description": "Page size.",
|
|
702
|
+
"name": "limit",
|
|
703
|
+
"default": 50,
|
|
704
|
+
"hasDynamicHelp": false,
|
|
705
|
+
"multiple": false,
|
|
706
|
+
"type": "option"
|
|
707
|
+
},
|
|
708
|
+
"offset": {
|
|
709
|
+
"description": "Starting offset.",
|
|
710
|
+
"name": "offset",
|
|
711
|
+
"default": 0,
|
|
712
|
+
"hasDynamicHelp": false,
|
|
713
|
+
"multiple": false,
|
|
714
|
+
"type": "option"
|
|
715
|
+
},
|
|
716
|
+
"q": {
|
|
717
|
+
"description": "Free-text filter.",
|
|
718
|
+
"name": "q",
|
|
719
|
+
"hasDynamicHelp": false,
|
|
720
|
+
"multiple": false,
|
|
721
|
+
"type": "option"
|
|
722
|
+
},
|
|
723
|
+
"sort": {
|
|
724
|
+
"description": "Sort field.",
|
|
725
|
+
"name": "sort",
|
|
726
|
+
"hasDynamicHelp": false,
|
|
727
|
+
"multiple": false,
|
|
728
|
+
"type": "option"
|
|
729
|
+
},
|
|
730
|
+
"workspace": {
|
|
731
|
+
"aliases": [
|
|
732
|
+
"ws"
|
|
733
|
+
],
|
|
734
|
+
"description": "The workspace whose log to read (UUID).",
|
|
735
|
+
"name": "workspace",
|
|
736
|
+
"required": true,
|
|
737
|
+
"hasDynamicHelp": false,
|
|
738
|
+
"multiple": false,
|
|
739
|
+
"type": "option"
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
"hasDynamicHelp": false,
|
|
743
|
+
"hiddenAliases": [],
|
|
744
|
+
"id": "log:list",
|
|
745
|
+
"pluginAlias": "flipstream",
|
|
746
|
+
"pluginName": "flipstream",
|
|
747
|
+
"pluginType": "core",
|
|
748
|
+
"strict": true,
|
|
749
|
+
"summary": "Read a workspace's log entries.",
|
|
750
|
+
"enableJsonFlag": true,
|
|
751
|
+
"isESM": true,
|
|
752
|
+
"relativePath": [
|
|
753
|
+
"dist",
|
|
754
|
+
"commands",
|
|
755
|
+
"log",
|
|
756
|
+
"list.js"
|
|
757
|
+
]
|
|
758
|
+
},
|
|
545
759
|
"workspaces:connections": {
|
|
546
760
|
"aliases": [
|
|
547
761
|
"ws:connections"
|
|
@@ -847,5 +1061,5 @@
|
|
|
847
1061
|
]
|
|
848
1062
|
}
|
|
849
1063
|
},
|
|
850
|
-
"version": "0.
|
|
1064
|
+
"version": "0.5.0"
|
|
851
1065
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flipstream",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Flipstream CLI — query your Flipstream data from the terminal.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"flipstream",
|
|
@@ -33,9 +33,19 @@
|
|
|
33
33
|
"lint": "eslint .",
|
|
34
34
|
"pack:prepare": "tsc -p tsconfig.json && oclif manifest",
|
|
35
35
|
"posttest": "eslint .",
|
|
36
|
+
"prepare": "husky || true",
|
|
36
37
|
"smoke": "bun scripts/smoke.ts",
|
|
37
38
|
"test": "bun test"
|
|
38
39
|
},
|
|
40
|
+
"lint-staged": {
|
|
41
|
+
"*.ts": [
|
|
42
|
+
"eslint --fix",
|
|
43
|
+
"prettier --write"
|
|
44
|
+
],
|
|
45
|
+
"*.{json,yml,yaml}": [
|
|
46
|
+
"prettier --write"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
39
49
|
"dependencies": {
|
|
40
50
|
"@napi-rs/keyring": "^1.3.0",
|
|
41
51
|
"@oclif/core": "^4",
|
|
@@ -51,6 +61,8 @@
|
|
|
51
61
|
"eslint": "^10.4.1",
|
|
52
62
|
"eslint-config-oclif": "^6.0.166",
|
|
53
63
|
"eslint-config-prettier": "^10.1.8",
|
|
64
|
+
"husky": "^9.1.7",
|
|
65
|
+
"lint-staged": "^17.0.7",
|
|
54
66
|
"oclif": "^4.23.8",
|
|
55
67
|
"prettier": "^3.8.3",
|
|
56
68
|
"typescript": "^5"
|
|
@@ -73,6 +85,9 @@
|
|
|
73
85
|
},
|
|
74
86
|
"connections": {
|
|
75
87
|
"description": "List your data-source connections, org-wide or per workspace (alias: conn)."
|
|
88
|
+
},
|
|
89
|
+
"log": {
|
|
90
|
+
"description": "Read and add workspace log entries — the logbook (list, add; alias: logs)."
|
|
76
91
|
}
|
|
77
92
|
}
|
|
78
93
|
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { CLIENT_NAME } from '../config/constants.js';
|
|
2
|
-
import { AuthFailedError } from '../errors.js';
|
|
3
|
-
// Dynamically register a PUBLIC OAuth client (RFC 7591) and return its
|
|
4
|
-
// client_id. The CLI never requests, parses, or stores a client secret — the
|
|
5
|
-
// return type is the bare client_id string so a refactor cannot leak a secret.
|
|
6
|
-
export async function registerClient(meta, redirectUri, scopes, options = {}) {
|
|
7
|
-
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
8
|
-
const controller = new AbortController();
|
|
9
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
10
|
-
const body = {
|
|
11
|
-
client_name: CLIENT_NAME,
|
|
12
|
-
grant_types: ['authorization_code', 'refresh_token'],
|
|
13
|
-
redirect_uris: [redirectUri],
|
|
14
|
-
response_types: ['code'],
|
|
15
|
-
scope: scopes.join(' '),
|
|
16
|
-
token_endpoint_auth_method: 'none',
|
|
17
|
-
};
|
|
18
|
-
let response;
|
|
19
|
-
try {
|
|
20
|
-
response = await fetch(meta.registrationEndpoint, {
|
|
21
|
-
body: JSON.stringify(body),
|
|
22
|
-
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
23
|
-
method: 'POST',
|
|
24
|
-
signal: controller.signal,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
const reason = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error.message;
|
|
29
|
-
throw new AuthFailedError(`Client registration failed: ${reason}`, 'registration_failed');
|
|
30
|
-
}
|
|
31
|
-
finally {
|
|
32
|
-
clearTimeout(timer);
|
|
33
|
-
}
|
|
34
|
-
if (response.status >= 400) {
|
|
35
|
-
const text = await response.text().catch(() => '');
|
|
36
|
-
throw new AuthFailedError(`Client registration failed: HTTP ${response.status} ${text}`.trim(), 'registration_failed');
|
|
37
|
-
}
|
|
38
|
-
const json = (await response.json().catch(() => ({})));
|
|
39
|
-
if (typeof json.client_id !== 'string' || json.client_id.length === 0) {
|
|
40
|
-
throw new AuthFailedError('Client registration response did not include a client_id', 'registration_failed');
|
|
41
|
-
}
|
|
42
|
-
return json.client_id;
|
|
43
|
-
}
|