@contenthero/cli 0.1.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 +108 -0
- package/dist/args.d.ts +13 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +35 -0
- package/dist/args.js.map +1 -0
- package/dist/commands/account.d.ts +7 -0
- package/dist/commands/account.d.ts.map +1 -0
- package/dist/commands/account.js +22 -0
- package/dist/commands/account.js.map +1 -0
- package/dist/commands/auth.d.ts +35 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +210 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/brandkit.d.ts +12 -0
- package/dist/commands/brandkit.d.ts.map +1 -0
- package/dist/commands/brandkit.js +159 -0
- package/dist/commands/brandkit.js.map +1 -0
- package/dist/commands/config.d.ts +11 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +69 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/connected.d.ts +13 -0
- package/dist/commands/connected.d.ts.map +1 -0
- package/dist/commands/connected.js +86 -0
- package/dist/commands/connected.js.map +1 -0
- package/dist/commands/generate.d.ts +15 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +215 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/generation.d.ts +12 -0
- package/dist/commands/generation.d.ts.map +1 -0
- package/dist/commands/generation.js +61 -0
- package/dist/commands/generation.js.map +1 -0
- package/dist/commands/identity.d.ts +10 -0
- package/dist/commands/identity.d.ts.map +1 -0
- package/dist/commands/identity.js +83 -0
- package/dist/commands/identity.js.map +1 -0
- package/dist/commands/inspiration.d.ts +17 -0
- package/dist/commands/inspiration.d.ts.map +1 -0
- package/dist/commands/inspiration.js +124 -0
- package/dist/commands/inspiration.js.map +1 -0
- package/dist/commands/media.d.ts +11 -0
- package/dist/commands/media.d.ts.map +1 -0
- package/dist/commands/media.js +76 -0
- package/dist/commands/media.js.map +1 -0
- package/dist/commands/model.d.ts +10 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +27 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/pipeline.d.ts +10 -0
- package/dist/commands/pipeline.d.ts.map +1 -0
- package/dist/commands/pipeline.js +21 -0
- package/dist/commands/pipeline.js.map +1 -0
- package/dist/commands/post.d.ts +14 -0
- package/dist/commands/post.d.ts.map +1 -0
- package/dist/commands/post.js +267 -0
- package/dist/commands/post.js.map +1 -0
- package/dist/commands/schema.d.ts +11 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +82 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/upscale.d.ts +11 -0
- package/dist/commands/upscale.d.ts.map +1 -0
- package/dist/commands/upscale.js +56 -0
- package/dist/commands/upscale.js.map +1 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +68 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +45 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +63 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/generation.d.ts +34 -0
- package/dist/generation.d.ts.map +1 -0
- package/dist/generation.js +111 -0
- package/dist/generation.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +14 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +33 -0
- package/dist/output.js.map +1 -0
- package/dist/program.d.ts +11 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +69 -0
- package/dist/program.js.map +1 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @contenthero/cli
|
|
2
|
+
|
|
3
|
+
The official ContentHero command-line interface. Generate media, run the content
|
|
4
|
+
pipeline, and read your brand and research context from the terminal or any agent
|
|
5
|
+
shell. It rides the [`@contenthero/sdk`](https://www.npmjs.com/package/@contenthero/sdk)
|
|
6
|
+
kernel, so it talks to the same `/api/v1` surface as the SDK and the
|
|
7
|
+
[MCP server](https://www.npmjs.com/package/@contenthero/mcp).
|
|
8
|
+
|
|
9
|
+
Built agent-first: JSON by default, predictable exit codes, and a `schema` command
|
|
10
|
+
so an agent can discover every command's inputs without docs.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g @contenthero/cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Node 20+. The binary is `contenthero`.
|
|
19
|
+
|
|
20
|
+
## Authenticate
|
|
21
|
+
|
|
22
|
+
The CLI resolves your API key in this order: the `--api-key` flag, then the
|
|
23
|
+
`CONTENTHERO_API_KEY` environment variable, then a stored credential.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Browser-assisted (recommended): opens your browser, mints a key for this machine
|
|
27
|
+
contenthero login
|
|
28
|
+
|
|
29
|
+
# Bring your own key (CI / headless): create one in the app under API Keys
|
|
30
|
+
export CONTENTHERO_API_KEY=ch_live_...
|
|
31
|
+
# ...or store it:
|
|
32
|
+
contenthero login --with-key ch_live_... # or: echo "$KEY" | contenthero login --with-key
|
|
33
|
+
|
|
34
|
+
contenthero auth status # verify the active key and show the account
|
|
35
|
+
contenthero logout # remove the stored credential
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The stored credential lives at `~/.contenthero/credentials` (mode 0600). The env
|
|
39
|
+
var always wins over the stored file, so CI can override a local login.
|
|
40
|
+
|
|
41
|
+
## Output
|
|
42
|
+
|
|
43
|
+
JSON is the default (built for agents and scripts). Add `--human` for readable
|
|
44
|
+
tables and key/value output.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
contenthero account balance
|
|
48
|
+
contenthero model list --type image --human
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Generate
|
|
52
|
+
|
|
53
|
+
`generate` covers image, video, audio, board, and lip-sync. The waitable kinds
|
|
54
|
+
share `--cost` (preflight, charges nothing), `--wait` / `--no-wait` (default
|
|
55
|
+
waits), and `--timeout <seconds>`.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Preflight the cost, then generate and wait for the URLs
|
|
59
|
+
contenthero generate image "a red ceramic cube on white" --model nano-banana-2 --cost
|
|
60
|
+
contenthero generate image "a red ceramic cube on white" --model nano-banana-2
|
|
61
|
+
|
|
62
|
+
# Submit without blocking, then poll
|
|
63
|
+
ID=$(contenthero generate video "drone shot over a canyon" --model veo-3.1-fast --no-wait | jq -r .outputId)
|
|
64
|
+
contenthero generation status "$ID"
|
|
65
|
+
contenthero generation wait "$ID" --timeout 300
|
|
66
|
+
|
|
67
|
+
# Chain: feed a previous output id straight in as a reference (URL or output id)
|
|
68
|
+
contenthero generate video "slow zoom in" --model veo-3.1-fast --start-frame "$ID"
|
|
69
|
+
|
|
70
|
+
# Audio (synchronous) and upscaling
|
|
71
|
+
contenthero generate audio --model elevenlabs-tts --text "Hello there" --voice <voiceId>
|
|
72
|
+
contenthero upscale "$ID" --model topaz-image-upscale --factor 2x
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Exit code 4 means a render was accepted but did not finish before the timeout. The
|
|
76
|
+
`outputId` is still emitted, so you can keep polling.
|
|
77
|
+
|
|
78
|
+
## The rest of the surface
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
contenthero media list | get <id>
|
|
82
|
+
contenthero post list | get | create | update | archive | schedule | publish
|
|
83
|
+
| destination add|update | asset add
|
|
84
|
+
contenthero pipeline stages
|
|
85
|
+
contenthero brand-kit list | get | update | archive | section add|update|archive
|
|
86
|
+
contenthero avatar list | get <id>
|
|
87
|
+
contenthero voice list | get <id>
|
|
88
|
+
contenthero inspiration accounts | account <id> | outliers | content <id>
|
|
89
|
+
contenthero brand-account list | performance <id>
|
|
90
|
+
contenthero connected-account list | get <id>
|
|
91
|
+
contenthero account balance
|
|
92
|
+
contenthero model list
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## For agents
|
|
96
|
+
|
|
97
|
+
`schema` dumps every command's arguments and options as JSON, so an agent can wire
|
|
98
|
+
up calls without reading these docs:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
contenthero schema # the whole surface
|
|
102
|
+
contenthero schema generate image # just one command
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Exit codes
|
|
106
|
+
|
|
107
|
+
`0` success, `1` general error, `2` usage error, `3` authentication error,
|
|
108
|
+
`4` timeout (the work was accepted but did not finish in time).
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small commander option coercers. They throw a usage-coded CliError on bad
|
|
3
|
+
* input so the central error boundary reports exit 2 with a clear message.
|
|
4
|
+
*/
|
|
5
|
+
/** Parse an integer option value. */
|
|
6
|
+
export declare function toInt(value: string): number;
|
|
7
|
+
/** Parse a numeric option value (integer or decimal). */
|
|
8
|
+
export declare function toFloat(value: string): number;
|
|
9
|
+
/** Collect a repeatable option (e.g. --ref a --ref b) into an array. */
|
|
10
|
+
export declare function collect(value: string, previous?: string[]): string[];
|
|
11
|
+
/** Parse a JSON option value into an unknown, with a usage error on bad JSON. */
|
|
12
|
+
export declare function toJson(value: string): unknown;
|
|
13
|
+
//# sourceMappingURL=args.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,qCAAqC;AACrC,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM3C;AAED,yDAAyD;AACzD,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C;AAED,wEAAwE;AACxE,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CAExE;AAED,iFAAiF;AACjF,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAM7C"}
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small commander option coercers. They throw a usage-coded CliError on bad
|
|
3
|
+
* input so the central error boundary reports exit 2 with a clear message.
|
|
4
|
+
*/
|
|
5
|
+
import { CliError, EXIT } from './errors.js';
|
|
6
|
+
/** Parse an integer option value. */
|
|
7
|
+
export function toInt(value) {
|
|
8
|
+
const n = Number(value);
|
|
9
|
+
if (!Number.isInteger(n)) {
|
|
10
|
+
throw new CliError(`Expected an integer, got "${value}".`, EXIT.USAGE);
|
|
11
|
+
}
|
|
12
|
+
return n;
|
|
13
|
+
}
|
|
14
|
+
/** Parse a numeric option value (integer or decimal). */
|
|
15
|
+
export function toFloat(value) {
|
|
16
|
+
const n = Number(value);
|
|
17
|
+
if (Number.isNaN(n)) {
|
|
18
|
+
throw new CliError(`Expected a number, got "${value}".`, EXIT.USAGE);
|
|
19
|
+
}
|
|
20
|
+
return n;
|
|
21
|
+
}
|
|
22
|
+
/** Collect a repeatable option (e.g. --ref a --ref b) into an array. */
|
|
23
|
+
export function collect(value, previous = []) {
|
|
24
|
+
return [...previous, value];
|
|
25
|
+
}
|
|
26
|
+
/** Parse a JSON option value into an unknown, with a usage error on bad JSON. */
|
|
27
|
+
export function toJson(value) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(value);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new CliError(`Expected valid JSON, got "${value}".`, EXIT.USAGE);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=args.js.map
|
package/dist/args.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAE5C,qCAAqC;AACrC,MAAM,UAAU,KAAK,CAAC,KAAa;IACjC,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IACvB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,QAAQ,CAAC,6BAA6B,KAAK,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;IACxE,CAAC;IACD,OAAO,CAAC,CAAA;AACV,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IACvB,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,QAAQ,CAAC,2BAA2B,KAAK,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;IACtE,CAAC;IACD,OAAO,CAAC,CAAA;AACV,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,OAAO,CAAC,KAAa,EAAE,WAAqB,EAAE;IAC5D,OAAO,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAA;AAC7B,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,QAAQ,CAAC,6BAA6B,KAAK,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;IACxE,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `contenthero account` - account-level reads.
|
|
3
|
+
* account balance credit balance, tier, and auto-top-up state
|
|
4
|
+
*/
|
|
5
|
+
import type { Command } from 'commander';
|
|
6
|
+
export declare function registerAccount(program: Command): void;
|
|
7
|
+
//# sourceMappingURL=account.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../src/commands/account.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAKxC,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiBtD"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `contenthero account` - account-level reads.
|
|
3
|
+
* account balance credit balance, tier, and auto-top-up state
|
|
4
|
+
*/
|
|
5
|
+
import { makeClient } from '../context.js';
|
|
6
|
+
import { emit, keyValues } from '../output.js';
|
|
7
|
+
export function registerAccount(program) {
|
|
8
|
+
const account = program.command('account').description('Account-level reads (balance, tier)');
|
|
9
|
+
account
|
|
10
|
+
.command('balance')
|
|
11
|
+
.description('Show the credit balance, subscription tier, and auto-top-up state')
|
|
12
|
+
.action(async (_opts, command) => {
|
|
13
|
+
const { client, ctx } = makeClient(command);
|
|
14
|
+
const balance = await client.getBalance();
|
|
15
|
+
emit(balance, ctx, (b) => keyValues([
|
|
16
|
+
['Balance', `${b.balance} credits`],
|
|
17
|
+
['Tier', b.tier],
|
|
18
|
+
['Auto top-up', b.autoTopupEnabled ? 'on' : 'off'],
|
|
19
|
+
]));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=account.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account.js","sourceRoot":"","sources":["../../src/commands/account.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAG9C,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,WAAW,CAAC,qCAAqC,CAAC,CAAA;IAE7F,OAAO;SACJ,OAAO,CAAC,SAAS,CAAC;SAClB,WAAW,CAAC,mEAAmE,CAAC;SAChF,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,OAAgB,EAAE,EAAE;QACxC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,CAAA;QAC3C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,CAAA;QACzC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAU,EAAE,EAAE,CAChC,SAAS,CAAC;YACR,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,OAAO,UAAU,CAAC;YACnC,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC;YAChB,CAAC,aAAa,EAAE,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;SACnD,CAAC,CACH,CAAA;IACH,CAAC,CAAC,CAAA;AACN,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `contenthero auth` - manage how the CLI authenticates.
|
|
3
|
+
* auth login browser-assisted key provisioning (the primary path)
|
|
4
|
+
* auth login --with-key store a key you already have (flag value or piped stdin)
|
|
5
|
+
* auth status verify the active key and show the account
|
|
6
|
+
* auth logout delete the stored credential
|
|
7
|
+
*
|
|
8
|
+
* `login` and `logout` are also exposed at the top level (`contenthero login`)
|
|
9
|
+
* since that is the headline command. Both forms share these handlers.
|
|
10
|
+
*
|
|
11
|
+
* Browser login: we start a localhost listener, open the app's /cli/auth page
|
|
12
|
+
* (passing the loopback port + a state nonce), the user approves, the page mints a
|
|
13
|
+
* named `CLI (<host>)` key and navigates back to the listener with it. We verify
|
|
14
|
+
* the key, then store it. `--with-key` and CONTENTHERO_API_KEY cover headless /
|
|
15
|
+
* bring-your-own-key (CI, or a key from the app's API Keys tab).
|
|
16
|
+
*/
|
|
17
|
+
import type { Command } from 'commander';
|
|
18
|
+
export interface LoopbackOptions {
|
|
19
|
+
/** The state nonce the browser callback must echo back. */
|
|
20
|
+
state: string;
|
|
21
|
+
/** Called with the bound port once the listener is up (to build + open the URL). */
|
|
22
|
+
onListening: (port: number) => void;
|
|
23
|
+
/** Give up after this many ms (default 3 minutes). */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Start a localhost listener and resolve with the API key the browser hands back
|
|
28
|
+
* to `/callback?state=<state>&key=<key>`. Rejects on `error=...`, a listener
|
|
29
|
+
* failure, or timeout. A callback whose state does not match is rejected with 400
|
|
30
|
+
* and ignored (the listener keeps waiting), so a stray or forged hit cannot end
|
|
31
|
+
* the flow. Exported for testing the loopback contract in-process.
|
|
32
|
+
*/
|
|
33
|
+
export declare function awaitLoopbackKey(opts: LoopbackOptions): Promise<string>;
|
|
34
|
+
export declare function registerAuth(program: Command): void;
|
|
35
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AA4DxC,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAqDvE;AAiGD,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAanD"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `contenthero auth` - manage how the CLI authenticates.
|
|
3
|
+
* auth login browser-assisted key provisioning (the primary path)
|
|
4
|
+
* auth login --with-key store a key you already have (flag value or piped stdin)
|
|
5
|
+
* auth status verify the active key and show the account
|
|
6
|
+
* auth logout delete the stored credential
|
|
7
|
+
*
|
|
8
|
+
* `login` and `logout` are also exposed at the top level (`contenthero login`)
|
|
9
|
+
* since that is the headline command. Both forms share these handlers.
|
|
10
|
+
*
|
|
11
|
+
* Browser login: we start a localhost listener, open the app's /cli/auth page
|
|
12
|
+
* (passing the loopback port + a state nonce), the user approves, the page mints a
|
|
13
|
+
* named `CLI (<host>)` key and navigates back to the listener with it. We verify
|
|
14
|
+
* the key, then store it. `--with-key` and CONTENTHERO_API_KEY cover headless /
|
|
15
|
+
* bring-your-own-key (CI, or a key from the app's API Keys tab).
|
|
16
|
+
*/
|
|
17
|
+
import http from 'node:http';
|
|
18
|
+
import { randomBytes } from 'node:crypto';
|
|
19
|
+
import { hostname } from 'node:os';
|
|
20
|
+
import { spawn } from 'node:child_process';
|
|
21
|
+
import { ContentHero } from '@contenthero/sdk';
|
|
22
|
+
import { clearCredential, credentialsPath, writeCredential } from '../config.js';
|
|
23
|
+
import { resolveContext } from '../context.js';
|
|
24
|
+
import { emit, keyValues } from '../output.js';
|
|
25
|
+
import { CliError, EXIT } from '../errors.js';
|
|
26
|
+
const DEFAULT_BASE_URL = 'https://app.contenthero.ai';
|
|
27
|
+
const LOGIN_TIMEOUT_MS = 180_000;
|
|
28
|
+
/** Read piped stdin (returns '' when attached to a TTY or nothing is piped). */
|
|
29
|
+
async function readStdin() {
|
|
30
|
+
if (process.stdin.isTTY)
|
|
31
|
+
return '';
|
|
32
|
+
const chunks = [];
|
|
33
|
+
for await (const chunk of process.stdin)
|
|
34
|
+
chunks.push(chunk);
|
|
35
|
+
return Buffer.concat(chunks).toString('utf8').trim();
|
|
36
|
+
}
|
|
37
|
+
/** Open a URL in the default browser. Best-effort; the URL is always printed too.
|
|
38
|
+
* Skipped when CONTENTHERO_NO_BROWSER is set (headless / CI). */
|
|
39
|
+
function openBrowser(url) {
|
|
40
|
+
if (process.env.CONTENTHERO_NO_BROWSER)
|
|
41
|
+
return;
|
|
42
|
+
const platform = process.platform;
|
|
43
|
+
const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
44
|
+
const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
45
|
+
try {
|
|
46
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
47
|
+
child.on('error', () => { });
|
|
48
|
+
child.unref();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Ignore: the URL was printed for manual navigation.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Minimal branded HTML for the loopback success / cancel page. */
|
|
55
|
+
function resultPage(title, sub) {
|
|
56
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title><style>
|
|
57
|
+
body{margin:0;height:100vh;display:flex;align-items:center;justify-content:center;background:#f8f9fa;
|
|
58
|
+
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#121212}
|
|
59
|
+
.card{background:#fff;border-radius:24px;padding:40px 48px;box-shadow:0 8px 32px rgba(0,0,0,.08);text-align:center}
|
|
60
|
+
h1{font-size:20px;margin:0 0 8px} p{color:#6b7280;margin:0;font-size:14px}
|
|
61
|
+
.dot{color:#d4af37}</style></head>
|
|
62
|
+
<body><div class="card"><h1><span class="dot">ContentHero</span> ${title}</h1><p>${sub}</p></div></body></html>`;
|
|
63
|
+
}
|
|
64
|
+
/** Verify a key against the API, then persist it and report. */
|
|
65
|
+
async function verifyAndStore(key, ctx, label) {
|
|
66
|
+
const client = new ContentHero({ apiKey: key, baseUrl: ctx.baseUrl });
|
|
67
|
+
const balance = await client.getBalance();
|
|
68
|
+
writeCredential({ apiKey: key, label, createdAt: new Date().toISOString() });
|
|
69
|
+
const data = { ok: true, stored: credentialsPath(), tier: balance.tier, balance: balance.balance };
|
|
70
|
+
emit(data, ctx, (d) => keyValues([
|
|
71
|
+
['Logged in', 'stored ' + d.stored],
|
|
72
|
+
['Tier', d.tier],
|
|
73
|
+
['Balance', `${d.balance} credits`],
|
|
74
|
+
]));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Start a localhost listener and resolve with the API key the browser hands back
|
|
78
|
+
* to `/callback?state=<state>&key=<key>`. Rejects on `error=...`, a listener
|
|
79
|
+
* failure, or timeout. A callback whose state does not match is rejected with 400
|
|
80
|
+
* and ignored (the listener keeps waiting), so a stray or forged hit cannot end
|
|
81
|
+
* the flow. Exported for testing the loopback contract in-process.
|
|
82
|
+
*/
|
|
83
|
+
export function awaitLoopbackKey(opts) {
|
|
84
|
+
const { state, onListening, timeoutMs = LOGIN_TIMEOUT_MS } = opts;
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
let settled = false;
|
|
87
|
+
const finish = (fn) => {
|
|
88
|
+
if (settled)
|
|
89
|
+
return;
|
|
90
|
+
settled = true;
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
server.close();
|
|
93
|
+
fn();
|
|
94
|
+
};
|
|
95
|
+
const server = http.createServer((req, res) => {
|
|
96
|
+
const reqUrl = new URL(req.url || '/', 'http://127.0.0.1');
|
|
97
|
+
if (reqUrl.pathname !== '/callback') {
|
|
98
|
+
res.writeHead(404, { 'content-type': 'text/plain' });
|
|
99
|
+
res.end('Not found');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Bind the callback to this invocation; ignore anything with a wrong state.
|
|
103
|
+
if (reqUrl.searchParams.get('state') !== state) {
|
|
104
|
+
res.writeHead(400, { 'content-type': 'text/html' });
|
|
105
|
+
res.end(resultPage('login error', 'Invalid request. You can close this tab.'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const error = reqUrl.searchParams.get('error');
|
|
109
|
+
const got = reqUrl.searchParams.get('key');
|
|
110
|
+
if (error || !got) {
|
|
111
|
+
res.writeHead(200, { 'content-type': 'text/html' });
|
|
112
|
+
res.end(resultPage('login cancelled', 'You can close this tab and return to your terminal.'));
|
|
113
|
+
finish(() => reject(new CliError('Login was cancelled in the browser.', EXIT.AUTH)));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
res.writeHead(200, { 'content-type': 'text/html' });
|
|
117
|
+
res.end(resultPage('CLI connected', 'You can close this tab and return to your terminal.'));
|
|
118
|
+
finish(() => resolve(got));
|
|
119
|
+
});
|
|
120
|
+
server.on('error', (e) => finish(() => reject(new CliError(`Could not start the local login listener: ${e.message}`, EXIT.GENERAL))));
|
|
121
|
+
const timer = setTimeout(() => finish(() => reject(new CliError('Login timed out after 3 minutes.', EXIT.AUTH))), timeoutMs);
|
|
122
|
+
server.listen(0, '127.0.0.1', () => {
|
|
123
|
+
const addr = server.address();
|
|
124
|
+
const port = addr && typeof addr === 'object' ? addr.port : 0;
|
|
125
|
+
onListening(port);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** Run the browser-assisted login: loopback listener + the app's /cli/auth page. */
|
|
130
|
+
async function browserLogin(ctx) {
|
|
131
|
+
const state = randomBytes(16).toString('hex');
|
|
132
|
+
const label = hostname();
|
|
133
|
+
const base = (ctx.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
134
|
+
const key = await awaitLoopbackKey({
|
|
135
|
+
state,
|
|
136
|
+
onListening: (port) => {
|
|
137
|
+
const authUrl = `${base}/cli/auth?port=${port}&state=${state}&label=${encodeURIComponent(label)}`;
|
|
138
|
+
process.stderr.write(`Opening your browser to authorize the CLI.\nIf it does not open, visit:\n ${authUrl}\n\nWaiting for authorization...\n`);
|
|
139
|
+
openBrowser(authUrl);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
await verifyAndStore(key, ctx, label);
|
|
143
|
+
}
|
|
144
|
+
async function loginAction(opts, command) {
|
|
145
|
+
const ctx = resolveContext(command);
|
|
146
|
+
// No --with-key: run the browser-assisted flow (the primary path).
|
|
147
|
+
if (opts.withKey === undefined) {
|
|
148
|
+
await browserLogin(ctx);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const key = typeof opts.withKey === 'string' ? opts.withKey.trim() : await readStdin();
|
|
152
|
+
if (!key) {
|
|
153
|
+
throw new CliError('No key provided. Pass it as `--with-key <key>` or pipe it via stdin.', EXIT.USAGE);
|
|
154
|
+
}
|
|
155
|
+
await verifyAndStore(key, ctx);
|
|
156
|
+
}
|
|
157
|
+
async function statusAction(_opts, command) {
|
|
158
|
+
const ctx = resolveContext(command);
|
|
159
|
+
if (!ctx.apiKey) {
|
|
160
|
+
throw new CliError('Not authenticated. Run `contenthero login`, set CONTENTHERO_API_KEY, or pass --api-key.', EXIT.AUTH);
|
|
161
|
+
}
|
|
162
|
+
// getBalance round-trips the key, so a bad key surfaces as an auth error (exit 3).
|
|
163
|
+
const client = new ContentHero({ apiKey: ctx.apiKey, baseUrl: ctx.baseUrl });
|
|
164
|
+
const balance = await client.getBalance();
|
|
165
|
+
const data = {
|
|
166
|
+
authenticated: true,
|
|
167
|
+
keySource: ctx.keySource,
|
|
168
|
+
baseUrl: ctx.baseUrl ?? DEFAULT_BASE_URL,
|
|
169
|
+
tier: balance.tier,
|
|
170
|
+
balance: balance.balance,
|
|
171
|
+
};
|
|
172
|
+
emit(data, ctx, (d) => keyValues([
|
|
173
|
+
['Authenticated', 'yes'],
|
|
174
|
+
['Key source', d.keySource],
|
|
175
|
+
['Base URL', d.baseUrl],
|
|
176
|
+
['Tier', d.tier],
|
|
177
|
+
['Balance', `${d.balance} credits`],
|
|
178
|
+
]));
|
|
179
|
+
}
|
|
180
|
+
function logoutAction(_opts, command) {
|
|
181
|
+
const ctx = resolveContext(command);
|
|
182
|
+
const removed = clearCredential();
|
|
183
|
+
emit({ ok: true, removed }, ctx, () => removed ? 'Logged out (stored credential removed).' : 'No stored credential to remove.');
|
|
184
|
+
}
|
|
185
|
+
/** Apply the login description/option/action to a command (shared by both mounts). */
|
|
186
|
+
function configureLogin(cmd) {
|
|
187
|
+
return cmd
|
|
188
|
+
.description('Authenticate the CLI. Browser-assisted by default; --with-key stores an existing key.')
|
|
189
|
+
.option('--with-key [key]', 'store an existing API key (pass the key, or pipe it via stdin). Omit to use the browser flow.')
|
|
190
|
+
.action(loginAction);
|
|
191
|
+
}
|
|
192
|
+
/** Apply the logout description/action to a command (shared by both mounts). */
|
|
193
|
+
function configureLogout(cmd) {
|
|
194
|
+
return cmd
|
|
195
|
+
.description('Delete the stored credential (env / --api-key are unaffected)')
|
|
196
|
+
.action(logoutAction);
|
|
197
|
+
}
|
|
198
|
+
export function registerAuth(program) {
|
|
199
|
+
const auth = program.command('auth').description('Manage CLI authentication');
|
|
200
|
+
configureLogin(auth.command('login'));
|
|
201
|
+
auth
|
|
202
|
+
.command('status')
|
|
203
|
+
.description('Verify the active API key and show the account it resolves to')
|
|
204
|
+
.action(statusAction);
|
|
205
|
+
configureLogout(auth.command('logout'));
|
|
206
|
+
// Top-level aliases for the two headline commands.
|
|
207
|
+
configureLogin(program.command('login'));
|
|
208
|
+
configureLogout(program.command('logout'));
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAClC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE1C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAChF,OAAO,EAAE,cAAc,EAAgB,MAAM,eAAe,CAAA;AAC5D,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAE7C,MAAM,gBAAgB,GAAG,4BAA4B,CAAA;AACrD,MAAM,gBAAgB,GAAG,OAAO,CAAA;AAEhC,gFAAgF;AAChF,KAAK,UAAU,SAAS;IACtB,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK;QAAE,OAAO,EAAE,CAAA;IAClC,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK;QAAE,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC,CAAA;IACrE,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAA;AACtD,CAAC;AAED;iEACiE;AACjE,SAAS,WAAW,CAAC,GAAW;IAC9B,IAAI,OAAO,CAAC,GAAG,CAAC,sBAAsB;QAAE,OAAM;IAC9C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAA;IACjC,MAAM,GAAG,GAAG,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAA;IACtF,MAAM,IAAI,GAAG,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACpE,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;QACnE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QAC3B,KAAK,CAAC,KAAK,EAAE,CAAA;IACf,CAAC;IAAC,MAAM,CAAC;QACP,qDAAqD;IACvD,CAAC;AACH,CAAC;AAED,mEAAmE;AACnE,SAAS,UAAU,CAAC,KAAa,EAAE,GAAW;IAC5C,OAAO,2DAA2D,KAAK;;;;;;uEAMF,KAAK,WAAW,GAAG,0BAA0B,CAAA;AACpH,CAAC;AAED,gEAAgE;AAChE,KAAK,UAAU,cAAc,CAAC,GAAW,EAAE,GAAY,EAAE,KAAc;IACrE,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAA;IACrE,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,CAAA;IACzC,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;IAC5E,MAAM,IAAI,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAA;IAClG,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAc,EAAE,EAAE,CACjC,SAAS,CAAC;QACR,CAAC,WAAW,EAAE,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC;QACnC,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC;QAChB,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,OAAO,UAAU,CAAC;KACpC,CAAC,CACH,CAAA;AACH,CAAC;AAWD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAqB;IACpD,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,GAAG,gBAAgB,EAAE,GAAG,IAAI,CAAA;IACjE,OAAO,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC7C,IAAI,OAAO,GAAG,KAAK,CAAA;QACnB,MAAM,MAAM,GAAG,CAAC,EAAc,EAAE,EAAE;YAChC,IAAI,OAAO;gBAAE,OAAM;YACnB,OAAO,GAAG,IAAI,CAAA;YACd,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,MAAM,CAAC,KAAK,EAAE,CAAA;YACd,EAAE,EAAE,CAAA;QACN,CAAC,CAAA;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAC5C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAA;YAC1D,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;gBACpC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAA;gBACpD,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;gBACpB,OAAM;YACR,CAAC;YACD,4EAA4E;YAC5E,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,EAAE,CAAC;gBAC/C,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAA;gBACnD,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,aAAa,EAAE,0CAA0C,CAAC,CAAC,CAAA;gBAC9E,OAAM;YACR,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YAC9C,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YAC1C,IAAI,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC;gBAClB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAA;gBACnD,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,iBAAiB,EAAE,qDAAqD,CAAC,CAAC,CAAA;gBAC7F,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,qCAAqC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;gBACpF,OAAM;YACR,CAAC;YACD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAA;YACnD,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,eAAe,EAAE,qDAAqD,CAAC,CAAC,CAAA;YAC3F,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAA;QAC5B,CAAC,CAAC,CAAA;QAEF,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CACvB,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,6CAA6C,CAAC,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAC3G,CAAA;QAED,MAAM,KAAK,GAAG,UAAU,CACtB,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,kCAAkC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EACvF,SAAS,CACV,CAAA;QAED,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE;YACjC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,EAAE,CAAA;YAC7B,MAAM,IAAI,GAAG,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;YAC7D,WAAW,CAAC,IAAI,CAAC,CAAA;QACnB,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,oFAAoF;AACpF,KAAK,UAAU,YAAY,CAAC,GAAY;IACtC,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC7C,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAA;IACxB,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,IAAI,gBAAgB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IAElE,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC;QACjC,KAAK;QACL,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE;YACpB,MAAM,OAAO,GAAG,GAAG,IAAI,kBAAkB,IAAI,UAAU,KAAK,UAAU,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAA;YACjG,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,8EAA8E,OAAO,oCAAoC,CAC1H,CAAA;YACD,WAAW,CAAC,OAAO,CAAC,CAAA;QACtB,CAAC;KACF,CAAC,CAAA;IAEF,MAAM,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;AACvC,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,IAAoC,EAAE,OAAgB;IAC/E,MAAM,GAAG,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;IAEnC,mEAAmE;IACnE,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,YAAY,CAAC,GAAG,CAAC,CAAA;QACvB,OAAM;IACR,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,SAAS,EAAE,CAAA;IACtF,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,QAAQ,CAChB,sEAAsE,EACtE,IAAI,CAAC,KAAK,CACX,CAAA;IACH,CAAC;IACD,MAAM,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;AAChC,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,KAAc,EAAE,OAAgB;IAC1D,MAAM,GAAG,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;IACnC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;QAChB,MAAM,IAAI,QAAQ,CAChB,yFAAyF,EACzF,IAAI,CAAC,IAAI,CACV,CAAA;IACH,CAAC;IACD,mFAAmF;IACnF,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAA;IAC5E,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,CAAA;IAEzC,MAAM,IAAI,GAAG;QACX,aAAa,EAAE,IAAI;QACnB,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,gBAAgB;QACxC,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAA;IACD,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAc,EAAE,EAAE,CACjC,SAAS,CAAC;QACR,CAAC,eAAe,EAAE,KAAK,CAAC;QACxB,CAAC,YAAY,EAAE,CAAC,CAAC,SAAS,CAAC;QAC3B,CAAC,UAAU,EAAE,CAAC,CAAC,OAAO,CAAC;QACvB,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC;QAChB,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,OAAO,UAAU,CAAC;KACpC,CAAC,CACH,CAAA;AACH,CAAC;AAED,SAAS,YAAY,CAAC,KAAc,EAAE,OAAgB;IACpD,MAAM,GAAG,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;IACnC,MAAM,OAAO,GAAG,eAAe,EAAE,CAAA;IACjC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,CACpC,OAAO,CAAC,CAAC,CAAC,yCAAyC,CAAC,CAAC,CAAC,iCAAiC,CACxF,CAAA;AACH,CAAC;AAED,sFAAsF;AACtF,SAAS,cAAc,CAAC,GAAY;IAClC,OAAO,GAAG;SACP,WAAW,CAAC,uFAAuF,CAAC;SACpG,MAAM,CACL,kBAAkB,EAClB,+FAA+F,CAChG;SACA,MAAM,CAAC,WAAW,CAAC,CAAA;AACxB,CAAC;AAED,gFAAgF;AAChF,SAAS,eAAe,CAAC,GAAY;IACnC,OAAO,GAAG;SACP,WAAW,CAAC,+DAA+D,CAAC;SAC5E,MAAM,CAAC,YAAY,CAAC,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAgB;IAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,2BAA2B,CAAC,CAAA;IAE7E,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAA;IACrC,IAAI;SACD,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,+DAA+D,CAAC;SAC5E,MAAM,CAAC,YAAY,CAAC,CAAA;IACvB,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEvC,mDAAmD;IACnD,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAA;IACxC,eAAe,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAA;AAC5C,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `contenthero brand-kit` - brand kits (the brand identity documents).
|
|
3
|
+
* brand-kit list | get | update | archive
|
|
4
|
+
* brand-kit section add | update | archive
|
|
5
|
+
*
|
|
6
|
+
* Reads are open; writes need brandkit:write. Free-form identity objects
|
|
7
|
+
* (positioning, audience, voice profile, content strategy) and section fields
|
|
8
|
+
* are passed as JSON.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
export declare function registerBrandKit(program: Command): void;
|
|
12
|
+
//# sourceMappingURL=brandkit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brandkit.d.ts","sourceRoot":"","sources":["../../src/commands/brandkit.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAuBxC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyJvD"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `contenthero brand-kit` - brand kits (the brand identity documents).
|
|
3
|
+
* brand-kit list | get | update | archive
|
|
4
|
+
* brand-kit section add | update | archive
|
|
5
|
+
*
|
|
6
|
+
* Reads are open; writes need brandkit:write. Free-form identity objects
|
|
7
|
+
* (positioning, audience, voice profile, content strategy) and section fields
|
|
8
|
+
* are passed as JSON.
|
|
9
|
+
*/
|
|
10
|
+
import { makeClient } from '../context.js';
|
|
11
|
+
import { emit, keyValues, table } from '../output.js';
|
|
12
|
+
import { CliError, EXIT } from '../errors.js';
|
|
13
|
+
import { compact } from '../generation.js';
|
|
14
|
+
import { collect, toInt, toJson } from '../args.js';
|
|
15
|
+
function recordHuman(s, action) {
|
|
16
|
+
return keyValues([
|
|
17
|
+
[action, s.sectionName],
|
|
18
|
+
['Section id', s.id],
|
|
19
|
+
['Tab', s.tab],
|
|
20
|
+
['Sort order', s.sortOrder],
|
|
21
|
+
['Fields', s.fields.length],
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
export function registerBrandKit(program) {
|
|
25
|
+
const brandKit = program.command('brand-kit').description('Brand kits (brand identity documents)');
|
|
26
|
+
brandKit
|
|
27
|
+
.command('list')
|
|
28
|
+
.description('List the account\'s brand kits (default first)')
|
|
29
|
+
.action(async (_opts, command) => {
|
|
30
|
+
const { client, ctx } = makeClient(command);
|
|
31
|
+
const kits = await client.listBrandKits();
|
|
32
|
+
emit(kits, ctx, (rows) => table(['ID', 'NAME', 'BUSINESS', 'DEFAULT', 'ACTIVE'], rows.map((k) => [
|
|
33
|
+
k.id.slice(0, 8),
|
|
34
|
+
k.name,
|
|
35
|
+
k.businessName ?? '',
|
|
36
|
+
k.isDefault ? 'yes' : '',
|
|
37
|
+
k.isActive ? 'yes' : '',
|
|
38
|
+
])));
|
|
39
|
+
});
|
|
40
|
+
brandKit
|
|
41
|
+
.command('get')
|
|
42
|
+
.description('Get one brand kit in full (the whole brand document)')
|
|
43
|
+
.argument('<id>', 'the brand kit id')
|
|
44
|
+
.action(async (id, _opts, command) => {
|
|
45
|
+
const { client, ctx } = makeClient(command);
|
|
46
|
+
const kit = await client.getBrandKit(id);
|
|
47
|
+
emit(kit, ctx, (k) => keyValues([
|
|
48
|
+
['Name', k.name],
|
|
49
|
+
['Id', k.id],
|
|
50
|
+
['Business', k.businessName ?? ''],
|
|
51
|
+
['Niche', k.nicheDefinition ?? ''],
|
|
52
|
+
['Primary offer', k.primaryOffer ?? ''],
|
|
53
|
+
['Visual style', k.visualStyle ?? ''],
|
|
54
|
+
['Sections', k.sections.length],
|
|
55
|
+
['Brand accounts', k.brandAccounts.length],
|
|
56
|
+
['Inspiration accounts', k.inspirationAccounts.length],
|
|
57
|
+
['Knowledge items', k.knowledge.length],
|
|
58
|
+
]));
|
|
59
|
+
});
|
|
60
|
+
brandKit
|
|
61
|
+
.command('update')
|
|
62
|
+
.description('Update a brand kit\'s identity fields (requires brandkit:write)')
|
|
63
|
+
.argument('<id>', 'the brand kit id')
|
|
64
|
+
.option('--name <text>')
|
|
65
|
+
.option('--business-name <text>')
|
|
66
|
+
.option('--website-url <url>')
|
|
67
|
+
.option('--primary-offer <text>')
|
|
68
|
+
.option('--niche <text>', 'niche definition')
|
|
69
|
+
.option('--visual-style <text>')
|
|
70
|
+
.option('--positioning <json>', 'positioning object (JSON)', toJson)
|
|
71
|
+
.option('--audience <json>', 'audience object (JSON)', toJson)
|
|
72
|
+
.option('--voice-profile <json>', 'voice profile object (JSON)', toJson)
|
|
73
|
+
.option('--content-strategy <json>', 'content strategy object (JSON)', toJson)
|
|
74
|
+
.option('--design-principle <text>', 'a design principle; repeatable', collect)
|
|
75
|
+
.action(async (id, opts, command) => {
|
|
76
|
+
const input = compact({
|
|
77
|
+
name: opts.name,
|
|
78
|
+
businessName: opts.businessName,
|
|
79
|
+
websiteUrl: opts.websiteUrl,
|
|
80
|
+
primaryOffer: opts.primaryOffer,
|
|
81
|
+
nicheDefinition: opts.niche,
|
|
82
|
+
visualStyle: opts.visualStyle,
|
|
83
|
+
positioning: opts.positioning,
|
|
84
|
+
audience: opts.audience,
|
|
85
|
+
voiceProfile: opts.voiceProfile,
|
|
86
|
+
contentStrategy: opts.contentStrategy,
|
|
87
|
+
designPrinciples: opts.designPrinciple,
|
|
88
|
+
});
|
|
89
|
+
if (Object.keys(input).length === 0) {
|
|
90
|
+
throw new CliError('Nothing to update. Pass at least one field to change.', EXIT.USAGE);
|
|
91
|
+
}
|
|
92
|
+
const { client, ctx } = makeClient(command);
|
|
93
|
+
const kit = await client.updateBrandKit(id, input);
|
|
94
|
+
emit(kit, ctx, (k) => keyValues([
|
|
95
|
+
['Updated', k.name],
|
|
96
|
+
['Id', k.id],
|
|
97
|
+
]));
|
|
98
|
+
});
|
|
99
|
+
brandKit
|
|
100
|
+
.command('archive')
|
|
101
|
+
.description('Archive a brand kit (reversible; requires brandkit:write)')
|
|
102
|
+
.argument('<id>', 'the brand kit id')
|
|
103
|
+
.action(async (id, _opts, command) => {
|
|
104
|
+
const { client, ctx } = makeClient(command);
|
|
105
|
+
const k = await client.archiveBrandKit(id);
|
|
106
|
+
emit(k, ctx, (kit) => keyValues([
|
|
107
|
+
['Archived', kit.name],
|
|
108
|
+
['Id', kit.id],
|
|
109
|
+
]));
|
|
110
|
+
});
|
|
111
|
+
// -- brand-kit section ----------------------------------------------------
|
|
112
|
+
const section = brandKit.command('section').description('Manage a brand kit\'s curated sections');
|
|
113
|
+
section
|
|
114
|
+
.command('add')
|
|
115
|
+
.description('Add a curated section to a brand kit (requires brandkit:write)')
|
|
116
|
+
.argument('<brandKitId>', 'the brand kit id')
|
|
117
|
+
.requiredOption('--tab <tab>', 'the tab the section belongs to (e.g. voice, overview)')
|
|
118
|
+
.requiredOption('--name <name>', 'the section title')
|
|
119
|
+
.option('--sort-order <n>', 'order within the tab (default end)', toInt)
|
|
120
|
+
.option('--fields <json>', 'field objects as a JSON array: [{ key, label, type, value }]', toJson)
|
|
121
|
+
.action(async (brandKitId, opts, command) => {
|
|
122
|
+
const { client, ctx } = makeClient(command);
|
|
123
|
+
const s = await client.addBrandKitSection(brandKitId, {
|
|
124
|
+
tab: opts.tab,
|
|
125
|
+
sectionName: opts.name,
|
|
126
|
+
sortOrder: opts.sortOrder,
|
|
127
|
+
fields: opts.fields,
|
|
128
|
+
});
|
|
129
|
+
emit(s, ctx, (rec) => recordHuman(rec, 'Added section'));
|
|
130
|
+
});
|
|
131
|
+
section
|
|
132
|
+
.command('update')
|
|
133
|
+
.description('Update a brand-kit section (requires brandkit:write)')
|
|
134
|
+
.argument('<brandKitId>', 'the brand kit id')
|
|
135
|
+
.argument('<sectionId>', 'the section id (from `brand-kit get`)')
|
|
136
|
+
.option('--name <name>', 'new section title')
|
|
137
|
+
.option('--sort-order <n>', 'new order within the tab', toInt)
|
|
138
|
+
.option('--fields <json>', 'replacement field objects as a JSON array', toJson)
|
|
139
|
+
.action(async (brandKitId, sectionId, opts, command) => {
|
|
140
|
+
const { client, ctx } = makeClient(command);
|
|
141
|
+
const s = await client.updateBrandKitSection(brandKitId, sectionId, {
|
|
142
|
+
sectionName: opts.name,
|
|
143
|
+
sortOrder: opts.sortOrder,
|
|
144
|
+
fields: opts.fields,
|
|
145
|
+
});
|
|
146
|
+
emit(s, ctx, (rec) => recordHuman(rec, 'Updated section'));
|
|
147
|
+
});
|
|
148
|
+
section
|
|
149
|
+
.command('archive')
|
|
150
|
+
.description('Archive a brand-kit section (soft delete, reversible; requires brandkit:write)')
|
|
151
|
+
.argument('<brandKitId>', 'the brand kit id')
|
|
152
|
+
.argument('<sectionId>', 'the section id to archive')
|
|
153
|
+
.action(async (brandKitId, sectionId, _opts, command) => {
|
|
154
|
+
const { client, ctx } = makeClient(command);
|
|
155
|
+
const s = await client.archiveBrandKitSection(brandKitId, sectionId);
|
|
156
|
+
emit(s, ctx, (rec) => recordHuman(rec, 'Archived section'));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=brandkit.js.map
|