@pikku/core 0.12.101 → 0.12.103
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/CHANGELOG.md +35 -0
- package/dist/services/email-service.d.ts +13 -0
- package/dist/services/index.d.ts +1 -1
- package/dist/services/local-email-service.js +9 -0
- package/dist/wirings/cli/cli-runner.js +1 -1
- package/package.json +1 -1
- package/src/app-leaf-surface.test.ts +5 -0
- package/src/ecosystem-tier-removed.test.ts +3 -0
- package/src/no-root-barrel.test.ts +9 -1
- package/src/services/email-service.test.ts +100 -0
- package/src/services/email-service.ts +14 -0
- package/src/services/index.ts +1 -0
- package/src/services/local-email-service.test.ts +74 -0
- package/src/services/local-email-service.ts +9 -0
- package/src/wirings/cli/cli-runner.test.ts +137 -0
- package/src/wirings/cli/cli-runner.ts +3 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/tsconfig.type-tests.json +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
## 0.12.103
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 206dc04: `--json` now serialises the result of any command, not only the eleven that ship a renderer of their own.
|
|
6
|
+
|
|
7
|
+
The flag swapped in the JSON renderer only when a command-specific renderer existed, so on the other 94 commands it fell through to the program's default renderer — which prints forwarded log lines and drops anything else. `pikku info functions --json` therefore exited 0 and printed no JSON at all, which reads as "this command has no output" rather than as an unsupported flag.
|
|
8
|
+
|
|
9
|
+
## 0.12.102
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- f4e2e89: Add file attachments to `EmailService`.
|
|
14
|
+
|
|
15
|
+
`BaseSendEmailInput` gains an optional `attachments: EmailAttachment[]`, so it is
|
|
16
|
+
available on all three input variants — text, HTML and template. The new
|
|
17
|
+
`EmailAttachment` type is exported from `@pikku/core/services` alongside the
|
|
18
|
+
input types, and is shaped so that mapping it onto Resend, SendGrid, Nodemailer
|
|
19
|
+
or SES v2 is a straight field rename rather than a translation.
|
|
20
|
+
|
|
21
|
+
`content` is `Uint8Array | string`, where a string is always read as base64.
|
|
22
|
+
Both forms are accepted because both are what callers already hold: bytes come
|
|
23
|
+
out of a fetch or a file read, and base64 comes out of a database column or a
|
|
24
|
+
provider API. `Buffer` is deliberately absent from the type — it is a subclass
|
|
25
|
+
of `Uint8Array`, so Node callers can still pass one, while the type stays usable
|
|
26
|
+
in Cloudflare Workers, where `Buffer` does not exist.
|
|
27
|
+
|
|
28
|
+
`LocalEmailService` now logs attachment metadata — filename, content type,
|
|
29
|
+
content id, disposition and content length — instead of dropping the field
|
|
30
|
+
silently. The content itself is deliberately not logged.
|
|
31
|
+
|
|
32
|
+
The template-rendering wrapper documented in the emails skill rebuilt its
|
|
33
|
+
delegate payload field by field and therefore dropped `attachments`; it now
|
|
34
|
+
forwards them.
|
|
35
|
+
|
|
1
36
|
## 0.12.101
|
|
2
37
|
|
|
3
38
|
### Patch Changes
|
|
@@ -4,6 +4,18 @@ export interface EmailTemplateReference {
|
|
|
4
4
|
locale?: string;
|
|
5
5
|
data?: Record<string, unknown>;
|
|
6
6
|
}
|
|
7
|
+
export interface EmailAttachment {
|
|
8
|
+
filename: string;
|
|
9
|
+
/**
|
|
10
|
+
* Raw bytes, or the content already base64-encoded. A `string` is always
|
|
11
|
+
* read as base64 — never as a plain-text body — so text attachments must be
|
|
12
|
+
* encoded by the caller.
|
|
13
|
+
*/
|
|
14
|
+
content: Uint8Array | string;
|
|
15
|
+
contentType?: string;
|
|
16
|
+
contentId?: string;
|
|
17
|
+
disposition?: 'attachment' | 'inline';
|
|
18
|
+
}
|
|
7
19
|
export interface BaseSendEmailInput {
|
|
8
20
|
to: string | string[];
|
|
9
21
|
from?: string;
|
|
@@ -12,6 +24,7 @@ export interface BaseSendEmailInput {
|
|
|
12
24
|
replyTo?: string | string[];
|
|
13
25
|
headers?: Record<string, string>;
|
|
14
26
|
subject?: string;
|
|
27
|
+
attachments?: EmailAttachment[];
|
|
15
28
|
}
|
|
16
29
|
export interface SendTextEmailInput extends BaseSendEmailInput {
|
|
17
30
|
text: string;
|
package/dist/services/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export { FileScenarioRunStore, scenarioArtifactContentType, scenarioRunSummary,
|
|
|
20
20
|
export type { ContentService, SignContentKeyArgs, SignURLArgs, GetUploadURLArgs, UploadURLResult, BucketKeyArgs, WriteFileArgs, CopyFileArgs, } from './content-service.js';
|
|
21
21
|
export type { ScenarioPersona, ResolvedPersona, ScenarioPersonas, } from './personas-service.js';
|
|
22
22
|
export type { JWTService } from './jwt-service.js';
|
|
23
|
-
export type { EmailService, SendEmailInput, SendEmailResult, SendHTMLEmailInput, SendTemplateEmailInput, SendTextEmailInput, } from './email-service.js';
|
|
23
|
+
export type { EmailAttachment, EmailService, SendEmailInput, SendEmailResult, SendHTMLEmailInput, SendTemplateEmailInput, SendTextEmailInput, } from './email-service.js';
|
|
24
24
|
export { renderEmail, type EmailAssets, type EmailTemplateAssets, type EmailTemplateHashes, type RenderEmailRequest, type RenderedEmailResult, } from './email-template.js';
|
|
25
25
|
export { DEFAULT_WEBHOOK_RETRIES, PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME, WebhookService, type SendWebhookInput, type SendWebhookResult, type WebhookAttemptResult, type WebhookDeliveryRecord, type WebhookDeliveryWithAttempts, type WebhookJobData, type WebhookServiceConfig, } from './webhook-service.js';
|
|
26
26
|
export { ACTOR_ROOT_SECRET_MIN_LENGTH, ACTOR_SECRET_INFO, ACTOR_SECRET_NAME, actorSecretSubject, deriveActorSecret, verifyActorSecret, } from './persona-actor-secret.js';
|
|
@@ -20,6 +20,15 @@ export class LocalEmailService {
|
|
|
20
20
|
if ('text' in input && typeof input.text === 'string') {
|
|
21
21
|
payload.textLength = input.text.length;
|
|
22
22
|
}
|
|
23
|
+
if (input.attachments?.length) {
|
|
24
|
+
payload.attachments = input.attachments.map((attachment) => ({
|
|
25
|
+
filename: attachment.filename,
|
|
26
|
+
contentType: attachment.contentType ?? null,
|
|
27
|
+
contentId: attachment.contentId ?? null,
|
|
28
|
+
disposition: attachment.disposition ?? 'attachment',
|
|
29
|
+
contentLength: attachment.content.length,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
23
32
|
console.info(JSON.stringify(payload));
|
|
24
33
|
return {};
|
|
25
34
|
}
|
|
@@ -245,7 +245,7 @@ export async function runCLICommand({ program, commandPath, data, singletonServi
|
|
|
245
245
|
const commandRenderer = programData?.renderers[commandId];
|
|
246
246
|
const jsonMode = data.json === true ||
|
|
247
247
|
data.output === 'json';
|
|
248
|
-
const finalRenderer = jsonMode
|
|
248
|
+
const finalRenderer = jsonMode
|
|
249
249
|
? defaultJSONRenderer
|
|
250
250
|
: (commandRenderer ?? programData?.defaultRenderer);
|
|
251
251
|
if (finalRenderer !== undefined) {
|
package/package.json
CHANGED
|
@@ -39,6 +39,11 @@ const skipped = new Set([
|
|
|
39
39
|
'build',
|
|
40
40
|
'.git',
|
|
41
41
|
'coverage',
|
|
42
|
+
// Agent scratch worktrees are whole Pikku projects generated by whatever CLI
|
|
43
|
+
// version happened to scaffold them, so discovery finds them and reports a
|
|
44
|
+
// barrel this repo does not own. They are gitignored and belong to no
|
|
45
|
+
// workspace, which is what makes them out of scope rather than merely noisy.
|
|
46
|
+
'.claude',
|
|
42
47
|
])
|
|
43
48
|
|
|
44
49
|
const findProjects = (dir: string, out: string[] = []): string[] => {
|
|
@@ -15,6 +15,9 @@ const skipped = new Set([
|
|
|
15
15
|
'.git',
|
|
16
16
|
'.deploy',
|
|
17
17
|
'coverage',
|
|
18
|
+
// A worktree here carries its own copy of this file, which necessarily
|
|
19
|
+
// contains the specifier being scanned for — the guard would report itself.
|
|
20
|
+
'.claude',
|
|
18
21
|
])
|
|
19
22
|
|
|
20
23
|
const collectSourceFiles = (
|
|
@@ -22,11 +22,19 @@ const skipDirectory = new Set([
|
|
|
22
22
|
'.git',
|
|
23
23
|
'build',
|
|
24
24
|
'coverage',
|
|
25
|
+
// Agent scratch worktrees are whole Pikku projects scaffolded by whatever CLI
|
|
26
|
+
// version happened to write them, so they carry specifiers this repo does not
|
|
27
|
+
// own. They are gitignored and belong to no workspace, which is what puts them
|
|
28
|
+
// out of scope rather than merely making them noisy.
|
|
29
|
+
'.claude',
|
|
25
30
|
])
|
|
26
31
|
|
|
32
|
+
/** A verifier's scratch project, left behind by a run that did not clean up. */
|
|
33
|
+
const isScratchProject = (entry: string) => entry.startsWith('.tmp-')
|
|
34
|
+
|
|
27
35
|
const walk = (dir: string, out: string[] = []): string[] => {
|
|
28
36
|
for (const entry of readdirSync(dir)) {
|
|
29
|
-
if (skipDirectory.has(entry)) continue
|
|
37
|
+
if (skipDirectory.has(entry) || isScratchProject(entry)) continue
|
|
30
38
|
const path = join(dir, entry)
|
|
31
39
|
if (statSync(path).isDirectory()) walk(path, out)
|
|
32
40
|
else if (path.endsWith('.ts') || path.endsWith('.tsx')) out.push(path)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
EmailAttachment,
|
|
6
|
+
EmailService,
|
|
7
|
+
SendEmailInput,
|
|
8
|
+
SendEmailResult,
|
|
9
|
+
SendHTMLEmailInput,
|
|
10
|
+
SendTemplateEmailInput,
|
|
11
|
+
SendTextEmailInput,
|
|
12
|
+
} from './email-service.js'
|
|
13
|
+
|
|
14
|
+
const receipt: EmailAttachment = {
|
|
15
|
+
filename: 'receipt.pdf',
|
|
16
|
+
content: new Uint8Array([1, 2, 3]),
|
|
17
|
+
contentType: 'application/pdf',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const logo: EmailAttachment = {
|
|
21
|
+
filename: 'logo.png',
|
|
22
|
+
content: 'aGVsbG8=',
|
|
23
|
+
contentType: 'image/png',
|
|
24
|
+
contentId: 'logo',
|
|
25
|
+
disposition: 'inline',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('a text email carries attachments', () => {
|
|
29
|
+
const input: SendTextEmailInput = {
|
|
30
|
+
to: 'user@example.com',
|
|
31
|
+
text: 'Your receipt is attached',
|
|
32
|
+
attachments: [receipt],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
assert.deepEqual(input.attachments, [receipt])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('an html email carries attachments', () => {
|
|
39
|
+
const input: SendHTMLEmailInput = {
|
|
40
|
+
to: 'user@example.com',
|
|
41
|
+
html: '<img src="cid:logo" />',
|
|
42
|
+
attachments: [logo],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
assert.equal(input.attachments?.[0]?.disposition, 'inline')
|
|
46
|
+
assert.equal(input.attachments?.[0]?.contentId, 'logo')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('a template email carries attachments', () => {
|
|
50
|
+
const input: SendTemplateEmailInput = {
|
|
51
|
+
to: 'user@example.com',
|
|
52
|
+
template: { name: 'receipt', locale: 'en', data: { total: 10 } },
|
|
53
|
+
attachments: [receipt, logo],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
assert.equal(input.attachments?.length, 2)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('attachments are optional on every variant', () => {
|
|
60
|
+
const inputs: SendEmailInput[] = [
|
|
61
|
+
{ to: 'user@example.com', text: 'hello' },
|
|
62
|
+
{ to: 'user@example.com', html: '<p>hello</p>' },
|
|
63
|
+
{ to: 'user@example.com', template: { name: 'welcome' } },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
for (const input of inputs) {
|
|
67
|
+
assert.equal(input.attachments, undefined)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('a wrapping service forwards attachments to its delegate', async () => {
|
|
72
|
+
const sent: SendEmailInput[] = []
|
|
73
|
+
const delegate: EmailService = {
|
|
74
|
+
async send(input): Promise<SendEmailResult> {
|
|
75
|
+
sent.push(input as SendEmailInput)
|
|
76
|
+
return { messageId: 'delegated' }
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const wrapper: EmailService = {
|
|
81
|
+
async send(input): Promise<SendEmailResult> {
|
|
82
|
+
const { template, ...rest } = input as SendTemplateEmailInput
|
|
83
|
+
return delegate.send({
|
|
84
|
+
...rest,
|
|
85
|
+
subject: `rendered:${template.name}`,
|
|
86
|
+
html: '<p>rendered</p>',
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const result = await wrapper.send({
|
|
92
|
+
to: 'user@example.com',
|
|
93
|
+
template: { name: 'receipt' },
|
|
94
|
+
attachments: [receipt],
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
assert.equal(result.messageId, 'delegated')
|
|
98
|
+
assert.equal(sent.length, 1)
|
|
99
|
+
assert.deepEqual(sent[0]!.attachments, [receipt])
|
|
100
|
+
})
|
|
@@ -6,6 +6,19 @@ export interface EmailTemplateReference {
|
|
|
6
6
|
data?: Record<string, unknown>
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface EmailAttachment {
|
|
10
|
+
filename: string
|
|
11
|
+
/**
|
|
12
|
+
* Raw bytes, or the content already base64-encoded. A `string` is always
|
|
13
|
+
* read as base64 — never as a plain-text body — so text attachments must be
|
|
14
|
+
* encoded by the caller.
|
|
15
|
+
*/
|
|
16
|
+
content: Uint8Array | string
|
|
17
|
+
contentType?: string
|
|
18
|
+
contentId?: string
|
|
19
|
+
disposition?: 'attachment' | 'inline'
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
export interface BaseSendEmailInput {
|
|
10
23
|
to: string | string[]
|
|
11
24
|
from?: string
|
|
@@ -14,6 +27,7 @@ export interface BaseSendEmailInput {
|
|
|
14
27
|
replyTo?: string | string[]
|
|
15
28
|
headers?: Record<string, string>
|
|
16
29
|
subject?: string
|
|
30
|
+
attachments?: EmailAttachment[]
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
export interface SendTextEmailInput extends BaseSendEmailInput {
|
package/src/services/index.ts
CHANGED
|
@@ -106,3 +106,77 @@ test('LocalEmailService logs null textLength for HTML email without plain text',
|
|
|
106
106
|
assert.equal(payload.htmlLength, '<p>Hello</p>'.length)
|
|
107
107
|
assert.equal(payload.textLength, null)
|
|
108
108
|
})
|
|
109
|
+
|
|
110
|
+
test('LocalEmailService logs attachment metadata without the content', async () => {
|
|
111
|
+
const service = new LocalEmailService()
|
|
112
|
+
const writes: string[] = []
|
|
113
|
+
const originalInfo = console.info
|
|
114
|
+
console.info = (value?: unknown) => {
|
|
115
|
+
writes.push(String(value))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await service.send({
|
|
120
|
+
to: 'user@example.com',
|
|
121
|
+
html: '<img src="cid:logo" />',
|
|
122
|
+
attachments: [
|
|
123
|
+
{
|
|
124
|
+
filename: 'receipt.pdf',
|
|
125
|
+
content: new Uint8Array([1, 2, 3, 4]),
|
|
126
|
+
contentType: 'application/pdf',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
filename: 'logo.png',
|
|
130
|
+
content: 'aGVsbG8=',
|
|
131
|
+
contentType: 'image/png',
|
|
132
|
+
contentId: 'logo',
|
|
133
|
+
disposition: 'inline',
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
})
|
|
137
|
+
} finally {
|
|
138
|
+
console.info = originalInfo
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const payload = JSON.parse(writes[0])
|
|
142
|
+
assert.deepEqual(payload.attachments, [
|
|
143
|
+
{
|
|
144
|
+
filename: 'receipt.pdf',
|
|
145
|
+
contentType: 'application/pdf',
|
|
146
|
+
contentId: null,
|
|
147
|
+
disposition: 'attachment',
|
|
148
|
+
contentLength: 4,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
filename: 'logo.png',
|
|
152
|
+
contentType: 'image/png',
|
|
153
|
+
contentId: 'logo',
|
|
154
|
+
disposition: 'inline',
|
|
155
|
+
contentLength: 8,
|
|
156
|
+
},
|
|
157
|
+
])
|
|
158
|
+
assert.equal(writes[0].includes('aGVsbG8='), false)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('LocalEmailService omits attachments when there are none', async () => {
|
|
162
|
+
const service = new LocalEmailService()
|
|
163
|
+
const writes: string[] = []
|
|
164
|
+
const originalInfo = console.info
|
|
165
|
+
console.info = (value?: unknown) => {
|
|
166
|
+
writes.push(String(value))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await service.send({ to: 'user@example.com', text: 'no files' })
|
|
171
|
+
await service.send({
|
|
172
|
+
to: 'user@example.com',
|
|
173
|
+
text: 'no files',
|
|
174
|
+
attachments: [],
|
|
175
|
+
})
|
|
176
|
+
} finally {
|
|
177
|
+
console.info = originalInfo
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
assert.equal(JSON.parse(writes[0]).attachments, undefined)
|
|
181
|
+
assert.equal(JSON.parse(writes[1]).attachments, undefined)
|
|
182
|
+
})
|
|
@@ -27,6 +27,15 @@ export class LocalEmailService implements EmailService {
|
|
|
27
27
|
if ('text' in input && typeof input.text === 'string') {
|
|
28
28
|
payload.textLength = input.text.length
|
|
29
29
|
}
|
|
30
|
+
if (input.attachments?.length) {
|
|
31
|
+
payload.attachments = input.attachments.map((attachment) => ({
|
|
32
|
+
filename: attachment.filename,
|
|
33
|
+
contentType: attachment.contentType ?? null,
|
|
34
|
+
contentId: attachment.contentId ?? null,
|
|
35
|
+
disposition: attachment.disposition ?? 'attachment',
|
|
36
|
+
contentLength: attachment.content.length,
|
|
37
|
+
}))
|
|
38
|
+
}
|
|
30
39
|
|
|
31
40
|
console.info(JSON.stringify(payload))
|
|
32
41
|
return {}
|
|
@@ -829,4 +829,141 @@ describe('CLI Runner', () => {
|
|
|
829
829
|
assert.ok(printed.includes('at '))
|
|
830
830
|
})
|
|
831
831
|
})
|
|
832
|
+
describe('--json output', () => {
|
|
833
|
+
const wireGreet = (renderers: Record<string, any>, defaultRenderer: any) => {
|
|
834
|
+
pikkuState(null, 'cli', 'meta', {
|
|
835
|
+
programs: {
|
|
836
|
+
'test-cli': {
|
|
837
|
+
program: 'test-cli',
|
|
838
|
+
commands: {
|
|
839
|
+
greet: {
|
|
840
|
+
command: 'greet',
|
|
841
|
+
pikkuFuncId: 'greetFunc',
|
|
842
|
+
positionals: [],
|
|
843
|
+
options: {},
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
options: {},
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
renderers: {},
|
|
850
|
+
})
|
|
851
|
+
|
|
852
|
+
pikkuState(null, 'cli', 'programs', {
|
|
853
|
+
'test-cli': { defaultRenderer, middleware: [], renderers },
|
|
854
|
+
})
|
|
855
|
+
|
|
856
|
+
pikkuState(null, 'function', 'meta', {
|
|
857
|
+
greetFunc: {
|
|
858
|
+
pikkuFuncId: 'greetFunc',
|
|
859
|
+
inputSchemaName: null,
|
|
860
|
+
outputSchemaName: null,
|
|
861
|
+
sessionless: true,
|
|
862
|
+
},
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
addFunction('greetFunc', {
|
|
866
|
+
func: async () => ({ name: 'Alice' }),
|
|
867
|
+
auth: false,
|
|
868
|
+
})
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const captureStdout = async (fn: () => Promise<unknown>) => {
|
|
872
|
+
const lines: string[] = []
|
|
873
|
+
const original = console.log
|
|
874
|
+
console.log = (...args: unknown[]) => {
|
|
875
|
+
lines.push(args.map(String).join(' '))
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
await fn()
|
|
879
|
+
} finally {
|
|
880
|
+
console.log = original
|
|
881
|
+
}
|
|
882
|
+
return lines
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
test('serialises the result for a command with no renderer of its own', async () => {
|
|
886
|
+
const textOnly = () => {}
|
|
887
|
+
wireGreet({}, textOnly)
|
|
888
|
+
|
|
889
|
+
const lines = await captureStdout(() =>
|
|
890
|
+
runCLICommand({
|
|
891
|
+
program: 'test-cli',
|
|
892
|
+
commandPath: ['greet'],
|
|
893
|
+
data: { json: true },
|
|
894
|
+
singletonServices,
|
|
895
|
+
createWireServices,
|
|
896
|
+
})
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
assert.deepEqual(lines, ['{"name":"Alice"}'])
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
test('--output json is equivalent to --json', async () => {
|
|
903
|
+
const textOnly = () => {}
|
|
904
|
+
wireGreet({}, textOnly)
|
|
905
|
+
|
|
906
|
+
const lines = await captureStdout(() =>
|
|
907
|
+
runCLICommand({
|
|
908
|
+
program: 'test-cli',
|
|
909
|
+
commandPath: ['greet'],
|
|
910
|
+
data: { output: 'json' },
|
|
911
|
+
singletonServices,
|
|
912
|
+
createWireServices,
|
|
913
|
+
})
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
assert.deepEqual(lines, ['{"name":"Alice"}'])
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
test('json overrides a command renderer', async () => {
|
|
920
|
+
let renderedWith: unknown
|
|
921
|
+
wireGreet(
|
|
922
|
+
{
|
|
923
|
+
greet: (_s: any, data: any) => {
|
|
924
|
+
renderedWith = data
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
() => {}
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
const lines = await captureStdout(() =>
|
|
931
|
+
runCLICommand({
|
|
932
|
+
program: 'test-cli',
|
|
933
|
+
commandPath: ['greet'],
|
|
934
|
+
data: { json: true },
|
|
935
|
+
singletonServices,
|
|
936
|
+
createWireServices,
|
|
937
|
+
})
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
assert.equal(renderedWith, undefined)
|
|
941
|
+
assert.deepEqual(lines, ['{"name":"Alice"}'])
|
|
942
|
+
})
|
|
943
|
+
|
|
944
|
+
test('without json the command renderer still wins', async () => {
|
|
945
|
+
let renderedWith: unknown
|
|
946
|
+
wireGreet(
|
|
947
|
+
{
|
|
948
|
+
greet: (_s: any, data: any) => {
|
|
949
|
+
renderedWith = data
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
() => {}
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
const lines = await captureStdout(() =>
|
|
956
|
+
runCLICommand({
|
|
957
|
+
program: 'test-cli',
|
|
958
|
+
commandPath: ['greet'],
|
|
959
|
+
data: {},
|
|
960
|
+
singletonServices,
|
|
961
|
+
createWireServices,
|
|
962
|
+
})
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
assert.deepEqual(renderedWith, { name: 'Alice' })
|
|
966
|
+
assert.deepEqual(lines, [])
|
|
967
|
+
})
|
|
968
|
+
})
|
|
832
969
|
})
|
|
@@ -391,10 +391,9 @@ export async function runCLICommand({
|
|
|
391
391
|
const jsonMode =
|
|
392
392
|
(data as { json?: unknown; output?: unknown }).json === true ||
|
|
393
393
|
(data as { json?: unknown; output?: unknown }).output === 'json'
|
|
394
|
-
const finalRenderer: CorePikkuCLIRender<any> | undefined =
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
: (commandRenderer ?? programData?.defaultRenderer)
|
|
394
|
+
const finalRenderer: CorePikkuCLIRender<any> | undefined = jsonMode
|
|
395
|
+
? defaultJSONRenderer
|
|
396
|
+
: (commandRenderer ?? programData?.defaultRenderer)
|
|
398
397
|
if (finalRenderer !== undefined) {
|
|
399
398
|
await Promise.resolve(
|
|
400
399
|
finalRenderer(
|