@skrr-ai/cli 0.1.38 → 0.1.40
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 +1 -1
- package/bin/dev-fallback.js +8 -0
- package/dist/base-command.js +8 -0
- package/dist/commands/agents/create.d.ts +7 -0
- package/dist/commands/agents/create.js +13 -1
- package/dist/commands/agents/triggers/policy.d.ts +1 -0
- package/dist/commands/agents/triggers/policy.js +32 -0
- package/dist/commands/agents/update.js +4 -3
- package/dist/commands/followups/observability.js +6 -2
- package/dist/commands/followups/remind.js +5 -2
- package/dist/commands/followups/reschedule.js +2 -1
- package/dist/commands/followups/show.js +4 -0
- package/dist/commands/followups/watch.d.ts +1 -0
- package/dist/commands/followups/watch.js +9 -3
- package/dist/commands/machines/dedicated/create.js +6 -2
- package/dist/commands/machines/dedicated/destroy.js +10 -10
- package/dist/commands/machines/dedicated/exec.js +3 -1
- package/dist/commands/machines/dedicated/price-book.d.ts +17 -0
- package/dist/commands/machines/dedicated/price-book.js +35 -0
- package/dist/commands/tasks/lifecycle/detach.js +26 -5
- package/dist/lib/dedicated-lease-command.d.ts +3 -1
- package/dist/lib/dedicated-lease-command.js +8 -3
- package/dist/lib/dedicated-machines.d.ts +48 -1
- package/dist/lib/dedicated-machines.js +93 -7
- package/dist/lib/dedicated-wait.js +13 -0
- package/dist/lib/followups.d.ts +8 -0
- package/dist/lib/label-ref.d.ts +8 -1
- package/dist/lib/label-ref.js +12 -2
- package/dist/lib/option-hint.d.ts +15 -0
- package/dist/lib/option-hint.js +75 -0
- package/dist/lib/workspaces.d.ts +3 -0
- package/dist/lib/workspaces.js +38 -0
- package/oclif.manifest.json +4952 -4885
- package/package.json +5 -3
|
@@ -5,6 +5,8 @@ exports.describeDedicatedImageStanding = describeDedicatedImageStanding;
|
|
|
5
5
|
exports.dedicatedRuntimeCreateInput = dedicatedRuntimeCreateInput;
|
|
6
6
|
exports.isDedicatedRepositoryRef = isDedicatedRepositoryRef;
|
|
7
7
|
exports.createDedicatedRuntime = createDedicatedRuntime;
|
|
8
|
+
exports.getDedicatedOperatorPriceBook = getDedicatedOperatorPriceBook;
|
|
9
|
+
exports.dedicatedOperatorPriceBookLines = dedicatedOperatorPriceBookLines;
|
|
8
10
|
exports.listDedicatedRuntimes = listDedicatedRuntimes;
|
|
9
11
|
exports.getDedicatedRuntime = getDedicatedRuntime;
|
|
10
12
|
exports.performDedicatedLeaseAction = performDedicatedLeaseAction;
|
|
@@ -157,6 +159,59 @@ async function createDedicatedRuntime(input) {
|
|
|
157
159
|
const { requestId, workspaceId, ...body } = input;
|
|
158
160
|
return (0, api_fetch_1.apiFetch)(withDedicatedScope('/api/machines/dedicated', { workspaceId }), { method: 'POST', body, headers: { 'Idempotency-Key': requestId } });
|
|
159
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* The Dedicated Runtime price book as an operator reads it: which version and
|
|
164
|
+
* which pricing policy, and each tier's rate, cost and margin. The customer
|
|
165
|
+
* catalog carries none of the policy or cost facts, and the admin plane that
|
|
166
|
+
* also serves this is not deployed in production (OSK-9562).
|
|
167
|
+
*/
|
|
168
|
+
async function getDedicatedOperatorPriceBook() {
|
|
169
|
+
return (0, api_fetch_1.apiFetch)('/api/machines/dedicated/price-book');
|
|
170
|
+
}
|
|
171
|
+
function formatBps(bps) {
|
|
172
|
+
return bps === null ? 'unknown' : `${(bps / 100).toFixed(1)}%`;
|
|
173
|
+
}
|
|
174
|
+
function formatRateCents(cents) {
|
|
175
|
+
return cents === null ? 'none' : `$${(cents / 100).toFixed(2)}/hr`;
|
|
176
|
+
}
|
|
177
|
+
/** Human lines for `machines dedicated price-book`. Pure, so it can be pinned. */
|
|
178
|
+
function dedicatedOperatorPriceBookLines(read) {
|
|
179
|
+
const book = read.priceBook;
|
|
180
|
+
const lines = [
|
|
181
|
+
`Dedicated Runtime price book — ${book.approved ? 'approved' : 'NOT approved'}${book.required ? '' : ' (not required here)'}`,
|
|
182
|
+
` version ${book.version ?? 'not recorded'}`,
|
|
183
|
+
` policy ${book.pricingPolicyRef ?? 'not recorded'}`,
|
|
184
|
+
` target margin ${formatBps(book.targetGrossMarginBps)}`,
|
|
185
|
+
];
|
|
186
|
+
if (!book.approved && book.missingReason)
|
|
187
|
+
lines.push(` why ${book.missingReason}`);
|
|
188
|
+
if (read.availability === 'disabled' && read.disabledReason) {
|
|
189
|
+
lines.push(` product disabled: ${read.disabledReason}`);
|
|
190
|
+
}
|
|
191
|
+
if (read.configError)
|
|
192
|
+
lines.push(` config unreadable: ${read.configError}`);
|
|
193
|
+
lines.push('', 'Offered tiers');
|
|
194
|
+
if (book.presetChecks.length === 0)
|
|
195
|
+
lines.push(' none');
|
|
196
|
+
for (const check of book.presetChecks) {
|
|
197
|
+
const verdict = !check.priced
|
|
198
|
+
? 'UNPRICED'
|
|
199
|
+
: !check.providerCostRecorded
|
|
200
|
+
? 'UNCOSTED'
|
|
201
|
+
: check.marginTargetMet
|
|
202
|
+
? 'meets target'
|
|
203
|
+
: 'BELOW TARGET';
|
|
204
|
+
lines.push(` ${check.sizePreset.padEnd(12)} rate ${formatRateCents(check.customerRateCentsPerHour)} · cost ${formatRateCents(check.providerCostCentsPerHour)} · margin ${formatBps(check.grossMarginBps)} · ${verdict}`);
|
|
205
|
+
}
|
|
206
|
+
const withheld = read.sizeAvailability.filter((entry) => !entry.offered);
|
|
207
|
+
if (withheld.length > 0) {
|
|
208
|
+
lines.push('', 'Withheld tiers');
|
|
209
|
+
for (const entry of withheld) {
|
|
210
|
+
lines.push(` ${entry.id.padEnd(12)} ${entry.withheldReason ?? 'no reason recorded'}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return lines;
|
|
214
|
+
}
|
|
160
215
|
async function listDedicatedRuntimes(options = {}) {
|
|
161
216
|
const query = new URLSearchParams();
|
|
162
217
|
if (options.limit)
|
|
@@ -411,10 +466,25 @@ function dedicatedSpendSummary(lease) {
|
|
|
411
466
|
parts.push(`projected ${formatUsdCents(projected)}`);
|
|
412
467
|
}
|
|
413
468
|
if (billing.spendingLimitExceeded) {
|
|
414
|
-
parts.push(
|
|
469
|
+
parts.push(`OVER CAP — ${overCapComputeClause(lease.state)}, files are kept`);
|
|
415
470
|
}
|
|
416
471
|
return parts.join(' · ');
|
|
417
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* What the cap has done to compute, in the tense the lease is actually in.
|
|
475
|
+
*
|
|
476
|
+
* This always said "compute is stopped", including for a machine that was still
|
|
477
|
+
* `requested` — which, over its cap, never provisions and cannot have its cap
|
|
478
|
+
* raised until it settles (OSK-9836). The reconciler stops compute for exactly
|
|
479
|
+
* `stopping`, `stopped` and `archived`; anything else is only on its way there.
|
|
480
|
+
*/
|
|
481
|
+
function overCapComputeClause(state) {
|
|
482
|
+
if (state === 'stopped' || state === 'archived')
|
|
483
|
+
return 'compute is stopped';
|
|
484
|
+
if (state === 'stopping')
|
|
485
|
+
return 'compute is stopping';
|
|
486
|
+
return 'compute will be stopped';
|
|
487
|
+
}
|
|
418
488
|
/**
|
|
419
489
|
* The human rendering of `machines dedicated show`, as lines.
|
|
420
490
|
*
|
|
@@ -1064,9 +1134,9 @@ function dedicatedRuntimeApiErrorDetails(err) {
|
|
|
1064
1134
|
* say nothing to the person running it (OSK-8768); `--json` keeps them in
|
|
1065
1135
|
* `error.details.reasons`.
|
|
1066
1136
|
*/
|
|
1067
|
-
function describeRefusalReasons(err) {
|
|
1137
|
+
function describeRefusalReasons(err, context = {}) {
|
|
1068
1138
|
const details = dedicatedRuntimeApiErrorDetails(err);
|
|
1069
|
-
const spend = describeSpendingCapRefusal(err.body, details);
|
|
1139
|
+
const spend = describeSpendingCapRefusal(err.body, details, context);
|
|
1070
1140
|
if (spend)
|
|
1071
1141
|
return spend;
|
|
1072
1142
|
const sentences = (0, harnesses_1.describeHealthReasonSentences)(details?.reasons, details?.state);
|
|
@@ -1078,7 +1148,7 @@ function describeRefusalReasons(err) {
|
|
|
1078
1148
|
* the cap. Without them "would exceed the monthly cap" left a user guessing by
|
|
1079
1149
|
* how much, and which cap (OSK-9552).
|
|
1080
1150
|
*/
|
|
1081
|
-
function describeSpendingCapRefusal(body, details) {
|
|
1151
|
+
function describeSpendingCapRefusal(body, details, context = {}) {
|
|
1082
1152
|
let code;
|
|
1083
1153
|
try {
|
|
1084
1154
|
code = JSON.parse(body || '{}').code;
|
|
@@ -1102,9 +1172,25 @@ function describeSpendingCapRefusal(body, details) {
|
|
|
1102
1172
|
].filter(Boolean);
|
|
1103
1173
|
if (parts.length === 0)
|
|
1104
1174
|
return '';
|
|
1105
|
-
return ` ${parts.join('; ')}.
|
|
1175
|
+
return ` ${parts.join('; ')}. ${raiseCapHint(context)}`;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* How to get past a cap refusal, for the command that was refused. A create or
|
|
1179
|
+
* a restore makes a NEW machine and takes its cap as a flag; only an existing
|
|
1180
|
+
* machine has a lease id to raise. Every refusal used to say
|
|
1181
|
+
* `machines dedicated spending-limit <lease-id>`, which a user refused on create
|
|
1182
|
+
* had no lease id to run (OSK-9552).
|
|
1183
|
+
*/
|
|
1184
|
+
function raiseCapHint(context) {
|
|
1185
|
+
const bin = context.bin || 'skrr';
|
|
1186
|
+
const creates = context.command === 'machines:dedicated:create' ||
|
|
1187
|
+
context.command === 'machines:dedicated:restore';
|
|
1188
|
+
if (creates) {
|
|
1189
|
+
return 'Pass a higher --spending-limit-cents: the cap is compared with the whole month, not with this machine alone.';
|
|
1190
|
+
}
|
|
1191
|
+
return `Raise the cap with \`${bin} machines dedicated spending-limit ${context.leaseId || '<lease-id>'} --cents <n>\`.`;
|
|
1106
1192
|
}
|
|
1107
|
-
function formatDedicatedRuntimeApiError(err) {
|
|
1193
|
+
function formatDedicatedRuntimeApiError(err, context = {}) {
|
|
1108
1194
|
if (!(err instanceof api_fetch_1.ApiFetchError) || !err.body) {
|
|
1109
1195
|
return null;
|
|
1110
1196
|
}
|
|
@@ -1132,7 +1218,7 @@ function formatDedicatedRuntimeApiError(err) {
|
|
|
1132
1218
|
// Machine noun even though the caller named a Dedicated Runtime. Do not
|
|
1133
1219
|
// repeat that contradictory product name while a mixed deployment rolls.
|
|
1134
1220
|
const sentence = (parsed.message || parsed.error || parsed.code)?.replace(/(?:skrr )?Hosted Machine/g, 'Dedicated Runtime');
|
|
1135
|
-
return sentence ? `${sentence}${describeRefusalReasons(err)}` : null;
|
|
1221
|
+
return sentence ? `${sentence}${describeRefusalReasons(err, context)}` : null;
|
|
1136
1222
|
}
|
|
1137
1223
|
catch {
|
|
1138
1224
|
return err.body.trim() || null;
|
|
@@ -17,8 +17,21 @@ function backupIsNewer(current, previous) {
|
|
|
17
17
|
const b = Date.parse(previous);
|
|
18
18
|
return Number.isFinite(a) && Number.isFinite(b) ? a > b : current !== previous;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* `recovering` is also the state an image move replaces the machine through, on
|
|
22
|
+
* purpose. Reading every `recovering` as a failed action being retried told an
|
|
23
|
+
* owner watching an ordinary `update-image --wait` that destroy was available
|
|
24
|
+
* "if it never settles".
|
|
25
|
+
*/
|
|
26
|
+
const PLANNED_REPLACEMENT_PROGRESS = {
|
|
27
|
+
dedicated_runtime_image_update: 'replacing the machine',
|
|
28
|
+
dedicated_runtime_image_update_rolling_back: 'replacing the machine again',
|
|
29
|
+
};
|
|
20
30
|
function progressOf(lease) {
|
|
21
31
|
const state = (0, dedicated_machines_1.dedicatedLeaseState)(lease);
|
|
32
|
+
const planned = lease.stateReason ? PLANNED_REPLACEMENT_PROGRESS[lease.stateReason] : undefined;
|
|
33
|
+
if (state === 'recovering' && planned)
|
|
34
|
+
return planned;
|
|
22
35
|
if (state === 'recovering') {
|
|
23
36
|
// `recovering` is where a failed provision or action lands, retried with
|
|
24
37
|
// backoff. It can settle — or never, which is why destroy is allowed from it.
|
package/dist/lib/followups.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface FollowUpRow {
|
|
|
34
34
|
terminal?: string | null;
|
|
35
35
|
terminalAt?: string | null;
|
|
36
36
|
terminalReason?: string | null;
|
|
37
|
+
/** Who settled it: `trigger_fire` for the runtime, otherwise the actor. */
|
|
38
|
+
terminalBy?: string | null;
|
|
37
39
|
escalatedInitiativeId?: string | null;
|
|
38
40
|
capsule?: unknown;
|
|
39
41
|
nextRunAt?: string | null;
|
|
@@ -62,6 +64,12 @@ export interface FollowUpObservability {
|
|
|
62
64
|
terminal: Record<string, number>;
|
|
63
65
|
escalationRate: number | null;
|
|
64
66
|
fires: number;
|
|
67
|
+
/** USD from the fires' UsageLog rows; null when the ledger could not be read. */
|
|
68
|
+
spend?: {
|
|
69
|
+
today: number;
|
|
70
|
+
total: number;
|
|
71
|
+
currency: string;
|
|
72
|
+
} | null;
|
|
65
73
|
budget: {
|
|
66
74
|
pool: string | null;
|
|
67
75
|
used: number;
|
package/dist/lib/label-ref.d.ts
CHANGED
|
@@ -59,12 +59,19 @@ export declare function closestNames(typed: string, names: string[], limit?: num
|
|
|
59
59
|
export interface LabelVocabularyContext {
|
|
60
60
|
/** Narrow to labels attachable to this entity type. Omit for all of them. */
|
|
61
61
|
applicableTo?: LabelApplicable;
|
|
62
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Narrows to this workspace scope. When omitted the AMBIENT workspace is
|
|
64
|
+
* resolved (`--workspace` / OVERSKY_WORKSPACE_ID / the server's default
|
|
65
|
+
* workspace): a session that never selected a workspace still writes to
|
|
66
|
+
* one, and a vocabulary that cannot see it refuses names that exist.
|
|
67
|
+
*/
|
|
63
68
|
workspaceId?: string;
|
|
64
69
|
/** Archived labels are excluded unless asked for. */
|
|
65
70
|
includeArchived?: boolean;
|
|
66
71
|
/** Injected in tests. */
|
|
67
72
|
getLabels?: typeof dataService.getLabels;
|
|
73
|
+
/** Injected in tests; defaults to `workspaces.ambientWorkspaceId`. */
|
|
74
|
+
resolveWorkspaceId?: () => Promise<string | undefined>;
|
|
68
75
|
}
|
|
69
76
|
export interface LabelRefContext extends LabelVocabularyContext {
|
|
70
77
|
/** Which entity the resolved labels will be attached to. */
|
package/dist/lib/label-ref.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.closestNames = closestNames;
|
|
|
39
39
|
exports.collectLabelVocabulary = collectLabelVocabulary;
|
|
40
40
|
exports.resolveLabelRefs = resolveLabelRefs;
|
|
41
41
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
42
|
+
const workspaces_1 = require("./workspaces");
|
|
42
43
|
/** A canonical v4 uuid, which is what every label id is. */
|
|
43
44
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
44
45
|
function looksLikeLabelId(ref) {
|
|
@@ -89,9 +90,18 @@ function closestNames(typed, names, limit = 3) {
|
|
|
89
90
|
*/
|
|
90
91
|
async function collectLabelVocabulary(context) {
|
|
91
92
|
const getLabels = context.getLabels ?? data_provider_1.dataService.getLabels;
|
|
93
|
+
let workspaceId = context.workspaceId;
|
|
94
|
+
if (!workspaceId) {
|
|
95
|
+
try {
|
|
96
|
+
workspaceId = await (context.resolveWorkspaceId ?? workspaces_1.ambientWorkspaceId)();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
workspaceId = undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
92
102
|
const requests = [{ scope: 'user' }];
|
|
93
|
-
if (
|
|
94
|
-
requests.push({ scope: 'workspace', scopeId:
|
|
103
|
+
if (workspaceId)
|
|
104
|
+
requests.push({ scope: 'workspace', scopeId: workspaceId });
|
|
95
105
|
const settled = await Promise.allSettled(requests.map((request) => getLabels({
|
|
96
106
|
...request,
|
|
97
107
|
...(context.applicableTo ? { applicableTo: context.applicableTo } : {}),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A "did you mean" for oclif's enum refusal (OSK-9735).
|
|
3
|
+
*
|
|
4
|
+
* Tasks, spaces, goals and key results each spell one lifecycle their own way —
|
|
5
|
+
* "under way" is `in_progress` for a space and `active` for a goal; "finished"
|
|
6
|
+
* is `done`, `completed` or `achieved` — and oclif's
|
|
7
|
+
* `Expected --status=in_progress to be one of: …` was the only place a user
|
|
8
|
+
* learned that. This names the value the command uses for the word that was
|
|
9
|
+
* typed, and otherwise the nearest spelling.
|
|
10
|
+
*
|
|
11
|
+
* It suggests and never substitutes. Some of the differences are deliberate (a
|
|
12
|
+
* key result's status is a health reading, not a lifecycle), so writing a mapped
|
|
13
|
+
* value would record a status nobody chose.
|
|
14
|
+
*/
|
|
15
|
+
export declare function invalidOptionHint(message: string): string | null;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A "did you mean" for oclif's enum refusal (OSK-9735).
|
|
4
|
+
*
|
|
5
|
+
* Tasks, spaces, goals and key results each spell one lifecycle their own way —
|
|
6
|
+
* "under way" is `in_progress` for a space and `active` for a goal; "finished"
|
|
7
|
+
* is `done`, `completed` or `achieved` — and oclif's
|
|
8
|
+
* `Expected --status=in_progress to be one of: …` was the only place a user
|
|
9
|
+
* learned that. This names the value the command uses for the word that was
|
|
10
|
+
* typed, and otherwise the nearest spelling.
|
|
11
|
+
*
|
|
12
|
+
* It suggests and never substitutes. Some of the differences are deliberate (a
|
|
13
|
+
* key result's status is a health reading, not a lifecycle), so writing a mapped
|
|
14
|
+
* value would record a status nobody chose.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.invalidOptionHint = invalidOptionHint;
|
|
18
|
+
/**
|
|
19
|
+
* Spellings of one concept across the CLI's vocabularies. A value is suggested
|
|
20
|
+
* only when it shares a group with what was typed.
|
|
21
|
+
*/
|
|
22
|
+
const CONCEPT_GROUPS = [
|
|
23
|
+
new Set(['in_progress', 'inprogress', 'active', 'started', 'ongoing', 'running', 'doing']),
|
|
24
|
+
new Set(['done', 'completed', 'complete', 'achieved', 'finished', 'closed', 'resolved']),
|
|
25
|
+
new Set(['cancelled', 'canceled', 'abandoned']),
|
|
26
|
+
new Set(['in_review', 'inreview', 'review', 'reviewing']),
|
|
27
|
+
new Set(['planned', 'todo', 'to_do', 'not_started']),
|
|
28
|
+
];
|
|
29
|
+
const REFUSAL = /Expected (--[\w-]+)=(.+?) to be one of: ([^\n]+)/;
|
|
30
|
+
const normalize = (value) => value
|
|
31
|
+
.trim()
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/[\s-]+/g, '_');
|
|
34
|
+
function editDistance(a, b) {
|
|
35
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
36
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
37
|
+
let diagonal = row[0];
|
|
38
|
+
row[0] = i;
|
|
39
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
40
|
+
const above = row[j];
|
|
41
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
42
|
+
diagonal = above;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return row[b.length];
|
|
46
|
+
}
|
|
47
|
+
function invalidOptionHint(message) {
|
|
48
|
+
const match = REFUSAL.exec(message || '');
|
|
49
|
+
if (!match)
|
|
50
|
+
return null;
|
|
51
|
+
const [, flag, input, list] = match;
|
|
52
|
+
const options = list
|
|
53
|
+
.split(',')
|
|
54
|
+
.map((option) => option.trim())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
const typed = normalize(input);
|
|
57
|
+
const exact = options.find((option) => normalize(option) === typed);
|
|
58
|
+
if (exact)
|
|
59
|
+
return ` Did you mean ${flag} ${exact}?`;
|
|
60
|
+
const group = CONCEPT_GROUPS.find((concept) => concept.has(typed));
|
|
61
|
+
const synonym = group && options.find((option) => group.has(normalize(option)));
|
|
62
|
+
if (synonym) {
|
|
63
|
+
return ` Did you mean ${flag} ${synonym}? Here "${input}" is spelled "${synonym}".`;
|
|
64
|
+
}
|
|
65
|
+
let nearest = null;
|
|
66
|
+
for (const option of options) {
|
|
67
|
+
const distance = editDistance(typed, normalize(option));
|
|
68
|
+
if (!nearest || distance < nearest.distance)
|
|
69
|
+
nearest = { option, distance };
|
|
70
|
+
}
|
|
71
|
+
if (nearest && nearest.distance <= Math.max(2, Math.floor(typed.length / 4))) {
|
|
72
|
+
return ` Did you mean ${flag} ${nearest.option}?`;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
package/dist/lib/workspaces.d.ts
CHANGED
|
@@ -56,6 +56,9 @@ export interface WorkspaceListing {
|
|
|
56
56
|
*/
|
|
57
57
|
export declare function describeUnselectedWorkspace(listing: WorkspaceListing): string;
|
|
58
58
|
export declare function listWorkspaces(): Promise<WorkspaceRow[]>;
|
|
59
|
+
export declare function ambientWorkspaceId(env?: NodeJS.ProcessEnv): Promise<string | undefined>;
|
|
60
|
+
/** Test seam — a memoized network result must not leak between specs. */
|
|
61
|
+
export declare function clearAmbientWorkspaceCache(): void;
|
|
59
62
|
/** `listWorkspaces`, keeping the envelope's caller-level fields. */
|
|
60
63
|
export declare function listWorkspaceListing(): Promise<WorkspaceListing>;
|
|
61
64
|
export declare function getWorkspace(workspaceId: string): Promise<WorkspaceRow>;
|
package/dist/lib/workspaces.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.describeUnselectedWorkspace = describeUnselectedWorkspace;
|
|
4
4
|
exports.listWorkspaces = listWorkspaces;
|
|
5
|
+
exports.ambientWorkspaceId = ambientWorkspaceId;
|
|
6
|
+
exports.clearAmbientWorkspaceCache = clearAmbientWorkspaceCache;
|
|
5
7
|
exports.listWorkspaceListing = listWorkspaceListing;
|
|
6
8
|
exports.getWorkspace = getWorkspace;
|
|
7
9
|
exports.matchWorkspace = matchWorkspace;
|
|
@@ -60,6 +62,42 @@ function describeUnselectedWorkspace(listing) {
|
|
|
60
62
|
async function listWorkspaces() {
|
|
61
63
|
return (await listWorkspaceListing()).rows;
|
|
62
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* The workspace a bare session actually writes to, for callers merging the
|
|
67
|
+
* caller's vocabulary or defaults across scopes.
|
|
68
|
+
*
|
|
69
|
+
* `OVERSKY_WORKSPACE_ID` is only populated when a workspace was SELECTED —
|
|
70
|
+
* `workspaces use`, `--workspace`, or a daemon session handoff. An account
|
|
71
|
+
* that never selected one still has a server-side `defaultWorkspaceId`, and
|
|
72
|
+
* every unscoped write lands there; a caller that only read the variable
|
|
73
|
+
* would reason about a smaller world than the one the server writes into
|
|
74
|
+
* (observed: `labels list` reported a user-only vocabulary while three
|
|
75
|
+
* workspace-scoped labels existed and were attachable).
|
|
76
|
+
*
|
|
77
|
+
* The listing fetch is memoized and fail-open: this runs to give a better
|
|
78
|
+
* answer, and must never become the error — an unreachable server returns
|
|
79
|
+
* `undefined`, which is the same world the caller saw before this existed.
|
|
80
|
+
*/
|
|
81
|
+
let memoizedDefaultWorkspaceId;
|
|
82
|
+
async function ambientWorkspaceId(env = process.env) {
|
|
83
|
+
const explicit = env.OVERSKY_WORKSPACE_ID?.trim();
|
|
84
|
+
if (explicit)
|
|
85
|
+
return explicit;
|
|
86
|
+
if (memoizedDefaultWorkspaceId !== undefined) {
|
|
87
|
+
return memoizedDefaultWorkspaceId ?? undefined;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
memoizedDefaultWorkspaceId = (await listWorkspaceListing()).defaultWorkspaceId ?? null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
memoizedDefaultWorkspaceId = null;
|
|
94
|
+
}
|
|
95
|
+
return memoizedDefaultWorkspaceId ?? undefined;
|
|
96
|
+
}
|
|
97
|
+
/** Test seam — a memoized network result must not leak between specs. */
|
|
98
|
+
function clearAmbientWorkspaceCache() {
|
|
99
|
+
memoizedDefaultWorkspaceId = undefined;
|
|
100
|
+
}
|
|
63
101
|
/** `listWorkspaces`, keeping the envelope's caller-level fields. */
|
|
64
102
|
async function listWorkspaceListing() {
|
|
65
103
|
const rows = [];
|