@skrr-ai/cli 0.1.37 → 0.1.39
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/dev-fallback.js +175 -4
- package/dist/base-command.js +46 -2
- package/dist/commands/agents/actions/create.js +18 -7
- package/dist/commands/agents/actions/delete.d.ts +1 -0
- package/dist/commands/agents/actions/delete.js +7 -0
- package/dist/commands/agents/actions/update.js +5 -3
- package/dist/commands/agents/api-actions/add.js +5 -2
- package/dist/commands/agents/api-actions/index.d.ts +11 -0
- package/dist/commands/agents/api-actions/index.js +27 -1
- package/dist/commands/agents/avatar/default/index.d.ts +11 -0
- package/dist/commands/agents/avatar/default/index.js +17 -0
- package/dist/commands/agents/avatar/default/set.d.ts +2 -1
- package/dist/commands/agents/avatar/default/set.js +32 -7
- package/dist/commands/agents/avatar/upload.js +7 -1
- package/dist/commands/agents/categories/delete.js +8 -0
- package/dist/commands/agents/categories/index.d.ts +21 -5
- package/dist/commands/agents/categories/index.js +68 -15
- package/dist/commands/agents/chat.d.ts +21 -0
- package/dist/commands/agents/chat.js +94 -7
- package/dist/commands/agents/create.js +11 -4
- package/dist/commands/agents/list.d.ts +15 -0
- package/dist/commands/agents/list.js +31 -1
- package/dist/commands/agents/scene-background/upload.d.ts +30 -0
- package/dist/commands/agents/scene-background/upload.js +97 -0
- package/dist/commands/agents/show.js +13 -0
- package/dist/commands/agents/tools/calls.d.ts +23 -5
- package/dist/commands/agents/tools/calls.js +81 -18
- package/dist/commands/agents/update.js +15 -5
- package/dist/commands/code/{oversky-agent.d.ts → skrr-agent.d.ts} +3 -3
- package/dist/commands/code/{oversky-agent.js → skrr-agent.js} +11 -8
- package/dist/commands/convos/messages.d.ts +13 -0
- package/dist/commands/convos/messages.js +40 -16
- package/dist/commands/daemon/index.js +8 -1
- package/dist/commands/daemon/logs.d.ts +26 -0
- package/dist/commands/daemon/logs.js +56 -0
- package/dist/commands/daemon/uninstall.js +1 -1
- package/dist/commands/followups/act.js +3 -2
- package/dist/commands/followups/cancel.d.ts +6 -3
- package/dist/commands/followups/cancel.js +16 -6
- package/dist/commands/followups/observability.d.ts +23 -0
- package/dist/commands/followups/observability.js +114 -0
- package/dist/commands/followups/reschedule.js +1 -1
- package/dist/commands/followups/resolve.js +3 -3
- package/dist/commands/followups/show.js +2 -1
- package/dist/commands/followups/watch.js +9 -5
- package/dist/commands/harnesses/forget.js +28 -0
- package/dist/commands/harnesses/install.d.ts +41 -0
- package/dist/commands/harnesses/install.js +165 -0
- package/dist/commands/harnesses/installers.d.ts +45 -0
- package/dist/commands/harnesses/installers.js +155 -0
- package/dist/commands/harnesses/list.d.ts +82 -1
- package/dist/commands/harnesses/list.js +104 -12
- package/dist/commands/harnesses/models.js +10 -1
- package/dist/commands/harnesses/show.d.ts +15 -0
- package/dist/commands/harnesses/show.js +44 -3
- package/dist/commands/harnesses/update.js +1 -1
- package/dist/commands/harnesses/usage.d.ts +25 -0
- package/dist/commands/harnesses/usage.js +24 -2
- package/dist/commands/instructions/status.js +13 -1
- package/dist/commands/machines/dedicated/catalog.js +4 -15
- package/dist/commands/machines/dedicated/create.d.ts +8 -0
- package/dist/commands/machines/dedicated/create.js +63 -2
- package/dist/commands/machines/dedicated/destroy.js +19 -5
- package/dist/commands/machines/dedicated/exec.js +3 -1
- package/dist/commands/machines/dedicated/list.js +9 -6
- package/dist/commands/machines/dedicated/price-book.d.ts +17 -0
- package/dist/commands/machines/dedicated/price-book.js +35 -0
- package/dist/commands/machines/hosted/list.js +12 -0
- package/dist/commands/machines/list.js +11 -0
- package/dist/commands/machines/show.d.ts +1 -0
- package/dist/commands/machines/show.js +40 -11
- package/dist/commands/profiles/list.d.ts +23 -0
- package/dist/commands/profiles/list.js +96 -0
- package/dist/commands/search/skills.d.ts +11 -1
- package/dist/commands/search/skills.js +26 -8
- package/dist/commands/skills/details.d.ts +2 -0
- package/dist/commands/skills/details.js +24 -1
- package/dist/commands/skills/import-as-actions.d.ts +32 -0
- package/dist/commands/skills/import-as-actions.js +175 -0
- package/dist/commands/skills/install.d.ts +2 -0
- package/dist/commands/skills/install.js +24 -1
- package/dist/commands/skills/invocable.js +6 -2
- package/dist/commands/skills/local.d.ts +40 -0
- package/dist/commands/skills/local.js +160 -0
- package/dist/commands/skills/search.d.ts +11 -1
- package/dist/commands/skills/search.js +29 -8
- package/dist/commands/spaces/actions/add.js +23 -5
- package/dist/commands/spaces/actions/available.js +3 -0
- package/dist/commands/spaces/actions/list.d.ts +5 -0
- package/dist/commands/spaces/actions/list.js +49 -3
- package/dist/commands/spaces/actions/run.d.ts +28 -0
- package/dist/commands/spaces/actions/run.js +146 -0
- package/dist/commands/spaces/actions/update.js +27 -5
- package/dist/commands/spaces/label-summaries.d.ts +27 -0
- package/dist/commands/spaces/label-summaries.js +64 -0
- package/dist/commands/spaces/task-rollups.d.ts +5 -0
- package/dist/commands/spaces/task-rollups.js +15 -4
- package/dist/commands/spaces/workflows/lifecycle-diagnostics.js +63 -3
- package/dist/commands/tasks/actions/attach.js +32 -7
- package/dist/commands/tasks/complete.js +13 -0
- package/dist/commands/tasks/create.js +1 -1
- package/dist/commands/tasks/events/append.js +29 -6
- package/dist/commands/tasks/expectations/assess.d.ts +1 -0
- package/dist/commands/tasks/expectations/assess.js +43 -5
- package/dist/commands/tasks/expectations.js +77 -16
- package/dist/commands/tasks/follow-up.js +2 -2
- package/dist/commands/tasks/lifecycle/definitions.js +21 -2
- package/dist/commands/tasks/lifecycle/detach.d.ts +7 -0
- package/dist/commands/tasks/lifecycle/detach.js +43 -3
- package/dist/commands/tasks/lifecycle/migrate.d.ts +2 -0
- package/dist/commands/tasks/lifecycle/migrate.js +42 -3
- package/dist/commands/tasks/lifecycle/reopen.d.ts +2 -0
- package/dist/commands/tasks/lifecycle/reopen.js +40 -3
- package/dist/commands/tasks/lifecycle/transition.js +33 -2
- package/dist/commands/tasks/list.js +16 -9
- package/dist/commands/tasks/move.js +11 -13
- package/dist/commands/tasks/review-queue.d.ts +15 -0
- package/dist/commands/tasks/review-queue.js +180 -31
- package/dist/commands/tasks/show.d.ts +11 -0
- package/dist/commands/tasks/show.js +105 -12
- package/dist/commands/tasks/trust.js +7 -2
- package/dist/commands/tasks/update.js +46 -10
- package/dist/commands/tasks/updates/add.js +1 -1
- package/dist/commands/token/list.d.ts +22 -0
- package/dist/commands/token/list.js +68 -0
- package/dist/commands/token/revoke.d.ts +23 -0
- package/dist/commands/token/revoke.js +53 -0
- package/dist/lib/agent-actions.d.ts +57 -2
- package/dist/lib/agent-actions.js +113 -21
- package/dist/lib/agent-pin-mute.d.ts +10 -5
- package/dist/lib/agent-pin-mute.js +17 -4
- package/dist/lib/agent-visual-refs.d.ts +27 -0
- package/dist/lib/agent-visual-refs.js +127 -2
- package/dist/lib/agentic-stream.d.ts +6 -0
- package/dist/lib/agentic-stream.js +34 -3
- package/dist/lib/assignee-resolver.d.ts +1 -1
- package/dist/lib/assignee-resolver.js +8 -0
- package/dist/lib/bulk-task-targets.d.ts +12 -0
- package/dist/lib/bulk-task-targets.js +34 -1
- package/dist/lib/cli-installers.d.ts +130 -0
- package/dist/lib/cli-installers.js +145 -0
- package/dist/lib/command-miss.js +55 -7
- package/dist/lib/dedicated-lease-command.d.ts +16 -1
- package/dist/lib/dedicated-lease-command.js +26 -3
- package/dist/lib/dedicated-machines.d.ts +135 -1
- package/dist/lib/dedicated-machines.js +307 -3
- package/dist/lib/failed-lookup.d.ts +3 -0
- package/dist/lib/failed-lookup.js +40 -0
- package/dist/lib/first-party-harness-agent.js +9 -2
- package/dist/lib/first-party-harness-doctor.d.ts +15 -0
- package/dist/lib/first-party-harness-doctor.js +126 -27
- package/dist/lib/followups.d.ts +14 -6
- package/dist/lib/followups.js +31 -6
- package/dist/lib/harnesses.d.ts +8 -0
- package/dist/lib/instruction-input.d.ts +19 -1
- package/dist/lib/instruction-input.js +52 -1
- package/dist/lib/label-scope.d.ts +15 -0
- package/dist/lib/label-scope.js +25 -5
- package/dist/lib/local-skills.d.ts +98 -0
- package/dist/lib/local-skills.js +195 -0
- package/dist/lib/machines.d.ts +10 -10
- package/dist/lib/machines.js +23 -1
- package/dist/lib/search-query.d.ts +19 -0
- package/dist/lib/search-query.js +35 -0
- package/dist/lib/space-custom-actions.d.ts +72 -0
- package/dist/lib/space-custom-actions.js +117 -0
- package/dist/lib/space-resolver.d.ts +1 -21
- package/dist/lib/space-resolver.js +19 -10
- package/dist/lib/task-closure.d.ts +3 -2
- package/dist/lib/task-closure.js +18 -20
- package/dist/lib/task-lifecycle-output.js +117 -9
- package/dist/lib/task-ref-resolver.d.ts +1 -17
- package/dist/lib/task-ref-resolver.js +18 -8
- package/dist/lib/tasks.js +9 -3
- package/dist/node_modules/@skrr-ai/data-provider/index.js +5076 -4947
- package/oclif.manifest.json +15505 -14460
- package/package.json +9 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectUsageRows = selectUsageRows;
|
|
4
|
+
exports.usageRowHarnessCell = usageRowHarnessCell;
|
|
3
5
|
exports.buildUsageTable = buildUsageTable;
|
|
4
6
|
const core_1 = require("@oclif/core");
|
|
5
7
|
const base_command_1 = require("../../base-command");
|
|
@@ -22,6 +24,26 @@ function count(value) {
|
|
|
22
24
|
const n = Number(value);
|
|
23
25
|
return Number.isFinite(n) ? n.toLocaleString('en-US') : '-';
|
|
24
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The rows to render, whichever spelling the server used.
|
|
29
|
+
*
|
|
30
|
+
* `byHarness` is what it sends today. This reader asked only for `byRuntime` —
|
|
31
|
+
* the pre-rename spelling — so the fleet rollup was ALWAYS empty and the
|
|
32
|
+
* command always printed "No usage recorded.", while `--json` on the same call
|
|
33
|
+
* showed $24.93 across 46 sessions. A rollup cannot be empty while its own
|
|
34
|
+
* members are not, which is what made it findable.
|
|
35
|
+
*
|
|
36
|
+
* The old spellings stay as fallbacks, after the current one, so an older
|
|
37
|
+
* deployment still renders. They are not the primary, which is the whole bug.
|
|
38
|
+
*/
|
|
39
|
+
function selectUsageRows(payload) {
|
|
40
|
+
return payload?.byHarness ?? payload?.byRuntime ?? payload?.usage ?? [];
|
|
41
|
+
}
|
|
42
|
+
/** The HARNESS cell, under either spelling of the id. */
|
|
43
|
+
function usageRowHarnessCell(row) {
|
|
44
|
+
const id = row.harnessId ?? row.runtimeId;
|
|
45
|
+
return id ? String(id).slice(0, 12) : '-';
|
|
46
|
+
}
|
|
25
47
|
const SHARED_COLUMNS = [
|
|
26
48
|
{ key: 'provider', header: 'PROVIDER' },
|
|
27
49
|
{ key: 'sessions', header: 'SESSIONS' },
|
|
@@ -66,7 +88,7 @@ function buildUsageTable(rows, perHarness) {
|
|
|
66
88
|
...SHARED_COLUMNS,
|
|
67
89
|
],
|
|
68
90
|
rows: rows.map((r) => ({
|
|
69
|
-
harness:
|
|
91
|
+
harness: usageRowHarnessCell(r),
|
|
70
92
|
name: (0, format_1.formatCell)(r.name),
|
|
71
93
|
...shared(r),
|
|
72
94
|
})),
|
|
@@ -105,7 +127,7 @@ class HarnessesUsage extends base_command_1.BaseCommand {
|
|
|
105
127
|
// `{usage, totals}`. Neither is a superset, so read both rather than
|
|
106
128
|
// assuming one and rendering a table of dashes for the other.
|
|
107
129
|
const payload = report;
|
|
108
|
-
const rows = payload
|
|
130
|
+
const rows = selectUsageRows(payload);
|
|
109
131
|
const totals = payload?.grandTotals ?? payload?.totals;
|
|
110
132
|
if (rows.length === 0) {
|
|
111
133
|
this.log(args.harness ? 'No usage recorded for this harness.' : 'No usage recorded.');
|
|
@@ -29,6 +29,18 @@ class InstructionsStatus extends base_command_1.BaseCommand {
|
|
|
29
29
|
// only `bundleId`, so join to the bundle list — including archived/deleted,
|
|
30
30
|
// since a target can outlive its bundle. A failed lookup is non-fatal: the
|
|
31
31
|
// column falls back to the full bundle id.
|
|
32
|
+
// The live daemons, so a stalled row can tell "this machine now starts its
|
|
33
|
+
// daemon a different way" from "the daemon is gone" — and only the second
|
|
34
|
+
// earns a destructive remedy. Non-fatal: an empty list degrades to the old
|
|
35
|
+
// wording, which is a guess, not a wrong claim about a daemon we checked.
|
|
36
|
+
let liveDaemonIds = [];
|
|
37
|
+
try {
|
|
38
|
+
const daemons = await data_provider_1.dataService.getDaemons();
|
|
39
|
+
liveDaemonIds = (daemons.active || []).map((d) => d.daemonId);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Leave it empty.
|
|
43
|
+
}
|
|
32
44
|
let bundleNames = new Map();
|
|
33
45
|
try {
|
|
34
46
|
const bundleResponse = await data_provider_1.dataService.listManagedInstructionBundles({
|
|
@@ -62,7 +74,7 @@ class InstructionsStatus extends base_command_1.BaseCommand {
|
|
|
62
74
|
(target.scope === 'global' ? 'global' : target.projectPath || 'project'),
|
|
63
75
|
desired: target.desiredState,
|
|
64
76
|
applied: target.appliedVersionNumber ? `v${target.appliedVersionNumber}` : '-',
|
|
65
|
-
status: (0, instruction_input_1.describeTargetSync)(target),
|
|
77
|
+
status: (0, instruction_input_1.describeTargetSync)(target, Date.now(), liveDaemonIds),
|
|
66
78
|
})), [
|
|
67
79
|
{ key: 'id', header: 'TARGET ID' },
|
|
68
80
|
// Wide enough for a prompt name (matches `instructions list`'s NAME
|
|
@@ -41,16 +41,8 @@ class DedicatedMachinesCatalog extends dedicated_lease_command_1.DedicatedLeaseC
|
|
|
41
41
|
this.log('\nSizes (--size-preset)');
|
|
42
42
|
if (catalog.sizes.length === 0)
|
|
43
43
|
this.log(' none offered right now');
|
|
44
|
-
for (const
|
|
45
|
-
|
|
46
|
-
size.vcpus ? `${size.vcpus} vCPU` : '',
|
|
47
|
-
size.memoryGiB ? `${size.memoryGiB} GiB RAM` : '',
|
|
48
|
-
size.accelerator || '',
|
|
49
|
-
]
|
|
50
|
-
.filter(Boolean)
|
|
51
|
-
.join(' · ');
|
|
52
|
-
this.log(` ${String(size.id).padEnd(14)}${spec}`);
|
|
53
|
-
}
|
|
44
|
+
for (const line of (0, dedicated_machines_1.dedicatedCatalogSizeLines)(catalog))
|
|
45
|
+
this.log(line);
|
|
54
46
|
if (catalog.storage) {
|
|
55
47
|
const { minGb, defaultGb, maxGb } = catalog.storage;
|
|
56
48
|
this.log(`\nWorkspace (--storage-gb)\n ${minGb}–${maxGb} GiB, default ${defaultGb}; grow later, never shrink`);
|
|
@@ -64,11 +56,8 @@ class DedicatedMachinesCatalog extends dedicated_lease_command_1.DedicatedLeaseC
|
|
|
64
56
|
if (catalog.regionClasses.length) {
|
|
65
57
|
this.log(`\nRegion (--region-class)\n ${catalog.regionClasses.join(', ')}`);
|
|
66
58
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.log(' not published by this server');
|
|
70
|
-
for (const line of catalog.priceLines) {
|
|
71
|
-
this.log(` ${line.display || `${(0, dedicated_machines_1.formatUsdCents)(line.amountCents)} ${line.label || ''}`}`);
|
|
59
|
+
for (const line of (0, dedicated_machines_1.dedicatedCatalogPriceLineOutput)(catalog.priceLines, catalog.priceBookVersion)) {
|
|
60
|
+
this.log(line);
|
|
72
61
|
}
|
|
73
62
|
}
|
|
74
63
|
}
|
|
@@ -17,6 +17,14 @@ export default class DedicatedMachinesCreate extends DedicatedLeaseCommand {
|
|
|
17
17
|
workspace: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
18
18
|
'request-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
19
19
|
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
20
|
+
yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
21
|
+
'dry-run': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
20
22
|
};
|
|
21
23
|
run(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* The catalog for the summary, or null. Not through `callDedicated`, which
|
|
26
|
+
* exits on an API error: an unreachable catalog must cost the summary its
|
|
27
|
+
* detail, not stop a create the user is about to confirm anyway.
|
|
28
|
+
*/
|
|
29
|
+
private readCatalogForSummary;
|
|
22
30
|
}
|
|
@@ -4,12 +4,14 @@ const core_1 = require("@oclif/core");
|
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const dedicated_lease_command_1 = require("../../../lib/dedicated-lease-command");
|
|
6
6
|
const dedicated_machines_1 = require("../../../lib/dedicated-machines");
|
|
7
|
+
const harnesses_1 = require("../../../lib/harnesses");
|
|
7
8
|
class DedicatedMachinesCreate extends dedicated_lease_command_1.DedicatedLeaseCommand {
|
|
8
9
|
static description = 'Request a Dedicated Runtime lease. See `machines dedicated catalog` for sizes, storage bounds and prices.';
|
|
9
10
|
static examples = [
|
|
10
11
|
'<%= config.bin %> machines dedicated create --name dev-box --size-preset standard',
|
|
11
12
|
'<%= config.bin %> machines dedicated create --name dev-box --size-preset standard --spending-limit-cents 20000',
|
|
12
|
-
'<%= config.bin %> machines dedicated create --name dev-box --size-preset standard --
|
|
13
|
+
'<%= config.bin %> machines dedicated create --name dev-box --size-preset standard --dry-run',
|
|
14
|
+
'<%= config.bin %> machines dedicated create --name dev-box --size-preset standard --spending-limit-cents 20000 --repository acme/app --yes --wait --json',
|
|
13
15
|
];
|
|
14
16
|
static flags = {
|
|
15
17
|
name: core_1.Flags.string({ description: 'Display name for the dedicated machine', required: true }),
|
|
@@ -55,6 +57,13 @@ class DedicatedMachinesCreate extends dedicated_lease_command_1.DedicatedLeaseCo
|
|
|
55
57
|
description: 'Idempotency key. Reuse it to safely retry the same create request.',
|
|
56
58
|
}),
|
|
57
59
|
json: core_1.Flags.boolean({ description: 'Output as JSON' }),
|
|
60
|
+
yes: core_1.Flags.boolean({
|
|
61
|
+
char: 'y',
|
|
62
|
+
description: 'Skip the confirmation. A create starts billing, so it asks first; required when not at a terminal.',
|
|
63
|
+
}),
|
|
64
|
+
'dry-run': core_1.Flags.boolean({
|
|
65
|
+
description: 'Show the size, price, storage, retention and cap this create would use, and create nothing',
|
|
66
|
+
}),
|
|
58
67
|
...dedicated_lease_command_1.dedicatedWaitFlags,
|
|
59
68
|
};
|
|
60
69
|
async run() {
|
|
@@ -63,11 +72,15 @@ class DedicatedMachinesCreate extends dedicated_lease_command_1.DedicatedLeaseCo
|
|
|
63
72
|
if (flags.repository !== undefined && !(0, dedicated_machines_1.isDedicatedRepositoryRef)(flags.repository)) {
|
|
64
73
|
this.error(`--repository must look like owner/repo, got "${flags.repository}".`, { exit: 2 });
|
|
65
74
|
}
|
|
75
|
+
// The server trims the preset, so ' standard' buys a standard machine; the
|
|
76
|
+
// preview must judge the same value, or it calls a size it is about to buy
|
|
77
|
+
// "not in the catalog" and then buys it anyway (OSK-9592).
|
|
78
|
+
const sizePreset = flags['size-preset'].trim();
|
|
66
79
|
const requestId = flags['request-id'] || newRequestId();
|
|
67
80
|
const body = (0, dedicated_machines_1.dedicatedRuntimeCreateInput)({
|
|
68
81
|
requestId,
|
|
69
82
|
displayName: flags.name,
|
|
70
|
-
sizePreset
|
|
83
|
+
sizePreset,
|
|
71
84
|
monthlySpendingLimitCents: flags['spending-limit-cents'],
|
|
72
85
|
regionClass: flags['region-class'],
|
|
73
86
|
storageGb: flags['storage-gb'],
|
|
@@ -76,6 +89,40 @@ class DedicatedMachinesCreate extends dedicated_lease_command_1.DedicatedLeaseCo
|
|
|
76
89
|
repositoryRef: flags.repository,
|
|
77
90
|
workspaceId: flags.workspace,
|
|
78
91
|
});
|
|
92
|
+
// Say what is about to be bought before it is, in the catalog's own numbers.
|
|
93
|
+
// `destroy` has always asked first; the command that starts a meter did not
|
|
94
|
+
// (OSK-9592). A catalog read that fails only loses the summary's detail,
|
|
95
|
+
// never the confirmation itself.
|
|
96
|
+
if (flags['dry-run'] || !flags.yes) {
|
|
97
|
+
const catalog = await this.readCatalogForSummary();
|
|
98
|
+
const summary = (0, dedicated_machines_1.dedicatedCreateSummary)(catalog, {
|
|
99
|
+
name: flags.name,
|
|
100
|
+
sizePreset,
|
|
101
|
+
storageGb: flags['storage-gb'],
|
|
102
|
+
retentionClass: flags['retention-class'],
|
|
103
|
+
spendingLimitCents: flags['spending-limit-cents'],
|
|
104
|
+
});
|
|
105
|
+
if (flags['dry-run']) {
|
|
106
|
+
if (flags.json) {
|
|
107
|
+
this.log(JSON.stringify({ dryRun: true, request: body, summary }, null, 2));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
for (const line of summary)
|
|
111
|
+
this.log(line);
|
|
112
|
+
this.log('Dry run: nothing was created.');
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!process.stdin.isTTY) {
|
|
117
|
+
this.error('Creating a Dedicated Runtime starts billing. Pass --yes to confirm when not at a terminal, or --dry-run to see what it would create.', { exit: 2 });
|
|
118
|
+
}
|
|
119
|
+
for (const line of summary)
|
|
120
|
+
this.log(line);
|
|
121
|
+
if (!(await this.confirm('Create it? [y/N] '))) {
|
|
122
|
+
this.log('Aborted. Nothing was created.');
|
|
123
|
+
this.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
79
126
|
const response = await this.callDedicated(() => (0, dedicated_machines_1.createDedicatedRuntime)(body));
|
|
80
127
|
const lease = response?.lease;
|
|
81
128
|
const id = (0, dedicated_machines_1.dedicatedLeaseId)(lease);
|
|
@@ -97,6 +144,20 @@ class DedicatedMachinesCreate extends dedicated_lease_command_1.DedicatedLeaseCo
|
|
|
97
144
|
this.log(`Poll: ${this.config.bin} machines dedicated show ${id} (or pass --wait)`);
|
|
98
145
|
}
|
|
99
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* The catalog for the summary, or null. Not through `callDedicated`, which
|
|
149
|
+
* exits on an API error: an unreachable catalog must cost the summary its
|
|
150
|
+
* detail, not stop a create the user is about to confirm anyway.
|
|
151
|
+
*/
|
|
152
|
+
async readCatalogForSummary() {
|
|
153
|
+
try {
|
|
154
|
+
return (0, dedicated_machines_1.dedicatedCatalogFromProducts)(await (0, harnesses_1.listHarnessProducts)());
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
process.stderr.write(`Could not read the catalog for the summary (${error?.message || 'unknown error'}); showing what was passed.\n`);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
100
161
|
}
|
|
101
162
|
exports.default = DedicatedMachinesCreate;
|
|
102
163
|
function newRequestId() {
|
|
@@ -45,12 +45,17 @@ class DedicatedMachinesDestroy extends dedicated_lease_command_1.DedicatedLeaseC
|
|
|
45
45
|
// so it does not ask. This mirrors the server's rule only to decide whether
|
|
46
46
|
// to PROMPT; the server still decides what happens.
|
|
47
47
|
let willPurge = Boolean(flags['purge-storage']);
|
|
48
|
+
// Read even under --yes when the workspace may be kept: whether a destroy
|
|
49
|
+
// keeps billing storage is worth saying afterwards, and only the lease as it
|
|
50
|
+
// stood before the destroy can say it.
|
|
51
|
+
const prior = willPurge
|
|
52
|
+
? undefined
|
|
53
|
+
: (await this.callDedicated(() => (0, dedicated_machines_1.getDedicatedRuntime)(leaseId, this.scopeFrom(flags))))
|
|
54
|
+
?.lease;
|
|
48
55
|
if (!willPurge && !flags.yes) {
|
|
49
|
-
const current = await this.callDedicated(() => (0, dedicated_machines_1.getDedicatedRuntime)(leaseId, this.scopeFrom(flags)));
|
|
50
|
-
const lease = current?.lease;
|
|
51
56
|
willPurge =
|
|
52
|
-
|
|
53
|
-
(!flags['retain-storage'] && (
|
|
57
|
+
prior?.state === 'archived' ||
|
|
58
|
+
(!flags['retain-storage'] && (prior?.storage?.retentionClass || 'none') === 'none');
|
|
54
59
|
}
|
|
55
60
|
if (willPurge && !flags.yes) {
|
|
56
61
|
if (!process.stdin.isTTY) {
|
|
@@ -68,7 +73,16 @@ class DedicatedMachinesDestroy extends dedicated_lease_command_1.DedicatedLeaseC
|
|
|
68
73
|
else if (flags['retain-storage'])
|
|
69
74
|
retentionIntent = 'retain';
|
|
70
75
|
const response = await this.callDedicated(() => (0, dedicated_machines_1.destroyDedicatedRuntime)(leaseId, { ...this.scopeFrom(flags), retentionIntent }));
|
|
71
|
-
|
|
76
|
+
const note = (0, dedicated_machines_1.dedicatedDestroyRetentionNote)(prior, {
|
|
77
|
+
purge: Boolean(flags['purge-storage']),
|
|
78
|
+
retain: Boolean(flags['retain-storage']),
|
|
79
|
+
bin: this.config.bin,
|
|
80
|
+
});
|
|
81
|
+
// A script reading --json is exactly the caller that never sees a printed
|
|
82
|
+
// line, so the kept-and-billing fact rides in the document too (OSK-9593).
|
|
83
|
+
await this.reportLeaseAction(response, flags, { kind: 'destroyed' }, 'Destroying', note ? { storageNote: note } : {});
|
|
84
|
+
if (!flags.json && note)
|
|
85
|
+
this.log(note);
|
|
72
86
|
}
|
|
73
87
|
}
|
|
74
88
|
exports.default = DedicatedMachinesDestroy;
|
|
@@ -55,6 +55,8 @@ class DedicatedMachinesExec extends base_command_1.BaseCommand {
|
|
|
55
55
|
// What a script depends on, in the help a script author reads (OSK-8703).
|
|
56
56
|
static description = [
|
|
57
57
|
'Runs the command once, as the workload user, in /workspace or --cwd, through a shell with no TTY. ' +
|
|
58
|
+
'The workload user can read /workspace but writes only under user-data/ (its HOME) and repos/, so a ' +
|
|
59
|
+
'command that writes to its starting directory needs --cwd user-data or a path under one of those. ' +
|
|
58
60
|
'Give it after -- as words (each one quoted for you), or as one shell string with --command; not both. ' +
|
|
59
61
|
'The agent shell policy does not apply. A start whose answer is lost is followed up under the same ' +
|
|
60
62
|
'request id, never run a second time.',
|
|
@@ -95,7 +97,7 @@ class DedicatedMachinesExec extends base_command_1.BaseCommand {
|
|
|
95
97
|
description: 'Command to run as one shell string, instead of the words after --',
|
|
96
98
|
}),
|
|
97
99
|
cwd: core_1.Flags.string({
|
|
98
|
-
description: 'Working directory, relative to /workspace (for example repos/app)',
|
|
100
|
+
description: 'Working directory, relative to /workspace (for example repos/app or user-data). /workspace itself is not writable by the workload user.',
|
|
99
101
|
}),
|
|
100
102
|
timeout: core_1.Flags.string({
|
|
101
103
|
description: `How long to let it run, such as 30s or 5m (default 2m, max ${dedicated_machines_1.DEDICATED_EXEC_MAX_TIMEOUT_MS / 60000}m). The process group is killed at the deadline.`,
|
|
@@ -40,9 +40,14 @@ class DedicatedMachinesList extends dedicated_lease_command_1.DedicatedLeaseComm
|
|
|
40
40
|
}
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
|
+
const scopeSpendCents = (0, dedicated_machines_1.dedicatedScopeMonthSpendCents)(leases);
|
|
44
|
+
if (scopeSpendCents !== null) {
|
|
45
|
+
this.log(`Dedicated Runtime spend this month, across every machine listed: ${(0, dedicated_machines_1.formatUsdCents)(scopeSpendCents)}`);
|
|
46
|
+
this.log('');
|
|
47
|
+
}
|
|
43
48
|
(0, format_1.renderTable)(leases.map((lease) => ({
|
|
44
49
|
id: (0, format_1.formatCell)((0, dedicated_machines_1.dedicatedLeaseId)(lease)),
|
|
45
|
-
status: (0, format_1.formatCell)((0, dedicated_machines_1.
|
|
50
|
+
status: (0, format_1.formatCell)((0, dedicated_machines_1.dedicatedLeaseStatusCell)(lease)),
|
|
46
51
|
name: (0, format_1.formatCell)((0, dedicated_machines_1.dedicatedLeaseDisplayName)(lease)),
|
|
47
52
|
size: (0, format_1.formatCell)([
|
|
48
53
|
lease.resources?.sizePreset,
|
|
@@ -50,17 +55,15 @@ class DedicatedMachinesList extends dedicated_lease_command_1.DedicatedLeaseComm
|
|
|
50
55
|
]
|
|
51
56
|
.filter(Boolean)
|
|
52
57
|
.join(' ') || '-'),
|
|
53
|
-
|
|
54
|
-
? `${(0, dedicated_machines_1.formatUsdCents)(lease.billing.currentMonthSpendCents)}/${(0, dedicated_machines_1.formatUsdCents)(lease.billing.monthlySpendLimitCents)}${lease.billing.spendingLimitExceeded ? ' OVER' : ''}`
|
|
55
|
-
: '-'),
|
|
58
|
+
cap: (0, format_1.formatCell)((0, dedicated_machines_1.dedicatedLeaseCapCell)(lease)),
|
|
56
59
|
retention: (0, format_1.formatCell)(lease.storage?.retentionClass || '-'),
|
|
57
60
|
backup: (0, format_1.formatCell)(lease.storage?.lastBackupAt || '-'),
|
|
58
61
|
})), [
|
|
59
62
|
{ key: 'id', header: 'ID' },
|
|
60
|
-
{ key: 'status', header: 'STATUS' },
|
|
63
|
+
{ key: 'status', header: 'STATUS', maxWidth: 28 },
|
|
61
64
|
{ key: 'name', header: 'NAME', maxWidth: 28 },
|
|
62
65
|
{ key: 'size', header: 'SIZE', maxWidth: 18 },
|
|
63
|
-
{ key: '
|
|
66
|
+
{ key: 'cap', header: 'MONTHLY CAP', maxWidth: 24 },
|
|
64
67
|
{ key: 'retention', header: 'RETENTION', maxWidth: 14 },
|
|
65
68
|
{ key: 'backup', header: 'LAST BACKUP', maxWidth: 24 },
|
|
66
69
|
], (line) => this.log(line));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DedicatedLeaseCommand } from '../../../lib/dedicated-lease-command';
|
|
2
|
+
/**
|
|
3
|
+
* `skrr machines dedicated price-book` — the operator's answer to "under what
|
|
4
|
+
* policy, and at what margin, is each tier sold".
|
|
5
|
+
*
|
|
6
|
+
* `catalog` is the customer view: prices and the book version, never a cost or
|
|
7
|
+
* a policy. This is admin-only on the server, because costs and margins are not
|
|
8
|
+
* customer facts.
|
|
9
|
+
*/
|
|
10
|
+
export default class DedicatedMachinesPriceBook extends DedicatedLeaseCommand {
|
|
11
|
+
static description: string;
|
|
12
|
+
static examples: string[];
|
|
13
|
+
static flags: {
|
|
14
|
+
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@oclif/core");
|
|
4
|
+
const dedicated_lease_command_1 = require("../../../lib/dedicated-lease-command");
|
|
5
|
+
const dedicated_machines_1 = require("../../../lib/dedicated-machines");
|
|
6
|
+
/**
|
|
7
|
+
* `skrr machines dedicated price-book` — the operator's answer to "under what
|
|
8
|
+
* policy, and at what margin, is each tier sold".
|
|
9
|
+
*
|
|
10
|
+
* `catalog` is the customer view: prices and the book version, never a cost or
|
|
11
|
+
* a policy. This is admin-only on the server, because costs and margins are not
|
|
12
|
+
* customer facts.
|
|
13
|
+
*/
|
|
14
|
+
class DedicatedMachinesPriceBook extends dedicated_lease_command_1.DedicatedLeaseCommand {
|
|
15
|
+
static description = 'Show the Dedicated Runtime price book an operator audits: version, pricing policy, and each tier rate, cost and margin (admin only)';
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> machines dedicated price-book',
|
|
18
|
+
'<%= config.bin %> machines dedicated price-book --json',
|
|
19
|
+
];
|
|
20
|
+
static flags = {
|
|
21
|
+
json: core_1.Flags.boolean({ description: 'Output as JSON' }),
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
this.requireAuth();
|
|
25
|
+
const { flags } = await this.parse(DedicatedMachinesPriceBook);
|
|
26
|
+
const read = await this.callDedicated(() => (0, dedicated_machines_1.getDedicatedOperatorPriceBook)());
|
|
27
|
+
if (flags.json) {
|
|
28
|
+
this.log(JSON.stringify(read, null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (const line of (0, dedicated_machines_1.dedicatedOperatorPriceBookLines)(read))
|
|
32
|
+
this.log(line);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.default = DedicatedMachinesPriceBook;
|
|
@@ -61,6 +61,18 @@ class HostedMachinesList extends base_command_1.BaseCommand {
|
|
|
61
61
|
{ key: 'repos', header: 'REPOS', maxWidth: 36 },
|
|
62
62
|
{ key: 'preview', header: 'PREVIEW', maxWidth: 48 },
|
|
63
63
|
], (line) => this.log(line));
|
|
64
|
+
// How to find the same box in the other list. `machines list` identifies a
|
|
65
|
+
// hosted machine by `lease:<leaseId>` — a different random string from the
|
|
66
|
+
// `sbs_` id in the ID column above — so side by side the two commands read
|
|
67
|
+
// as two unrelated machines. Saying the mapping once, where both values are
|
|
68
|
+
// already on screen, is cheaper than a column nobody can interpret.
|
|
69
|
+
const withLease = sessions.filter((session) => session.machineLeaseId);
|
|
70
|
+
if (withLease.length > 0) {
|
|
71
|
+
this.log('');
|
|
72
|
+
this.log(`In \`${this.config.bin} machines list\` these appear under their LEASE id as ` +
|
|
73
|
+
`\`lease:<lease>\` — e.g. \`lease:${String(withLease[0].machineLeaseId)}\`. ` +
|
|
74
|
+
`\`machines show\` takes either.`);
|
|
75
|
+
}
|
|
64
76
|
}
|
|
65
77
|
/**
|
|
66
78
|
* OSK-2045 — this command lists Hosted Machine *sessions* and nothing else,
|
|
@@ -78,6 +78,17 @@ class MachinesList extends base_command_1.BaseCommand {
|
|
|
78
78
|
{ key: 'platform', header: 'PLATFORM' },
|
|
79
79
|
{ key: 'seen', header: 'LAST SEEN' },
|
|
80
80
|
], (line) => this.log(line));
|
|
81
|
+
// Why a machine is not `online`, under the table. `degraded` on its own is
|
|
82
|
+
// unactionable — the dogfood note on this output was "I still do not know
|
|
83
|
+
// whether something is wrong with my laptop" — and it is usually one named
|
|
84
|
+
// row, not the machine.
|
|
85
|
+
const explained = shown.filter((m) => m.status !== 'online' && m.statusReason);
|
|
86
|
+
if (explained.length > 0) {
|
|
87
|
+
this.log('');
|
|
88
|
+
for (const m of explained) {
|
|
89
|
+
this.log(` ${m.preferences?.name || m.name}: ${m.statusReason}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
81
92
|
const hidden = all.length - shown.length;
|
|
82
93
|
if (hidden > 0) {
|
|
83
94
|
this.log('');
|
|
@@ -9,6 +9,7 @@ class MachinesShow extends base_command_1.BaseCommand {
|
|
|
9
9
|
static description = 'Show one machine — its identity, harnesses, and available actions';
|
|
10
10
|
static examples = [
|
|
11
11
|
'<%= config.bin %> machines show 03548116',
|
|
12
|
+
'<%= config.bin %> machines show 03548116 --all',
|
|
12
13
|
'<%= config.bin %> machines show "frankdu@DS-3" --json',
|
|
13
14
|
];
|
|
14
15
|
static args = {
|
|
@@ -18,7 +19,13 @@ class MachinesShow extends base_command_1.BaseCommand {
|
|
|
18
19
|
required: true,
|
|
19
20
|
}),
|
|
20
21
|
};
|
|
21
|
-
static flags = {
|
|
22
|
+
static flags = {
|
|
23
|
+
json: core_1.Flags.boolean({ description: 'Output as JSON' }),
|
|
24
|
+
all: core_1.Flags.boolean({
|
|
25
|
+
description: 'Include harness registrations superseded by newer ones',
|
|
26
|
+
default: false,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
22
29
|
async run() {
|
|
23
30
|
this.requireAuth();
|
|
24
31
|
const { args, flags } = await this.parse(MachinesShow);
|
|
@@ -43,6 +50,10 @@ class MachinesShow extends base_command_1.BaseCommand {
|
|
|
43
50
|
this.log(` id ${machine.id}`);
|
|
44
51
|
this.log(` identity ${machine.identityKind}`);
|
|
45
52
|
this.log(` status ${machine.status}`);
|
|
53
|
+
// The status alone does not say what to do. Naming the rows that decided it
|
|
54
|
+
// is the actionable half — bring that provider back, or forget the row.
|
|
55
|
+
if (machine.statusReason)
|
|
56
|
+
this.log(` why ${machine.statusReason}`);
|
|
46
57
|
if (machine.machineUuid)
|
|
47
58
|
this.log(` machineUuid ${machine.machineUuid}`);
|
|
48
59
|
if (machine.daemonId)
|
|
@@ -60,20 +71,34 @@ class MachinesShow extends base_command_1.BaseCommand {
|
|
|
60
71
|
const harnesses = machine.harnesses ?? [];
|
|
61
72
|
const live = (0, machines_1.liveHarnessCount)(machine);
|
|
62
73
|
const superseded = harnesses.length - live;
|
|
74
|
+
// Superseded registrations are history, not capabilities, and they are not
|
|
75
|
+
// counted as missing ones next to a status of `online`. They are hidden by
|
|
76
|
+
// default, the convention `machines list` already set for dead machines:
|
|
77
|
+
// printed, they were 35 of a machine's 42 rows, back two months, under a
|
|
78
|
+
// header whose count read like a summary of rows it then printed anyway
|
|
79
|
+
// (OSK-9389). `--all` shows them, and `--json` always carries every row.
|
|
80
|
+
const shown = flags.all ? harnesses : harnesses.filter((h) => !h.superseded);
|
|
63
81
|
this.log('');
|
|
64
|
-
// The superseded rows are still listed — labelled, not dropped, so they can
|
|
65
|
-
// be seen and reaped — but they are not missing capabilities and must not
|
|
66
|
-
// be counted as such next to a status of `online`.
|
|
67
82
|
this.log(`Harnesses (${machine.onlineHarnessCount}/${live} online` +
|
|
68
|
-
(superseded > 0 ? `, ${superseded} superseded by newer registrations` : '') +
|
|
83
|
+
(flags.all && superseded > 0 ? `, ${superseded} superseded by newer registrations` : '') +
|
|
69
84
|
')');
|
|
70
|
-
if (
|
|
71
|
-
(0, format_1.renderTable)(
|
|
85
|
+
if (shown.length > 0) {
|
|
86
|
+
(0, format_1.renderTable)(shown.map((h) => ({
|
|
72
87
|
id: (0, format_1.formatCell)(h.id),
|
|
73
88
|
provider: (0, format_1.formatCell)(h.provider),
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
89
|
+
// Liveness only. Whether a newer registration replaced the row is a
|
|
90
|
+
// different fact — a superseded row can have been online — so it
|
|
91
|
+
// gets its own column rather than overwriting this one.
|
|
92
|
+
status: (0, format_1.formatCell)(h.status),
|
|
93
|
+
registration: h.superseded ? 'superseded' : 'current',
|
|
94
|
+
// `authStatus` is the DAEMON's standing with skrr, not this
|
|
95
|
+
// provider's standing with its vendor. It is written across every row
|
|
96
|
+
// of a daemon at once (`HarnessService.markAuthOkAfterRegister` does
|
|
97
|
+
// one `updateMany({owner, daemonId})`), so it is structurally the
|
|
98
|
+
// same value for all of them and can never answer "is this harness
|
|
99
|
+
// signed in". Under a bare `AUTH` header it read as the second
|
|
100
|
+
// question and answered `ok` for a Devin whose own CLI said
|
|
101
|
+
// "Not logged in." The header now names what the value is.
|
|
77
102
|
auth: (0, format_1.formatCell)(h.authStatus),
|
|
78
103
|
seen: h.lastSeenAt
|
|
79
104
|
? new Date(h.lastSeenAt).toISOString().slice(0, 16).replace('T', ' ')
|
|
@@ -82,10 +107,14 @@ class MachinesShow extends base_command_1.BaseCommand {
|
|
|
82
107
|
{ key: 'id', header: 'HARNESS ID' },
|
|
83
108
|
{ key: 'provider', header: 'PROVIDER' },
|
|
84
109
|
{ key: 'status', header: 'STATUS' },
|
|
85
|
-
{ key: '
|
|
110
|
+
...(flags.all ? [{ key: 'registration', header: 'REGISTRATION' }] : []),
|
|
111
|
+
{ key: 'auth', header: 'DAEMON AUTH' },
|
|
86
112
|
{ key: 'seen', header: 'LAST SEEN' },
|
|
87
113
|
], (line) => this.log(` ${line}`));
|
|
88
114
|
}
|
|
115
|
+
if (!flags.all && superseded > 0) {
|
|
116
|
+
this.log(` ${superseded} superseded registration(s) hidden — pass --all to include them.`);
|
|
117
|
+
}
|
|
89
118
|
// Print what is PERMITTED, not the whole control map: a list of six
|
|
90
119
|
// booleans mostly reading false is noise, and the useful question is
|
|
91
120
|
// "what can I do to this machine right now".
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command';
|
|
2
|
+
/**
|
|
3
|
+
* Which stored sessions exist on this machine.
|
|
4
|
+
*
|
|
5
|
+
* Every command advertises `--profile` and nothing could tell you what to pass
|
|
6
|
+
* it. Profiles are additive and stored on disk — the default at
|
|
7
|
+
* `<root>/cli-config.json`, named ones at `<root>/profiles/<name>/cli-config.json`
|
|
8
|
+
* — so the list was always knowable; there was simply no way to ask.
|
|
9
|
+
*
|
|
10
|
+
* That is not only a discoverability nit. A second profile is how one machine
|
|
11
|
+
* holds a second identity, which is what any test of a NON-OWNER path needs. A
|
|
12
|
+
* dogfood run of the `force`-past-a-required-Review flow stalled on exactly
|
|
13
|
+
* this: the flag was there, the second identity was reachable, and nothing named
|
|
14
|
+
* it.
|
|
15
|
+
*/
|
|
16
|
+
export default class ProfilesList extends BaseCommand {
|
|
17
|
+
static description: string;
|
|
18
|
+
static examples: string[];
|
|
19
|
+
static flags: {
|
|
20
|
+
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
21
|
+
};
|
|
22
|
+
run(): Promise<void>;
|
|
23
|
+
}
|