@eide/foir-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/dist/auth/credentials.d.ts +29 -0
- package/dist/auth/credentials.d.ts.map +1 -0
- package/dist/auth/credentials.js +79 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +74 -0
- package/dist/commands/api-keys.d.ts +4 -0
- package/dist/commands/api-keys.d.ts.map +1 -0
- package/dist/commands/api-keys.js +127 -0
- package/dist/commands/context.d.ts +4 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +92 -0
- package/dist/commands/customers.d.ts +4 -0
- package/dist/commands/customers.d.ts.map +1 -0
- package/dist/commands/customers.js +120 -0
- package/dist/commands/experiments.d.ts +4 -0
- package/dist/commands/experiments.d.ts.map +1 -0
- package/dist/commands/experiments.js +177 -0
- package/dist/commands/extensions.d.ts +4 -0
- package/dist/commands/extensions.d.ts.map +1 -0
- package/dist/commands/extensions.js +94 -0
- package/dist/commands/files.d.ts +4 -0
- package/dist/commands/files.d.ts.map +1 -0
- package/dist/commands/files.js +143 -0
- package/dist/commands/locales.d.ts +4 -0
- package/dist/commands/locales.d.ts.map +1 -0
- package/dist/commands/locales.js +126 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +125 -0
- package/dist/commands/logout.d.ts +4 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +16 -0
- package/dist/commands/media.d.ts +4 -0
- package/dist/commands/media.d.ts.map +1 -0
- package/dist/commands/media.js +44 -0
- package/dist/commands/models.d.ts +4 -0
- package/dist/commands/models.d.ts.map +1 -0
- package/dist/commands/models.js +155 -0
- package/dist/commands/notes.d.ts +4 -0
- package/dist/commands/notes.d.ts.map +1 -0
- package/dist/commands/notes.js +120 -0
- package/dist/commands/notifications.d.ts +4 -0
- package/dist/commands/notifications.d.ts.map +1 -0
- package/dist/commands/notifications.js +73 -0
- package/dist/commands/operations.d.ts +4 -0
- package/dist/commands/operations.d.ts.map +1 -0
- package/dist/commands/operations.js +161 -0
- package/dist/commands/records.d.ts +4 -0
- package/dist/commands/records.d.ts.map +1 -0
- package/dist/commands/records.js +216 -0
- package/dist/commands/schedules.d.ts +4 -0
- package/dist/commands/schedules.d.ts.map +1 -0
- package/dist/commands/schedules.js +150 -0
- package/dist/commands/search.d.ts +4 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +60 -0
- package/dist/commands/segments.d.ts +4 -0
- package/dist/commands/segments.d.ts.map +1 -0
- package/dist/commands/segments.js +143 -0
- package/dist/commands/select-project.d.ts +4 -0
- package/dist/commands/select-project.d.ts.map +1 -0
- package/dist/commands/select-project.js +144 -0
- package/dist/commands/settings.d.ts +4 -0
- package/dist/commands/settings.d.ts.map +1 -0
- package/dist/commands/settings.js +113 -0
- package/dist/commands/variant-catalog.d.ts +4 -0
- package/dist/commands/variant-catalog.d.ts.map +1 -0
- package/dist/commands/variant-catalog.js +111 -0
- package/dist/commands/whoami.d.ts +4 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +50 -0
- package/dist/graphql/queries.d.ts +101 -0
- package/dist/graphql/queries.d.ts.map +1 -0
- package/dist/graphql/queries.js +373 -0
- package/dist/lib/client.d.ts +17 -0
- package/dist/lib/client.d.ts.map +1 -0
- package/dist/lib/client.js +61 -0
- package/dist/lib/config.d.ts +12 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +8 -0
- package/dist/lib/errors.d.ts +6 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +62 -0
- package/dist/lib/input.d.ts +36 -0
- package/dist/lib/input.d.ts.map +1 -0
- package/dist/lib/input.js +106 -0
- package/dist/lib/output.d.ts +31 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +107 -0
- package/package.json +59 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import open from 'open';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { writeCredentials, } from '../auth/credentials.js';
|
|
5
|
+
import { getApiUrl } from '../lib/config.js';
|
|
6
|
+
import { withErrorHandler } from '../lib/errors.js';
|
|
7
|
+
async function findAvailablePort(start, end) {
|
|
8
|
+
for (let port = start; port <= end; port++) {
|
|
9
|
+
const available = await new Promise((resolve) => {
|
|
10
|
+
const server = http.createServer();
|
|
11
|
+
server.listen(port, () => {
|
|
12
|
+
server.close();
|
|
13
|
+
resolve(true);
|
|
14
|
+
});
|
|
15
|
+
server.on('error', () => resolve(false));
|
|
16
|
+
});
|
|
17
|
+
if (available)
|
|
18
|
+
return port;
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`No available ports in range ${start}-${end}`);
|
|
21
|
+
}
|
|
22
|
+
async function exchangeCodeForTokens(apiUrl, code, codeVerifier, redirectUri) {
|
|
23
|
+
const tokenUrl = new URL('/auth/cli/token', apiUrl);
|
|
24
|
+
const response = await fetch(tokenUrl.toString(), {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
grant_type: 'authorization_code',
|
|
29
|
+
code,
|
|
30
|
+
code_verifier: codeVerifier,
|
|
31
|
+
redirect_uri: redirectUri,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
const error = await response.text();
|
|
36
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
37
|
+
}
|
|
38
|
+
const data = (await response.json());
|
|
39
|
+
return {
|
|
40
|
+
accessToken: data.access_token,
|
|
41
|
+
refreshToken: data.refresh_token,
|
|
42
|
+
expiresIn: data.expires_in,
|
|
43
|
+
user: data.user,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function loginAction(globalOpts) {
|
|
47
|
+
const apiUrl = getApiUrl(globalOpts);
|
|
48
|
+
console.log('Starting authentication...\n');
|
|
49
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
50
|
+
const codeChallenge = crypto
|
|
51
|
+
.createHash('sha256')
|
|
52
|
+
.update(codeVerifier)
|
|
53
|
+
.digest('base64url');
|
|
54
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
55
|
+
const port = await findAvailablePort(9876, 9900);
|
|
56
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
57
|
+
const authCode = await new Promise((resolve, reject) => {
|
|
58
|
+
const server = http.createServer((req, res) => {
|
|
59
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
60
|
+
if (url.pathname === '/callback') {
|
|
61
|
+
const code = url.searchParams.get('code');
|
|
62
|
+
const returnedState = url.searchParams.get('state');
|
|
63
|
+
const error = url.searchParams.get('error');
|
|
64
|
+
if (error) {
|
|
65
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
66
|
+
res.end(`<html><body style="font-family:system-ui;text-align:center;padding:50px"><h1>Authentication failed</h1><p>${error}</p></body></html>`);
|
|
67
|
+
server.close();
|
|
68
|
+
reject(new Error(`Authentication failed: ${error}`));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (returnedState !== state) {
|
|
72
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
73
|
+
res.end(`<html><body style="font-family:system-ui;text-align:center;padding:50px"><h1>Security error</h1><p>Invalid state. Please try again.</p></body></html>`);
|
|
74
|
+
server.close();
|
|
75
|
+
reject(new Error('Invalid state parameter'));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const apiHost = new URL(apiUrl).host;
|
|
79
|
+
const mainHost = apiHost.replace(/^api\./, '');
|
|
80
|
+
const mainUrl = `https://${mainHost}`;
|
|
81
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
82
|
+
res.end(`<html><head><meta http-equiv="refresh" content="2;url=${mainUrl}"></head><body style="font-family:system-ui;text-align:center;padding:50px"><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>`);
|
|
83
|
+
server.close();
|
|
84
|
+
resolve(code);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
server.listen(port);
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
server.close();
|
|
90
|
+
reject(new Error('Authentication timed out after 5 minutes'));
|
|
91
|
+
}, 5 * 60 * 1000);
|
|
92
|
+
const authUrl = new URL('/auth/cli', apiUrl);
|
|
93
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
94
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
95
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
96
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
97
|
+
authUrl.searchParams.set('state', state);
|
|
98
|
+
console.log('Opening browser for authentication...');
|
|
99
|
+
console.log(`If the browser doesn't open, visit: ${authUrl.toString()}\n`);
|
|
100
|
+
open(authUrl.toString()).catch(() => {
|
|
101
|
+
console.log('Could not open browser automatically.');
|
|
102
|
+
console.log(`Please visit: ${authUrl.toString()}\n`);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
console.log('Exchanging code for tokens...');
|
|
106
|
+
const tokens = await exchangeCodeForTokens(apiUrl, authCode, codeVerifier, redirectUri);
|
|
107
|
+
const credentials = {
|
|
108
|
+
accessToken: tokens.accessToken,
|
|
109
|
+
refreshToken: tokens.refreshToken,
|
|
110
|
+
expiresAt: new Date(Date.now() + tokens.expiresIn * 1000).toISOString(),
|
|
111
|
+
user: tokens.user,
|
|
112
|
+
};
|
|
113
|
+
await writeCredentials(credentials);
|
|
114
|
+
console.log(`\n✓ Logged in as ${tokens.user.email}`);
|
|
115
|
+
console.log('\nRun `foir select-project` to choose a project.');
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
export function registerLoginCommand(program, globalOpts) {
|
|
119
|
+
program
|
|
120
|
+
.command('login')
|
|
121
|
+
.description('Authenticate via browser OAuth')
|
|
122
|
+
.action(withErrorHandler(globalOpts, async () => {
|
|
123
|
+
await loginAction(globalOpts());
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logout.d.ts","sourceRoot":"","sources":["../../src/commands/logout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAeN"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { deleteCredentials, getCredentials } from '../auth/credentials.js';
|
|
2
|
+
import { withErrorHandler } from '../lib/errors.js';
|
|
3
|
+
export function registerLogoutCommand(program, globalOpts) {
|
|
4
|
+
program
|
|
5
|
+
.command('logout')
|
|
6
|
+
.description('Clear stored credentials')
|
|
7
|
+
.action(withErrorHandler(globalOpts, async () => {
|
|
8
|
+
const credentials = await getCredentials();
|
|
9
|
+
if (!credentials) {
|
|
10
|
+
console.log('Not logged in.');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
await deleteCredentials();
|
|
14
|
+
console.log(`✓ Logged out (was ${credentials.user.email})`);
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/commands/media.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAKtD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CA6CN"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { basename } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { withErrorHandler } from '../lib/errors.js';
|
|
5
|
+
import { getRestAuth } from '../lib/client.js';
|
|
6
|
+
import { formatOutput, success } from '../lib/output.js';
|
|
7
|
+
export function registerMediaCommands(program, globalOpts) {
|
|
8
|
+
const media = program.command('media').description('Media file operations');
|
|
9
|
+
// upload
|
|
10
|
+
media
|
|
11
|
+
.command('upload <filepath>')
|
|
12
|
+
.description('Upload a file')
|
|
13
|
+
.action(withErrorHandler(globalOpts, async (filepath) => {
|
|
14
|
+
const opts = globalOpts();
|
|
15
|
+
const { apiUrl, headers } = await getRestAuth(opts);
|
|
16
|
+
const fileBuffer = await fs.readFile(filepath);
|
|
17
|
+
const fileName = basename(filepath);
|
|
18
|
+
const formData = new FormData();
|
|
19
|
+
formData.append('file', new Blob([fileBuffer]), fileName);
|
|
20
|
+
const uploadUrl = `${apiUrl.replace(/\/$/, '')}/api/files/upload`;
|
|
21
|
+
const response = await fetch(uploadUrl, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers,
|
|
24
|
+
body: formData,
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const errorText = await response.text();
|
|
28
|
+
throw new Error(`Upload failed (${response.status}): ${errorText}`);
|
|
29
|
+
}
|
|
30
|
+
const result = (await response.json());
|
|
31
|
+
if (opts.json || opts.jsonl) {
|
|
32
|
+
formatOutput(result, opts);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
success(`Uploaded ${fileName}`);
|
|
36
|
+
if (result.url) {
|
|
37
|
+
console.log(chalk.bold(` URL: ${result.url}`));
|
|
38
|
+
}
|
|
39
|
+
if (result.storageKey) {
|
|
40
|
+
console.log(chalk.gray(` Key: ${result.storageKey}`));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/commands/models.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AActD,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAuMN"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { withErrorHandler } from '../lib/errors.js';
|
|
2
|
+
import { createClient } from '../lib/client.js';
|
|
3
|
+
import { formatOutput, formatList, timeAgo, success } from '../lib/output.js';
|
|
4
|
+
import { parseInputData, confirmAction } from '../lib/input.js';
|
|
5
|
+
import { MODELS, MODEL_BY_KEY, CREATE_MODEL, UPDATE_MODEL, DELETE_MODEL, MODEL_VERSIONS, } from '../graphql/queries.js';
|
|
6
|
+
export function registerModelsCommands(program, globalOpts) {
|
|
7
|
+
const models = program.command('models').description('Manage models');
|
|
8
|
+
// list
|
|
9
|
+
models
|
|
10
|
+
.command('list')
|
|
11
|
+
.description('List all models')
|
|
12
|
+
.option('--category <cat>', 'Filter by category')
|
|
13
|
+
.option('--search <term>', 'Search by name')
|
|
14
|
+
.option('--limit <n>', 'Max results', '50')
|
|
15
|
+
.option('--offset <n>', 'Skip results', '0')
|
|
16
|
+
.action(withErrorHandler(globalOpts, async (cmdOpts) => {
|
|
17
|
+
const opts = globalOpts();
|
|
18
|
+
const client = await createClient(opts);
|
|
19
|
+
const data = await client.request(MODELS, {
|
|
20
|
+
search: cmdOpts.search,
|
|
21
|
+
category: cmdOpts.category,
|
|
22
|
+
limit: parseInt(cmdOpts.limit ?? '50', 10),
|
|
23
|
+
offset: parseInt(cmdOpts.offset ?? '0', 10),
|
|
24
|
+
});
|
|
25
|
+
formatList(data.models.items, opts, {
|
|
26
|
+
columns: [
|
|
27
|
+
{ key: 'key', header: 'Key', width: 24 },
|
|
28
|
+
{ key: 'name', header: 'Name', width: 24 },
|
|
29
|
+
{ key: 'category', header: 'Category', width: 14 },
|
|
30
|
+
{
|
|
31
|
+
key: 'systemEntity',
|
|
32
|
+
header: 'System',
|
|
33
|
+
width: 8,
|
|
34
|
+
format: (v) => (v ? 'yes' : 'no'),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'updatedAt',
|
|
38
|
+
header: 'Updated',
|
|
39
|
+
width: 12,
|
|
40
|
+
format: (v) => timeAgo(v),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
total: data.models.total,
|
|
44
|
+
});
|
|
45
|
+
}));
|
|
46
|
+
// get
|
|
47
|
+
models
|
|
48
|
+
.command('get <key>')
|
|
49
|
+
.description('Get a model by key')
|
|
50
|
+
.action(withErrorHandler(globalOpts, async (key) => {
|
|
51
|
+
const opts = globalOpts();
|
|
52
|
+
const client = await createClient(opts);
|
|
53
|
+
const data = await client.request(MODEL_BY_KEY, { key });
|
|
54
|
+
if (!data.modelByKey) {
|
|
55
|
+
throw new Error(`Model "${key}" not found.`);
|
|
56
|
+
}
|
|
57
|
+
formatOutput(data.modelByKey, opts);
|
|
58
|
+
}));
|
|
59
|
+
// create
|
|
60
|
+
models
|
|
61
|
+
.command('create')
|
|
62
|
+
.description('Create a new model')
|
|
63
|
+
.option('-d, --data <json>', 'Model data as JSON')
|
|
64
|
+
.option('-f, --file <path>', 'Read data from file')
|
|
65
|
+
.action(withErrorHandler(globalOpts, async (cmdOpts) => {
|
|
66
|
+
const opts = globalOpts();
|
|
67
|
+
const client = await createClient(opts);
|
|
68
|
+
const inputData = await parseInputData(cmdOpts);
|
|
69
|
+
const data = await client.request(CREATE_MODEL, { input: inputData });
|
|
70
|
+
formatOutput(data.createModel, opts);
|
|
71
|
+
if (!(opts.json || opts.jsonl || opts.quiet)) {
|
|
72
|
+
success(`Created model ${data.createModel.key}`);
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
// update
|
|
76
|
+
models
|
|
77
|
+
.command('update <key>')
|
|
78
|
+
.description('Update a model')
|
|
79
|
+
.option('-d, --data <json>', 'Model data as JSON')
|
|
80
|
+
.option('-f, --file <path>', 'Read data from file')
|
|
81
|
+
.action(withErrorHandler(globalOpts, async (key, cmdOpts) => {
|
|
82
|
+
const opts = globalOpts();
|
|
83
|
+
const client = await createClient(opts);
|
|
84
|
+
// Resolve key → id
|
|
85
|
+
const existing = await client.request(MODEL_BY_KEY, { key });
|
|
86
|
+
if (!existing.modelByKey) {
|
|
87
|
+
throw new Error(`Model "${key}" not found.`);
|
|
88
|
+
}
|
|
89
|
+
const inputData = await parseInputData(cmdOpts);
|
|
90
|
+
const input = { id: existing.modelByKey.id, ...inputData };
|
|
91
|
+
const data = await client.request(UPDATE_MODEL, { input });
|
|
92
|
+
formatOutput(data.updateModel, opts);
|
|
93
|
+
if (!(opts.json || opts.jsonl || opts.quiet)) {
|
|
94
|
+
success(`Updated model ${key}`);
|
|
95
|
+
}
|
|
96
|
+
}));
|
|
97
|
+
// delete
|
|
98
|
+
models
|
|
99
|
+
.command('delete <key>')
|
|
100
|
+
.description('Delete a model')
|
|
101
|
+
.option('--confirm', 'Skip confirmation prompt')
|
|
102
|
+
.action(withErrorHandler(globalOpts, async (key, cmdOpts) => {
|
|
103
|
+
const opts = globalOpts();
|
|
104
|
+
const confirmed = await confirmAction(`Delete model "${key}"?`, {
|
|
105
|
+
confirm: !!cmdOpts.confirm,
|
|
106
|
+
});
|
|
107
|
+
if (!confirmed) {
|
|
108
|
+
console.log('Aborted.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const client = await createClient(opts);
|
|
112
|
+
const existing = await client.request(MODEL_BY_KEY, { key });
|
|
113
|
+
if (!existing.modelByKey) {
|
|
114
|
+
throw new Error(`Model "${key}" not found.`);
|
|
115
|
+
}
|
|
116
|
+
await client.request(DELETE_MODEL, { id: existing.modelByKey.id });
|
|
117
|
+
if (opts.json || opts.jsonl) {
|
|
118
|
+
formatOutput({ deleted: true, key }, opts);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
success(`Deleted model ${key}`);
|
|
122
|
+
}
|
|
123
|
+
}));
|
|
124
|
+
// versions
|
|
125
|
+
models
|
|
126
|
+
.command('versions <key>')
|
|
127
|
+
.description('List schema versions for a model')
|
|
128
|
+
.option('--limit <n>', 'Max results', '20')
|
|
129
|
+
.action(withErrorHandler(globalOpts, async (key, cmdOpts) => {
|
|
130
|
+
const opts = globalOpts();
|
|
131
|
+
const client = await createClient(opts);
|
|
132
|
+
const existing = await client.request(MODEL_BY_KEY, { key });
|
|
133
|
+
if (!existing.modelByKey) {
|
|
134
|
+
throw new Error(`Model "${key}" not found.`);
|
|
135
|
+
}
|
|
136
|
+
const data = await client.request(MODEL_VERSIONS, {
|
|
137
|
+
modelId: existing.modelByKey.id,
|
|
138
|
+
limit: parseInt(cmdOpts.limit ?? '20', 10),
|
|
139
|
+
});
|
|
140
|
+
formatList(data.modelVersions.items, opts, {
|
|
141
|
+
columns: [
|
|
142
|
+
{ key: 'id', header: 'Version ID', width: 28 },
|
|
143
|
+
{ key: 'versionNumber', header: '#', width: 5 },
|
|
144
|
+
{ key: 'changeDescription', header: 'Description', width: 32 },
|
|
145
|
+
{
|
|
146
|
+
key: 'createdAt',
|
|
147
|
+
header: 'Created',
|
|
148
|
+
width: 12,
|
|
149
|
+
format: (v) => timeAgo(v),
|
|
150
|
+
},
|
|
151
|
+
{ key: 'createdBy', header: 'By', width: 18 },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notes.d.ts","sourceRoot":"","sources":["../../src/commands/notes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAatD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CA2IN"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { withErrorHandler } from '../lib/errors.js';
|
|
2
|
+
import { createClient } from '../lib/client.js';
|
|
3
|
+
import { formatOutput, formatList, timeAgo, success } from '../lib/output.js';
|
|
4
|
+
import { confirmAction } from '../lib/input.js';
|
|
5
|
+
import { NOTES, NOTE, CREATE_NOTE, RESOLVE_NOTE, DELETE_NOTE, } from '../graphql/queries.js';
|
|
6
|
+
export function registerNotesCommands(program, globalOpts) {
|
|
7
|
+
const notes = program
|
|
8
|
+
.command('notes')
|
|
9
|
+
.description('Manage notes and comments');
|
|
10
|
+
// list
|
|
11
|
+
notes
|
|
12
|
+
.command('list')
|
|
13
|
+
.description('List notes for an entity')
|
|
14
|
+
.requiredOption('--entity-type <type>', 'Entity type (e.g. record, model)')
|
|
15
|
+
.requiredOption('--entity-id <id>', 'Entity ID')
|
|
16
|
+
.option('--include-resolved', 'Include resolved notes')
|
|
17
|
+
.option('--limit <n>', 'Max results', '20')
|
|
18
|
+
.action(withErrorHandler(globalOpts, async (cmdOpts) => {
|
|
19
|
+
const opts = globalOpts();
|
|
20
|
+
const client = await createClient(opts);
|
|
21
|
+
const data = await client.request(NOTES, {
|
|
22
|
+
entityType: cmdOpts.entityType,
|
|
23
|
+
entityId: cmdOpts.entityId,
|
|
24
|
+
includeResolved: !!cmdOpts.includeResolved,
|
|
25
|
+
limit: parseInt(String(cmdOpts.limit ?? '20'), 10),
|
|
26
|
+
});
|
|
27
|
+
formatList(data.notes.items, opts, {
|
|
28
|
+
columns: [
|
|
29
|
+
{ key: 'id', header: 'ID', width: 28 },
|
|
30
|
+
{ key: 'body', header: 'Body', width: 40 },
|
|
31
|
+
{ key: 'authorId', header: 'Author', width: 16 },
|
|
32
|
+
{
|
|
33
|
+
key: 'isResolved',
|
|
34
|
+
header: 'Resolved',
|
|
35
|
+
width: 9,
|
|
36
|
+
format: (v) => (v ? 'yes' : ''),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: 'createdAt',
|
|
40
|
+
header: 'Created',
|
|
41
|
+
width: 12,
|
|
42
|
+
format: (v) => timeAgo(v),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
total: data.notes.total,
|
|
46
|
+
});
|
|
47
|
+
}));
|
|
48
|
+
// get
|
|
49
|
+
notes
|
|
50
|
+
.command('get <id>')
|
|
51
|
+
.description('Get a note by ID')
|
|
52
|
+
.action(withErrorHandler(globalOpts, async (id) => {
|
|
53
|
+
const opts = globalOpts();
|
|
54
|
+
const client = await createClient(opts);
|
|
55
|
+
const data = await client.request(NOTE, { id });
|
|
56
|
+
if (!data.note)
|
|
57
|
+
throw new Error(`Note "${id}" not found.`);
|
|
58
|
+
formatOutput(data.note, opts);
|
|
59
|
+
}));
|
|
60
|
+
// create
|
|
61
|
+
notes
|
|
62
|
+
.command('create')
|
|
63
|
+
.description('Create a note')
|
|
64
|
+
.requiredOption('--entity-type <type>', 'Entity type')
|
|
65
|
+
.requiredOption('--entity-id <id>', 'Entity ID')
|
|
66
|
+
.requiredOption('--body <text>', 'Note body text')
|
|
67
|
+
.option('--parent-note-id <id>', 'Reply to a note')
|
|
68
|
+
.action(withErrorHandler(globalOpts, async (cmdOpts) => {
|
|
69
|
+
const opts = globalOpts();
|
|
70
|
+
const client = await createClient(opts);
|
|
71
|
+
const input = {
|
|
72
|
+
entityType: cmdOpts.entityType,
|
|
73
|
+
entityId: cmdOpts.entityId,
|
|
74
|
+
body: cmdOpts.body,
|
|
75
|
+
};
|
|
76
|
+
if (cmdOpts.parentNoteId)
|
|
77
|
+
input.parentNoteId = cmdOpts.parentNoteId;
|
|
78
|
+
const data = await client.request(CREATE_NOTE, { input });
|
|
79
|
+
formatOutput(data.createNote, opts);
|
|
80
|
+
if (!(opts.json || opts.jsonl || opts.quiet))
|
|
81
|
+
success('Note created');
|
|
82
|
+
}));
|
|
83
|
+
// resolve
|
|
84
|
+
notes
|
|
85
|
+
.command('resolve <id>')
|
|
86
|
+
.description('Resolve a note')
|
|
87
|
+
.option('--resolution <text>', 'Resolution message')
|
|
88
|
+
.action(withErrorHandler(globalOpts, async (id, cmdOpts) => {
|
|
89
|
+
const opts = globalOpts();
|
|
90
|
+
const client = await createClient(opts);
|
|
91
|
+
const input = { noteId: id };
|
|
92
|
+
if (cmdOpts.resolution)
|
|
93
|
+
input.resolution = cmdOpts.resolution;
|
|
94
|
+
const data = await client.request(RESOLVE_NOTE, { input });
|
|
95
|
+
formatOutput(data.resolveNote, opts);
|
|
96
|
+
if (!(opts.json || opts.jsonl || opts.quiet))
|
|
97
|
+
success(`Resolved note ${id}`);
|
|
98
|
+
}));
|
|
99
|
+
// delete
|
|
100
|
+
notes
|
|
101
|
+
.command('delete <id>')
|
|
102
|
+
.description('Delete a note')
|
|
103
|
+
.option('--confirm', 'Skip confirmation prompt')
|
|
104
|
+
.action(withErrorHandler(globalOpts, async (id, cmdOpts) => {
|
|
105
|
+
const opts = globalOpts();
|
|
106
|
+
const confirmed = await confirmAction(`Delete note ${id}?`, {
|
|
107
|
+
confirm: !!cmdOpts.confirm,
|
|
108
|
+
});
|
|
109
|
+
if (!confirmed) {
|
|
110
|
+
console.log('Aborted.');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const client = await createClient(opts);
|
|
114
|
+
await client.request(DELETE_NOTE, { noteId: id });
|
|
115
|
+
if (opts.json || opts.jsonl)
|
|
116
|
+
formatOutput({ deleted: true, id }, opts);
|
|
117
|
+
else
|
|
118
|
+
success(`Deleted note ${id}`);
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../../src/commands/notifications.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAUtD,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CA0FN"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { withErrorHandler } from '../lib/errors.js';
|
|
2
|
+
import { createClient } from '../lib/client.js';
|
|
3
|
+
import { formatOutput, formatList, timeAgo, success } from '../lib/output.js';
|
|
4
|
+
import { NOTIFICATIONS, MARK_NOTIFICATION_READ, MARK_ALL_NOTIFICATIONS_READ, } from '../graphql/queries.js';
|
|
5
|
+
export function registerNotificationsCommands(program, globalOpts) {
|
|
6
|
+
const notifications = program
|
|
7
|
+
.command('notifications')
|
|
8
|
+
.description('Manage notifications');
|
|
9
|
+
// list
|
|
10
|
+
notifications
|
|
11
|
+
.command('list')
|
|
12
|
+
.description('List notifications')
|
|
13
|
+
.option('--unread', 'Only unread notifications')
|
|
14
|
+
.option('--limit <n>', 'Max results', '20')
|
|
15
|
+
.action(withErrorHandler(globalOpts, async (cmdOpts) => {
|
|
16
|
+
const opts = globalOpts();
|
|
17
|
+
const client = await createClient(opts);
|
|
18
|
+
const data = await client.request(NOTIFICATIONS, {
|
|
19
|
+
unreadOnly: !!cmdOpts.unread,
|
|
20
|
+
limit: parseInt(String(cmdOpts.limit ?? '20'), 10),
|
|
21
|
+
});
|
|
22
|
+
if (!(opts.json || opts.jsonl || opts.quiet)) {
|
|
23
|
+
console.log(`Unread: ${data.notifications.unreadCount}\n`);
|
|
24
|
+
}
|
|
25
|
+
formatList(data.notifications.items, opts, {
|
|
26
|
+
columns: [
|
|
27
|
+
{ key: 'id', header: 'ID', width: 28 },
|
|
28
|
+
{ key: 'type', header: 'Type', width: 16 },
|
|
29
|
+
{ key: 'title', header: 'Title', width: 32 },
|
|
30
|
+
{
|
|
31
|
+
key: 'isRead',
|
|
32
|
+
header: 'Read',
|
|
33
|
+
width: 6,
|
|
34
|
+
format: (v) => (v ? 'yes' : 'no'),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'createdAt',
|
|
38
|
+
header: 'When',
|
|
39
|
+
width: 12,
|
|
40
|
+
format: (v) => timeAgo(v),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
total: data.notifications.total,
|
|
44
|
+
});
|
|
45
|
+
}));
|
|
46
|
+
// read
|
|
47
|
+
notifications
|
|
48
|
+
.command('read <id>')
|
|
49
|
+
.description('Mark a notification as read')
|
|
50
|
+
.action(withErrorHandler(globalOpts, async (id) => {
|
|
51
|
+
const opts = globalOpts();
|
|
52
|
+
const client = await createClient(opts);
|
|
53
|
+
const data = await client.request(MARK_NOTIFICATION_READ, { id });
|
|
54
|
+
formatOutput(data.markNotificationRead, opts);
|
|
55
|
+
if (!(opts.json || opts.jsonl || opts.quiet))
|
|
56
|
+
success('Marked as read');
|
|
57
|
+
}));
|
|
58
|
+
// read-all
|
|
59
|
+
notifications
|
|
60
|
+
.command('read-all')
|
|
61
|
+
.description('Mark all notifications as read')
|
|
62
|
+
.action(withErrorHandler(globalOpts, async () => {
|
|
63
|
+
const opts = globalOpts();
|
|
64
|
+
const client = await createClient(opts);
|
|
65
|
+
const data = await client.request(MARK_ALL_NOTIFICATIONS_READ);
|
|
66
|
+
if (opts.json || opts.jsonl) {
|
|
67
|
+
formatOutput({ marked: data.markAllNotificationsRead }, opts);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
success(`Marked ${data.markAllNotificationsRead} notifications as read`);
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../../src/commands/operations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAetD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CA0MN"}
|