@nickchristensen/ppls 1.0.1

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 (107) hide show
  1. package/README.md +1190 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/add-command.d.ts +24 -0
  7. package/dist/add-command.js +47 -0
  8. package/dist/base-command.d.ts +63 -0
  9. package/dist/base-command.js +306 -0
  10. package/dist/commands/config/get.d.ts +11 -0
  11. package/dist/commands/config/get.js +34 -0
  12. package/dist/commands/config/init.d.ts +15 -0
  13. package/dist/commands/config/init.js +43 -0
  14. package/dist/commands/config/list.d.ts +13 -0
  15. package/dist/commands/config/list.js +64 -0
  16. package/dist/commands/config/remove.d.ts +11 -0
  17. package/dist/commands/config/remove.js +26 -0
  18. package/dist/commands/config/set.d.ts +12 -0
  19. package/dist/commands/config/set.js +58 -0
  20. package/dist/commands/correspondents/add.d.ts +12 -0
  21. package/dist/commands/correspondents/add.js +20 -0
  22. package/dist/commands/correspondents/delete.d.ts +16 -0
  23. package/dist/commands/correspondents/delete.js +18 -0
  24. package/dist/commands/correspondents/list.d.ts +9 -0
  25. package/dist/commands/correspondents/list.js +15 -0
  26. package/dist/commands/correspondents/show.d.ts +14 -0
  27. package/dist/commands/correspondents/show.js +17 -0
  28. package/dist/commands/correspondents/update.d.ts +18 -0
  29. package/dist/commands/correspondents/update.js +24 -0
  30. package/dist/commands/custom-fields/add.d.ts +16 -0
  31. package/dist/commands/custom-fields/add.js +91 -0
  32. package/dist/commands/custom-fields/delete.d.ts +16 -0
  33. package/dist/commands/custom-fields/delete.js +18 -0
  34. package/dist/commands/custom-fields/list.d.ts +9 -0
  35. package/dist/commands/custom-fields/list.js +12 -0
  36. package/dist/commands/custom-fields/show.d.ts +14 -0
  37. package/dist/commands/custom-fields/show.js +17 -0
  38. package/dist/commands/custom-fields/update.d.ts +20 -0
  39. package/dist/commands/custom-fields/update.js +93 -0
  40. package/dist/commands/document-types/add.d.ts +12 -0
  41. package/dist/commands/document-types/add.js +22 -0
  42. package/dist/commands/document-types/delete.d.ts +16 -0
  43. package/dist/commands/document-types/delete.js +18 -0
  44. package/dist/commands/document-types/list.d.ts +9 -0
  45. package/dist/commands/document-types/list.js +12 -0
  46. package/dist/commands/document-types/show.d.ts +14 -0
  47. package/dist/commands/document-types/show.js +17 -0
  48. package/dist/commands/document-types/update.d.ts +18 -0
  49. package/dist/commands/document-types/update.js +26 -0
  50. package/dist/commands/documents/add.d.ts +70 -0
  51. package/dist/commands/documents/add.js +166 -0
  52. package/dist/commands/documents/delete.d.ts +16 -0
  53. package/dist/commands/documents/delete.js +18 -0
  54. package/dist/commands/documents/download.d.ts +48 -0
  55. package/dist/commands/documents/download.js +152 -0
  56. package/dist/commands/documents/list.d.ts +9 -0
  57. package/dist/commands/documents/list.js +14 -0
  58. package/dist/commands/documents/show.d.ts +14 -0
  59. package/dist/commands/documents/show.js +19 -0
  60. package/dist/commands/documents/update.d.ts +25 -0
  61. package/dist/commands/documents/update.js +42 -0
  62. package/dist/commands/profile.d.ts +13 -0
  63. package/dist/commands/profile.js +19 -0
  64. package/dist/commands/tags/add.d.ts +17 -0
  65. package/dist/commands/tags/add.js +29 -0
  66. package/dist/commands/tags/delete.d.ts +16 -0
  67. package/dist/commands/tags/delete.js +18 -0
  68. package/dist/commands/tags/list.d.ts +10 -0
  69. package/dist/commands/tags/list.js +18 -0
  70. package/dist/commands/tags/show.d.ts +16 -0
  71. package/dist/commands/tags/show.js +24 -0
  72. package/dist/commands/tags/update.d.ts +21 -0
  73. package/dist/commands/tags/update.js +30 -0
  74. package/dist/delete-command.d.ts +20 -0
  75. package/dist/delete-command.js +48 -0
  76. package/dist/helpers/config-store.d.ts +4 -0
  77. package/dist/helpers/config-store.js +35 -0
  78. package/dist/helpers/date-utils.d.ts +3 -0
  79. package/dist/helpers/date-utils.js +27 -0
  80. package/dist/helpers/table.d.ts +20 -0
  81. package/dist/helpers/table.js +42 -0
  82. package/dist/index.d.ts +1 -0
  83. package/dist/index.js +1 -0
  84. package/dist/list-command.d.ts +49 -0
  85. package/dist/list-command.js +98 -0
  86. package/dist/paginated-command.d.ts +48 -0
  87. package/dist/paginated-command.js +89 -0
  88. package/dist/show-command.d.ts +27 -0
  89. package/dist/show-command.js +50 -0
  90. package/dist/types/correspondents.d.ts +28 -0
  91. package/dist/types/correspondents.js +1 -0
  92. package/dist/types/custom-fields.d.ts +18 -0
  93. package/dist/types/custom-fields.js +1 -0
  94. package/dist/types/document-types.d.ts +27 -0
  95. package/dist/types/document-types.js +1 -0
  96. package/dist/types/documents.d.ts +70 -0
  97. package/dist/types/documents.js +1 -0
  98. package/dist/types/profile.d.ts +15 -0
  99. package/dist/types/profile.js +1 -0
  100. package/dist/types/shared.d.ts +4 -0
  101. package/dist/types/shared.js +1 -0
  102. package/dist/types/tags.d.ts +31 -0
  103. package/dist/types/tags.js +1 -0
  104. package/dist/update-command.d.ts +28 -0
  105. package/dist/update-command.js +51 -0
  106. package/oclif.manifest.json +3313 -0
  107. package/package.json +87 -0
package/bin/dev.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %*
package/bin/dev.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
2
+
3
+ import {execute} from '@oclif/core'
4
+
5
+ await execute({development: true, dir: import.meta.url})
package/bin/run.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node "%~dp0\run" %*
package/bin/run.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {execute} from '@oclif/core'
4
+
5
+ await execute({dir: import.meta.url})
@@ -0,0 +1,24 @@
1
+ import type { ApiFlags } from './base-command.js';
2
+ import { BaseCommand } from './base-command.js';
3
+ type AddCommandFlags = ApiFlags & {
4
+ 'date-format': string;
5
+ plain?: boolean;
6
+ table?: boolean;
7
+ };
8
+ type AddTableRow = {
9
+ field: string;
10
+ value: unknown;
11
+ };
12
+ export declare abstract class AddCommand<TCreate = Record<string, unknown>, TRaw extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> = TRaw> extends BaseCommand {
13
+ protected abstract createPath: string;
14
+ protected addRows(result: TOutput): AddTableRow[];
15
+ protected abstract buildPayload(args: unknown, flags: Record<string, unknown>): TCreate;
16
+ protected abstract plainTemplate(item: TOutput): null | string | undefined;
17
+ protected renderAddOutput(options: {
18
+ flags: AddCommandFlags;
19
+ result: TOutput;
20
+ }): void;
21
+ run(): Promise<TOutput>;
22
+ protected transformResult(result: TRaw): TOutput;
23
+ }
24
+ export {};
@@ -0,0 +1,47 @@
1
+ import { BaseCommand } from './base-command.js';
2
+ import { createValueFormatter, formatField } from './helpers/table.js';
3
+ export class AddCommand extends BaseCommand {
4
+ addRows(result) {
5
+ return Object.entries(result).map(([field, value]) => ({
6
+ field,
7
+ value,
8
+ }));
9
+ }
10
+ renderAddOutput(options) {
11
+ if (this.jsonEnabled()) {
12
+ return;
13
+ }
14
+ const { flags, result } = options;
15
+ const dateFormat = flags['date-format'];
16
+ if (flags.plain) {
17
+ const line = this.plainTemplate(result);
18
+ if (line) {
19
+ this.log(line);
20
+ }
21
+ return;
22
+ }
23
+ const valueFormatter = createValueFormatter(dateFormat);
24
+ this.logTable([
25
+ { align: 'right', formatter: formatField, value: 'field' },
26
+ { formatter: valueFormatter, value: 'value' },
27
+ ], this.addRows(result), { showHeader: false });
28
+ }
29
+ async run() {
30
+ const { args, flags, metadata } = await this.parse();
31
+ const { dateFormat, ...apiFlags } = await this.resolveGlobalFlags(flags, metadata);
32
+ const outputFlags = {
33
+ ...apiFlags,
34
+ 'date-format': dateFormat,
35
+ plain: flags.plain,
36
+ table: flags.table,
37
+ };
38
+ const payload = this.buildPayload(args, flags);
39
+ const rawResult = await this.postApiJson(apiFlags, this.createPath, payload);
40
+ const result = this.transformResult(rawResult);
41
+ this.renderAddOutput({ flags: outputFlags, result });
42
+ return result;
43
+ }
44
+ transformResult(result) {
45
+ return result;
46
+ }
47
+ }
@@ -0,0 +1,63 @@
1
+ import { Command } from '@oclif/core';
2
+ import yoctoSpinner from 'yocto-spinner';
3
+ import { type TableColumn, type TableOptions, type TableRow } from './helpers/table.js';
4
+ export type ApiFlags = {
5
+ headers: Record<string, string>;
6
+ hostname: string;
7
+ token: string;
8
+ };
9
+ type ResolvedGlobalFlags = ApiFlags & {
10
+ dateFormat: string;
11
+ };
12
+ type UserConfig = Partial<ApiFlags> & {
13
+ dateFormat?: string;
14
+ headers?: Record<string, unknown>;
15
+ };
16
+ type CommandMetadata = {
17
+ flags?: Record<string, {
18
+ setFromDefault?: boolean;
19
+ }>;
20
+ };
21
+ export declare abstract class BaseCommand extends Command {
22
+ static baseFlags: {
23
+ 'date-format': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
24
+ header: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
+ hostname: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
+ plain: import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
+ table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
28
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
+ };
30
+ static enableJsonFlag: boolean;
31
+ private userConfigPromise?;
32
+ protected buildApiUrl(hostnameValue: string, path: string, params?: Record<string, number | string | undefined>): URL;
33
+ protected buildApiUrlFromFlags(flags: ApiFlags, path: string, params?: Record<string, number | string | undefined>): URL;
34
+ protected deleteApiJson<T>(flags: ApiFlags, path: string): Promise<null | T>;
35
+ protected fetchApiBinary(flags: ApiFlags, path: string, params?: Record<string, number | string | undefined>): Promise<{
36
+ data: Uint8Array;
37
+ headers: Headers;
38
+ }>;
39
+ protected fetchApiJson<T>(flags: ApiFlags, path: string, params?: Record<string, number | string | undefined>): Promise<T>;
40
+ protected fetchJson<T>(url: URL, tokenValue: string, headers?: Record<string, string>): Promise<T>;
41
+ protected formatErrorMessage(response: Response): Promise<string>;
42
+ protected loadUserConfig(): Promise<UserConfig>;
43
+ protected logTable(headers: TableColumn[], rows: TableRow[], options?: TableOptions): void;
44
+ protected parseHeaderEntries(entries: string[], source: string): Record<string, string>;
45
+ protected parseHeadersConfig(input: unknown, source: string): Record<string, string>;
46
+ protected parseHeadersInput(input: unknown, source: string): Record<string, string>;
47
+ protected patchApiJson<T>(flags: ApiFlags, path: string, body: unknown): Promise<T>;
48
+ protected postApiFormData<T>(flags: ApiFlags, path: string, body: FormData): Promise<T>;
49
+ protected postApiJson<T>(flags: ApiFlags, path: string, body: unknown): Promise<T>;
50
+ protected requestJson<T>(options: {
51
+ body?: unknown;
52
+ headers: Record<string, string>;
53
+ method: string;
54
+ token: string;
55
+ url: URL;
56
+ }): Promise<T>;
57
+ protected resolveDateFormat(flags: Record<string, unknown>, metadata: CommandMetadata | undefined, userConfig: UserConfig): string;
58
+ protected resolveGlobalFlags(flags: Record<string, unknown>, metadata?: CommandMetadata): Promise<ResolvedGlobalFlags>;
59
+ protected resolveHeaders(flags: Record<string, unknown>, userConfig: UserConfig): Record<string, string>;
60
+ protected shouldShowSpinner(): boolean;
61
+ protected startSpinner(text: string): null | ReturnType<typeof yoctoSpinner>;
62
+ }
63
+ export {};
@@ -0,0 +1,306 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import yoctoSpinner from 'yocto-spinner';
3
+ import { readConfig } from './helpers/config-store.js';
4
+ import { renderTable } from './helpers/table.js';
5
+ export class BaseCommand extends Command {
6
+ static baseFlags = {
7
+ 'date-format': Flags.string({
8
+ default: 'yyyy-MM-dd',
9
+ description: 'Format output dates using a template.',
10
+ env: 'PPLS_DATE_FORMAT',
11
+ helpGroup: 'GLOBAL',
12
+ }),
13
+ header: Flags.string({
14
+ description: 'Add a custom request header (repeatable, format: Key=Value)',
15
+ env: 'PPLS_HEADERS',
16
+ helpGroup: 'ENVIRONMENT',
17
+ multiple: true,
18
+ }),
19
+ hostname: Flags.string({
20
+ description: 'Paperless-ngx base URL',
21
+ env: 'PPLS_HOSTNAME',
22
+ helpGroup: 'ENVIRONMENT',
23
+ }),
24
+ plain: Flags.boolean({
25
+ description: 'Format output as plain text.',
26
+ exclusive: ['json', 'table'],
27
+ helpGroup: 'GLOBAL',
28
+ }),
29
+ table: Flags.boolean({
30
+ description: 'Format output as table.',
31
+ exclusive: ['json', 'plain'],
32
+ helpGroup: 'GLOBAL',
33
+ }),
34
+ token: Flags.string({
35
+ description: 'Paperless-ngx API token',
36
+ env: 'PPLS_TOKEN',
37
+ helpGroup: 'ENVIRONMENT',
38
+ }),
39
+ };
40
+ static enableJsonFlag = true;
41
+ userConfigPromise;
42
+ buildApiUrl(hostnameValue, path, params = {}) {
43
+ let url;
44
+ try {
45
+ url = new URL(path, new URL(hostnameValue));
46
+ }
47
+ catch {
48
+ this.error('Invalid hostname. Expected a full URL like https://paperless.example.com');
49
+ }
50
+ for (const [key, value] of Object.entries(params)) {
51
+ if (value !== undefined) {
52
+ url.searchParams.set(key, String(value));
53
+ }
54
+ }
55
+ return url;
56
+ }
57
+ buildApiUrlFromFlags(flags, path, params = {}) {
58
+ return this.buildApiUrl(flags.hostname, path, params);
59
+ }
60
+ async deleteApiJson(flags, path) {
61
+ const url = this.buildApiUrlFromFlags(flags, path);
62
+ const requestHeaders = {
63
+ Accept: 'application/json',
64
+ Authorization: `Token ${flags.token}`,
65
+ ...flags.headers,
66
+ };
67
+ const response = await fetch(url, {
68
+ headers: requestHeaders,
69
+ method: 'DELETE',
70
+ });
71
+ if (!response.ok) {
72
+ this.error(await this.formatErrorMessage(response));
73
+ }
74
+ const contentType = response.headers.get('content-type') ?? '';
75
+ const payloadText = await response.text();
76
+ if (!payloadText.trim()) {
77
+ return null;
78
+ }
79
+ if (contentType.includes('application/json')) {
80
+ return JSON.parse(payloadText);
81
+ }
82
+ return payloadText;
83
+ }
84
+ async fetchApiBinary(flags, path, params = {}) {
85
+ const url = this.buildApiUrlFromFlags(flags, path, params);
86
+ const requestHeaders = {
87
+ Accept: '*/*',
88
+ Authorization: `Token ${flags.token}`,
89
+ ...flags.headers,
90
+ };
91
+ const response = await fetch(url, {
92
+ headers: requestHeaders,
93
+ });
94
+ if (!response.ok) {
95
+ this.error(await this.formatErrorMessage(response));
96
+ }
97
+ return {
98
+ data: new Uint8Array(await response.arrayBuffer()),
99
+ headers: response.headers,
100
+ };
101
+ }
102
+ async fetchApiJson(flags, path, params = {}) {
103
+ const url = this.buildApiUrlFromFlags(flags, path, params);
104
+ return this.fetchJson(url, flags.token, flags.headers);
105
+ }
106
+ async fetchJson(url, tokenValue, headers = {}) {
107
+ return this.requestJson({
108
+ headers,
109
+ method: 'GET',
110
+ token: tokenValue,
111
+ url,
112
+ });
113
+ }
114
+ async formatErrorMessage(response) {
115
+ const baseMessage = `Request failed with ${response.status} ${response.statusText}`.trim();
116
+ if (response.status >= 400 && response.status < 600) {
117
+ try {
118
+ const payloadText = await response.clone().text();
119
+ if (payloadText) {
120
+ const payload = JSON.parse(payloadText);
121
+ if (typeof payload?.error === 'string' && payload.error.trim()) {
122
+ return `${payload.error}`;
123
+ }
124
+ }
125
+ }
126
+ catch {
127
+ // Fall back to generic error when response isn't JSON.
128
+ }
129
+ }
130
+ return baseMessage;
131
+ }
132
+ async loadUserConfig() {
133
+ if (this.userConfigPromise) {
134
+ return this.userConfigPromise;
135
+ }
136
+ this.userConfigPromise = (async () => {
137
+ try {
138
+ return (await readConfig(this.config.configDir));
139
+ }
140
+ catch (error) {
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ this.error(message);
143
+ }
144
+ })();
145
+ return this.userConfigPromise;
146
+ }
147
+ logTable(headers, rows, options) {
148
+ this.log(renderTable(headers, rows, options));
149
+ }
150
+ parseHeaderEntries(entries, source) {
151
+ const headers = {};
152
+ for (const entry of entries) {
153
+ const trimmed = entry.trim();
154
+ if (!trimmed) {
155
+ continue;
156
+ }
157
+ const equalsIndex = trimmed.indexOf('=');
158
+ const separatorIndex = equalsIndex;
159
+ if (separatorIndex === -1) {
160
+ this.error(`Invalid header "${entry}" from ${source}. Use "Key=Value".`);
161
+ }
162
+ const key = trimmed.slice(0, separatorIndex).trim();
163
+ const value = trimmed.slice(separatorIndex + 1).trim();
164
+ if (!key) {
165
+ this.error(`Invalid header "${entry}" from ${source}. Header name cannot be empty.`);
166
+ }
167
+ headers[key] = value;
168
+ }
169
+ return headers;
170
+ }
171
+ parseHeadersConfig(input, source) {
172
+ if (input === undefined || input === null) {
173
+ return {};
174
+ }
175
+ if (typeof input !== 'object' || Array.isArray(input)) {
176
+ this.error(`Invalid headers from ${source}. Expected an object of header key/value pairs.`);
177
+ }
178
+ const headers = {};
179
+ for (const [key, value] of Object.entries(input)) {
180
+ if (value === undefined || value === null) {
181
+ continue;
182
+ }
183
+ headers[key] = String(value);
184
+ }
185
+ return headers;
186
+ }
187
+ parseHeadersInput(input, source) {
188
+ if (input === undefined || input === null) {
189
+ return {};
190
+ }
191
+ if (typeof input === 'string') {
192
+ const trimmed = input.trim();
193
+ if (!trimmed) {
194
+ return {};
195
+ }
196
+ return this.parseHeaderEntries([trimmed], source);
197
+ }
198
+ if (Array.isArray(input)) {
199
+ const headers = {};
200
+ for (const [index, entry] of input.entries()) {
201
+ Object.assign(headers, this.parseHeadersInput(entry, `${source}[${index}]`));
202
+ }
203
+ return headers;
204
+ }
205
+ this.error(`Invalid headers from ${source}. Expected a string or array of strings.`);
206
+ }
207
+ async patchApiJson(flags, path, body) {
208
+ const url = this.buildApiUrlFromFlags(flags, path);
209
+ return this.requestJson({
210
+ body,
211
+ headers: flags.headers,
212
+ method: 'PATCH',
213
+ token: flags.token,
214
+ url,
215
+ });
216
+ }
217
+ async postApiFormData(flags, path, body) {
218
+ const url = this.buildApiUrlFromFlags(flags, path);
219
+ const requestHeaders = {
220
+ Accept: 'application/json',
221
+ Authorization: `Token ${flags.token}`,
222
+ ...flags.headers,
223
+ };
224
+ const response = await fetch(url, {
225
+ body,
226
+ headers: requestHeaders,
227
+ method: 'POST',
228
+ });
229
+ if (!response.ok) {
230
+ this.error(await this.formatErrorMessage(response));
231
+ }
232
+ const contentType = response.headers.get('content-type') ?? '';
233
+ if (contentType.includes('application/json')) {
234
+ return (await response.json());
235
+ }
236
+ return (await response.text());
237
+ }
238
+ async postApiJson(flags, path, body) {
239
+ const url = this.buildApiUrlFromFlags(flags, path);
240
+ return this.requestJson({
241
+ body,
242
+ headers: flags.headers,
243
+ method: 'POST',
244
+ token: flags.token,
245
+ url,
246
+ });
247
+ }
248
+ async requestJson(options) {
249
+ const { body, headers, method, token, url } = options;
250
+ const requestHeaders = {
251
+ Accept: 'application/json',
252
+ Authorization: `Token ${token}`,
253
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
254
+ ...headers,
255
+ };
256
+ const response = await fetch(url, {
257
+ body: body === undefined ? undefined : JSON.stringify(body),
258
+ headers: requestHeaders,
259
+ method,
260
+ });
261
+ if (!response.ok) {
262
+ this.error(await this.formatErrorMessage(response));
263
+ }
264
+ return (await response.json());
265
+ }
266
+ resolveDateFormat(flags, metadata, userConfig) {
267
+ const configDateFormat = userConfig.dateFormat;
268
+ const flagValue = flags['date-format'];
269
+ const usedDefault = metadata?.flags?.['date-format']?.setFromDefault;
270
+ if (usedDefault && configDateFormat) {
271
+ return configDateFormat;
272
+ }
273
+ return flagValue ?? configDateFormat ?? 'yyyy-MM-dd';
274
+ }
275
+ async resolveGlobalFlags(flags, metadata) {
276
+ const userConfig = await this.loadUserConfig();
277
+ const inputFlags = flags;
278
+ const hostname = inputFlags.hostname ?? userConfig.hostname;
279
+ const token = inputFlags.token ?? userConfig.token;
280
+ const dateFormat = this.resolveDateFormat(flags, metadata, userConfig);
281
+ const headers = this.resolveHeaders(flags, userConfig);
282
+ if (!hostname) {
283
+ this.error('hostname required. Use --hostname, set PPLS_HOSTNAME, or use `ppls config set hostname <value>`');
284
+ }
285
+ if (!token) {
286
+ this.error('token required. Use --token, set PPLS_TOKEN, or use `ppls config set token <value>`');
287
+ }
288
+ return { dateFormat, headers, hostname, token };
289
+ }
290
+ resolveHeaders(flags, userConfig) {
291
+ const configHeaders = this.parseHeadersConfig(userConfig.headers, 'config.json headers');
292
+ const envHeaders = this.parseHeadersInput(process.env.PPLS_HEADERS, 'PPLS_HEADERS');
293
+ const flagHeaders = this.parseHeadersInput(flags.header, '--header');
294
+ return {
295
+ ...configHeaders,
296
+ ...envHeaders,
297
+ ...flagHeaders,
298
+ };
299
+ }
300
+ shouldShowSpinner() {
301
+ return Boolean(process.stderr.isTTY);
302
+ }
303
+ startSpinner(text) {
304
+ return this.shouldShowSpinner() ? yoctoSpinner({ text }).start() : null;
305
+ }
306
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ import { type ConfigData } from '../../helpers/config-store.js';
3
+ export default class ConfigGet extends Command {
4
+ static args: {
5
+ key: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static enableJsonFlag: boolean;
9
+ static examples: string[];
10
+ run(): Promise<ConfigData>;
11
+ }
@@ -0,0 +1,34 @@
1
+ import { Args, Command } from '@oclif/core';
2
+ import { readConfig } from '../../helpers/config-store.js';
3
+ export default class ConfigGet extends Command {
4
+ static args = {
5
+ key: Args.string({ description: 'Config key', required: true }),
6
+ };
7
+ static description = 'Get a config value';
8
+ static enableJsonFlag = true;
9
+ static examples = ['<%= config.bin %> <%= command.id %> hostname'];
10
+ async run() {
11
+ const { args } = await this.parse();
12
+ const typedArgs = args;
13
+ const config = await readConfig(this.config.configDir);
14
+ if (!Object.hasOwn(config, typedArgs.key)) {
15
+ if (!this.jsonEnabled()) {
16
+ this.log(`Config key ${typedArgs.key} not set.`);
17
+ }
18
+ return {};
19
+ }
20
+ const value = config[typedArgs.key];
21
+ if (!this.jsonEnabled()) {
22
+ if (value === null || value === undefined) {
23
+ this.log('');
24
+ }
25
+ else if (typeof value === 'string') {
26
+ this.log(value);
27
+ }
28
+ else {
29
+ this.log(JSON.stringify(value));
30
+ }
31
+ }
32
+ return { [typedArgs.key]: value };
33
+ }
34
+ }
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ type ConfigInitResult = {
3
+ overwritten: boolean;
4
+ path: string;
5
+ };
6
+ export default class ConfigInit extends Command {
7
+ static description: string;
8
+ static enableJsonFlag: boolean;
9
+ static examples: string[];
10
+ static flags: {
11
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<ConfigInitResult>;
14
+ }
15
+ export {};
@@ -0,0 +1,43 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { stat } from 'node:fs/promises';
3
+ import { configPath, writeConfig } from '../../helpers/config-store.js';
4
+ export default class ConfigInit extends Command {
5
+ static description = 'Initialize a config file';
6
+ static enableJsonFlag = true;
7
+ static examples = ['<%= config.bin %> <%= command.id %>'];
8
+ static flags = {
9
+ force: Flags.boolean({ char: 'f', description: 'Overwrite existing config file' }),
10
+ };
11
+ async run() {
12
+ const { flags } = await this.parse();
13
+ const typedFlags = flags;
14
+ const configFile = configPath(this.config.configDir);
15
+ let exists = false;
16
+ try {
17
+ await stat(configFile);
18
+ exists = true;
19
+ }
20
+ catch (error) {
21
+ const typedError = error;
22
+ if (typedError.code !== 'ENOENT') {
23
+ this.error(`Failed to access config at ${configFile}: ${typedError.message ?? String(error)}`);
24
+ }
25
+ }
26
+ if (exists && !typedFlags.force) {
27
+ this.error(`Config already exists at ${configFile}. Use --force to overwrite.`);
28
+ }
29
+ await writeConfig(this.config.configDir, {
30
+ dateFormat: 'YYYY-MM-DD',
31
+ headers: {
32
+ 'Custom-Header': 'value',
33
+ },
34
+ hostname: 'http://example.com',
35
+ token: 'your-api-token-here',
36
+ });
37
+ const result = { overwritten: exists, path: configFile };
38
+ if (!this.jsonEnabled()) {
39
+ this.log(`Created config at ${configFile}`);
40
+ }
41
+ return result;
42
+ }
43
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ import { type ConfigData } from '../../helpers/config-store.js';
3
+ export default class ConfigList extends Command {
4
+ static description: string;
5
+ static enableJsonFlag: boolean;
6
+ static examples: string[];
7
+ static flags: {
8
+ plain: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<ConfigData>;
12
+ private buildRows;
13
+ }
@@ -0,0 +1,64 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { readConfig } from '../../helpers/config-store.js';
3
+ import { renderTable } from '../../helpers/table.js';
4
+ const formatConfigValue = (value) => {
5
+ if (value === null || value === undefined) {
6
+ return '';
7
+ }
8
+ if (typeof value === 'string') {
9
+ return value;
10
+ }
11
+ if (typeof value === 'number' || typeof value === 'boolean') {
12
+ return String(value);
13
+ }
14
+ return JSON.stringify(value);
15
+ };
16
+ export default class ConfigList extends Command {
17
+ static description = 'List config values';
18
+ static enableJsonFlag = true;
19
+ static examples = ['<%= config.bin %> <%= command.id %>'];
20
+ static flags = {
21
+ plain: Flags.boolean({
22
+ description: 'Format output as plain text.',
23
+ exclusive: ['json', 'table'],
24
+ }),
25
+ table: Flags.boolean({
26
+ description: 'Format output as table.',
27
+ exclusive: ['json', 'plain'],
28
+ }),
29
+ };
30
+ async run() {
31
+ const { flags } = await this.parse();
32
+ const typedFlags = flags;
33
+ const config = await readConfig(this.config.configDir);
34
+ if (this.jsonEnabled()) {
35
+ return config;
36
+ }
37
+ const entries = Object.entries(config);
38
+ if (entries.length === 0) {
39
+ this.log('No config values set.');
40
+ return config;
41
+ }
42
+ if (typedFlags.plain) {
43
+ for (const [key, value] of entries) {
44
+ this.log(`${key}=${formatConfigValue(value)}`);
45
+ }
46
+ return config;
47
+ }
48
+ const rows = this.buildRows(config);
49
+ const output = renderTable([
50
+ { align: 'right', formatter: String, value: 'key' },
51
+ { value: 'value' },
52
+ ], rows, { showHeader: false });
53
+ this.log(output);
54
+ return config;
55
+ }
56
+ buildRows(config) {
57
+ return Object.entries(config)
58
+ .sort(([a], [b]) => a.localeCompare(b))
59
+ .map(([key, value]) => ({
60
+ key,
61
+ value: formatConfigValue(value),
62
+ }));
63
+ }
64
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ import { type ConfigData } from '../../helpers/config-store.js';
3
+ export default class ConfigRemove extends Command {
4
+ static args: {
5
+ key: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static enableJsonFlag: boolean;
9
+ static examples: string[];
10
+ run(): Promise<ConfigData>;
11
+ }