@outputai/cli 0.7.1-next.be9352c.0 → 0.7.1-next.db8ddd7.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/bin/run.js +1 -1
- package/dist/api/generated/api.d.ts +40 -2
- package/dist/api/generated/api.js +2 -2
- package/dist/api/workflow_catalog.d.ts +7 -0
- package/dist/api/workflow_catalog.js +11 -0
- package/dist/api/workflow_catalog.spec.d.ts +1 -0
- package/dist/api/workflow_catalog.spec.js +30 -0
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/commands/workflow/list.d.ts +2 -0
- package/dist/commands/workflow/list.js +23 -21
- package/dist/commands/workflow/list.spec.js +57 -0
- package/dist/commands/workflow/status.js +6 -1
- package/dist/generated/framework_version.json +1 -1
- package/dist/hooks/init.js +12 -3
- package/dist/hooks/init.spec.js +18 -8
- package/dist/scripts/refresh_version_check.d.ts +1 -0
- package/dist/scripts/refresh_version_check.js +9 -0
- package/dist/services/cost_calculator.d.ts +1 -5
- package/dist/services/cost_calculator.js +214 -102
- package/dist/services/cost_calculator.spec.js +329 -253
- package/dist/services/npm_update_service.js +11 -3
- package/dist/services/npm_update_service.spec.js +20 -7
- package/dist/services/version_check.d.ts +19 -1
- package/dist/services/version_check.js +53 -17
- package/dist/services/version_check.spec.js +88 -58
- package/dist/services/workflow_runs.js +6 -1
- package/dist/types/cost.d.ts +64 -23
- package/dist/types/cost.js +4 -0
- package/dist/utils/cost_formatter.js +65 -43
- package/dist/utils/format_workflow_result.js +4 -2
- package/dist/utils/format_workflow_result.spec.js +13 -3
- package/dist/utils/normalize_workflow_status.d.ts +8 -0
- package/dist/utils/normalize_workflow_status.js +8 -0
- package/dist/utils/normalize_workflow_status.spec.d.ts +1 -0
- package/dist/utils/normalize_workflow_status.spec.js +13 -0
- package/dist/utils/proxy.d.ts +3 -2
- package/dist/utils/proxy.js +4 -3
- package/dist/utils/proxy.spec.js +4 -4
- package/dist/utils/scenario_resolver.js +3 -11
- package/dist/utils/scenario_resolver.spec.js +5 -9
- package/dist/views/dev/components/workflow_status.js +1 -1
- package/dist/views/dev/hooks/use_run_detail.js +6 -1
- package/dist/views/dev/hooks/use_run_detail.spec.js +1 -1
- package/dist/views/dev/hooks/use_workflow_catalog.js +2 -6
- package/dist/views/dev/panels/runs_panel.js +1 -1
- package/oclif.manifest.json +1445 -0
- package/package.json +6 -5
package/bin/run.js
CHANGED
|
@@ -7,7 +7,7 @@ import { loadCredentialRefs } from '../dist/utils/credentials_loader.js';
|
|
|
7
7
|
|
|
8
8
|
// Load environment variables from .env files before executing CLI
|
|
9
9
|
loadEnvironment();
|
|
10
|
-
bootstrapProxy();
|
|
10
|
+
await bootstrapProxy();
|
|
11
11
|
loadCredentialRefs();
|
|
12
12
|
|
|
13
13
|
await execute( { dir: import.meta.url } );
|
|
@@ -157,7 +157,7 @@ export declare const WorkflowRunInfoStatus: {
|
|
|
157
157
|
readonly canceled: "canceled";
|
|
158
158
|
readonly terminated: "terminated";
|
|
159
159
|
readonly timed_out: "timed_out";
|
|
160
|
-
readonly
|
|
160
|
+
readonly continued_as_new: "continued_as_new";
|
|
161
161
|
};
|
|
162
162
|
export interface WorkflowRunInfo {
|
|
163
163
|
/** Unique identifier for this run */
|
|
@@ -224,8 +224,41 @@ export declare const WorkflowResultResponseStatus: {
|
|
|
224
224
|
readonly canceled: "canceled";
|
|
225
225
|
readonly terminated: "terminated";
|
|
226
226
|
readonly timed_out: "timed_out";
|
|
227
|
-
readonly
|
|
227
|
+
readonly continued_as_new: "continued_as_new";
|
|
228
228
|
};
|
|
229
|
+
/**
|
|
230
|
+
* Structured failure details if the workflow failed, null otherwise
|
|
231
|
+
* @nullable
|
|
232
|
+
*/
|
|
233
|
+
export type WorkflowResultResponseErrorDetails = {
|
|
234
|
+
/**
|
|
235
|
+
* Friendly failure message (from the underlying application error)
|
|
236
|
+
* @nullable
|
|
237
|
+
*/
|
|
238
|
+
message?: string | null;
|
|
239
|
+
/**
|
|
240
|
+
* Error name/type (the original error's class)
|
|
241
|
+
* @nullable
|
|
242
|
+
*/
|
|
243
|
+
name?: string | null;
|
|
244
|
+
/**
|
|
245
|
+
* Whether Temporal flagged the failure retryable; null if unknown
|
|
246
|
+
* @nullable
|
|
247
|
+
*/
|
|
248
|
+
retryable?: boolean | null;
|
|
249
|
+
/**
|
|
250
|
+
* Failing activity key ("workflow#step"); null if no activity failed
|
|
251
|
+
* @nullable
|
|
252
|
+
*/
|
|
253
|
+
activityId?: string | null;
|
|
254
|
+
/**
|
|
255
|
+
* Sanitized error cause chain (name/message per level, no stack)
|
|
256
|
+
* @nullable
|
|
257
|
+
*/
|
|
258
|
+
cause?: {
|
|
259
|
+
[key: string]: unknown;
|
|
260
|
+
} | null;
|
|
261
|
+
} | null | null;
|
|
229
262
|
export interface WorkflowResultResponse {
|
|
230
263
|
/** The workflow execution id */
|
|
231
264
|
workflowId?: string;
|
|
@@ -248,6 +281,11 @@ export interface WorkflowResultResponse {
|
|
|
248
281
|
* @nullable
|
|
249
282
|
*/
|
|
250
283
|
error?: string | null;
|
|
284
|
+
/**
|
|
285
|
+
* Structured failure details if the workflow failed, null otherwise
|
|
286
|
+
* @nullable
|
|
287
|
+
*/
|
|
288
|
+
errorDetails?: WorkflowResultResponseErrorDetails;
|
|
251
289
|
}
|
|
252
290
|
export interface StopWorkflowResponse {
|
|
253
291
|
workflowId?: string;
|
|
@@ -22,7 +22,7 @@ export const WorkflowRunInfoStatus = {
|
|
|
22
22
|
canceled: 'canceled',
|
|
23
23
|
terminated: 'terminated',
|
|
24
24
|
timed_out: 'timed_out',
|
|
25
|
-
|
|
25
|
+
continued_as_new: 'continued_as_new',
|
|
26
26
|
};
|
|
27
27
|
export const WorkflowStatusResponseStatus = {
|
|
28
28
|
canceled: 'canceled',
|
|
@@ -40,7 +40,7 @@ export const WorkflowResultResponseStatus = {
|
|
|
40
40
|
canceled: 'canceled',
|
|
41
41
|
terminated: 'terminated',
|
|
42
42
|
timed_out: 'timed_out',
|
|
43
|
-
|
|
43
|
+
continued_as_new: 'continued_as_new',
|
|
44
44
|
};
|
|
45
45
|
;
|
|
46
46
|
export const getGetHealthUrl = () => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Workflow } from './generated/api.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the workflows in a catalog. When `catalog` is provided (e.g. from
|
|
4
|
+
* `--catalog`/`OUTPUT_CATALOG_ID`) it resolves that specific catalog, otherwise
|
|
5
|
+
* the API server's default catalog. Returns `[]` when the catalog has no workflows.
|
|
6
|
+
*/
|
|
7
|
+
export declare function fetchWorkflowCatalog(catalog?: string): Promise<Workflow[]>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getWorkflowCatalog, getWorkflowCatalogId } from './generated/api.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the workflows in a catalog. When `catalog` is provided (e.g. from
|
|
4
|
+
* `--catalog`/`OUTPUT_CATALOG_ID`) it resolves that specific catalog, otherwise
|
|
5
|
+
* the API server's default catalog. Returns `[]` when the catalog has no workflows.
|
|
6
|
+
*/
|
|
7
|
+
export async function fetchWorkflowCatalog(catalog) {
|
|
8
|
+
const response = catalog ? await getWorkflowCatalogId(catalog) : await getWorkflowCatalog();
|
|
9
|
+
const data = response?.data;
|
|
10
|
+
return data?.workflows ?? [];
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import * as api from './generated/api.js';
|
|
3
|
+
import { fetchWorkflowCatalog } from './workflow_catalog.js';
|
|
4
|
+
vi.mock('./generated/api.js', () => ({
|
|
5
|
+
getWorkflowCatalog: vi.fn(),
|
|
6
|
+
getWorkflowCatalogId: vi.fn()
|
|
7
|
+
}));
|
|
8
|
+
describe('fetchWorkflowCatalog', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
it('fetches the default catalog when no catalog id is provided', async () => {
|
|
13
|
+
vi.mocked(api.getWorkflowCatalog).mockResolvedValue({ data: { workflows: [{ name: 'a' }] } });
|
|
14
|
+
const result = await fetchWorkflowCatalog();
|
|
15
|
+
expect(api.getWorkflowCatalog).toHaveBeenCalledTimes(1);
|
|
16
|
+
expect(api.getWorkflowCatalogId).not.toHaveBeenCalled();
|
|
17
|
+
expect(result).toEqual([{ name: 'a' }]);
|
|
18
|
+
});
|
|
19
|
+
it('fetches a specific catalog by id when one is provided', async () => {
|
|
20
|
+
vi.mocked(api.getWorkflowCatalogId).mockResolvedValue({ data: { workflows: [{ name: 'b' }] } });
|
|
21
|
+
const result = await fetchWorkflowCatalog('my-catalog');
|
|
22
|
+
expect(api.getWorkflowCatalogId).toHaveBeenCalledWith('my-catalog');
|
|
23
|
+
expect(api.getWorkflowCatalog).not.toHaveBeenCalled();
|
|
24
|
+
expect(result).toEqual([{ name: 'b' }]);
|
|
25
|
+
});
|
|
26
|
+
it('returns an empty array when the catalog response has no workflows', async () => {
|
|
27
|
+
vi.mocked(api.getWorkflowCatalog).mockResolvedValue({ data: {} });
|
|
28
|
+
expect(await fetchWorkflowCatalog()).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
});
|
package/dist/commands/update.js
CHANGED
|
@@ -31,7 +31,7 @@ export default class Update extends Command {
|
|
|
31
31
|
async updateCli() {
|
|
32
32
|
const latest = await fetchLatestVersion();
|
|
33
33
|
if (!latest) {
|
|
34
|
-
this.error('Could not fetch the latest version from npm.
|
|
34
|
+
this.error('Could not fetch the latest version from the npm registry. Run with DEBUG=output-cli:npm-update for details.');
|
|
35
35
|
}
|
|
36
36
|
this.log(`\nLatest @outputai/cli version: v${latest}\n`);
|
|
37
37
|
await this.handleGlobalUpdate(latest);
|
|
@@ -9,10 +9,12 @@ interface WorkflowDisplay {
|
|
|
9
9
|
aliases: string;
|
|
10
10
|
}
|
|
11
11
|
export declare function parseWorkflowForDisplay(workflow: Workflow): WorkflowDisplay;
|
|
12
|
+
export declare function formatWorkflowsAsList(workflows: Workflow[]): string;
|
|
12
13
|
export default class WorkflowList extends Command {
|
|
13
14
|
static description: string;
|
|
14
15
|
static examples: string[];
|
|
15
16
|
static flags: {
|
|
17
|
+
catalog: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
18
|
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
19
|
detailed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
20
|
filter: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import Table from 'cli-table3';
|
|
3
|
-
import {
|
|
3
|
+
import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
|
|
4
4
|
import { parseWorkflowDefinition, formatParameters } from '#api/parser.js';
|
|
5
5
|
import { handleApiError } from '#utils/error_handler.js';
|
|
6
6
|
import { listScenariosForWorkflow } from '#utils/scenario_resolver.js';
|
|
@@ -65,10 +65,13 @@ function createWorkflowTable(workflows, detailed) {
|
|
|
65
65
|
});
|
|
66
66
|
return table.toString();
|
|
67
67
|
}
|
|
68
|
-
function
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
function formatWorkflowAsListItem(workflow) {
|
|
69
|
+
const { name, aliases } = parseWorkflowForDisplay(workflow);
|
|
70
|
+
return aliases === 'none' ? `- ${name}` : `- ${name} (aliases: ${aliases})`;
|
|
71
|
+
}
|
|
72
|
+
export function formatWorkflowsAsList(workflows) {
|
|
73
|
+
const lines = sortWorkflowsByName(workflows).map(formatWorkflowAsListItem);
|
|
74
|
+
return `\nWorkflows:\n\n${lines.join('\n')}`;
|
|
72
75
|
}
|
|
73
76
|
function formatWorkflowsAsJson(workflows) {
|
|
74
77
|
const output = {
|
|
@@ -103,9 +106,18 @@ export default class WorkflowList extends Command {
|
|
|
103
106
|
'<%= config.bin %> <%= command.id %> --format table',
|
|
104
107
|
'<%= config.bin %> <%= command.id %> --format json',
|
|
105
108
|
'<%= config.bin %> <%= command.id %> --detailed',
|
|
106
|
-
'<%= config.bin %> <%= command.id %> --filter simple'
|
|
109
|
+
'<%= config.bin %> <%= command.id %> --filter simple',
|
|
110
|
+
'<%= config.bin %> <%= command.id %> --catalog my-catalog'
|
|
107
111
|
];
|
|
108
112
|
static flags = {
|
|
113
|
+
catalog: Flags.string({
|
|
114
|
+
char: 'c',
|
|
115
|
+
aliases: ['task-queue'],
|
|
116
|
+
charAliases: ['q'],
|
|
117
|
+
deprecateAliases: true,
|
|
118
|
+
description: 'Catalog to list workflows from (defaults to OUTPUT_CATALOG_ID)',
|
|
119
|
+
env: 'OUTPUT_CATALOG_ID'
|
|
120
|
+
}),
|
|
109
121
|
format: Flags.string({
|
|
110
122
|
char: 'f',
|
|
111
123
|
description: 'Output format',
|
|
@@ -123,25 +135,15 @@ export default class WorkflowList extends Command {
|
|
|
123
135
|
};
|
|
124
136
|
async run() {
|
|
125
137
|
const { flags } = await this.parse(WorkflowList);
|
|
126
|
-
this.log('Fetching workflow catalog...');
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
this.error('Failed to connect to API server. Is it running?', { exit: 1 });
|
|
130
|
-
}
|
|
131
|
-
if (!response.data) {
|
|
132
|
-
this.error('API returned invalid response (missing data)', { exit: 1 });
|
|
133
|
-
}
|
|
134
|
-
const data = response.data;
|
|
135
|
-
if (!data.workflows) {
|
|
136
|
-
this.error('API returned invalid response (missing workflows)', { exit: 1 });
|
|
137
|
-
}
|
|
138
|
-
if (data.workflows.length === 0) {
|
|
138
|
+
this.log(flags.catalog ? `Fetching workflow catalog: ${flags.catalog}...` : 'Fetching workflow catalog...');
|
|
139
|
+
const catalogWorkflows = await fetchWorkflowCatalog(flags.catalog);
|
|
140
|
+
if (catalogWorkflows.length === 0) {
|
|
139
141
|
this.log('No workflows found in catalog.');
|
|
140
142
|
return;
|
|
141
143
|
}
|
|
142
144
|
const workflows = flags.filter ?
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
catalogWorkflows.filter(matchName(flags.filter)) :
|
|
146
|
+
catalogWorkflows;
|
|
145
147
|
if (workflows.length === 0 && flags.filter) {
|
|
146
148
|
this.log(`No workflows matching filter: ${flags.filter}`);
|
|
147
149
|
return;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
3
|
const mockListScenarios = vi.fn().mockReturnValue([]);
|
|
3
4
|
vi.mock('#utils/scenario_resolver.js', () => ({
|
|
4
5
|
listScenariosForWorkflow: mockListScenarios
|
|
5
6
|
}));
|
|
7
|
+
vi.mock('#api/workflow_catalog.js', () => ({
|
|
8
|
+
fetchWorkflowCatalog: vi.fn()
|
|
9
|
+
}));
|
|
6
10
|
describe('workflow list command', () => {
|
|
7
11
|
beforeEach(() => {
|
|
8
12
|
vi.clearAllMocks();
|
|
@@ -15,6 +19,12 @@ describe('workflow list command', () => {
|
|
|
15
19
|
expect(WorkflowList.flags).toHaveProperty('format');
|
|
16
20
|
expect(WorkflowList.flags).toHaveProperty('detailed');
|
|
17
21
|
expect(WorkflowList.flags).toHaveProperty('filter');
|
|
22
|
+
expect(WorkflowList.flags).toHaveProperty('catalog');
|
|
23
|
+
});
|
|
24
|
+
it('reads the catalog flag from OUTPUT_CATALOG_ID', async () => {
|
|
25
|
+
const WorkflowList = (await import('./list.js')).default;
|
|
26
|
+
expect(WorkflowList.flags.catalog.env).toBe('OUTPUT_CATALOG_ID');
|
|
27
|
+
expect(WorkflowList.flags.catalog.char).toBe('c');
|
|
18
28
|
});
|
|
19
29
|
it('should have correct flag configuration', async () => {
|
|
20
30
|
const WorkflowList = (await import('./list.js')).default;
|
|
@@ -118,3 +128,50 @@ describe('workflow list parsing', () => {
|
|
|
118
128
|
expect(parsed.inputs).toContain('user.email: string');
|
|
119
129
|
});
|
|
120
130
|
});
|
|
131
|
+
describe('formatWorkflowsAsList', () => {
|
|
132
|
+
it('appends aliases to the default list when present', async () => {
|
|
133
|
+
const { formatWorkflowsAsList } = await import('./list.js');
|
|
134
|
+
const output = formatWorkflowsAsList([
|
|
135
|
+
{ name: 'galileoExtractKeyword', aliases: ['seoContentExtractKeywordWorkflow'] }
|
|
136
|
+
]);
|
|
137
|
+
expect(output).toContain('- galileoExtractKeyword (aliases: seoContentExtractKeywordWorkflow)');
|
|
138
|
+
});
|
|
139
|
+
it('omits the aliases segment when a workflow has none', async () => {
|
|
140
|
+
const { formatWorkflowsAsList } = await import('./list.js');
|
|
141
|
+
const output = formatWorkflowsAsList([{ name: 'simple' }]);
|
|
142
|
+
expect(output).toContain('- simple');
|
|
143
|
+
expect(output).not.toContain('aliases:');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('run() catalog resolution', () => {
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
vi.clearAllMocks();
|
|
149
|
+
});
|
|
150
|
+
const catalogWorkflows = [{ name: 'simple' }];
|
|
151
|
+
const createCommand = async (flagOverrides) => {
|
|
152
|
+
const WorkflowList = (await import('./list.js')).default;
|
|
153
|
+
const cmd = new WorkflowList([], {});
|
|
154
|
+
cmd.log = vi.fn();
|
|
155
|
+
cmd.error = vi.fn(() => {
|
|
156
|
+
throw new Error('error called');
|
|
157
|
+
});
|
|
158
|
+
cmd.parse = vi.fn().mockResolvedValue({
|
|
159
|
+
flags: { format: 'list', detailed: false, filter: undefined, catalog: undefined, ...flagOverrides }
|
|
160
|
+
});
|
|
161
|
+
return cmd;
|
|
162
|
+
};
|
|
163
|
+
it('fetches a specific catalog by id when a catalog is provided', async () => {
|
|
164
|
+
const { fetchWorkflowCatalog } = await import('#api/workflow_catalog.js');
|
|
165
|
+
vi.mocked(fetchWorkflowCatalog).mockResolvedValue(catalogWorkflows);
|
|
166
|
+
const cmd = await createCommand({ catalog: 'my-catalog' });
|
|
167
|
+
await cmd.run();
|
|
168
|
+
expect(fetchWorkflowCatalog).toHaveBeenCalledWith('my-catalog');
|
|
169
|
+
});
|
|
170
|
+
it('falls back to the default catalog when no catalog is provided', async () => {
|
|
171
|
+
const { fetchWorkflowCatalog } = await import('#api/workflow_catalog.js');
|
|
172
|
+
vi.mocked(fetchWorkflowCatalog).mockResolvedValue(catalogWorkflows);
|
|
173
|
+
const cmd = await createCommand({ catalog: undefined });
|
|
174
|
+
await cmd.run();
|
|
175
|
+
expect(fetchWorkflowCatalog).toHaveBeenCalledWith(undefined);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -3,6 +3,7 @@ import { getWorkflowIdStatus } from '#api/generated/api.js';
|
|
|
3
3
|
import { OUTPUT_FORMAT } from '#utils/constants.js';
|
|
4
4
|
import { formatOutput } from '#utils/output_formatter.js';
|
|
5
5
|
import { handleApiError } from '#utils/error_handler.js';
|
|
6
|
+
import { normalizeWorkflowStatus } from '#utils/normalize_workflow_status.js';
|
|
6
7
|
export default class WorkflowStatus extends Command {
|
|
7
8
|
static description = 'Get workflow execution status';
|
|
8
9
|
static examples = [
|
|
@@ -30,7 +31,11 @@ export default class WorkflowStatus extends Command {
|
|
|
30
31
|
if (!response || !response.data) {
|
|
31
32
|
this.error('API returned invalid response', { exit: 1 });
|
|
32
33
|
}
|
|
33
|
-
const
|
|
34
|
+
const rawData = response.data;
|
|
35
|
+
const data = {
|
|
36
|
+
...rawData,
|
|
37
|
+
status: normalizeWorkflowStatus(rawData.status)
|
|
38
|
+
};
|
|
34
39
|
const output = formatOutput(data, flags.format, (result) => {
|
|
35
40
|
const lines = [
|
|
36
41
|
`Workflow ID: ${result.workflowId || 'unknown'}`,
|
package/dist/hooks/init.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ux } from '@oclif/core';
|
|
2
|
-
import
|
|
2
|
+
import debugFactory from 'debug';
|
|
3
|
+
import { readCachedResult, spawnBackgroundRefresh } from '#services/version_check.js';
|
|
3
4
|
import { setNonInteractive } from '#utils/interactive.js';
|
|
5
|
+
const debug = debugFactory('output-cli:init');
|
|
4
6
|
export const INTERACTIVE_FLAGS = ['--yes', '--non-interactive'];
|
|
5
7
|
export const GLOBAL_FLAGS = new Set(INTERACTIVE_FLAGS);
|
|
6
8
|
export const hasInteractiveFlag = (argv) => argv.some(arg => INTERACTIVE_FLAGS.includes(arg));
|
|
@@ -18,7 +20,13 @@ const hook = async function (opts) {
|
|
|
18
20
|
setNonInteractive(true);
|
|
19
21
|
}
|
|
20
22
|
try {
|
|
21
|
-
|
|
23
|
+
// Only the local cache is read here; the registry roundtrip happens in a
|
|
24
|
+
// detached child so it never delays the invoked command.
|
|
25
|
+
const result = await readCachedResult(this.config.version, this.config.cacheDir);
|
|
26
|
+
if (!result) {
|
|
27
|
+
spawnBackgroundRefresh(this.config.version, this.config.cacheDir);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
22
30
|
if (!result.updateAvailable) {
|
|
23
31
|
return;
|
|
24
32
|
}
|
|
@@ -39,8 +47,9 @@ const hook = async function (opts) {
|
|
|
39
47
|
ux.stdout(border);
|
|
40
48
|
ux.stdout('');
|
|
41
49
|
}
|
|
42
|
-
catch {
|
|
50
|
+
catch (error) {
|
|
43
51
|
// Never block CLI execution
|
|
52
|
+
debug('Version banner failed: %O', error);
|
|
44
53
|
}
|
|
45
54
|
};
|
|
46
55
|
export default hook;
|
package/dist/hooks/init.spec.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
3
|
-
import {
|
|
3
|
+
import { readCachedResult, spawnBackgroundRefresh } from '#services/version_check.js';
|
|
4
4
|
import { setNonInteractive } from '#utils/interactive.js';
|
|
5
5
|
vi.mock('#services/version_check.js', () => ({
|
|
6
|
-
|
|
6
|
+
readCachedResult: vi.fn(),
|
|
7
|
+
spawnBackgroundRefresh: vi.fn()
|
|
7
8
|
}));
|
|
8
9
|
vi.mock('#utils/interactive.js', () => ({
|
|
9
10
|
setNonInteractive: vi.fn()
|
|
@@ -23,15 +24,16 @@ describe('init hook', () => {
|
|
|
23
24
|
const createHookContext = (version = '0.8.4') => ({
|
|
24
25
|
config: { version, cacheDir: '/tmp/test-cache' }
|
|
25
26
|
});
|
|
26
|
-
it('should display warning when update is available', async () => {
|
|
27
|
-
vi.mocked(
|
|
27
|
+
it('should display warning when cached result says an update is available', async () => {
|
|
28
|
+
vi.mocked(readCachedResult).mockResolvedValue({
|
|
28
29
|
updateAvailable: true,
|
|
29
30
|
currentVersion: '0.8.4',
|
|
30
31
|
latestVersion: '1.0.0'
|
|
31
32
|
});
|
|
32
33
|
const ctx = createHookContext();
|
|
33
34
|
await hook.call(ctx, { argv: [], id: undefined });
|
|
34
|
-
expect(
|
|
35
|
+
expect(readCachedResult).toHaveBeenCalledWith('0.8.4', '/tmp/test-cache');
|
|
36
|
+
expect(spawnBackgroundRefresh).not.toHaveBeenCalled();
|
|
35
37
|
expect(ux.stdout).toHaveBeenCalled();
|
|
36
38
|
const output = vi.mocked(ux.stdout).mock.calls.map(c => c[0]).join('\n');
|
|
37
39
|
expect(output).toContain('Uhoh');
|
|
@@ -40,17 +42,25 @@ describe('init hook', () => {
|
|
|
40
42
|
expect(output).toContain('npx output update');
|
|
41
43
|
});
|
|
42
44
|
it('should not display anything when up to date', async () => {
|
|
43
|
-
vi.mocked(
|
|
45
|
+
vi.mocked(readCachedResult).mockResolvedValue({
|
|
44
46
|
updateAvailable: false,
|
|
45
47
|
currentVersion: '0.8.4',
|
|
46
48
|
latestVersion: '0.8.4'
|
|
47
49
|
});
|
|
48
50
|
const ctx = createHookContext();
|
|
49
51
|
await hook.call(ctx, { argv: [], id: undefined });
|
|
52
|
+
expect(spawnBackgroundRefresh).not.toHaveBeenCalled();
|
|
53
|
+
expect(ux.stdout).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
it('should kick off a background refresh and stay silent when the cache is stale', async () => {
|
|
56
|
+
vi.mocked(readCachedResult).mockResolvedValue(null);
|
|
57
|
+
const ctx = createHookContext();
|
|
58
|
+
await hook.call(ctx, { argv: [], id: undefined });
|
|
59
|
+
expect(spawnBackgroundRefresh).toHaveBeenCalledWith('0.8.4', '/tmp/test-cache');
|
|
50
60
|
expect(ux.stdout).not.toHaveBeenCalled();
|
|
51
61
|
});
|
|
52
62
|
it('should silently handle errors', async () => {
|
|
53
|
-
vi.mocked(
|
|
63
|
+
vi.mocked(readCachedResult).mockRejectedValue(new Error('cache failure'));
|
|
54
64
|
const ctx = createHookContext();
|
|
55
65
|
await hook.call(ctx, { argv: [], id: undefined });
|
|
56
66
|
expect(ux.stdout).not.toHaveBeenCalled();
|
|
@@ -58,7 +68,7 @@ describe('init hook', () => {
|
|
|
58
68
|
describe('global interactive flags', () => {
|
|
59
69
|
const originalArgv = process.argv;
|
|
60
70
|
beforeEach(() => {
|
|
61
|
-
vi.mocked(
|
|
71
|
+
vi.mocked(readCachedResult).mockResolvedValue({
|
|
62
72
|
updateAvailable: false,
|
|
63
73
|
currentVersion: '0.8.4',
|
|
64
74
|
latestVersion: '0.8.4'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Detached helper spawned by spawnBackgroundRefresh (version_check.ts):
|
|
2
|
+
// refreshes the version-check cache off the critical path.
|
|
3
|
+
// Args: <currentVersion> <cacheDir>
|
|
4
|
+
import debugFactory from 'debug';
|
|
5
|
+
import { bootstrapProxy } from '#utils/proxy.js';
|
|
6
|
+
import { runRefresh } from '#services/version_check.js';
|
|
7
|
+
const debug = debugFactory('output-cli:version-check');
|
|
8
|
+
await bootstrapProxy().catch(error => debug('Proxy bootstrap failed: %O', error));
|
|
9
|
+
process.exitCode = await runRefresh(process.argv);
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import type { TraceNode, LLMCall, HTTPCall,
|
|
1
|
+
import type { TraceNode, LLMCall, HTTPCall, PricingConfig, ServiceConfig, ServiceCostResult, CostReport } from '#types/cost.js';
|
|
2
2
|
export declare function extractValue(obj: unknown, path: string): unknown;
|
|
3
3
|
export declare function loadPricingConfig(configPath?: string): PricingConfig;
|
|
4
4
|
export declare function findLLMCalls(node: TraceNode, parentStepName?: string | null, seenIds?: Set<string>): LLMCall[];
|
|
5
5
|
export declare function findHTTPCalls(node: TraceNode, parentStepName?: string | null, seenIds?: Set<string>): HTTPCall[];
|
|
6
|
-
export declare function calculateLLMCallCost(usage: TokenUsage, modelPricing: ModelPricing | undefined): {
|
|
7
|
-
cost: number;
|
|
8
|
-
warning?: string;
|
|
9
|
-
};
|
|
10
6
|
export declare function identifyService(httpCall: HTTPCall, services: Record<string, ServiceConfig>): {
|
|
11
7
|
serviceName: string;
|
|
12
8
|
config: ServiceConfig;
|