@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.
Files changed (91) hide show
  1. package/dist/auth/credentials.d.ts +29 -0
  2. package/dist/auth/credentials.d.ts.map +1 -0
  3. package/dist/auth/credentials.js +79 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +74 -0
  7. package/dist/commands/api-keys.d.ts +4 -0
  8. package/dist/commands/api-keys.d.ts.map +1 -0
  9. package/dist/commands/api-keys.js +127 -0
  10. package/dist/commands/context.d.ts +4 -0
  11. package/dist/commands/context.d.ts.map +1 -0
  12. package/dist/commands/context.js +92 -0
  13. package/dist/commands/customers.d.ts +4 -0
  14. package/dist/commands/customers.d.ts.map +1 -0
  15. package/dist/commands/customers.js +120 -0
  16. package/dist/commands/experiments.d.ts +4 -0
  17. package/dist/commands/experiments.d.ts.map +1 -0
  18. package/dist/commands/experiments.js +177 -0
  19. package/dist/commands/extensions.d.ts +4 -0
  20. package/dist/commands/extensions.d.ts.map +1 -0
  21. package/dist/commands/extensions.js +94 -0
  22. package/dist/commands/files.d.ts +4 -0
  23. package/dist/commands/files.d.ts.map +1 -0
  24. package/dist/commands/files.js +143 -0
  25. package/dist/commands/locales.d.ts +4 -0
  26. package/dist/commands/locales.d.ts.map +1 -0
  27. package/dist/commands/locales.js +126 -0
  28. package/dist/commands/login.d.ts +4 -0
  29. package/dist/commands/login.d.ts.map +1 -0
  30. package/dist/commands/login.js +125 -0
  31. package/dist/commands/logout.d.ts +4 -0
  32. package/dist/commands/logout.d.ts.map +1 -0
  33. package/dist/commands/logout.js +16 -0
  34. package/dist/commands/media.d.ts +4 -0
  35. package/dist/commands/media.d.ts.map +1 -0
  36. package/dist/commands/media.js +44 -0
  37. package/dist/commands/models.d.ts +4 -0
  38. package/dist/commands/models.d.ts.map +1 -0
  39. package/dist/commands/models.js +155 -0
  40. package/dist/commands/notes.d.ts +4 -0
  41. package/dist/commands/notes.d.ts.map +1 -0
  42. package/dist/commands/notes.js +120 -0
  43. package/dist/commands/notifications.d.ts +4 -0
  44. package/dist/commands/notifications.d.ts.map +1 -0
  45. package/dist/commands/notifications.js +73 -0
  46. package/dist/commands/operations.d.ts +4 -0
  47. package/dist/commands/operations.d.ts.map +1 -0
  48. package/dist/commands/operations.js +161 -0
  49. package/dist/commands/records.d.ts +4 -0
  50. package/dist/commands/records.d.ts.map +1 -0
  51. package/dist/commands/records.js +216 -0
  52. package/dist/commands/schedules.d.ts +4 -0
  53. package/dist/commands/schedules.d.ts.map +1 -0
  54. package/dist/commands/schedules.js +150 -0
  55. package/dist/commands/search.d.ts +4 -0
  56. package/dist/commands/search.d.ts.map +1 -0
  57. package/dist/commands/search.js +60 -0
  58. package/dist/commands/segments.d.ts +4 -0
  59. package/dist/commands/segments.d.ts.map +1 -0
  60. package/dist/commands/segments.js +143 -0
  61. package/dist/commands/select-project.d.ts +4 -0
  62. package/dist/commands/select-project.d.ts.map +1 -0
  63. package/dist/commands/select-project.js +144 -0
  64. package/dist/commands/settings.d.ts +4 -0
  65. package/dist/commands/settings.d.ts.map +1 -0
  66. package/dist/commands/settings.js +113 -0
  67. package/dist/commands/variant-catalog.d.ts +4 -0
  68. package/dist/commands/variant-catalog.d.ts.map +1 -0
  69. package/dist/commands/variant-catalog.js +111 -0
  70. package/dist/commands/whoami.d.ts +4 -0
  71. package/dist/commands/whoami.d.ts.map +1 -0
  72. package/dist/commands/whoami.js +50 -0
  73. package/dist/graphql/queries.d.ts +101 -0
  74. package/dist/graphql/queries.d.ts.map +1 -0
  75. package/dist/graphql/queries.js +373 -0
  76. package/dist/lib/client.d.ts +17 -0
  77. package/dist/lib/client.d.ts.map +1 -0
  78. package/dist/lib/client.js +61 -0
  79. package/dist/lib/config.d.ts +12 -0
  80. package/dist/lib/config.d.ts.map +1 -0
  81. package/dist/lib/config.js +8 -0
  82. package/dist/lib/errors.d.ts +6 -0
  83. package/dist/lib/errors.d.ts.map +1 -0
  84. package/dist/lib/errors.js +62 -0
  85. package/dist/lib/input.d.ts +36 -0
  86. package/dist/lib/input.d.ts.map +1 -0
  87. package/dist/lib/input.js +106 -0
  88. package/dist/lib/output.d.ts +31 -0
  89. package/dist/lib/output.d.ts.map +1 -0
  90. package/dist/lib/output.js +107 -0
  91. 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,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { GlobalOptions } from '../lib/config.js';
3
+ export declare function registerLogoutCommand(program: Command, globalOpts: () => GlobalOptions): void;
4
+ //# sourceMappingURL=logout.d.ts.map
@@ -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,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { GlobalOptions } from '../lib/config.js';
3
+ export declare function registerMediaCommands(program: Command, globalOpts: () => GlobalOptions): void;
4
+ //# sourceMappingURL=media.d.ts.map
@@ -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,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { GlobalOptions } from '../lib/config.js';
3
+ export declare function registerModelsCommands(program: Command, globalOpts: () => GlobalOptions): void;
4
+ //# sourceMappingURL=models.d.ts.map
@@ -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,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { GlobalOptions } from '../lib/config.js';
3
+ export declare function registerNotesCommands(program: Command, globalOpts: () => GlobalOptions): void;
4
+ //# sourceMappingURL=notes.d.ts.map
@@ -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,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { GlobalOptions } from '../lib/config.js';
3
+ export declare function registerNotificationsCommands(program: Command, globalOpts: () => GlobalOptions): void;
4
+ //# sourceMappingURL=notifications.d.ts.map
@@ -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,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { GlobalOptions } from '../lib/config.js';
3
+ export declare function registerOperationsCommands(program: Command, globalOpts: () => GlobalOptions): void;
4
+ //# sourceMappingURL=operations.d.ts.map
@@ -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"}