@ishlabs/cli 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -5
- package/dist/commands/ask.d.ts +12 -0
- package/dist/commands/ask.js +127 -2
- package/dist/commands/chat.d.ts +17 -0
- package/dist/commands/chat.js +655 -0
- package/dist/commands/iteration.js +134 -14
- package/dist/commands/secret.d.ts +20 -0
- package/dist/commands/secret.js +246 -0
- package/dist/commands/study-run.d.ts +38 -0
- package/dist/commands/study-run.js +199 -80
- package/dist/commands/study-tester.js +17 -2
- package/dist/commands/study.js +309 -37
- package/dist/commands/workspace.js +81 -0
- package/dist/config.d.ts +3 -0
- package/dist/connect.d.ts +3 -0
- package/dist/connect.js +346 -22
- package/dist/index.js +64 -6
- package/dist/lib/alias-hydrate.d.ts +42 -0
- package/dist/lib/alias-hydrate.js +175 -0
- package/dist/lib/alias-store.d.ts +1 -0
- package/dist/lib/alias-store.js +28 -1
- package/dist/lib/auth.js +4 -2
- package/dist/lib/chat-endpoint-formatters.d.ts +74 -0
- package/dist/lib/chat-endpoint-formatters.js +154 -0
- package/dist/lib/chat-endpoint-templates.d.ts +35 -0
- package/dist/lib/chat-endpoint-templates.js +210 -0
- package/dist/lib/command-helpers.d.ts +18 -0
- package/dist/lib/command-helpers.js +105 -3
- package/dist/lib/docs.js +641 -17
- package/dist/lib/modality.d.ts +42 -0
- package/dist/lib/modality.js +192 -0
- package/dist/lib/output.d.ts +41 -0
- package/dist/lib/output.js +453 -19
- package/dist/lib/paths.d.ts +1 -0
- package/dist/lib/paths.js +3 -0
- package/dist/lib/skill-content.d.ts +18 -0
- package/dist/lib/skill-content.js +223 -12
- package/dist/lib/types.d.ts +15 -0
- package/package.json +2 -2
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-curated `ChatbotEndpointConfig` templates for `ish chat endpoint init
|
|
3
|
+
* --template <name>`.
|
|
4
|
+
*
|
|
5
|
+
* Each template is a known-good wire shape an agent can drop straight into
|
|
6
|
+
* `create_chatbot_endpoint` — derived from public docs for the named
|
|
7
|
+
* provider. Auth / API key values are placeholder secret refs
|
|
8
|
+
* (`{{secret:NAME}}`); the agent stores the real value via
|
|
9
|
+
* `ish secret set` before testing.
|
|
10
|
+
*
|
|
11
|
+
* Templates are intentionally minimal:
|
|
12
|
+
* - `transport`, `outgoing`, `incoming` (slots-only).
|
|
13
|
+
* - No `retry` block — defaults are fine.
|
|
14
|
+
* - No `asyncPoll` — none of the listed providers use it.
|
|
15
|
+
* - `streaming` only when the provider's default flow is SSE-shaped
|
|
16
|
+
* (we currently keep all templates in `sync` mode; agents who want
|
|
17
|
+
* streaming flip the transport themselves after init).
|
|
18
|
+
*
|
|
19
|
+
* Slots / references are populated when the provider documents a stable
|
|
20
|
+
* structured-output container (e.g. Bot Framework's `suggestedActions`,
|
|
21
|
+
* Watson Assistant's `output.generic[]`). Where the provider is pure text
|
|
22
|
+
* (vanilla OpenAI / Anthropic chat-completions), the lists stay empty —
|
|
23
|
+
* the runtime auto-classifier handles any structured content the bot
|
|
24
|
+
* decides to emit per turn.
|
|
25
|
+
*/
|
|
26
|
+
const OPENAI = {
|
|
27
|
+
name: "openai",
|
|
28
|
+
description: "OpenAI chat-completions wire shape. Stateless by default; ish ships the rolled-up history per turn. Drop in any OpenAI-compatible bot (Groq / Together / vLLM / OpenRouter / LiteLLM).",
|
|
29
|
+
config: {
|
|
30
|
+
transport: "sync",
|
|
31
|
+
outgoing: {
|
|
32
|
+
url: "https://api.openai.com/v1/chat/completions",
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
"content-type": "application/json",
|
|
36
|
+
Authorization: "Bearer {{secret:OPENAI_API_KEY}}",
|
|
37
|
+
},
|
|
38
|
+
bodyTemplate: {
|
|
39
|
+
model: "gpt-4o-mini",
|
|
40
|
+
messages: "{{history_with_current}}",
|
|
41
|
+
},
|
|
42
|
+
mode: "stateless",
|
|
43
|
+
roleAliases: {},
|
|
44
|
+
},
|
|
45
|
+
incoming: {
|
|
46
|
+
messagePath: "choices[0].message.content",
|
|
47
|
+
toolCallsPath: "choices[0].message.tool_calls",
|
|
48
|
+
tokenUsagePath: "usage",
|
|
49
|
+
slots: [],
|
|
50
|
+
references: [],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const ANTHROPIC = {
|
|
55
|
+
name: "anthropic",
|
|
56
|
+
description: "Anthropic Messages API. Stateless: each request carries the full history and a fresh system prompt. Default model is the latest Sonnet.",
|
|
57
|
+
config: {
|
|
58
|
+
transport: "sync",
|
|
59
|
+
outgoing: {
|
|
60
|
+
url: "https://api.anthropic.com/v1/messages",
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
"content-type": "application/json",
|
|
64
|
+
"anthropic-version": "2023-06-01",
|
|
65
|
+
"x-api-key": "{{secret:ANTHROPIC_API_KEY}}",
|
|
66
|
+
},
|
|
67
|
+
bodyTemplate: {
|
|
68
|
+
model: "claude-sonnet-4-20250514",
|
|
69
|
+
max_tokens: 1024,
|
|
70
|
+
messages: "{{history_with_current}}",
|
|
71
|
+
},
|
|
72
|
+
mode: "stateless",
|
|
73
|
+
roleAliases: {},
|
|
74
|
+
},
|
|
75
|
+
incoming: {
|
|
76
|
+
messagePath: "content[0].text",
|
|
77
|
+
tokenUsagePath: "usage",
|
|
78
|
+
slots: [],
|
|
79
|
+
references: [],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const VOICEFLOW = {
|
|
84
|
+
name: "voiceflow",
|
|
85
|
+
description: "Voiceflow Dialog Manager API. Stateful — the URL embeds the user/session id, and Voiceflow returns a list of trace objects. Body is `{action: {type: 'text', payload: <text>}}` per turn.",
|
|
86
|
+
config: {
|
|
87
|
+
transport: "sync",
|
|
88
|
+
outgoing: {
|
|
89
|
+
url: "https://general-runtime.voiceflow.com/state/user/{{conversation_id}}/interact",
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
"content-type": "application/json",
|
|
93
|
+
Authorization: "{{secret:VOICEFLOW_API_KEY}}",
|
|
94
|
+
versionID: "production",
|
|
95
|
+
},
|
|
96
|
+
bodyTemplate: {
|
|
97
|
+
action: {
|
|
98
|
+
type: "text",
|
|
99
|
+
payload: "{{action.text}}",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
mode: "stateful",
|
|
103
|
+
roleAliases: {},
|
|
104
|
+
},
|
|
105
|
+
incoming: {
|
|
106
|
+
// Voiceflow's runtime returns an array of trace objects; the bot's
|
|
107
|
+
// textual reply lives in the first `speak` / `text` trace's payload.
|
|
108
|
+
// Agents typically tighten this path after a probe turn.
|
|
109
|
+
messagePath: "[0].payload.message",
|
|
110
|
+
slots: [
|
|
111
|
+
{
|
|
112
|
+
containerPath: "[0].payload.choices",
|
|
113
|
+
kind: "alternatives",
|
|
114
|
+
labelPath: "name",
|
|
115
|
+
idPath: "intent",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
references: [],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const DIALOGFLOW_CX = {
|
|
123
|
+
name: "dialogflow-cx",
|
|
124
|
+
description: "Google Dialogflow CX `detectIntent`. Stateful: the URL embeds the agent + session id and the response carries the next `currentPage` plus a list of fulfillment messages.",
|
|
125
|
+
config: {
|
|
126
|
+
transport: "sync",
|
|
127
|
+
outgoing: {
|
|
128
|
+
url: "https://dialogflow.googleapis.com/v3/projects/{{secret:GCP_PROJECT}}/locations/global/agents/{{secret:DIALOGFLOW_AGENT_ID}}/sessions/{{conversation_id}}:detectIntent",
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
"content-type": "application/json",
|
|
132
|
+
Authorization: "Bearer {{secret:GOOGLE_ACCESS_TOKEN}}",
|
|
133
|
+
},
|
|
134
|
+
bodyTemplate: {
|
|
135
|
+
queryInput: {
|
|
136
|
+
text: { text: "{{action.text}}" },
|
|
137
|
+
languageCode: "en",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
mode: "stateful",
|
|
141
|
+
roleAliases: {},
|
|
142
|
+
},
|
|
143
|
+
incoming: {
|
|
144
|
+
messagePath: "queryResult.responseMessages[0].text.text[0]",
|
|
145
|
+
conversationIdPath: "responseId",
|
|
146
|
+
slots: [
|
|
147
|
+
{
|
|
148
|
+
containerPath: "queryResult.responseMessages",
|
|
149
|
+
kind: null,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
references: [],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const BOTFRAMEWORK = {
|
|
157
|
+
name: "botframework",
|
|
158
|
+
description: "Microsoft Bot Framework Direct Line `conversations/{id}/activities`. Stateful via the conversation id; activities[] carry the bot's reply plus suggestedActions for choice slots.",
|
|
159
|
+
config: {
|
|
160
|
+
transport: "sync",
|
|
161
|
+
outgoing: {
|
|
162
|
+
url: "https://directline.botframework.com/v3/directline/conversations/{{conversation_id}}/activities",
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: {
|
|
165
|
+
"content-type": "application/json",
|
|
166
|
+
Authorization: "Bearer {{secret:DIRECTLINE_SECRET}}",
|
|
167
|
+
},
|
|
168
|
+
bodyTemplate: {
|
|
169
|
+
type: "message",
|
|
170
|
+
from: { id: "{{tester.name}}" },
|
|
171
|
+
text: "{{action.text}}",
|
|
172
|
+
},
|
|
173
|
+
mode: "stateful",
|
|
174
|
+
roleAliases: {},
|
|
175
|
+
},
|
|
176
|
+
incoming: {
|
|
177
|
+
messagePath: "activities[0].text",
|
|
178
|
+
conversationIdPath: "activities[0].conversation.id",
|
|
179
|
+
slots: [
|
|
180
|
+
{
|
|
181
|
+
containerPath: "activities[0].suggestedActions.actions",
|
|
182
|
+
kind: "alternatives",
|
|
183
|
+
labelPath: "title",
|
|
184
|
+
idPath: "value",
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
references: [
|
|
188
|
+
{
|
|
189
|
+
containerPath: "activities[0].attachments",
|
|
190
|
+
labelPath: "name",
|
|
191
|
+
urlPath: "contentUrl",
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
const TEMPLATES = {
|
|
198
|
+
openai: OPENAI,
|
|
199
|
+
anthropic: ANTHROPIC,
|
|
200
|
+
voiceflow: VOICEFLOW,
|
|
201
|
+
"dialogflow-cx": DIALOGFLOW_CX,
|
|
202
|
+
botframework: BOTFRAMEWORK,
|
|
203
|
+
};
|
|
204
|
+
export const TEMPLATE_NAMES = Object.keys(TEMPLATES);
|
|
205
|
+
export function getChatEndpointTemplate(name) {
|
|
206
|
+
return TEMPLATES[name];
|
|
207
|
+
}
|
|
208
|
+
export function listChatEndpointTemplates() {
|
|
209
|
+
return TEMPLATE_NAMES.map((n) => TEMPLATES[n]);
|
|
210
|
+
}
|
|
@@ -123,6 +123,12 @@ export declare function confirmDestructive(prompt: string, opts: {
|
|
|
123
123
|
export declare function resolveWorkspace(explicit?: string): string;
|
|
124
124
|
export declare function resolveStudy(explicit?: string): string;
|
|
125
125
|
export declare function resolveAsk(explicit?: string): string;
|
|
126
|
+
/**
|
|
127
|
+
* Resolve a chat endpoint id from (in order): the positional argument, the
|
|
128
|
+
* `--endpoint <id>` flag, the `ISH_CHAT_ENDPOINT` env var, or the active
|
|
129
|
+
* endpoint persisted by `ish chat endpoint use`. Throws when none are set.
|
|
130
|
+
*/
|
|
131
|
+
export declare function resolveChatEndpoint(positional?: string, flag?: string): string;
|
|
126
132
|
/** Commander option-collector for repeatable flags (e.g. `--variant text:"..."` repeated). */
|
|
127
133
|
export declare function collectRepeatable(value: string, prev?: string[]): string[];
|
|
128
134
|
/**
|
|
@@ -150,4 +156,16 @@ export declare function parseWaitTimeout(raw: string | undefined, defaultMs?: nu
|
|
|
150
156
|
* body. Resolvers (`resolveWorkspace`, `resolveAudienceProfileIds`) ignore
|
|
151
157
|
* unused values.
|
|
152
158
|
*/
|
|
159
|
+
/**
|
|
160
|
+
* Read a `--*-config <file>` style flag value, treating "-" as "read from
|
|
161
|
+
* stdin" and any other value as a file path on disk. Trailing newlines on
|
|
162
|
+
* stdin input are stripped so the resulting string parses cleanly as JSON.
|
|
163
|
+
*
|
|
164
|
+
* Throws when "-" is passed but stdin is a TTY (no upstream pipe).
|
|
165
|
+
*
|
|
166
|
+
* Mirrors the readSecretFlag pattern in src/commands/workspace.ts; extracted
|
|
167
|
+
* so every `--<x>-config <file>` flag across commands shares one
|
|
168
|
+
* implementation.
|
|
169
|
+
*/
|
|
170
|
+
export declare function readFileOrStdin(path: string): Promise<string>;
|
|
153
171
|
export declare function injectGlobalWorkspaceOption(program: Command): void;
|
|
@@ -348,6 +348,20 @@ export function exitCodeFromError(err) {
|
|
|
348
348
|
// Client-side validation failures
|
|
349
349
|
if (err.name === "ValidationError" || /^invalid |^cannot read |is empty:|--\w[\w-]* must be|pick an audience|use either /i.test(err.message))
|
|
350
350
|
return 2;
|
|
351
|
+
// Errors that pre-declare their own retryability / error_code take
|
|
352
|
+
// precedence — WaitTimeoutError sets `error_code: "wait_timeout"`
|
|
353
|
+
// and `retryable: true`, so callers can branch on exit 5 (transient)
|
|
354
|
+
// distinct from the api-client's generic 408/timeout family.
|
|
355
|
+
const tagged = err;
|
|
356
|
+
if (tagged.error_code === "wait_timeout")
|
|
357
|
+
return 5;
|
|
358
|
+
if (typeof tagged.retryable === "boolean" && tagged.retryable)
|
|
359
|
+
return 5;
|
|
360
|
+
// Structured error_kind on the Error object (set by chat endpoint test/init,
|
|
361
|
+
// simulation routes, etc.). TunnelInactive is the canonical transient one.
|
|
362
|
+
const kind = err.error_kind;
|
|
363
|
+
if (typeof kind === "string" && kind === "TunnelInactive")
|
|
364
|
+
return 5;
|
|
351
365
|
}
|
|
352
366
|
return 1;
|
|
353
367
|
}
|
|
@@ -439,6 +453,18 @@ export function readJsonFileOrStdin(filePath) {
|
|
|
439
453
|
process.stdin.on("error", reject);
|
|
440
454
|
});
|
|
441
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Build the suggested re-invocation example by taking the live argv and
|
|
458
|
+
* appending `--yes` if it isn't already there. Strips the `node` /
|
|
459
|
+
* `dist/index.js` prefix so the example reads as a normal `ish` command.
|
|
460
|
+
*/
|
|
461
|
+
function buildConfirmationExample() {
|
|
462
|
+
const argv = process.argv.slice(2);
|
|
463
|
+
const args = argv.includes("--yes") || argv.includes("-y")
|
|
464
|
+
? argv
|
|
465
|
+
: [...argv, "--yes"];
|
|
466
|
+
return ["ish", ...args].join(" ");
|
|
467
|
+
}
|
|
442
468
|
/**
|
|
443
469
|
* Prompt for confirmation of a destructive action, or short-circuit when
|
|
444
470
|
* `--yes` is set. In `--json` mode without `--yes` we refuse with a usage
|
|
@@ -451,11 +477,15 @@ export async function confirmDestructive(prompt, opts) {
|
|
|
451
477
|
if (opts.json) {
|
|
452
478
|
const err = new Error(`--yes is required for destructive actions in --json mode. Refusing to proceed without explicit confirmation.`);
|
|
453
479
|
err.name = "ValidationError";
|
|
480
|
+
err.error_kind = "ConfirmationRequired";
|
|
481
|
+
err.example = buildConfirmationExample();
|
|
454
482
|
throw err;
|
|
455
483
|
}
|
|
456
484
|
if (!process.stdin.isTTY) {
|
|
457
485
|
const err = new Error(`--yes is required for destructive actions when stdin is not a TTY. Refusing to proceed without explicit confirmation.`);
|
|
458
486
|
err.name = "ValidationError";
|
|
487
|
+
err.error_kind = "ConfirmationRequired";
|
|
488
|
+
err.example = buildConfirmationExample();
|
|
459
489
|
throw err;
|
|
460
490
|
}
|
|
461
491
|
process.stderr.write(`${prompt} [y/N] `);
|
|
@@ -486,6 +516,23 @@ export async function confirmDestructive(prompt, opts) {
|
|
|
486
516
|
throw err;
|
|
487
517
|
}
|
|
488
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* Construct a structured "no active <thing>" Error so the JSON envelope from
|
|
521
|
+
* `outputError` carries `error_code` + `suggestions` rather than a generic
|
|
522
|
+
* `client_error`. Pattern A (Sprint 2): when the active study/workspace
|
|
523
|
+
* evaporates between commands, agents need to branch on the error code, not
|
|
524
|
+
* scrape prose. Without this, downstream modality validation in
|
|
525
|
+
* `iteration create` would surface a misleading "Image iterations require
|
|
526
|
+
* --image-urls" message instead of the real "no active study" cause.
|
|
527
|
+
*/
|
|
528
|
+
function noActiveContextError(message, errorCode, suggestions) {
|
|
529
|
+
const err = new Error(message);
|
|
530
|
+
err.name = "NoActiveContextError";
|
|
531
|
+
err.error_code = errorCode;
|
|
532
|
+
err.retryable = false;
|
|
533
|
+
err.suggestions = suggestions;
|
|
534
|
+
return err;
|
|
535
|
+
}
|
|
489
536
|
export function resolveWorkspace(explicit) {
|
|
490
537
|
if (explicit)
|
|
491
538
|
return resolveId(explicit);
|
|
@@ -500,7 +547,10 @@ export function resolveWorkspace(explicit) {
|
|
|
500
547
|
const config = loadConfig();
|
|
501
548
|
if (config.workspace)
|
|
502
549
|
return config.workspace;
|
|
503
|
-
throw
|
|
550
|
+
throw noActiveContextError('No active workspace. Run `ish workspace use <workspace-id>` or pass --workspace <id>.', "no_active_workspace", [
|
|
551
|
+
"ish workspace list",
|
|
552
|
+
"ish workspace use <workspace-id>",
|
|
553
|
+
]);
|
|
504
554
|
}
|
|
505
555
|
export function resolveStudy(explicit) {
|
|
506
556
|
if (explicit)
|
|
@@ -511,7 +561,10 @@ export function resolveStudy(explicit) {
|
|
|
511
561
|
const config = loadConfig();
|
|
512
562
|
if (config.study)
|
|
513
563
|
return config.study;
|
|
514
|
-
throw
|
|
564
|
+
throw noActiveContextError('No active study. Run `ish study use <study-id>` or pass --study <id>.', "no_active_study", [
|
|
565
|
+
"ish study use <study-id>",
|
|
566
|
+
"ish iteration create --study <study-id> ...",
|
|
567
|
+
]);
|
|
515
568
|
}
|
|
516
569
|
export function resolveAsk(explicit) {
|
|
517
570
|
if (explicit)
|
|
@@ -522,7 +575,28 @@ export function resolveAsk(explicit) {
|
|
|
522
575
|
const config = loadConfig();
|
|
523
576
|
if (config.ask)
|
|
524
577
|
return config.ask;
|
|
525
|
-
throw
|
|
578
|
+
throw noActiveContextError('No active ask. Run `ish ask use <ask-id>` or pass the ask ID as an argument.', "no_active_ask", [
|
|
579
|
+
"ish ask list",
|
|
580
|
+
"ish ask use <ask-id>",
|
|
581
|
+
]);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Resolve a chat endpoint id from (in order): the positional argument, the
|
|
585
|
+
* `--endpoint <id>` flag, the `ISH_CHAT_ENDPOINT` env var, or the active
|
|
586
|
+
* endpoint persisted by `ish chat endpoint use`. Throws when none are set.
|
|
587
|
+
*/
|
|
588
|
+
export function resolveChatEndpoint(positional, flag) {
|
|
589
|
+
if (positional)
|
|
590
|
+
return resolveId(positional);
|
|
591
|
+
if (flag)
|
|
592
|
+
return resolveId(flag);
|
|
593
|
+
const env = process.env.ISH_CHAT_ENDPOINT;
|
|
594
|
+
if (env)
|
|
595
|
+
return resolveId(env);
|
|
596
|
+
const config = loadConfig();
|
|
597
|
+
if (config.chat_endpoint)
|
|
598
|
+
return config.chat_endpoint;
|
|
599
|
+
throw new Error('No chat endpoint set. Use `ish chat endpoint use <id>`, pass the endpoint id, or set --endpoint.');
|
|
526
600
|
}
|
|
527
601
|
/** Commander option-collector for repeatable flags (e.g. `--variant text:"..."` repeated). */
|
|
528
602
|
export function collectRepeatable(value, prev = []) {
|
|
@@ -571,6 +645,34 @@ const WORKSPACE_SCOPED_GROUPS = new Set([
|
|
|
571
645
|
* body. Resolvers (`resolveWorkspace`, `resolveAudienceProfileIds`) ignore
|
|
572
646
|
* unused values.
|
|
573
647
|
*/
|
|
648
|
+
/**
|
|
649
|
+
* Read a `--*-config <file>` style flag value, treating "-" as "read from
|
|
650
|
+
* stdin" and any other value as a file path on disk. Trailing newlines on
|
|
651
|
+
* stdin input are stripped so the resulting string parses cleanly as JSON.
|
|
652
|
+
*
|
|
653
|
+
* Throws when "-" is passed but stdin is a TTY (no upstream pipe).
|
|
654
|
+
*
|
|
655
|
+
* Mirrors the readSecretFlag pattern in src/commands/workspace.ts; extracted
|
|
656
|
+
* so every `--<x>-config <file>` flag across commands shares one
|
|
657
|
+
* implementation.
|
|
658
|
+
*/
|
|
659
|
+
export async function readFileOrStdin(path) {
|
|
660
|
+
if (path === "-") {
|
|
661
|
+
if (process.stdin.isTTY) {
|
|
662
|
+
throw new Error('Use "-" only when piping the value on stdin.');
|
|
663
|
+
}
|
|
664
|
+
return await new Promise((resolve, reject) => {
|
|
665
|
+
let data = "";
|
|
666
|
+
process.stdin.setEncoding("utf-8");
|
|
667
|
+
process.stdin.on("data", (chunk) => {
|
|
668
|
+
data += chunk;
|
|
669
|
+
});
|
|
670
|
+
process.stdin.on("end", () => resolve(data.replace(/\r?\n$/, "")));
|
|
671
|
+
process.stdin.on("error", reject);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return fs.readFileSync(path, "utf-8");
|
|
675
|
+
}
|
|
574
676
|
export function injectGlobalWorkspaceOption(program) {
|
|
575
677
|
const walk = (cmd) => {
|
|
576
678
|
if (cmd.commands.length === 0) {
|