@lifestreamdynamics/vault-cli 1.1.0 → 1.3.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 +140 -30
- package/dist/client.d.ts +4 -0
- package/dist/client.js +12 -11
- package/dist/commands/admin.js +5 -5
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.js +124 -0
- package/dist/commands/analytics.d.ts +2 -0
- package/dist/commands/analytics.js +84 -0
- package/dist/commands/auth.js +10 -105
- package/dist/commands/booking.d.ts +2 -0
- package/dist/commands/booking.js +739 -0
- package/dist/commands/calendar.js +778 -6
- package/dist/commands/completion.d.ts +5 -0
- package/dist/commands/completion.js +60 -0
- package/dist/commands/config.js +17 -16
- package/dist/commands/connectors.js +12 -1
- package/dist/commands/custom-domains.d.ts +2 -0
- package/dist/commands/custom-domains.js +154 -0
- package/dist/commands/docs.js +152 -5
- package/dist/commands/hooks.js +6 -1
- package/dist/commands/links.js +9 -2
- package/dist/commands/mfa.js +1 -70
- package/dist/commands/plugins.d.ts +2 -0
- package/dist/commands/plugins.js +172 -0
- package/dist/commands/publish-vault.d.ts +2 -0
- package/dist/commands/publish-vault.js +117 -0
- package/dist/commands/publish.js +63 -2
- package/dist/commands/saml.d.ts +2 -0
- package/dist/commands/saml.js +220 -0
- package/dist/commands/scim.d.ts +2 -0
- package/dist/commands/scim.js +238 -0
- package/dist/commands/shares.js +25 -3
- package/dist/commands/subscription.js +9 -2
- package/dist/commands/sync.js +3 -0
- package/dist/commands/teams.js +233 -4
- package/dist/commands/user.js +444 -0
- package/dist/commands/vaults.js +240 -8
- package/dist/commands/webhooks.js +6 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +7 -3
- package/dist/index.js +28 -1
- package/dist/lib/credential-manager.js +32 -7
- package/dist/lib/migration.js +2 -2
- package/dist/lib/profiles.js +4 -4
- package/dist/sync/config.js +2 -2
- package/dist/sync/daemon-worker.js +13 -6
- package/dist/sync/daemon.js +2 -1
- package/dist/sync/remote-poller.js +7 -3
- package/dist/sync/state.js +2 -2
- package/dist/utils/confirm.d.ts +11 -0
- package/dist/utils/confirm.js +23 -0
- package/dist/utils/format.js +1 -1
- package/dist/utils/output.js +4 -1
- package/dist/utils/prompt.d.ts +29 -0
- package/dist/utils/prompt.js +146 -0
- package/package.json +2 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getClientAsync } from '../client.js';
|
|
3
|
+
import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
|
|
4
|
+
import { createOutput, handleError } from '../utils/output.js';
|
|
5
|
+
export function registerPluginCommands(program) {
|
|
6
|
+
const plugins = program.command('plugins').description('Plugin/extension marketplace management');
|
|
7
|
+
// ── list ──────────────────────────────────────────────────────────────────
|
|
8
|
+
addGlobalFlags(plugins.command('list')
|
|
9
|
+
.description('List all installed plugins'))
|
|
10
|
+
.action(async (_opts) => {
|
|
11
|
+
const flags = resolveFlags(_opts);
|
|
12
|
+
const out = createOutput(flags);
|
|
13
|
+
out.startSpinner('Loading plugins...');
|
|
14
|
+
try {
|
|
15
|
+
const client = await getClientAsync();
|
|
16
|
+
const list = await client.plugins.list();
|
|
17
|
+
out.stopSpinner();
|
|
18
|
+
if (list.length === 0 && flags.output !== 'json') {
|
|
19
|
+
out.raw('No plugins installed.\n');
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
out.list(list.map((p) => ({
|
|
23
|
+
pluginId: p.pluginId,
|
|
24
|
+
version: p.version,
|
|
25
|
+
enabled: p.enabled ? 'yes' : 'no',
|
|
26
|
+
settings: Object.keys(p.settings).length > 0 ? JSON.stringify(p.settings) : '',
|
|
27
|
+
installedAt: p.installedAt.slice(0, 10),
|
|
28
|
+
})), {
|
|
29
|
+
columns: [
|
|
30
|
+
{ key: 'pluginId', header: 'Plugin ID' },
|
|
31
|
+
{ key: 'version', header: 'Version' },
|
|
32
|
+
{ key: 'enabled', header: 'Enabled' },
|
|
33
|
+
{ key: 'installedAt', header: 'Installed' },
|
|
34
|
+
],
|
|
35
|
+
textFn: (p) => `${chalk.cyan(String(p.pluginId))}@${p.version} ${p.enabled === 'yes' ? chalk.green('enabled') : chalk.dim('disabled')} ${chalk.dim(String(p.installedAt))}`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
handleError(out, err, 'Failed to list plugins');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// ── install ───────────────────────────────────────────────────────────────
|
|
44
|
+
addGlobalFlags(plugins.command('install')
|
|
45
|
+
.description('Install a plugin from the marketplace')
|
|
46
|
+
.requiredOption('--plugin-id <pluginId>', 'Plugin marketplace identifier (e.g. org/plugin-name)')
|
|
47
|
+
.requiredOption('--version <version>', 'Version to install'))
|
|
48
|
+
.action(async (_opts) => {
|
|
49
|
+
const flags = resolveFlags(_opts);
|
|
50
|
+
const out = createOutput(flags);
|
|
51
|
+
out.startSpinner('Installing plugin...');
|
|
52
|
+
try {
|
|
53
|
+
const client = await getClientAsync();
|
|
54
|
+
const installed = await client.plugins.install({
|
|
55
|
+
pluginId: _opts.pluginId,
|
|
56
|
+
version: _opts.version,
|
|
57
|
+
});
|
|
58
|
+
out.stopSpinner();
|
|
59
|
+
if (flags.output === 'json') {
|
|
60
|
+
out.raw(JSON.stringify(installed, null, 2) + '\n');
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
out.raw(chalk.green(`Plugin installed: ${installed.pluginId}@${installed.version}`) + '\n');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
handleError(out, err, 'Failed to install plugin');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// ── uninstall ─────────────────────────────────────────────────────────────
|
|
71
|
+
addGlobalFlags(plugins.command('uninstall')
|
|
72
|
+
.description('Uninstall a plugin')
|
|
73
|
+
.argument('<pluginId>', 'Plugin marketplace identifier')
|
|
74
|
+
.option('--confirm', 'Skip confirmation prompt'))
|
|
75
|
+
.action(async (pluginId, _opts) => {
|
|
76
|
+
const flags = resolveFlags(_opts);
|
|
77
|
+
const out = createOutput(flags);
|
|
78
|
+
if (!_opts.confirm) {
|
|
79
|
+
out.raw(chalk.yellow(`Pass --confirm to uninstall plugin ${pluginId}.`) + '\n');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
out.startSpinner('Uninstalling plugin...');
|
|
83
|
+
try {
|
|
84
|
+
const client = await getClientAsync();
|
|
85
|
+
await client.plugins.uninstall(pluginId);
|
|
86
|
+
out.stopSpinner();
|
|
87
|
+
out.raw(chalk.green(`Plugin ${pluginId} uninstalled.`) + '\n');
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
handleError(out, err, 'Failed to uninstall plugin');
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// ── enable ────────────────────────────────────────────────────────────────
|
|
94
|
+
addGlobalFlags(plugins.command('enable')
|
|
95
|
+
.description('Enable a plugin')
|
|
96
|
+
.argument('<pluginId>', 'Plugin marketplace identifier'))
|
|
97
|
+
.action(async (pluginId, _opts) => {
|
|
98
|
+
const flags = resolveFlags(_opts);
|
|
99
|
+
const out = createOutput(flags);
|
|
100
|
+
out.startSpinner('Enabling plugin...');
|
|
101
|
+
try {
|
|
102
|
+
const client = await getClientAsync();
|
|
103
|
+
const updated = await client.plugins.enable(pluginId);
|
|
104
|
+
out.stopSpinner();
|
|
105
|
+
if (flags.output === 'json') {
|
|
106
|
+
out.raw(JSON.stringify(updated, null, 2) + '\n');
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
out.raw(chalk.green(`Plugin ${updated.pluginId} enabled.`) + '\n');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
handleError(out, err, 'Failed to enable plugin');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// ── disable ───────────────────────────────────────────────────────────────
|
|
117
|
+
addGlobalFlags(plugins.command('disable')
|
|
118
|
+
.description('Disable a plugin')
|
|
119
|
+
.argument('<pluginId>', 'Plugin marketplace identifier'))
|
|
120
|
+
.action(async (pluginId, _opts) => {
|
|
121
|
+
const flags = resolveFlags(_opts);
|
|
122
|
+
const out = createOutput(flags);
|
|
123
|
+
out.startSpinner('Disabling plugin...');
|
|
124
|
+
try {
|
|
125
|
+
const client = await getClientAsync();
|
|
126
|
+
const updated = await client.plugins.disable(pluginId);
|
|
127
|
+
out.stopSpinner();
|
|
128
|
+
if (flags.output === 'json') {
|
|
129
|
+
out.raw(JSON.stringify(updated, null, 2) + '\n');
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
out.raw(chalk.dim(`Plugin ${updated.pluginId} disabled.`) + '\n');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
handleError(out, err, 'Failed to disable plugin');
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
// ── update-settings ───────────────────────────────────────────────────────
|
|
140
|
+
addGlobalFlags(plugins.command('update-settings')
|
|
141
|
+
.description('Update plugin-specific settings')
|
|
142
|
+
.argument('<pluginId>', 'Plugin marketplace identifier')
|
|
143
|
+
.requiredOption('--settings <json>', 'Settings as a JSON string (e.g. \'{"theme":"dark"}\')'))
|
|
144
|
+
.action(async (pluginId, _opts) => {
|
|
145
|
+
const flags = resolveFlags(_opts);
|
|
146
|
+
const out = createOutput(flags);
|
|
147
|
+
let settings;
|
|
148
|
+
try {
|
|
149
|
+
settings = JSON.parse(_opts.settings);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
out.error('Invalid JSON for --settings. Provide a valid JSON object.');
|
|
153
|
+
process.exitCode = 2;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
out.startSpinner('Updating plugin settings...');
|
|
157
|
+
try {
|
|
158
|
+
const client = await getClientAsync();
|
|
159
|
+
const updated = await client.plugins.updateSettings(pluginId, settings);
|
|
160
|
+
out.stopSpinner();
|
|
161
|
+
if (flags.output === 'json') {
|
|
162
|
+
out.raw(JSON.stringify(updated, null, 2) + '\n');
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
out.raw(chalk.green(`Settings updated for ${updated.pluginId}.`) + '\n');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
handleError(out, err, 'Failed to update plugin settings');
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getClientAsync } from '../client.js';
|
|
3
|
+
import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
|
|
4
|
+
import { createOutput, handleError } from '../utils/output.js';
|
|
5
|
+
export function registerPublishVaultCommands(program) {
|
|
6
|
+
const pv = program.command('publish-vault').description('Manage whole-vault publishing (public sites)');
|
|
7
|
+
addGlobalFlags(pv.command('list')
|
|
8
|
+
.description('List your published vault sites'))
|
|
9
|
+
.action(async (_opts) => {
|
|
10
|
+
const flags = resolveFlags(_opts);
|
|
11
|
+
const out = createOutput(flags);
|
|
12
|
+
out.startSpinner('Fetching published vaults...');
|
|
13
|
+
try {
|
|
14
|
+
const client = await getClientAsync();
|
|
15
|
+
const list = await client.publishVault.listMine();
|
|
16
|
+
out.stopSpinner();
|
|
17
|
+
out.list(list.map(pv => ({ slug: pv.slug, title: pv.title, isPublished: pv.isPublished ? 'yes' : 'no', createdAt: pv.createdAt })), {
|
|
18
|
+
emptyMessage: 'No published vault sites found.',
|
|
19
|
+
columns: [
|
|
20
|
+
{ key: 'slug', header: 'Slug' },
|
|
21
|
+
{ key: 'title', header: 'Title' },
|
|
22
|
+
{ key: 'isPublished', header: 'Published' },
|
|
23
|
+
{ key: 'createdAt', header: 'Created' },
|
|
24
|
+
],
|
|
25
|
+
textFn: (pv) => `${chalk.cyan(String(pv.slug))} — ${String(pv.title)} [${pv.isPublished === 'yes' ? chalk.green('live') : chalk.dim('draft')}]`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
handleError(out, err, 'Failed to fetch published vaults');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
addGlobalFlags(pv.command('publish')
|
|
33
|
+
.description('Publish a vault as a public site')
|
|
34
|
+
.argument('<vaultId>', 'Vault ID')
|
|
35
|
+
.requiredOption('--slug <slug>', 'URL slug for the site')
|
|
36
|
+
.requiredOption('--title <title>', 'Site title')
|
|
37
|
+
.option('--description <desc>', 'Site description')
|
|
38
|
+
.option('--show-sidebar', 'Show sidebar navigation')
|
|
39
|
+
.option('--enable-search', 'Enable search on the site')
|
|
40
|
+
.option('--theme <theme>', 'Site theme')
|
|
41
|
+
.option('--domain <domainId>', 'Custom domain ID'))
|
|
42
|
+
.action(async (vaultId, _opts) => {
|
|
43
|
+
const flags = resolveFlags(_opts);
|
|
44
|
+
const out = createOutput(flags);
|
|
45
|
+
out.startSpinner('Publishing vault...');
|
|
46
|
+
try {
|
|
47
|
+
const client = await getClientAsync();
|
|
48
|
+
const published = await client.publishVault.publish(vaultId, {
|
|
49
|
+
slug: _opts.slug,
|
|
50
|
+
title: _opts.title,
|
|
51
|
+
description: _opts.description,
|
|
52
|
+
showSidebar: _opts.showSidebar === true,
|
|
53
|
+
enableSearch: _opts.enableSearch === true,
|
|
54
|
+
theme: _opts.theme,
|
|
55
|
+
customDomainId: _opts.domain,
|
|
56
|
+
});
|
|
57
|
+
out.success(`Vault published at /${published.slug}`, { id: published.id, slug: published.slug, title: published.title });
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
handleError(out, err, 'Failed to publish vault');
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
addGlobalFlags(pv.command('update')
|
|
64
|
+
.description('Update a published vault site')
|
|
65
|
+
.argument('<vaultId>', 'Vault ID')
|
|
66
|
+
.option('--slug <slug>', 'URL slug')
|
|
67
|
+
.option('--title <title>', 'Site title')
|
|
68
|
+
.option('--description <desc>', 'Site description')
|
|
69
|
+
.option('--show-sidebar', 'Show sidebar')
|
|
70
|
+
.option('--enable-search', 'Enable search')
|
|
71
|
+
.option('--theme <theme>', 'Site theme')
|
|
72
|
+
.option('--domain <domainId>', 'Custom domain ID'))
|
|
73
|
+
.action(async (vaultId, _opts) => {
|
|
74
|
+
const flags = resolveFlags(_opts);
|
|
75
|
+
const out = createOutput(flags);
|
|
76
|
+
out.startSpinner('Updating published vault...');
|
|
77
|
+
try {
|
|
78
|
+
const client = await getClientAsync();
|
|
79
|
+
const params = {};
|
|
80
|
+
if (_opts.slug)
|
|
81
|
+
params.slug = _opts.slug;
|
|
82
|
+
if (_opts.title)
|
|
83
|
+
params.title = _opts.title;
|
|
84
|
+
if (_opts.description)
|
|
85
|
+
params.description = _opts.description;
|
|
86
|
+
if (_opts.showSidebar !== undefined)
|
|
87
|
+
params.showSidebar = _opts.showSidebar === true;
|
|
88
|
+
if (_opts.enableSearch !== undefined)
|
|
89
|
+
params.enableSearch = _opts.enableSearch === true;
|
|
90
|
+
if (_opts.theme)
|
|
91
|
+
params.theme = _opts.theme;
|
|
92
|
+
if (_opts.domain)
|
|
93
|
+
params.customDomainId = _opts.domain;
|
|
94
|
+
const published = await client.publishVault.update(vaultId, params);
|
|
95
|
+
out.success('Published vault updated', { id: published.id, slug: published.slug });
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
handleError(out, err, 'Failed to update published vault');
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
addGlobalFlags(pv.command('unpublish')
|
|
102
|
+
.description('Unpublish a vault site')
|
|
103
|
+
.argument('<vaultId>', 'Vault ID'))
|
|
104
|
+
.action(async (vaultId, _opts) => {
|
|
105
|
+
const flags = resolveFlags(_opts);
|
|
106
|
+
const out = createOutput(flags);
|
|
107
|
+
out.startSpinner('Unpublishing vault...');
|
|
108
|
+
try {
|
|
109
|
+
const client = await getClientAsync();
|
|
110
|
+
await client.publishVault.unpublish(vaultId);
|
|
111
|
+
out.success('Vault unpublished', { vaultId });
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
handleError(out, err, 'Failed to unpublish vault');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
2
|
import { getClientAsync } from '../client.js';
|
|
2
3
|
import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
|
|
3
4
|
import { createOutput, handleError } from '../utils/output.js';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
5
|
export function registerPublishCommands(program) {
|
|
6
6
|
const publish = program.command('publish').description('Publish documents to public profile pages');
|
|
7
7
|
addGlobalFlags(publish.command('list')
|
|
@@ -121,10 +121,15 @@ export function registerPublishCommands(program) {
|
|
|
121
121
|
addGlobalFlags(publish.command('delete')
|
|
122
122
|
.description('Unpublish a document')
|
|
123
123
|
.argument('<vaultId>', 'Vault ID')
|
|
124
|
-
.argument('<docPath>', 'Document path (e.g., blog/post.md)')
|
|
124
|
+
.argument('<docPath>', 'Document path (e.g., blog/post.md)')
|
|
125
|
+
.option('-y, --yes', 'Skip confirmation prompt'))
|
|
125
126
|
.action(async (vaultId, docPath, _opts) => {
|
|
126
127
|
const flags = resolveFlags(_opts);
|
|
127
128
|
const out = createOutput(flags);
|
|
129
|
+
if (!_opts.yes) {
|
|
130
|
+
out.status(chalk.yellow(`Pass --yes to unpublish document ${docPath}.`));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
128
133
|
out.startSpinner('Unpublishing document...');
|
|
129
134
|
try {
|
|
130
135
|
const client = await getClientAsync();
|
|
@@ -135,4 +140,60 @@ export function registerPublishCommands(program) {
|
|
|
135
140
|
handleError(out, err, 'Failed to unpublish document');
|
|
136
141
|
}
|
|
137
142
|
});
|
|
143
|
+
const subdomain = publish.command('subdomain').description('Subdomain management for published vaults');
|
|
144
|
+
addGlobalFlags(subdomain.command('get')
|
|
145
|
+
.description('Get the subdomain for a published vault')
|
|
146
|
+
.argument('<vaultId>', 'Vault ID'))
|
|
147
|
+
.action(async (vaultId, _opts) => {
|
|
148
|
+
const flags = resolveFlags(_opts);
|
|
149
|
+
const out = createOutput(flags);
|
|
150
|
+
out.startSpinner('Fetching subdomain...');
|
|
151
|
+
try {
|
|
152
|
+
const client = await getClientAsync();
|
|
153
|
+
const result = await client.publish.getSubdomain(vaultId);
|
|
154
|
+
out.stopSpinner();
|
|
155
|
+
out.record({ subdomain: result.subdomain });
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
handleError(out, err, 'Failed to fetch subdomain');
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
addGlobalFlags(subdomain.command('set')
|
|
162
|
+
.description('Set a subdomain for a published vault')
|
|
163
|
+
.argument('<vaultId>', 'Vault ID')
|
|
164
|
+
.argument('<subdomain>', 'Subdomain to assign'))
|
|
165
|
+
.action(async (vaultId, subdomainArg, _opts) => {
|
|
166
|
+
const flags = resolveFlags(_opts);
|
|
167
|
+
const out = createOutput(flags);
|
|
168
|
+
out.startSpinner('Setting subdomain...');
|
|
169
|
+
try {
|
|
170
|
+
const client = await getClientAsync();
|
|
171
|
+
const result = await client.publish.setSubdomain(vaultId, subdomainArg);
|
|
172
|
+
out.success(`Subdomain set: ${result.subdomain}`, { subdomain: result.subdomain });
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
handleError(out, err, 'Failed to set subdomain');
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
addGlobalFlags(subdomain.command('delete')
|
|
179
|
+
.description('Remove the subdomain for a published vault')
|
|
180
|
+
.argument('<vaultId>', 'Vault ID')
|
|
181
|
+
.option('-y, --yes', 'Skip confirmation prompt'))
|
|
182
|
+
.action(async (vaultId, _opts) => {
|
|
183
|
+
const flags = resolveFlags(_opts);
|
|
184
|
+
const out = createOutput(flags);
|
|
185
|
+
if (!_opts.yes) {
|
|
186
|
+
out.status(chalk.yellow(`Pass --yes to remove the subdomain for vault ${vaultId}.`));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
out.startSpinner('Removing subdomain...');
|
|
190
|
+
try {
|
|
191
|
+
const client = await getClientAsync();
|
|
192
|
+
const result = await client.publish.deleteSubdomain(vaultId);
|
|
193
|
+
out.success(result.message, { message: result.message });
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
handleError(out, err, 'Failed to delete subdomain');
|
|
197
|
+
}
|
|
198
|
+
});
|
|
138
199
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getClientAsync } from '../client.js';
|
|
3
|
+
import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
|
|
4
|
+
import { createOutput, handleError } from '../utils/output.js';
|
|
5
|
+
export function registerSamlCommands(program) {
|
|
6
|
+
const saml = program.command('saml').description('SAML SSO configuration management (requires admin role)');
|
|
7
|
+
// ── list-configs ─────────────────────────────────────────────────────────
|
|
8
|
+
addGlobalFlags(saml.command('list-configs')
|
|
9
|
+
.description('List all SSO configurations'))
|
|
10
|
+
.action(async (_opts) => {
|
|
11
|
+
const flags = resolveFlags(_opts);
|
|
12
|
+
const out = createOutput(flags);
|
|
13
|
+
out.startSpinner('Fetching SSO configs...');
|
|
14
|
+
try {
|
|
15
|
+
const client = await getClientAsync();
|
|
16
|
+
const configs = await client.saml.listConfigs();
|
|
17
|
+
out.stopSpinner();
|
|
18
|
+
if (configs.length === 0 && flags.output !== 'json') {
|
|
19
|
+
out.raw('No SSO configurations found.\n');
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
out.list(configs.map((c) => ({
|
|
23
|
+
id: c.id,
|
|
24
|
+
domain: c.domain,
|
|
25
|
+
slug: c.slug,
|
|
26
|
+
entityId: c.entityId,
|
|
27
|
+
ssoUrl: c.ssoUrl,
|
|
28
|
+
})), {
|
|
29
|
+
columns: [
|
|
30
|
+
{ key: 'id', header: 'ID' },
|
|
31
|
+
{ key: 'domain', header: 'Domain' },
|
|
32
|
+
{ key: 'slug', header: 'Slug' },
|
|
33
|
+
{ key: 'entityId', header: 'Entity ID' },
|
|
34
|
+
{ key: 'ssoUrl', header: 'SSO URL' },
|
|
35
|
+
],
|
|
36
|
+
textFn: (c) => `${chalk.cyan(String(c.domain))} ${chalk.dim(`[${String(c.slug)}]`)} — ${chalk.dim(String(c.ssoUrl))}`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
handleError(out, err, 'Failed to fetch SSO configs');
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
// ── get-config ───────────────────────────────────────────────────────────
|
|
45
|
+
addGlobalFlags(saml.command('get-config')
|
|
46
|
+
.description('Get a single SSO configuration by ID')
|
|
47
|
+
.argument('<id>', 'SSO config ID'))
|
|
48
|
+
.action(async (id, _opts) => {
|
|
49
|
+
const flags = resolveFlags(_opts);
|
|
50
|
+
const out = createOutput(flags);
|
|
51
|
+
out.startSpinner('Fetching SSO config...');
|
|
52
|
+
try {
|
|
53
|
+
const client = await getClientAsync();
|
|
54
|
+
const config = await client.saml.getConfig(id);
|
|
55
|
+
out.stopSpinner();
|
|
56
|
+
out.record({
|
|
57
|
+
id: config.id,
|
|
58
|
+
domain: config.domain,
|
|
59
|
+
slug: config.slug,
|
|
60
|
+
entityId: config.entityId,
|
|
61
|
+
ssoUrl: config.ssoUrl,
|
|
62
|
+
spEntityId: config.spEntityId,
|
|
63
|
+
createdAt: config.createdAt,
|
|
64
|
+
updatedAt: config.updatedAt,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
handleError(out, err, 'Failed to fetch SSO config');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// ── create-config ─────────────────────────────────────────────────────────
|
|
72
|
+
addGlobalFlags(saml.command('create-config')
|
|
73
|
+
.description('Create a new SSO configuration')
|
|
74
|
+
.requiredOption('--domain <domain>', 'Customer/tenant domain (e.g. acmecorp.com)')
|
|
75
|
+
.requiredOption('--slug <slug>', 'URL slug for SAML endpoints (e.g. acmecorp)')
|
|
76
|
+
.requiredOption('--entity-id <entityId>', 'Identity Provider entity ID URI')
|
|
77
|
+
.requiredOption('--sso-url <ssoUrl>', 'Identity Provider Single Sign-On URL')
|
|
78
|
+
.requiredOption('--certificate <cert>', 'X.509 certificate (PEM-encoded)')
|
|
79
|
+
.option('--sp-entity-id <spEntityId>', 'Optional Service Provider entity ID override'))
|
|
80
|
+
.action(async (_opts) => {
|
|
81
|
+
const flags = resolveFlags(_opts);
|
|
82
|
+
const out = createOutput(flags);
|
|
83
|
+
out.startSpinner('Creating SSO config...');
|
|
84
|
+
try {
|
|
85
|
+
const client = await getClientAsync();
|
|
86
|
+
const created = await client.saml.createConfig({
|
|
87
|
+
domain: _opts.domain,
|
|
88
|
+
slug: _opts.slug,
|
|
89
|
+
entityId: _opts.entityId,
|
|
90
|
+
ssoUrl: _opts.ssoUrl,
|
|
91
|
+
certificate: _opts.certificate,
|
|
92
|
+
spEntityId: _opts.spEntityId,
|
|
93
|
+
});
|
|
94
|
+
out.stopSpinner();
|
|
95
|
+
if (flags.output === 'json') {
|
|
96
|
+
out.raw(JSON.stringify(created, null, 2) + '\n');
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
out.raw(chalk.green(`SSO config created for ${created.domain} (${created.id})`) + '\n');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
handleError(out, err, 'Failed to create SSO config');
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
// ── update-config ─────────────────────────────────────────────────────────
|
|
107
|
+
addGlobalFlags(saml.command('update-config')
|
|
108
|
+
.description('Update an existing SSO configuration')
|
|
109
|
+
.argument('<id>', 'SSO config ID')
|
|
110
|
+
.option('--domain <domain>', 'Updated customer domain')
|
|
111
|
+
.option('--slug <slug>', 'Updated URL slug')
|
|
112
|
+
.option('--entity-id <entityId>', 'Updated Identity Provider entity ID')
|
|
113
|
+
.option('--sso-url <ssoUrl>', 'Updated Identity Provider SSO URL')
|
|
114
|
+
.option('--certificate <cert>', 'Updated X.509 certificate')
|
|
115
|
+
.option('--sp-entity-id <spEntityId>', 'Updated Service Provider entity ID'))
|
|
116
|
+
.action(async (id, _opts) => {
|
|
117
|
+
const flags = resolveFlags(_opts);
|
|
118
|
+
const out = createOutput(flags);
|
|
119
|
+
const data = {};
|
|
120
|
+
if (_opts.domain)
|
|
121
|
+
data.domain = _opts.domain;
|
|
122
|
+
if (_opts.slug)
|
|
123
|
+
data.slug = _opts.slug;
|
|
124
|
+
if (_opts.entityId)
|
|
125
|
+
data.entityId = _opts.entityId;
|
|
126
|
+
if (_opts.ssoUrl)
|
|
127
|
+
data.ssoUrl = _opts.ssoUrl;
|
|
128
|
+
if (_opts.certificate)
|
|
129
|
+
data.certificate = _opts.certificate;
|
|
130
|
+
if (_opts.spEntityId)
|
|
131
|
+
data.spEntityId = _opts.spEntityId;
|
|
132
|
+
if (Object.keys(data).length === 0) {
|
|
133
|
+
out.error('No updates specified. Use --domain, --slug, --entity-id, --sso-url, --certificate, or --sp-entity-id.');
|
|
134
|
+
process.exitCode = 2;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
out.startSpinner('Updating SSO config...');
|
|
138
|
+
try {
|
|
139
|
+
const client = await getClientAsync();
|
|
140
|
+
const updated = await client.saml.updateConfig(id, data);
|
|
141
|
+
out.stopSpinner();
|
|
142
|
+
if (flags.output === 'json') {
|
|
143
|
+
out.raw(JSON.stringify(updated, null, 2) + '\n');
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
out.raw(chalk.green(`SSO config updated: ${updated.domain} (${updated.id})`) + '\n');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
handleError(out, err, 'Failed to update SSO config');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
// ── delete-config ─────────────────────────────────────────────────────────
|
|
154
|
+
addGlobalFlags(saml.command('delete-config')
|
|
155
|
+
.description('Delete an SSO configuration')
|
|
156
|
+
.argument('<id>', 'SSO config ID')
|
|
157
|
+
.option('--force', 'Skip confirmation prompt'))
|
|
158
|
+
.action(async (id, _opts) => {
|
|
159
|
+
const flags = resolveFlags(_opts);
|
|
160
|
+
const out = createOutput(flags);
|
|
161
|
+
if (!_opts.force) {
|
|
162
|
+
out.raw(chalk.yellow(`Pass --force to delete SSO config ${id}.`) + '\n');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
out.startSpinner('Deleting SSO config...');
|
|
166
|
+
try {
|
|
167
|
+
const client = await getClientAsync();
|
|
168
|
+
await client.saml.deleteConfig(id);
|
|
169
|
+
out.stopSpinner();
|
|
170
|
+
out.raw(chalk.green(`SSO config ${id} deleted.`) + '\n');
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
handleError(out, err, 'Failed to delete SSO config');
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
// ── metadata ──────────────────────────────────────────────────────────────
|
|
177
|
+
addGlobalFlags(saml.command('metadata')
|
|
178
|
+
.description('Show Service Provider metadata XML for an IdP slug')
|
|
179
|
+
.argument('<slug>', 'IdP slug'))
|
|
180
|
+
.action(async (slug, _opts) => {
|
|
181
|
+
const flags = resolveFlags(_opts);
|
|
182
|
+
const out = createOutput(flags);
|
|
183
|
+
out.startSpinner('Fetching SP metadata...');
|
|
184
|
+
try {
|
|
185
|
+
const client = await getClientAsync();
|
|
186
|
+
const xml = await client.saml.getMetadata(slug);
|
|
187
|
+
out.stopSpinner();
|
|
188
|
+
if (flags.output === 'json') {
|
|
189
|
+
out.raw(JSON.stringify({ xml }, null, 2) + '\n');
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
out.raw(xml + '\n');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
handleError(out, err, 'Failed to fetch SP metadata');
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
// ── login-url ─────────────────────────────────────────────────────────────
|
|
200
|
+
addGlobalFlags(saml.command('login-url')
|
|
201
|
+
.description('Show the IdP login redirect URL for a slug')
|
|
202
|
+
.argument('<slug>', 'IdP slug'))
|
|
203
|
+
.action(async (slug, _opts) => {
|
|
204
|
+
const flags = resolveFlags(_opts);
|
|
205
|
+
const out = createOutput(flags);
|
|
206
|
+
try {
|
|
207
|
+
const client = await getClientAsync();
|
|
208
|
+
const url = client.saml.getLoginUrl(slug);
|
|
209
|
+
if (flags.output === 'json') {
|
|
210
|
+
out.raw(JSON.stringify({ url }, null, 2) + '\n');
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
out.raw(url + '\n');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
handleError(out, err, 'Failed to build login URL');
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|