sim 2.0.0-preview.10.1 → 2.0.0-preview.11.1
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 +11 -2
- package/dist/index.js +662 -278
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2466,8 +2466,18 @@ function deleteProfile(profile) {
|
|
|
2466
2466
|
writeIni(credentialsPath(), credentialsDoc, true);
|
|
2467
2467
|
return { config, credentials };
|
|
2468
2468
|
}
|
|
2469
|
-
function normalizeEndpoint(endpoint) {
|
|
2470
|
-
|
|
2469
|
+
function normalizeEndpoint(endpoint, source) {
|
|
2470
|
+
const trimmed = endpoint.replace(/\/+$/, "");
|
|
2471
|
+
let parsed;
|
|
2472
|
+
try {
|
|
2473
|
+
parsed = new URL(trimmed);
|
|
2474
|
+
} catch {
|
|
2475
|
+
throw new ProfileConfigError(`Invalid endpoint "${endpoint}" from ${source}. Use an absolute URL, e.g. ${DEFAULT_ENDPOINT} or http://localhost:3000`);
|
|
2476
|
+
}
|
|
2477
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
2478
|
+
throw new ProfileConfigError(`Unsupported endpoint scheme "${parsed.protocol.replace(/:$/, "")}" from ${source}. Use http or https, e.g. ${DEFAULT_ENDPOINT}`);
|
|
2479
|
+
}
|
|
2480
|
+
return trimmed;
|
|
2471
2481
|
}
|
|
2472
2482
|
function resolve(candidates, fallback, fallbackSource) {
|
|
2473
2483
|
for (const [source, value] of candidates) {
|
|
@@ -2505,7 +2515,7 @@ function resolveProfile(overrides = {}) {
|
|
|
2505
2515
|
}
|
|
2506
2516
|
return {
|
|
2507
2517
|
name,
|
|
2508
|
-
endpoint: normalizeEndpoint(endpoint.value),
|
|
2518
|
+
endpoint: normalizeEndpoint(endpoint.value, endpoint.source),
|
|
2509
2519
|
apiKey: apiKey.value,
|
|
2510
2520
|
workspaceId: workspaceId.value,
|
|
2511
2521
|
output: output.value,
|
|
@@ -2539,13 +2549,25 @@ function buildUrl(endpoint, path, query) {
|
|
|
2539
2549
|
}
|
|
2540
2550
|
return url.toString();
|
|
2541
2551
|
}
|
|
2542
|
-
|
|
2552
|
+
var REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
2553
|
+
var MARKUP_PREFIX = /^\s*<(?:!doctype|html|\?xml)/i;
|
|
2554
|
+
var WORKSPACE_KEY_REFUSAL = "WORKSPACE_KEY_OPERATION_NOT_PERMITTED";
|
|
2555
|
+
function toNonJsonError(url, status, contentType, raw) {
|
|
2556
|
+
const type = contentType?.split(";")[0]?.trim().toLowerCase();
|
|
2557
|
+
const isMarkup = type === "text/html" || type === "application/xhtml+xml" || MARKUP_PREFIX.test(raw);
|
|
2558
|
+
const kind = isMarkup ? "HTML" : type && type !== "application/json" ? type : "a non-JSON response";
|
|
2559
|
+
const text = raw.trim();
|
|
2560
|
+
const keepSnippet = !isMarkup && text.length > 0 && text.length <= 200;
|
|
2561
|
+
return new SimApiError(`${url} returned ${kind}, not JSON (HTTP ${status}) — check your endpoint.${keepSnippet ? ` Response: ${truncate(text, 200)}` : ""}`, status);
|
|
2562
|
+
}
|
|
2563
|
+
function toApiError(url, status, contentType, raw) {
|
|
2543
2564
|
let parsed;
|
|
2544
2565
|
try {
|
|
2545
2566
|
parsed = JSON.parse(raw);
|
|
2546
2567
|
} catch {
|
|
2547
|
-
|
|
2548
|
-
|
|
2568
|
+
if (!raw.trim())
|
|
2569
|
+
return new SimApiError(`Request failed with status ${status}`, status);
|
|
2570
|
+
return toNonJsonError(url, status, contentType, raw);
|
|
2549
2571
|
}
|
|
2550
2572
|
const body = parsed;
|
|
2551
2573
|
if (body.error && typeof body.error === "object") {
|
|
@@ -2561,8 +2583,33 @@ function toApiError(status, raw) {
|
|
|
2561
2583
|
function truncate(value, max) {
|
|
2562
2584
|
return value.length <= max ? value : `${value.slice(0, max)}…`;
|
|
2563
2585
|
}
|
|
2586
|
+
function namesWorkspaceKeyRefusal(error) {
|
|
2587
|
+
if (error.code === WORKSPACE_KEY_REFUSAL)
|
|
2588
|
+
return true;
|
|
2589
|
+
const details = error.details;
|
|
2590
|
+
if (!details || typeof details !== "object")
|
|
2591
|
+
return false;
|
|
2592
|
+
return details.code === WORKSPACE_KEY_REFUSAL;
|
|
2593
|
+
}
|
|
2594
|
+
function isStrictPrefix(path, other) {
|
|
2595
|
+
if (path.length >= other.length)
|
|
2596
|
+
return false;
|
|
2597
|
+
return path.every((segment, index) => segment === other[index]);
|
|
2598
|
+
}
|
|
2599
|
+
function rejectsAKeyThatValidated(issue, other) {
|
|
2600
|
+
if (!issue.unrecognizedKeys || !isStrictPrefix(issue.path, other.path))
|
|
2601
|
+
return false;
|
|
2602
|
+
return issue.unrecognizedKeys.includes(other.path[issue.path.length]);
|
|
2603
|
+
}
|
|
2604
|
+
function dropUnionBranchNoise(issues) {
|
|
2605
|
+
if (issues.length < 2)
|
|
2606
|
+
return issues;
|
|
2607
|
+
const kept = issues.filter((issue) => !issues.some((other) => rejectsAKeyThatValidated(issue, other)));
|
|
2608
|
+
return kept.length > 0 ? kept : issues;
|
|
2609
|
+
}
|
|
2564
2610
|
function formatApiErrorDetails(details) {
|
|
2565
|
-
const issues =
|
|
2611
|
+
const issues = [];
|
|
2612
|
+
const seen = new Set;
|
|
2566
2613
|
const visit = (value, parentPath = []) => {
|
|
2567
2614
|
if (Array.isArray(value)) {
|
|
2568
2615
|
value.forEach((item) => visit(item, parentPath));
|
|
@@ -2580,15 +2627,27 @@ function formatApiErrorDetails(details) {
|
|
|
2580
2627
|
}
|
|
2581
2628
|
if (typeof issue.message !== "string" || issue.message === "Invalid input")
|
|
2582
2629
|
return;
|
|
2583
|
-
|
|
2630
|
+
const line = `${path.join(".")}: ${issue.message}`;
|
|
2631
|
+
if (seen.has(line))
|
|
2632
|
+
return;
|
|
2633
|
+
seen.add(line);
|
|
2634
|
+
issues.push({
|
|
2635
|
+
path,
|
|
2636
|
+
message: issue.message,
|
|
2637
|
+
unrecognizedKeys: issue.code === "unrecognized_keys" && Array.isArray(issue.keys) ? issue.keys.map(String) : null
|
|
2638
|
+
});
|
|
2584
2639
|
};
|
|
2585
2640
|
visit(details);
|
|
2586
|
-
if (issues.
|
|
2641
|
+
if (issues.length === 0)
|
|
2587
2642
|
return [` details: ${truncate(JSON.stringify(details), 1000)}`];
|
|
2588
|
-
const
|
|
2589
|
-
const
|
|
2590
|
-
|
|
2591
|
-
|
|
2643
|
+
const kept = dropUnionBranchNoise(issues);
|
|
2644
|
+
const visible = kept.slice(0, 8);
|
|
2645
|
+
const lines = [
|
|
2646
|
+
" details:",
|
|
2647
|
+
...visible.map((issue) => ` ${issue.path.length > 0 ? issue.path.join(".") : "request"}: ${issue.message}`)
|
|
2648
|
+
];
|
|
2649
|
+
if (kept.length > visible.length)
|
|
2650
|
+
lines.push(` … ${kept.length - visible.length} more issues`);
|
|
2592
2651
|
return lines;
|
|
2593
2652
|
}
|
|
2594
2653
|
|
|
@@ -2614,6 +2673,20 @@ class SimClient {
|
|
|
2614
2673
|
return workspaceId;
|
|
2615
2674
|
}
|
|
2616
2675
|
async requestRaw(path, options = {}) {
|
|
2676
|
+
return (await this.send(path, options)).response;
|
|
2677
|
+
}
|
|
2678
|
+
async request(path, options = {}) {
|
|
2679
|
+
const { response, url } = await this.send(path, options);
|
|
2680
|
+
const raw = await response.text();
|
|
2681
|
+
if (!raw)
|
|
2682
|
+
return;
|
|
2683
|
+
try {
|
|
2684
|
+
return JSON.parse(raw);
|
|
2685
|
+
} catch {
|
|
2686
|
+
throw toNonJsonError(url, response.status, response.headers.get("content-type"), raw);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
async send(path, options) {
|
|
2617
2690
|
const apiKey = this.resolveApiKey(options.auth);
|
|
2618
2691
|
const url = buildUrl(this.profile.endpoint, path, options.query);
|
|
2619
2692
|
const hasBody = options.body !== undefined;
|
|
@@ -2628,7 +2701,8 @@ class SimClient {
|
|
|
2628
2701
|
...options.headers
|
|
2629
2702
|
},
|
|
2630
2703
|
body: hasBody ? JSON.stringify(options.body) : undefined,
|
|
2631
|
-
signal: options.signal
|
|
2704
|
+
signal: options.signal,
|
|
2705
|
+
redirect: "manual"
|
|
2632
2706
|
});
|
|
2633
2707
|
} catch (cause) {
|
|
2634
2708
|
if (options.signal?.aborted) {
|
|
@@ -2636,43 +2710,87 @@ class SimClient {
|
|
|
2636
2710
|
}
|
|
2637
2711
|
throw new SimApiError(`Could not reach ${this.profile.endpoint}: ${cause.message}`, 0);
|
|
2638
2712
|
}
|
|
2713
|
+
if (REDIRECT_STATUSES.has(response.status))
|
|
2714
|
+
throw this.toRedirectError(url, path, response);
|
|
2639
2715
|
if (!response.ok) {
|
|
2640
2716
|
const raw = await response.text();
|
|
2641
|
-
const error = toApiError(response.status, raw);
|
|
2717
|
+
const error = toApiError(url, response.status, response.headers.get("content-type"), raw);
|
|
2642
2718
|
if (response.status === 401) {
|
|
2643
2719
|
error.message = `${error.message} — run: sim login --profile ${this.profile.name}`;
|
|
2644
2720
|
}
|
|
2721
|
+
if (namesWorkspaceKeyRefusal(error)) {
|
|
2722
|
+
error.message = `${error.message} — this operation needs a personal API key: sim login --profile ${this.profile.name}`;
|
|
2723
|
+
}
|
|
2645
2724
|
throw error;
|
|
2646
2725
|
}
|
|
2647
|
-
return response;
|
|
2726
|
+
return { response, url };
|
|
2648
2727
|
}
|
|
2649
|
-
|
|
2650
|
-
const
|
|
2651
|
-
|
|
2652
|
-
if (
|
|
2653
|
-
|
|
2654
|
-
|
|
2728
|
+
toRedirectError(url, path, response) {
|
|
2729
|
+
const location = response.headers.get("location")?.trim();
|
|
2730
|
+
let target = null;
|
|
2731
|
+
if (location) {
|
|
2732
|
+
try {
|
|
2733
|
+
target = new URL(location, url);
|
|
2734
|
+
} catch {
|
|
2735
|
+
target = null;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
if (!target) {
|
|
2739
|
+
return new SimApiError(`${url} answered HTTP ${response.status} with no usable redirect target. Check the endpoint for profile "${this.profile.name}".`, response.status);
|
|
2740
|
+
}
|
|
2741
|
+
const suggested = redirectEndpoint(this.profile.endpoint, path, target);
|
|
2742
|
+
if (!suggested) {
|
|
2743
|
+
return new SimApiError(`${url} redirected to ${target.href}. The CLI does not follow redirects, because a redirect can drop the request body and turn a write into a silent no-op.`, response.status);
|
|
2744
|
+
}
|
|
2745
|
+
return new SimApiError(`Endpoint redirected to ${suggested}. Run: sim configure --profile ${this.profile.name} --set-endpoint ${suggested}`, response.status);
|
|
2655
2746
|
}
|
|
2656
2747
|
}
|
|
2748
|
+
function redirectEndpoint(endpoint, requestPath, target) {
|
|
2749
|
+
const prefix = target.pathname.endsWith(requestPath) ? target.pathname.slice(0, target.pathname.length - requestPath.length) : "";
|
|
2750
|
+
const suggested = `${target.origin}${prefix}`.replace(/\/+$/, "");
|
|
2751
|
+
return suggested === endpoint.replace(/\/+$/, "") ? null : suggested;
|
|
2752
|
+
}
|
|
2753
|
+
function pageProgress() {
|
|
2754
|
+
let reported = false;
|
|
2755
|
+
return {
|
|
2756
|
+
advance: (fetched) => {
|
|
2757
|
+
if (!process.stderr.isTTY)
|
|
2758
|
+
return;
|
|
2759
|
+
reported = true;
|
|
2760
|
+
process.stderr.write(`\r${source_default.dim(`fetched ${fetched}…`)}\x1B[K`);
|
|
2761
|
+
},
|
|
2762
|
+
finish: () => {
|
|
2763
|
+
if (reported)
|
|
2764
|
+
process.stderr.write("\r\x1B[K");
|
|
2765
|
+
}
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2657
2768
|
async function requestAllPages(client, path, options) {
|
|
2658
2769
|
const { query, pageSize, limit: requestedLimit, ...requestOptions } = options;
|
|
2659
2770
|
const limit = requestedLimit ?? Number.POSITIVE_INFINITY;
|
|
2660
2771
|
if (limit <= 0)
|
|
2661
2772
|
return [];
|
|
2662
2773
|
const items = [];
|
|
2774
|
+
const progress = pageProgress();
|
|
2663
2775
|
let cursor = null;
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2776
|
+
try {
|
|
2777
|
+
do {
|
|
2778
|
+
const page = await client.request(path, {
|
|
2779
|
+
...requestOptions,
|
|
2780
|
+
query: {
|
|
2781
|
+
...query,
|
|
2782
|
+
limit: Math.min(pageSize, limit - items.length),
|
|
2783
|
+
cursor
|
|
2784
|
+
}
|
|
2785
|
+
});
|
|
2786
|
+
items.push(...page.data);
|
|
2787
|
+
cursor = page.nextCursor;
|
|
2788
|
+
if (cursor && items.length < limit)
|
|
2789
|
+
progress.advance(items.length);
|
|
2790
|
+
} while (cursor && items.length < limit);
|
|
2791
|
+
} finally {
|
|
2792
|
+
progress.finish();
|
|
2793
|
+
}
|
|
2676
2794
|
return items.slice(0, limit);
|
|
2677
2795
|
}
|
|
2678
2796
|
function resolvePath(template, params = {}) {
|
|
@@ -5894,7 +6012,7 @@ function duration(ms) {
|
|
|
5894
6012
|
if (ms === null || ms === undefined)
|
|
5895
6013
|
return EMPTY;
|
|
5896
6014
|
if (ms < 1000)
|
|
5897
|
-
return `${ms}ms`;
|
|
6015
|
+
return `${Math.round(ms)}ms`;
|
|
5898
6016
|
if (ms < 60000)
|
|
5899
6017
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
5900
6018
|
return `${Math.floor(ms / 60000)}m${Math.round(ms % 60000 / 1000)}s`;
|
|
@@ -5914,17 +6032,18 @@ function oneLine(value) {
|
|
|
5914
6032
|
return value.replace(/\s*[\r\n\t]+\s*/g, " ");
|
|
5915
6033
|
}
|
|
5916
6034
|
var MAX_CELL_WIDTH = 60;
|
|
5917
|
-
|
|
5918
|
-
|
|
6035
|
+
var MAX_RECORD_WIDTH = 160;
|
|
6036
|
+
function clamp(value, width) {
|
|
6037
|
+
if (visibleWidth(value) <= width || value !== value.replace(ANSI_PATTERN, "")) {
|
|
5919
6038
|
return value;
|
|
5920
6039
|
}
|
|
5921
|
-
return `${value.slice(0,
|
|
6040
|
+
return `${value.slice(0, width - 1)}…`;
|
|
5922
6041
|
}
|
|
5923
6042
|
function renderTable(rows, columns) {
|
|
5924
6043
|
if (rows.length === 0)
|
|
5925
6044
|
return source_default.dim("No results.");
|
|
5926
6045
|
const headers = columns.map((column) => sanitize(column.header));
|
|
5927
|
-
const cells = rows.map((row) => columns.map((column) =>
|
|
6046
|
+
const cells = rows.map((row) => columns.map((column) => clamp(oneLine(column.value(row)), MAX_CELL_WIDTH)));
|
|
5928
6047
|
const widths = columns.map((_column, index) => Math.max(visibleWidth(headers[index]), ...cells.map((line) => visibleWidth(line[index]))));
|
|
5929
6048
|
const header = headers.map((label, index) => source_default.dim(pad(label.toUpperCase(), widths[index]))).join(" ").trimEnd();
|
|
5930
6049
|
const body = cells.map((line) => line.map((cell, index) => pad(cell, widths[index])).join(" ").trimEnd());
|
|
@@ -5970,7 +6089,7 @@ function printRecord(format, fields, raw) {
|
|
|
5970
6089
|
}
|
|
5971
6090
|
const width = Math.max(...safeFields.map(([label]) => visibleWidth(label)));
|
|
5972
6091
|
for (const [label, value] of safeFields) {
|
|
5973
|
-
console.log(`${source_default.dim(pad(`${label}:`, width + 1))} ${oneLine(value)}`);
|
|
6092
|
+
console.log(`${source_default.dim(pad(`${label}:`, width + 1))} ${clamp(oneLine(value), MAX_RECORD_WIDTH)}`);
|
|
5974
6093
|
}
|
|
5975
6094
|
}
|
|
5976
6095
|
|
|
@@ -6036,6 +6155,24 @@ function buildApprovalUrl(endpoint, auth, scope, workspaceId) {
|
|
|
6036
6155
|
url.searchParams.set("workspace", workspaceId);
|
|
6037
6156
|
return url.toString();
|
|
6038
6157
|
}
|
|
6158
|
+
var POLL_PATH = "/api/cli/auth/poll";
|
|
6159
|
+
function toRedirectError(endpoint, response) {
|
|
6160
|
+
const location = response.headers.get("location")?.trim();
|
|
6161
|
+
let target = null;
|
|
6162
|
+
if (location) {
|
|
6163
|
+
try {
|
|
6164
|
+
target = new URL(location, endpoint);
|
|
6165
|
+
} catch {
|
|
6166
|
+
target = null;
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
if (!target) {
|
|
6170
|
+
return new SimApiError(`${endpoint} answered the login poll with HTTP ${response.status} and no usable redirect target. Check the endpoint.`, response.status);
|
|
6171
|
+
}
|
|
6172
|
+
const refusal = `${endpoint} redirected the login poll to ${target.href}. The CLI does not follow redirects, because a redirect drops the request body and would carry the login secret to another origin.`;
|
|
6173
|
+
const suggested = redirectEndpoint(endpoint, POLL_PATH, target);
|
|
6174
|
+
return new SimApiError(suggested ? `${refusal} Re-run with --endpoint ${suggested}, or run: sim configure --set-endpoint ${suggested}` : refusal, response.status);
|
|
6175
|
+
}
|
|
6039
6176
|
async function pollForKey(endpoint, auth, signal) {
|
|
6040
6177
|
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
6041
6178
|
while (Date.now() < deadline) {
|
|
@@ -6043,16 +6180,19 @@ async function pollForKey(endpoint, auth, signal) {
|
|
|
6043
6180
|
throw new SimApiError("Login cancelled.", 0);
|
|
6044
6181
|
let response = null;
|
|
6045
6182
|
try {
|
|
6046
|
-
response = await fetch(new URL(
|
|
6183
|
+
response = await fetch(new URL(POLL_PATH, endpoint), {
|
|
6047
6184
|
method: "POST",
|
|
6048
6185
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
6049
6186
|
body: JSON.stringify({ request: auth.request, verifier: auth.pollSecret }),
|
|
6050
|
-
signal
|
|
6187
|
+
signal,
|
|
6188
|
+
redirect: "manual"
|
|
6051
6189
|
});
|
|
6052
6190
|
} catch {
|
|
6053
6191
|
response = null;
|
|
6054
6192
|
}
|
|
6055
6193
|
if (response) {
|
|
6194
|
+
if (REDIRECT_STATUSES.has(response.status))
|
|
6195
|
+
throw toRedirectError(endpoint, response);
|
|
6056
6196
|
const raw = await response.text();
|
|
6057
6197
|
if (!response.ok) {
|
|
6058
6198
|
if (!RETRYABLE_POLL_STATUSES.has(response.status)) {
|
|
@@ -6106,193 +6246,6 @@ function clientFrom(command) {
|
|
|
6106
6246
|
return { client: new SimClient(profile), profile };
|
|
6107
6247
|
}
|
|
6108
6248
|
|
|
6109
|
-
// src/commands/auth.ts
|
|
6110
|
-
function openBrowser(url) {
|
|
6111
|
-
const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : [process.platform === "darwin" ? "open" : "xdg-open", [url]];
|
|
6112
|
-
try {
|
|
6113
|
-
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
6114
|
-
child.on("error", () => {});
|
|
6115
|
-
child.unref();
|
|
6116
|
-
} catch {}
|
|
6117
|
-
}
|
|
6118
|
-
function presentAuthentication(source) {
|
|
6119
|
-
switch (source) {
|
|
6120
|
-
case "flag":
|
|
6121
|
-
return { authenticated: true, source: "flag" };
|
|
6122
|
-
case "env":
|
|
6123
|
-
return { authenticated: true, source: "env" };
|
|
6124
|
-
case "credentials":
|
|
6125
|
-
return { authenticated: true, source: "credentials" };
|
|
6126
|
-
case "unset":
|
|
6127
|
-
return { authenticated: false, source: "unset" };
|
|
6128
|
-
case "config":
|
|
6129
|
-
case "default":
|
|
6130
|
-
throw new SimApiError(`Unexpected API key source "${source}".`, 0);
|
|
6131
|
-
}
|
|
6132
|
-
}
|
|
6133
|
-
async function confirmProfileOverwrite(profileName) {
|
|
6134
|
-
if (!process.stdin.isTTY) {
|
|
6135
|
-
throw new SimApiError(`Profile "${profileName}" already exists. Re-run with --yes to overwrite it.`, 0);
|
|
6136
|
-
}
|
|
6137
|
-
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
6138
|
-
try {
|
|
6139
|
-
const answer = await prompt.question(`Profile "${profileName}" already exists. Replace its API key and login defaults? (y/N) `);
|
|
6140
|
-
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
6141
|
-
} finally {
|
|
6142
|
-
prompt.close();
|
|
6143
|
-
}
|
|
6144
|
-
}
|
|
6145
|
-
function loginCommand() {
|
|
6146
|
-
return new Command("login").description("Authorize this terminal and store an API key for the profile").option("--scope <scope>", "Key space to mint from: platform or copilot", "platform").option("--no-browser", "Print the URL instead of opening a browser").option("-y, --yes", "Overwrite an existing profile without prompting").action(async (options, command) => {
|
|
6147
|
-
const profile = profileFrom(command);
|
|
6148
|
-
if (options.scope !== "platform" && options.scope !== "copilot") {
|
|
6149
|
-
throw new SimApiError(`Unknown scope "${options.scope}". Use platform or copilot.`, 0);
|
|
6150
|
-
}
|
|
6151
|
-
const scope = options.scope;
|
|
6152
|
-
if (readCredentialsProfile(profile.name).api_key && !options.yes) {
|
|
6153
|
-
const confirmed = await confirmProfileOverwrite(profile.name);
|
|
6154
|
-
if (!confirmed) {
|
|
6155
|
-
console.log(source_default.dim("Login cancelled; the existing profile was not changed."));
|
|
6156
|
-
return;
|
|
6157
|
-
}
|
|
6158
|
-
}
|
|
6159
|
-
const auth = createAuthRequest();
|
|
6160
|
-
const url = buildApprovalUrl(profile.endpoint, auth, scope, profile.workspaceId ?? undefined);
|
|
6161
|
-
console.log(`Signing in to ${source_default.bold(profile.endpoint)} as profile ${source_default.bold(profile.name)}`);
|
|
6162
|
-
console.log(`
|
|
6163
|
-
Pairing code: ${source_default.bold(auth.pairing)}`);
|
|
6164
|
-
console.log(source_default.dim(`Confirm this code matches what the browser shows before approving.
|
|
6165
|
-
`));
|
|
6166
|
-
console.log(url);
|
|
6167
|
-
if (options.browser)
|
|
6168
|
-
openBrowser(url);
|
|
6169
|
-
console.log(source_default.dim(`
|
|
6170
|
-
Waiting for approval…`));
|
|
6171
|
-
const key = await pollForKey(profile.endpoint, auth);
|
|
6172
|
-
if (key.scope !== scope) {
|
|
6173
|
-
throw new SimApiError(`Server issued a ${key.scope} key but this profile needs a ${scope} key. Update the Sim deployment, or run: sim login --scope ${key.scope}`, 0);
|
|
6174
|
-
}
|
|
6175
|
-
writeCredentialsProfile(profile.name, key.apiKey);
|
|
6176
|
-
const settings = {
|
|
6177
|
-
endpoint: profile.endpoint,
|
|
6178
|
-
workspace: key.workspaceId ?? null
|
|
6179
|
-
};
|
|
6180
|
-
writeConfigProfile(profile.name, settings);
|
|
6181
|
-
console.log(source_default.green(`
|
|
6182
|
-
✓ Logged in. Key stored in ${credentialsPath()}`));
|
|
6183
|
-
if (key.workspaceBound && key.workspaceId) {
|
|
6184
|
-
console.log(source_default.dim(` Workspace-scoped key — it can only reach ${key.workspaceId}.`));
|
|
6185
|
-
} else if (key.workspaceId) {
|
|
6186
|
-
console.log(source_default.dim(` Personal key, defaulting to ${key.workspaceId}. Override per command with --workspace.`));
|
|
6187
|
-
} else {
|
|
6188
|
-
console.log(source_default.dim(" Personal key with no default workspace. Set one with: sim configure --set-workspace <id>"));
|
|
6189
|
-
}
|
|
6190
|
-
});
|
|
6191
|
-
}
|
|
6192
|
-
function logoutCommand() {
|
|
6193
|
-
return new Command("logout").description("Remove the profile's stored API key").option("--all", "Remove the profile entirely, including its settings").action((options, command) => {
|
|
6194
|
-
const profile = profileFrom(command);
|
|
6195
|
-
if (options.all) {
|
|
6196
|
-
const removed = deleteProfile(profile.name);
|
|
6197
|
-
if (!removed.config && !removed.credentials) {
|
|
6198
|
-
console.log(source_default.dim(`Nothing stored for profile "${profile.name}".`));
|
|
6199
|
-
return;
|
|
6200
|
-
}
|
|
6201
|
-
console.log(source_default.green(`✓ Removed profile "${profile.name}".`));
|
|
6202
|
-
return;
|
|
6203
|
-
}
|
|
6204
|
-
if (!readCredentialsProfile(profile.name).api_key) {
|
|
6205
|
-
console.log(source_default.dim(`No stored key for profile "${profile.name}".`));
|
|
6206
|
-
return;
|
|
6207
|
-
}
|
|
6208
|
-
writeCredentialsProfile(profile.name, null);
|
|
6209
|
-
console.log(source_default.green(`✓ Removed the stored key for profile "${profile.name}".`));
|
|
6210
|
-
console.log(source_default.dim(" The key itself is still active — revoke it in Settings → API keys."));
|
|
6211
|
-
});
|
|
6212
|
-
}
|
|
6213
|
-
function whoamiCommand() {
|
|
6214
|
-
return new Command("whoami").description("Show the resolved profile and where each setting came from").action((_options, command) => {
|
|
6215
|
-
const profile = profileFrom(command);
|
|
6216
|
-
const { sources } = profile;
|
|
6217
|
-
const authentication = presentAuthentication(sources.apiKey);
|
|
6218
|
-
const annotate = (value, source) => source === "unset" ? source_default.dim("not set") : `${value} ${source_default.dim(`(${source})`)}`;
|
|
6219
|
-
printRecord(profile.output, [
|
|
6220
|
-
["Profile", profile.name],
|
|
6221
|
-
["Endpoint", annotate(profile.endpoint, sources.endpoint)],
|
|
6222
|
-
[
|
|
6223
|
-
"API key",
|
|
6224
|
-
authentication.authenticated ? annotate("configured", authentication.source) : source_default.yellow("not logged in")
|
|
6225
|
-
],
|
|
6226
|
-
["Workspace", annotate(profile.workspaceId ?? "", sources.workspaceId)],
|
|
6227
|
-
["Output", annotate(profile.output, sources.output)]
|
|
6228
|
-
], {
|
|
6229
|
-
profile: profile.name,
|
|
6230
|
-
endpoint: profile.endpoint,
|
|
6231
|
-
workspaceId: profile.workspaceId,
|
|
6232
|
-
output: profile.output,
|
|
6233
|
-
authenticated: authentication.authenticated,
|
|
6234
|
-
sources: {
|
|
6235
|
-
endpoint: sources.endpoint,
|
|
6236
|
-
authentication: authentication.source,
|
|
6237
|
-
workspaceId: sources.workspaceId,
|
|
6238
|
-
output: sources.output
|
|
6239
|
-
}
|
|
6240
|
-
});
|
|
6241
|
-
});
|
|
6242
|
-
}
|
|
6243
|
-
function profilesCommand() {
|
|
6244
|
-
return new Command("profiles").alias("profile").description("List the profiles defined in the config and credentials files").action((_options, command) => {
|
|
6245
|
-
const profiles = listProfiles();
|
|
6246
|
-
if (profiles.length === 0) {
|
|
6247
|
-
console.log(source_default.dim("No profiles yet. Run: sim login"));
|
|
6248
|
-
return;
|
|
6249
|
-
}
|
|
6250
|
-
const active = profileFrom(command).name;
|
|
6251
|
-
for (const name of profiles) {
|
|
6252
|
-
const marker = name === active ? source_default.green("*") : " ";
|
|
6253
|
-
const hasKey = Boolean(readCredentialsProfile(name).api_key);
|
|
6254
|
-
console.log(`${marker} ${name}${hasKey ? "" : source_default.dim(" (no key)")}`);
|
|
6255
|
-
}
|
|
6256
|
-
});
|
|
6257
|
-
}
|
|
6258
|
-
|
|
6259
|
-
// src/commands/configure.ts
|
|
6260
|
-
function configureCommand() {
|
|
6261
|
-
return new Command("configure").description("Set a profile's endpoint, default workspace, or output format").option("--set-endpoint <url>", "Sim deployment to talk to").option("--set-workspace <id>", "Default workspace for workspace-scoped commands").option("--set-output <format>", `Default output format (${OUTPUT_FORMATS.join(" | ")})`).option("--unset <key...>", "Remove settings (endpoint, workspace, output)").action((options, command) => {
|
|
6262
|
-
const profile = profileFrom(command);
|
|
6263
|
-
const updates = {};
|
|
6264
|
-
if (options.setEndpoint)
|
|
6265
|
-
updates.endpoint = options.setEndpoint.replace(/\/+$/, "");
|
|
6266
|
-
if (options.setWorkspace)
|
|
6267
|
-
updates.workspace = options.setWorkspace;
|
|
6268
|
-
if (options.setOutput) {
|
|
6269
|
-
if (!OUTPUT_FORMATS.includes(options.setOutput)) {
|
|
6270
|
-
throw new SimApiError(`Unknown output format "${options.setOutput}". Use one of: ${OUTPUT_FORMATS.join(", ")}`, 0);
|
|
6271
|
-
}
|
|
6272
|
-
updates.output = options.setOutput;
|
|
6273
|
-
}
|
|
6274
|
-
for (const key of options.unset ?? []) {
|
|
6275
|
-
if (!["endpoint", "workspace", "output"].includes(key)) {
|
|
6276
|
-
throw new SimApiError(`Cannot unset "${key}". Use endpoint, workspace, or output.`, 0);
|
|
6277
|
-
}
|
|
6278
|
-
updates[key] = null;
|
|
6279
|
-
}
|
|
6280
|
-
if (Object.keys(updates).length === 0) {
|
|
6281
|
-
const current = readConfigProfile(profile.name);
|
|
6282
|
-
if (Object.keys(current).length === 0) {
|
|
6283
|
-
console.log(source_default.dim(`No settings stored for profile "${profile.name}".`));
|
|
6284
|
-
return;
|
|
6285
|
-
}
|
|
6286
|
-
for (const [key, value] of Object.entries(current)) {
|
|
6287
|
-
console.log(`${source_default.dim(`${key}:`)} ${value}`);
|
|
6288
|
-
}
|
|
6289
|
-
return;
|
|
6290
|
-
}
|
|
6291
|
-
writeConfigProfile(profile.name, updates);
|
|
6292
|
-
console.log(source_default.green(`✓ Updated profile "${profile.name}" in ${configPath()}`));
|
|
6293
|
-
});
|
|
6294
|
-
}
|
|
6295
|
-
|
|
6296
6249
|
// src/generated/v2-api.ts
|
|
6297
6250
|
var V2_OPERATIONS = {
|
|
6298
6251
|
abortFileUpload: {
|
|
@@ -9380,6 +9333,261 @@ var V2_OPERATIONS = {
|
|
|
9380
9333
|
}
|
|
9381
9334
|
};
|
|
9382
9335
|
|
|
9336
|
+
// src/commands/auth.ts
|
|
9337
|
+
function openBrowser(url) {
|
|
9338
|
+
const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : [process.platform === "darwin" ? "open" : "xdg-open", [url]];
|
|
9339
|
+
try {
|
|
9340
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
9341
|
+
child.on("error", () => {});
|
|
9342
|
+
child.unref();
|
|
9343
|
+
} catch {}
|
|
9344
|
+
}
|
|
9345
|
+
function presentAuthentication(source) {
|
|
9346
|
+
switch (source) {
|
|
9347
|
+
case "flag":
|
|
9348
|
+
return { authenticated: true, source: "flag" };
|
|
9349
|
+
case "env":
|
|
9350
|
+
return { authenticated: true, source: "env" };
|
|
9351
|
+
case "credentials":
|
|
9352
|
+
return { authenticated: true, source: "credentials" };
|
|
9353
|
+
case "unset":
|
|
9354
|
+
return { authenticated: false, source: "unset" };
|
|
9355
|
+
case "config":
|
|
9356
|
+
case "default":
|
|
9357
|
+
throw new SimApiError(`Unexpected API key source "${source}".`, 0);
|
|
9358
|
+
}
|
|
9359
|
+
}
|
|
9360
|
+
async function confirmProfileOverwrite(profileName) {
|
|
9361
|
+
if (!process.stdin.isTTY) {
|
|
9362
|
+
throw new SimApiError(`Profile "${profileName}" already exists. Re-run with --yes to overwrite it.`, 0);
|
|
9363
|
+
}
|
|
9364
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
9365
|
+
try {
|
|
9366
|
+
const answer = await prompt.question(`Profile "${profileName}" already exists. Replace its API key and login defaults? (y/N) `);
|
|
9367
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
9368
|
+
} finally {
|
|
9369
|
+
prompt.close();
|
|
9370
|
+
}
|
|
9371
|
+
}
|
|
9372
|
+
function loginCommand() {
|
|
9373
|
+
return new Command("login").description("Authorize this terminal and store an API key for the profile").option("--scope <scope>", "Key space to mint from: platform or copilot", "platform").option("--no-browser", "Print the URL instead of opening a browser").option("-y, --yes", "Overwrite an existing profile without prompting").action(async (options, command) => {
|
|
9374
|
+
const profile = profileFrom(command);
|
|
9375
|
+
if (options.scope !== "platform" && options.scope !== "copilot") {
|
|
9376
|
+
throw new SimApiError(`Unknown scope "${options.scope}". Use platform or copilot.`, 0);
|
|
9377
|
+
}
|
|
9378
|
+
const scope = options.scope;
|
|
9379
|
+
if (readCredentialsProfile(profile.name).api_key && !options.yes) {
|
|
9380
|
+
const confirmed = await confirmProfileOverwrite(profile.name);
|
|
9381
|
+
if (!confirmed) {
|
|
9382
|
+
console.log(source_default.dim("Login cancelled; the existing profile was not changed."));
|
|
9383
|
+
return;
|
|
9384
|
+
}
|
|
9385
|
+
}
|
|
9386
|
+
const auth = createAuthRequest();
|
|
9387
|
+
const url = buildApprovalUrl(profile.endpoint, auth, scope, profile.workspaceId ?? undefined);
|
|
9388
|
+
console.log(`Signing in to ${source_default.bold(profile.endpoint)} as profile ${source_default.bold(profile.name)}`);
|
|
9389
|
+
console.log(`
|
|
9390
|
+
Pairing code: ${source_default.bold(auth.pairing)}`);
|
|
9391
|
+
console.log(source_default.dim(`Confirm this code matches what the browser shows before approving.
|
|
9392
|
+
`));
|
|
9393
|
+
console.log(url);
|
|
9394
|
+
if (options.browser)
|
|
9395
|
+
openBrowser(url);
|
|
9396
|
+
console.log(source_default.dim(`
|
|
9397
|
+
Waiting for approval…`));
|
|
9398
|
+
const key = await pollForKey(profile.endpoint, auth);
|
|
9399
|
+
if (key.scope !== scope) {
|
|
9400
|
+
throw new SimApiError(`Server issued a ${key.scope} key but this profile needs a ${scope} key. Update the Sim deployment, or run: sim login --scope ${key.scope}`, 0);
|
|
9401
|
+
}
|
|
9402
|
+
writeCredentialsProfile(profile.name, key.apiKey);
|
|
9403
|
+
const settings = {
|
|
9404
|
+
endpoint: profile.endpoint,
|
|
9405
|
+
workspace: key.workspaceId ?? null
|
|
9406
|
+
};
|
|
9407
|
+
writeConfigProfile(profile.name, settings);
|
|
9408
|
+
console.log(source_default.green(`
|
|
9409
|
+
✓ Logged in. Key stored in ${credentialsPath()}`));
|
|
9410
|
+
if (key.workspaceBound && key.workspaceId) {
|
|
9411
|
+
console.log(source_default.dim(` Workspace-scoped key — it can only reach ${key.workspaceId}.`));
|
|
9412
|
+
} else if (key.workspaceId) {
|
|
9413
|
+
console.log(source_default.dim(` Personal key, defaulting to ${key.workspaceId}. Override per command with --workspace.`));
|
|
9414
|
+
} else {
|
|
9415
|
+
console.log(source_default.dim(" Personal key with no default workspace. Set one with: sim configure --set-workspace <id>"));
|
|
9416
|
+
}
|
|
9417
|
+
});
|
|
9418
|
+
}
|
|
9419
|
+
function logoutCommand() {
|
|
9420
|
+
return new Command("logout").description("Remove the profile's stored API key").option("--all", "Remove the profile entirely, including its settings").action((options, command) => {
|
|
9421
|
+
const profile = profileFrom(command);
|
|
9422
|
+
if (options.all) {
|
|
9423
|
+
const removed = deleteProfile(profile.name);
|
|
9424
|
+
if (!removed.config && !removed.credentials) {
|
|
9425
|
+
console.log(source_default.dim(`Nothing stored for profile "${profile.name}".`));
|
|
9426
|
+
return;
|
|
9427
|
+
}
|
|
9428
|
+
console.log(source_default.green(`✓ Removed profile "${profile.name}".`));
|
|
9429
|
+
return;
|
|
9430
|
+
}
|
|
9431
|
+
if (!readCredentialsProfile(profile.name).api_key) {
|
|
9432
|
+
console.log(source_default.dim(`No stored key for profile "${profile.name}".`));
|
|
9433
|
+
return;
|
|
9434
|
+
}
|
|
9435
|
+
writeCredentialsProfile(profile.name, null);
|
|
9436
|
+
console.log(source_default.green(`✓ Removed the stored key for profile "${profile.name}".`));
|
|
9437
|
+
console.log(source_default.dim(" The key itself is still active — revoke it in Settings → API keys."));
|
|
9438
|
+
});
|
|
9439
|
+
}
|
|
9440
|
+
var CREDENTIAL_VERDICT_STATUSES = new Set([401, 403, 404]);
|
|
9441
|
+
var WHOAMI_EXIT_CODES = {
|
|
9442
|
+
verified: 0,
|
|
9443
|
+
disabled: 0,
|
|
9444
|
+
unauthenticated: 1,
|
|
9445
|
+
rejected: 1,
|
|
9446
|
+
unreachable: 2,
|
|
9447
|
+
"no-workspace": 2
|
|
9448
|
+
};
|
|
9449
|
+
async function verifyProfile(client, profile) {
|
|
9450
|
+
if (!profile.apiKey) {
|
|
9451
|
+
return {
|
|
9452
|
+
status: "unauthenticated",
|
|
9453
|
+
workspace: null,
|
|
9454
|
+
detail: `no API key — run: sim login --profile ${profile.name}`
|
|
9455
|
+
};
|
|
9456
|
+
}
|
|
9457
|
+
if (!profile.workspaceId) {
|
|
9458
|
+
return {
|
|
9459
|
+
status: "no-workspace",
|
|
9460
|
+
workspace: null,
|
|
9461
|
+
detail: `no workspace to check against — run: sim configure --profile ${profile.name} --set-workspace <id>`
|
|
9462
|
+
};
|
|
9463
|
+
}
|
|
9464
|
+
const operation = V2_OPERATIONS.getWorkspace;
|
|
9465
|
+
try {
|
|
9466
|
+
const response = await client.request(resolvePath(operation.path, { workspaceId: profile.workspaceId }), { method: operation.method });
|
|
9467
|
+
const { id, name, memberCount } = response.data;
|
|
9468
|
+
return { status: "verified", workspace: { id, name, memberCount }, detail: null };
|
|
9469
|
+
} catch (error) {
|
|
9470
|
+
if (!(error instanceof SimApiError))
|
|
9471
|
+
throw error;
|
|
9472
|
+
return {
|
|
9473
|
+
status: CREDENTIAL_VERDICT_STATUSES.has(error.status) ? "rejected" : "unreachable",
|
|
9474
|
+
workspace: null,
|
|
9475
|
+
detail: error.message
|
|
9476
|
+
};
|
|
9477
|
+
}
|
|
9478
|
+
}
|
|
9479
|
+
function presentVerification(verification) {
|
|
9480
|
+
if (verification.status === "verified") {
|
|
9481
|
+
const { name, memberCount } = verification.workspace;
|
|
9482
|
+
const members = `${memberCount} ${memberCount === 1 ? "member" : "members"}`;
|
|
9483
|
+
return `${source_default.green("✓")} ${safeOneLine(name)} · ${members}`;
|
|
9484
|
+
}
|
|
9485
|
+
const detail = safeOneLine(verification.detail);
|
|
9486
|
+
switch (verification.status) {
|
|
9487
|
+
case "rejected":
|
|
9488
|
+
return `${source_default.red("✗")} ${detail}`;
|
|
9489
|
+
case "unauthenticated":
|
|
9490
|
+
return source_default.yellow(`not logged in — ${detail}`);
|
|
9491
|
+
case "disabled":
|
|
9492
|
+
return source_default.dim(detail);
|
|
9493
|
+
default:
|
|
9494
|
+
return source_default.yellow(`could not check — ${detail}`);
|
|
9495
|
+
}
|
|
9496
|
+
}
|
|
9497
|
+
function whoamiCommand() {
|
|
9498
|
+
return new Command("whoami").description("Show the resolved profile, where each setting came from, and whether it works").option("--no-verify", "Skip the API check and only print the resolved settings").action(async (options, command) => {
|
|
9499
|
+
const { client, profile } = clientFrom(command);
|
|
9500
|
+
const { sources } = profile;
|
|
9501
|
+
const authentication = presentAuthentication(sources.apiKey);
|
|
9502
|
+
const verification = options.verify ? await verifyProfile(client, profile) : { status: "disabled", workspace: null, detail: "not checked (--no-verify)" };
|
|
9503
|
+
const annotate = (value, source) => source === "unset" ? source_default.dim("not set") : `${value} ${source_default.dim(`(${source})`)}`;
|
|
9504
|
+
printRecord(profile.output, [
|
|
9505
|
+
["Profile", profile.name],
|
|
9506
|
+
["Endpoint", annotate(profile.endpoint, sources.endpoint)],
|
|
9507
|
+
[
|
|
9508
|
+
"API key",
|
|
9509
|
+
authentication.authenticated ? annotate("configured", authentication.source) : source_default.yellow("not logged in")
|
|
9510
|
+
],
|
|
9511
|
+
["Workspace", annotate(profile.workspaceId ?? "", sources.workspaceId)],
|
|
9512
|
+
["Output", annotate(profile.output, sources.output)],
|
|
9513
|
+
["Verified", presentVerification(verification)]
|
|
9514
|
+
], {
|
|
9515
|
+
profile: profile.name,
|
|
9516
|
+
endpoint: profile.endpoint,
|
|
9517
|
+
workspaceId: profile.workspaceId,
|
|
9518
|
+
output: profile.output,
|
|
9519
|
+
authenticated: authentication.authenticated,
|
|
9520
|
+
sources: {
|
|
9521
|
+
endpoint: sources.endpoint,
|
|
9522
|
+
authentication: authentication.source,
|
|
9523
|
+
workspaceId: sources.workspaceId,
|
|
9524
|
+
output: sources.output
|
|
9525
|
+
},
|
|
9526
|
+
verification: {
|
|
9527
|
+
status: verification.status,
|
|
9528
|
+
workspace: verification.workspace,
|
|
9529
|
+
detail: verification.detail
|
|
9530
|
+
}
|
|
9531
|
+
});
|
|
9532
|
+
const exitCode = WHOAMI_EXIT_CODES[verification.status];
|
|
9533
|
+
if (exitCode !== 0)
|
|
9534
|
+
process.exitCode = exitCode;
|
|
9535
|
+
});
|
|
9536
|
+
}
|
|
9537
|
+
function profilesCommand() {
|
|
9538
|
+
return new Command("profiles").alias("profile").description("List the profiles defined in the config and credentials files").action((_options, command) => {
|
|
9539
|
+
const profiles = listProfiles();
|
|
9540
|
+
if (profiles.length === 0) {
|
|
9541
|
+
console.log(source_default.dim("No profiles yet. Run: sim login"));
|
|
9542
|
+
return;
|
|
9543
|
+
}
|
|
9544
|
+
const active = profileFrom(command).name;
|
|
9545
|
+
for (const name of profiles) {
|
|
9546
|
+
const marker = name === active ? source_default.green("*") : " ";
|
|
9547
|
+
const hasKey = Boolean(readCredentialsProfile(name).api_key);
|
|
9548
|
+
console.log(`${marker} ${name}${hasKey ? "" : source_default.dim(" (no key)")}`);
|
|
9549
|
+
}
|
|
9550
|
+
});
|
|
9551
|
+
}
|
|
9552
|
+
|
|
9553
|
+
// src/commands/configure.ts
|
|
9554
|
+
function configureCommand() {
|
|
9555
|
+
return new Command("configure").description("Set a profile's endpoint, default workspace, or output format").option("--set-endpoint <url>", "Sim deployment to talk to").option("--set-workspace <id>", "Default workspace for workspace-scoped commands").option("--set-output <format>", `Default output format (${OUTPUT_FORMATS.join(" | ")})`).option("--unset <key...>", "Remove settings (endpoint, workspace, output)").action((options, command) => {
|
|
9556
|
+
const profile = profileFrom(command);
|
|
9557
|
+
const updates = {};
|
|
9558
|
+
if (options.setEndpoint) {
|
|
9559
|
+
updates.endpoint = normalizeEndpoint(options.setEndpoint, "--set-endpoint");
|
|
9560
|
+
}
|
|
9561
|
+
if (options.setWorkspace)
|
|
9562
|
+
updates.workspace = options.setWorkspace;
|
|
9563
|
+
if (options.setOutput) {
|
|
9564
|
+
if (!OUTPUT_FORMATS.includes(options.setOutput)) {
|
|
9565
|
+
throw new SimApiError(`Unknown output format "${options.setOutput}". Use one of: ${OUTPUT_FORMATS.join(", ")}`, 0);
|
|
9566
|
+
}
|
|
9567
|
+
updates.output = options.setOutput;
|
|
9568
|
+
}
|
|
9569
|
+
for (const key of options.unset ?? []) {
|
|
9570
|
+
if (!["endpoint", "workspace", "output"].includes(key)) {
|
|
9571
|
+
throw new SimApiError(`Cannot unset "${key}". Use endpoint, workspace, or output.`, 0);
|
|
9572
|
+
}
|
|
9573
|
+
updates[key] = null;
|
|
9574
|
+
}
|
|
9575
|
+
if (Object.keys(updates).length === 0) {
|
|
9576
|
+
const current = readConfigProfile(profile.name);
|
|
9577
|
+
if (Object.keys(current).length === 0) {
|
|
9578
|
+
console.log(source_default.dim(`No settings stored for profile "${profile.name}".`));
|
|
9579
|
+
return;
|
|
9580
|
+
}
|
|
9581
|
+
for (const [key, value] of Object.entries(current)) {
|
|
9582
|
+
console.log(`${source_default.dim(`${key}:`)} ${value}`);
|
|
9583
|
+
}
|
|
9584
|
+
return;
|
|
9585
|
+
}
|
|
9586
|
+
writeConfigProfile(profile.name, updates);
|
|
9587
|
+
console.log(source_default.green(`✓ Updated profile "${profile.name}" in ${configPath()}`));
|
|
9588
|
+
});
|
|
9589
|
+
}
|
|
9590
|
+
|
|
9383
9591
|
// src/runtime/request.ts
|
|
9384
9592
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readSync } from "node:fs";
|
|
9385
9593
|
|
|
@@ -9389,7 +9597,8 @@ var TABLE_FILTER_HELP = 'Predicate: {"all":[{"field":"status","op":"eq","value":
|
|
|
9389
9597
|
var TABLE_SORT_HELP = 'Ordered sort keys: [{"field":"createdAt","direction":"desc"}] (direction: asc or desc)';
|
|
9390
9598
|
var CUSTOM_TOOL_SCHEMA_HELP = 'OpenAI function schema: {"type":"function","function":{"name":"...","parameters":{"type":"object","properties":{}}}}';
|
|
9391
9599
|
var FOLDER_PATH_INPUT = {
|
|
9392
|
-
describe: "Folder path; the leading / is optional"
|
|
9600
|
+
describe: "Folder path as shown in the app; the leading / is optional",
|
|
9601
|
+
folderPath: true
|
|
9393
9602
|
};
|
|
9394
9603
|
var FOLDER_PATH_FLAG = {
|
|
9395
9604
|
...FOLDER_PATH_INPUT,
|
|
@@ -9399,7 +9608,7 @@ var FOLDER_DELETE_FLAGS = {
|
|
|
9399
9608
|
path: FOLDER_PATH_INPUT,
|
|
9400
9609
|
recursive: { boolean: true, describe: "Delete the folder and its descendants" }
|
|
9401
9610
|
};
|
|
9402
|
-
var
|
|
9611
|
+
var KNOWLEDGE_BASE_PATH_ARGUMENT = { id: "knowledgeBaseId" };
|
|
9403
9612
|
var WORKFLOW_RUN_SCOPE = {
|
|
9404
9613
|
id: {
|
|
9405
9614
|
name: "workflow",
|
|
@@ -9407,10 +9616,11 @@ var WORKFLOW_RUN_SCOPE = {
|
|
|
9407
9616
|
describe: "Workflow ID"
|
|
9408
9617
|
}
|
|
9409
9618
|
};
|
|
9619
|
+
var FOLDER_COLUMN = { header: "folder", path: "folderPath", format: "folder-path" };
|
|
9410
9620
|
var FOLDER_LIST_COLUMNS = [
|
|
9411
|
-
{ header: "path" },
|
|
9621
|
+
{ header: "path", format: "folder-path" },
|
|
9412
9622
|
{ header: "name" },
|
|
9413
|
-
{ header: "parent", path: "parentPath" },
|
|
9623
|
+
{ header: "parent", path: "parentPath", format: "folder-path" },
|
|
9414
9624
|
{ header: "updated", path: "updatedAt", format: "timestamp" }
|
|
9415
9625
|
];
|
|
9416
9626
|
function moveResource(command, resource) {
|
|
@@ -9480,7 +9690,7 @@ var CLI_CONTRACT = {
|
|
|
9480
9690
|
bulkUpdateKnowledgeDocuments: {
|
|
9481
9691
|
command: "knowledge documents batch-update",
|
|
9482
9692
|
describe: "Enable or disable every matching document",
|
|
9483
|
-
pathArgumentNames:
|
|
9693
|
+
pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT,
|
|
9484
9694
|
flags: {
|
|
9485
9695
|
documentIds: { name: "document", list: true },
|
|
9486
9696
|
selectAll: { boolean: true, describe: "Apply to every document in the knowledge base" }
|
|
@@ -9490,6 +9700,11 @@ var CLI_CONTRACT = {
|
|
|
9490
9700
|
command: "workflows undeploy",
|
|
9491
9701
|
describe: "Take a workflow out of deployment"
|
|
9492
9702
|
},
|
|
9703
|
+
getWorkflowDeployment: {
|
|
9704
|
+
command: "workflows deployment status",
|
|
9705
|
+
renamedFrom: ["workflows deployment list"],
|
|
9706
|
+
describe: "Show a workflow’s current deployment"
|
|
9707
|
+
},
|
|
9493
9708
|
setSecret: { hidden: true },
|
|
9494
9709
|
deleteTable: { confirm: "This deletes the table and all of its rows." },
|
|
9495
9710
|
deleteTableRow: { confirm: "This deletes the row." },
|
|
@@ -9499,7 +9714,7 @@ var CLI_CONTRACT = {
|
|
|
9499
9714
|
},
|
|
9500
9715
|
deleteKnowledgeBase: { confirm: "This deletes the knowledge base and every document in it." },
|
|
9501
9716
|
deleteKnowledgeDocument: {
|
|
9502
|
-
pathArgumentNames:
|
|
9717
|
+
pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT,
|
|
9503
9718
|
confirm: "This deletes the document and its embeddings."
|
|
9504
9719
|
},
|
|
9505
9720
|
deleteFile: { confirm: "This archives the file." },
|
|
@@ -9529,7 +9744,10 @@ var CLI_CONTRACT = {
|
|
|
9529
9744
|
workflowIds: { name: "workflow", list: true },
|
|
9530
9745
|
folderPaths: { ...FOLDER_PATH_FLAG, list: true },
|
|
9531
9746
|
triggers: { name: "trigger", list: true },
|
|
9532
|
-
details: {
|
|
9747
|
+
details: {
|
|
9748
|
+
requestDefault: "full",
|
|
9749
|
+
describe: "Response detail level; full is requested by default to name each run’s workflow"
|
|
9750
|
+
},
|
|
9533
9751
|
includeTraceSpans: {
|
|
9534
9752
|
boolean: true,
|
|
9535
9753
|
describe: "Include trace spans in JSON or YAML output (implies full detail)"
|
|
@@ -9656,7 +9874,7 @@ var CLI_CONTRACT = {
|
|
|
9656
9874
|
columns: [
|
|
9657
9875
|
{ header: "id" },
|
|
9658
9876
|
{ header: "name" },
|
|
9659
|
-
|
|
9877
|
+
FOLDER_COLUMN,
|
|
9660
9878
|
{ header: "rows", path: "rowCount" },
|
|
9661
9879
|
{ header: "updated", path: "updatedAt", format: "timestamp" }
|
|
9662
9880
|
]
|
|
@@ -9666,7 +9884,7 @@ var CLI_CONTRACT = {
|
|
|
9666
9884
|
columns: [
|
|
9667
9885
|
{ header: "id" },
|
|
9668
9886
|
{ header: "name" },
|
|
9669
|
-
|
|
9887
|
+
FOLDER_COLUMN,
|
|
9670
9888
|
{ header: "deployed", path: "isDeployed", format: "bool" },
|
|
9671
9889
|
{ header: "runs", path: "runCount" },
|
|
9672
9890
|
{ header: "last run", path: "lastRunAt", format: "timestamp" }
|
|
@@ -9677,7 +9895,7 @@ var CLI_CONTRACT = {
|
|
|
9677
9895
|
columns: [
|
|
9678
9896
|
{ header: "id" },
|
|
9679
9897
|
{ header: "name" },
|
|
9680
|
-
|
|
9898
|
+
FOLDER_COLUMN,
|
|
9681
9899
|
{ header: "size", format: "bytes" },
|
|
9682
9900
|
{ header: "type" },
|
|
9683
9901
|
{ header: "uploaded by", path: "uploadedByEmail" },
|
|
@@ -9690,15 +9908,17 @@ var CLI_CONTRACT = {
|
|
|
9690
9908
|
columns: [
|
|
9691
9909
|
{ header: "id" },
|
|
9692
9910
|
{ header: "name" },
|
|
9693
|
-
|
|
9911
|
+
FOLDER_COLUMN,
|
|
9694
9912
|
{ header: "docs", path: "docCount" },
|
|
9695
9913
|
{ header: "tokens", path: "tokenCount" },
|
|
9696
9914
|
{ header: "model", path: "embeddingModel" }
|
|
9697
9915
|
]
|
|
9698
9916
|
},
|
|
9699
|
-
getKnowledgeDocument: { pathArgumentNames:
|
|
9917
|
+
getKnowledgeDocument: { pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT },
|
|
9918
|
+
updateKnowledgeDocument: { pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT },
|
|
9919
|
+
listKnowledgeTags: { pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT },
|
|
9700
9920
|
listKnowledgeDocuments: {
|
|
9701
|
-
pathArgumentNames:
|
|
9921
|
+
pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT,
|
|
9702
9922
|
columns: [
|
|
9703
9923
|
{ header: "id" },
|
|
9704
9924
|
{ header: "filename" },
|
|
@@ -9813,7 +10033,7 @@ var CLI_CONTRACT = {
|
|
|
9813
10033
|
{ header: "name" },
|
|
9814
10034
|
{ header: "size", format: "bytes" },
|
|
9815
10035
|
{ header: "type" },
|
|
9816
|
-
|
|
10036
|
+
FOLDER_COLUMN,
|
|
9817
10037
|
{ header: "uploaded by", path: "uploadedByEmail" },
|
|
9818
10038
|
{ header: "uploaded", path: "uploadedAt", format: "timestamp" },
|
|
9819
10039
|
{ header: "updated", path: "updatedAt", format: "timestamp" },
|
|
@@ -9840,6 +10060,11 @@ var CLI_CONTRACT = {
|
|
|
9840
10060
|
command: "files rename",
|
|
9841
10061
|
describe: "Rename a file"
|
|
9842
10062
|
},
|
|
10063
|
+
restoreFile: {
|
|
10064
|
+
command: "files restore",
|
|
10065
|
+
renamedFrom: ["files restore create"],
|
|
10066
|
+
describe: "Restore an archived file"
|
|
10067
|
+
},
|
|
9843
10068
|
updateFileContent: {
|
|
9844
10069
|
command: "files set-content",
|
|
9845
10070
|
describe: "Replace a file’s contents",
|
|
@@ -9992,13 +10217,26 @@ var CLI_CONTRACT = {
|
|
|
9992
10217
|
command: "tables rows find",
|
|
9993
10218
|
describe: "Find rows matching a predicate",
|
|
9994
10219
|
flags: {
|
|
9995
|
-
q: { describe: "Value to find" },
|
|
10220
|
+
q: { name: "query", renamedFrom: ["q"], describe: "Value to find" },
|
|
9996
10221
|
predicate: { name: "filter", json: true, describe: TABLE_FILTER_HELP },
|
|
9997
10222
|
sort: { json: true, describe: TABLE_SORT_HELP }
|
|
9998
10223
|
},
|
|
9999
10224
|
itemsPath: "matches",
|
|
10000
10225
|
columns: [{ header: "ordinal" }, { header: "row", path: "rowId" }, { header: "column" }]
|
|
10001
10226
|
},
|
|
10227
|
+
queryRowsCount: {
|
|
10228
|
+
command: "tables rows count",
|
|
10229
|
+
renamedFrom: ["tables count create"],
|
|
10230
|
+
describe: "Count rows matching a filter",
|
|
10231
|
+
flags: {
|
|
10232
|
+
predicate: {
|
|
10233
|
+
name: "filter",
|
|
10234
|
+
renamedFrom: ["predicate"],
|
|
10235
|
+
json: true,
|
|
10236
|
+
describe: TABLE_FILTER_HELP
|
|
10237
|
+
}
|
|
10238
|
+
}
|
|
10239
|
+
},
|
|
10002
10240
|
runTableColumn: {
|
|
10003
10241
|
command: "tables columns run",
|
|
10004
10242
|
describe: "Run a column’s workflow",
|
|
@@ -10251,6 +10489,26 @@ function readListValues(raw, flagName) {
|
|
|
10251
10489
|
return trimmed;
|
|
10252
10490
|
});
|
|
10253
10491
|
}
|
|
10492
|
+
var PERCENT_ESCAPE = /%[0-9A-Fa-f]{2}/;
|
|
10493
|
+
var SUB_DELIMITERS = /[!'()*]/g;
|
|
10494
|
+
function encodeFolderPathSegment(name) {
|
|
10495
|
+
if (name === ".")
|
|
10496
|
+
return "%2E";
|
|
10497
|
+
if (name === "..")
|
|
10498
|
+
return "%2E%2E";
|
|
10499
|
+
return encodeURIComponent(name).replace(SUB_DELIMITERS, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
10500
|
+
}
|
|
10501
|
+
function encodeFolderPath(value) {
|
|
10502
|
+
return value.split("/").map((segment) => {
|
|
10503
|
+
if (!PERCENT_ESCAPE.test(segment))
|
|
10504
|
+
return encodeFolderPathSegment(segment);
|
|
10505
|
+
try {
|
|
10506
|
+
return encodeFolderPathSegment(decodeURIComponent(segment));
|
|
10507
|
+
} catch {
|
|
10508
|
+
return encodeFolderPathSegment(segment);
|
|
10509
|
+
}
|
|
10510
|
+
}).join("/");
|
|
10511
|
+
}
|
|
10254
10512
|
function pathHint(raw) {
|
|
10255
10513
|
if (raw.startsWith("@") || /^\s*[[{"\-\d]|^\s*(true|false|null)/.test(raw))
|
|
10256
10514
|
return "";
|
|
@@ -10260,7 +10518,7 @@ function coerce(raw, field, flag, flagName) {
|
|
|
10260
10518
|
if (raw === undefined)
|
|
10261
10519
|
return;
|
|
10262
10520
|
if (flag.list) {
|
|
10263
|
-
const values = readListValues(raw, flagName);
|
|
10521
|
+
const values = readListValues(raw, flagName).map((value) => flag.folderPath ? encodeFolderPath(value) : value);
|
|
10264
10522
|
return field.kind === "string" ? values.join(",") : values;
|
|
10265
10523
|
}
|
|
10266
10524
|
if (takesJson(field, flag)) {
|
|
@@ -10285,6 +10543,8 @@ function coerce(raw, field, flag, flagName) {
|
|
|
10285
10543
|
if (choices && !choices.includes(String(raw))) {
|
|
10286
10544
|
throw new SimApiError(`--${flagName} must be one of: ${choices.join(", ")}`, 0);
|
|
10287
10545
|
}
|
|
10546
|
+
if (flag.folderPath && typeof raw === "string")
|
|
10547
|
+
return encodeFolderPath(raw);
|
|
10288
10548
|
return raw;
|
|
10289
10549
|
}
|
|
10290
10550
|
function asQueryValue(value) {
|
|
@@ -10325,7 +10585,8 @@ function buildRequest(operation, positional, flags, workspaceId) {
|
|
|
10325
10585
|
continue;
|
|
10326
10586
|
const flagName = flagNameFor(operation, field);
|
|
10327
10587
|
const omitProfileWorkspace = commandSpec.allWorkspaces && flags.allWorkspaces === true;
|
|
10328
|
-
const
|
|
10588
|
+
const provided = field === PROFILE_INJECTED_FIELD ? omitProfileWorkspace ? undefined : workspaceId : flags[camel(flagName)];
|
|
10589
|
+
const raw = provided ?? flag.requestDefault;
|
|
10329
10590
|
const value = coerce(raw ?? undefined, descriptor, flag, flagName);
|
|
10330
10591
|
if (value === undefined) {
|
|
10331
10592
|
if (descriptor.required) {
|
|
@@ -10498,6 +10759,15 @@ function countTraceSpans(value) {
|
|
|
10498
10759
|
function at(row, path) {
|
|
10499
10760
|
return path.split(".").reduce((value, key) => value && typeof value === "object" ? value[key] : undefined, row);
|
|
10500
10761
|
}
|
|
10762
|
+
function decodeFolderPath(value) {
|
|
10763
|
+
return value.split("/").map((segment) => {
|
|
10764
|
+
try {
|
|
10765
|
+
return decodeURIComponent(segment);
|
|
10766
|
+
} catch {
|
|
10767
|
+
return segment;
|
|
10768
|
+
}
|
|
10769
|
+
}).join("/");
|
|
10770
|
+
}
|
|
10501
10771
|
function renderCell(value, format, options = {}) {
|
|
10502
10772
|
switch (format) {
|
|
10503
10773
|
case "timestamp":
|
|
@@ -10512,6 +10782,8 @@ function renderCell(value, format, options = {}) {
|
|
|
10512
10782
|
return typeof value === "number" ? `$${value.toFixed(4)}` : text(null);
|
|
10513
10783
|
case "count":
|
|
10514
10784
|
return Array.isArray(value) ? String(value.length) : text(null);
|
|
10785
|
+
case "folder-path":
|
|
10786
|
+
return typeof value === "string" ? text(decodeFolderPath(value)) : text(value);
|
|
10515
10787
|
case "trace-count": {
|
|
10516
10788
|
const count = countTraceSpans(value);
|
|
10517
10789
|
return `${count} ${count === 1 ? "span" : "spans"}${options.expandedTrace ? "" : " (use --trace)"}`;
|
|
@@ -10522,10 +10794,42 @@ function renderCell(value, format, options = {}) {
|
|
|
10522
10794
|
return sanitize(typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
10523
10795
|
}
|
|
10524
10796
|
}
|
|
10525
|
-
var
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10797
|
+
var TIMESTAMP_KEY = /(?:At|Date)$/;
|
|
10798
|
+
var DURATION_KEY = /Ms$|^duration/;
|
|
10799
|
+
var BYTES_KEY = /^size$|(?:Size|Bytes)$/;
|
|
10800
|
+
var BOOL_KEY = /^(?:is|has)[A-Z]/;
|
|
10801
|
+
var RATIO_KEY = /^(?:similarity|score)$|(?:Similarity|Score)$/;
|
|
10802
|
+
var FOLDER_PATH_KEY = /^(?:path|parentPath|folderPath)$/;
|
|
10803
|
+
var ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
|
|
10804
|
+
var RATIO_PRECISION = 4;
|
|
10805
|
+
function inferFormat(key, value) {
|
|
10806
|
+
if (typeof value === "boolean")
|
|
10807
|
+
return BOOL_KEY.test(key) ? "bool" : null;
|
|
10808
|
+
if (typeof value === "string") {
|
|
10809
|
+
if (FOLDER_PATH_KEY.test(key))
|
|
10810
|
+
return "folder-path";
|
|
10811
|
+
return TIMESTAMP_KEY.test(key) && ISO_TIMESTAMP.test(value) && !Number.isNaN(Date.parse(value)) ? "timestamp" : null;
|
|
10812
|
+
}
|
|
10813
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
10814
|
+
return null;
|
|
10815
|
+
if (DURATION_KEY.test(key))
|
|
10816
|
+
return "duration";
|
|
10817
|
+
if (BYTES_KEY.test(key))
|
|
10818
|
+
return "bytes";
|
|
10819
|
+
return null;
|
|
10820
|
+
}
|
|
10821
|
+
function inferredCell(key, value) {
|
|
10822
|
+
if (typeof value === "number" && Number.isFinite(value) && RATIO_KEY.test(key)) {
|
|
10823
|
+
return value.toFixed(RATIO_PRECISION);
|
|
10824
|
+
}
|
|
10825
|
+
return renderCell(value, inferFormat(key, value) ?? "auto");
|
|
10826
|
+
}
|
|
10827
|
+
function humanizeKey(key) {
|
|
10828
|
+
return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
10829
|
+
}
|
|
10830
|
+
function inferHeader(key, format) {
|
|
10831
|
+
const trimmed = format === "duration" || format === "bytes" ? key.replace(/(?:Ms|Bytes)$/, "") : format === "bool" ? key.replace(/^is(?=[A-Z])/, "") : key;
|
|
10832
|
+
return humanizeKey(trimmed || key);
|
|
10529
10833
|
}
|
|
10530
10834
|
function columnsFrom(specs) {
|
|
10531
10835
|
return specs.map((spec) => ({
|
|
@@ -10534,9 +10838,9 @@ function columnsFrom(specs) {
|
|
|
10534
10838
|
}));
|
|
10535
10839
|
}
|
|
10536
10840
|
function fieldsFrom(data, specs, options = {}) {
|
|
10537
|
-
return specs.
|
|
10841
|
+
return specs.map((spec) => {
|
|
10538
10842
|
const value = at(data, spec.path ?? spec.header);
|
|
10539
|
-
return value === undefined ?
|
|
10843
|
+
return [spec.header, value === undefined ? text(null) : renderCell(value, spec.format, options)];
|
|
10540
10844
|
});
|
|
10541
10845
|
}
|
|
10542
10846
|
function inferColumns(rows, expand) {
|
|
@@ -10551,7 +10855,7 @@ function inferColumns(rows, expand) {
|
|
|
10551
10855
|
if (value !== null && typeof value === "object")
|
|
10552
10856
|
continue;
|
|
10553
10857
|
seen.add(key);
|
|
10554
|
-
paths.push({ path: key, header: key });
|
|
10858
|
+
paths.push({ path: key, key, header: inferHeader(key, inferFormat(key, value)), owned: true });
|
|
10555
10859
|
}
|
|
10556
10860
|
}
|
|
10557
10861
|
if (expand) {
|
|
@@ -10564,13 +10868,18 @@ function inferColumns(rows, expand) {
|
|
|
10564
10868
|
if (nested.has(key))
|
|
10565
10869
|
continue;
|
|
10566
10870
|
nested.add(key);
|
|
10567
|
-
paths.push({
|
|
10871
|
+
paths.push({
|
|
10872
|
+
path: `${expand}.${key}`,
|
|
10873
|
+
key,
|
|
10874
|
+
header: seen.has(key) ? `${expand}.${key}` : key,
|
|
10875
|
+
owned: false
|
|
10876
|
+
});
|
|
10568
10877
|
}
|
|
10569
10878
|
}
|
|
10570
10879
|
}
|
|
10571
|
-
return paths.map(({ path, header }) => ({
|
|
10880
|
+
return paths.map(({ path, key, header, owned }) => ({
|
|
10572
10881
|
header: sanitize(header),
|
|
10573
|
-
value: (row) => renderCell(at(row, path), "auto")
|
|
10882
|
+
value: (row) => owned ? inferredCell(key, at(row, path)) : renderCell(at(row, path), "auto")
|
|
10574
10883
|
}));
|
|
10575
10884
|
}
|
|
10576
10885
|
function unwrapResource(data) {
|
|
@@ -10603,7 +10912,10 @@ function renderResult(operation, format, raw, spec, options = {}) {
|
|
|
10603
10912
|
printList(format, data, spec.columns ? columnsFrom(spec.columns) : inferColumns(data, spec.expand));
|
|
10604
10913
|
return;
|
|
10605
10914
|
}
|
|
10606
|
-
const fields = spec.fields ? fieldsFrom(data, spec.fields, options) : data && typeof data === "object" ? Object.entries(data).map(([key, value]) => [
|
|
10915
|
+
const fields = spec.fields ? fieldsFrom(data, spec.fields, options) : data && typeof data === "object" ? Object.entries(data).map(([key, value]) => [
|
|
10916
|
+
inferHeader(key, inferFormat(key, value)),
|
|
10917
|
+
inferredCell(key, value)
|
|
10918
|
+
]) : [];
|
|
10607
10919
|
printRecord(format, fields, data);
|
|
10608
10920
|
if (spec.expandedTrace && options.expandedTrace) {
|
|
10609
10921
|
const traceSpans = at(data, "traceSpans");
|
|
@@ -11001,7 +11313,7 @@ async function finishUploadSession(client, workspaceId, session, path) {
|
|
|
11001
11313
|
|
|
11002
11314
|
// src/commands/protocol/files-upload.ts
|
|
11003
11315
|
function attachFileUpload(files) {
|
|
11004
|
-
files.command("upload").argument("<path>", "Local file to upload").description("Upload a file to the workspace").option("--folder <path>", "
|
|
11316
|
+
files.command("upload").argument("<path>", "Local file to upload").description("Upload a file to the workspace").option("--folder <path>", "Folder path as shown in the app; defaults to the root folder").option("--name <name>", "Store it under a different name").action(async (path, options, command) => {
|
|
11005
11317
|
const { client, profile } = clientFrom(command);
|
|
11006
11318
|
const workspaceId = client.requireWorkspace();
|
|
11007
11319
|
const { name, size } = await localFile(path, options.name);
|
|
@@ -11012,7 +11324,7 @@ function attachFileUpload(files) {
|
|
|
11012
11324
|
name,
|
|
11013
11325
|
contentType: contentTypeFor(name),
|
|
11014
11326
|
size,
|
|
11015
|
-
...options.folder !== undefined ? { folderPath: options.folder } : {}
|
|
11327
|
+
...options.folder !== undefined ? { folderPath: encodeFolderPath(options.folder) } : {}
|
|
11016
11328
|
}
|
|
11017
11329
|
});
|
|
11018
11330
|
const { session, uploadToken, transfer } = created.data;
|
|
@@ -11114,15 +11426,22 @@ function addFieldOption(command, operation, field, descriptor) {
|
|
|
11114
11426
|
const placeholder = takesList ? "<value...>" : wantsJson ? "<json|@file>" : "<value>";
|
|
11115
11427
|
const choices = flag.choices ?? descriptor.values;
|
|
11116
11428
|
const describe = `${documented}${takesList ? " (space-separated, or @path / @- with one value per line)" : wantsJson ? " (JSON, or @path / @- to read a file or stdin)" : ""}${descriptor.required ? " (required)" : ""}`;
|
|
11429
|
+
const renamedFrom = flag.renamedFrom ?? [];
|
|
11117
11430
|
const option = new Option(`${short}--${name} ${placeholder}`, describe);
|
|
11118
11431
|
if (choices && !takesList)
|
|
11119
11432
|
option.choices([...choices]);
|
|
11120
11433
|
if (descriptor.default !== undefined && field !== "limit") {
|
|
11121
11434
|
option.default(undefined, String(descriptor.default));
|
|
11122
11435
|
}
|
|
11123
|
-
if (descriptor.required)
|
|
11436
|
+
if (descriptor.required && renamedFrom.length === 0)
|
|
11124
11437
|
option.makeOptionMandatory();
|
|
11125
11438
|
command.addOption(option);
|
|
11439
|
+
for (const previous of renamedFrom) {
|
|
11440
|
+
const retired = new Option(`--${previous} ${placeholder}`).hideHelp();
|
|
11441
|
+
if (choices && !takesList)
|
|
11442
|
+
retired.choices([...choices]);
|
|
11443
|
+
command.addOption(retired);
|
|
11444
|
+
}
|
|
11126
11445
|
}
|
|
11127
11446
|
function addOperationOptions(command, operation, commandSpec, operationSpec) {
|
|
11128
11447
|
for (const param of operationSpec.pathParams) {
|
|
@@ -11158,7 +11477,7 @@ function addOperationOptions(command, operation, commandSpec, operationSpec) {
|
|
|
11158
11477
|
}
|
|
11159
11478
|
}
|
|
11160
11479
|
if (commandSpec.confirm) {
|
|
11161
|
-
command.option("-y, --yes", "
|
|
11480
|
+
command.option("-y, --yes", "Confirm this destructive operation (required)");
|
|
11162
11481
|
}
|
|
11163
11482
|
}
|
|
11164
11483
|
|
|
@@ -11166,8 +11485,11 @@ function addOperationOptions(command, operation, commandSpec, operationSpec) {
|
|
|
11166
11485
|
var COLUMNS = [
|
|
11167
11486
|
{ header: "kind", value: (entry) => text(entry.kind) },
|
|
11168
11487
|
{ header: "name", value: (entry) => text(entry.name) },
|
|
11169
|
-
{
|
|
11170
|
-
|
|
11488
|
+
{
|
|
11489
|
+
header: "ref",
|
|
11490
|
+
value: (entry) => text(entry.kind === "folder" ? decodeFolderPath(entry.ref) : entry.ref)
|
|
11491
|
+
},
|
|
11492
|
+
{ header: "folder", value: (entry) => text(decodeFolderPath(entry.folderPath)) },
|
|
11171
11493
|
{ header: "updated", value: (entry) => timestamp2(entry.updatedAt) }
|
|
11172
11494
|
];
|
|
11173
11495
|
function operationPath(operation) {
|
|
@@ -11218,7 +11540,7 @@ function attachResourceDirectoryCommands(group, config) {
|
|
|
11218
11540
|
throw new SimApiError("--limit must be a non-negative integer", 0);
|
|
11219
11541
|
}
|
|
11220
11542
|
const limit = rawLimit === 0 ? Number.POSITIVE_INFINITY : rawLimit;
|
|
11221
|
-
const folderPath = path ?? "/";
|
|
11543
|
+
const folderPath = encodeFolderPath(path ?? "/");
|
|
11222
11544
|
const { client, profile } = clientFrom(command);
|
|
11223
11545
|
const workspaceId = client.requireWorkspace();
|
|
11224
11546
|
const [folders, resources] = await Promise.all([
|
|
@@ -11233,7 +11555,7 @@ function attachResourceDirectoryCommands(group, config) {
|
|
|
11233
11555
|
const operation = V2_OPERATIONS[config.createFolder];
|
|
11234
11556
|
const result = await client.request(operation.path, {
|
|
11235
11557
|
method: operation.method,
|
|
11236
|
-
body: { workspaceId: client.requireWorkspace(), path }
|
|
11558
|
+
body: { workspaceId: client.requireWorkspace(), path: encodeFolderPath(path) }
|
|
11237
11559
|
});
|
|
11238
11560
|
renderResult(config.createFolder, profile.output, result.data ?? result, {});
|
|
11239
11561
|
});
|
|
@@ -11287,7 +11609,7 @@ function validateTargetOptions(options) {
|
|
|
11287
11609
|
return intoExisting;
|
|
11288
11610
|
}
|
|
11289
11611
|
function attachTableImport(tables) {
|
|
11290
|
-
tables.command("import").argument("[path]", "Local CSV file to import; omit when using --file-id").description("Import a CSV, into a new table by default").option("--name <name>", "Identifier for the new table: letters, numbers, and underscores; defaults to the sanitized file name").option("--table-id <id>", "Import into this existing table instead of creating one").addOption(new Option("--mode <append|replace>", "How to write into --table-id (default: append)").choices(["append", "replace"])).option("--folder <path>", "Folder path for the new table").option("--file-id <id>", "Import a file already in the workspace instead of a local path").option("--mapping <json|@file>", "Column mapping (--table-id only)").option("--create-columns <json|@file>", "Columns to create (--table-id only)").option("--timezone <iana>", "Timezone for date parsing, e.g. America/New_York").option("--no-wait", "Return once the import is queued instead of watching it").action(async (path, options, command) => {
|
|
11612
|
+
tables.command("import").argument("[path]", "Local CSV file to import; omit when using --file-id").description("Import a CSV, into a new table by default").option("--name <name>", "Identifier for the new table: letters, numbers, and underscores; defaults to the sanitized file name").option("--table-id <id>", "Import into this existing table instead of creating one").addOption(new Option("--mode <append|replace>", "How to write into --table-id (default: append)").choices(["append", "replace"])).option("--folder <path>", "Folder path for the new table, as shown in the app").option("--file-id <id>", "Import a file already in the workspace instead of a local path").option("--mapping <json|@file>", "Column mapping (--table-id only)").option("--create-columns <json|@file>", "Columns to create (--table-id only)").option("--timezone <iana>", "Timezone for date parsing, e.g. America/New_York").option("--no-wait", "Return once the import is queued instead of watching it").action(async (path, options, command) => {
|
|
11291
11613
|
const { client, profile } = clientFrom(command);
|
|
11292
11614
|
const workspaceId = client.requireWorkspace();
|
|
11293
11615
|
if (Boolean(path) === Boolean(options.fileId)) {
|
|
@@ -11312,7 +11634,7 @@ function attachTableImport(tables) {
|
|
|
11312
11634
|
target = {
|
|
11313
11635
|
type: "new",
|
|
11314
11636
|
name,
|
|
11315
|
-
...options.folder !== undefined ? { folderPath: options.folder } : {}
|
|
11637
|
+
...options.folder !== undefined ? { folderPath: encodeFolderPath(options.folder) } : {}
|
|
11316
11638
|
};
|
|
11317
11639
|
}
|
|
11318
11640
|
const started = await client.request(V2_OPERATIONS.createTableImport.path, {
|
|
@@ -11513,6 +11835,23 @@ function attachSecretCommands(program2) {
|
|
|
11513
11835
|
secrets.command("set").argument("<name>", "Secret name, as referenced in workflows").description("Create or replace a named secret").addOption(new Option("--scope <scope>", "Secret ownership scope").choices([...SECRET_SCOPES]).makeOptionMandatory()).option("--value <value>", "Secret value; visible to shell history when supplied directly").action((name, options, command) => setSecret(name, options, command));
|
|
11514
11836
|
}
|
|
11515
11837
|
|
|
11838
|
+
// src/runtime/renamed.ts
|
|
11839
|
+
var warned = new Set;
|
|
11840
|
+
function warn(kind, from, to) {
|
|
11841
|
+
const key = `${kind}:${from}`;
|
|
11842
|
+
if (warned.has(key))
|
|
11843
|
+
return;
|
|
11844
|
+
warned.add(key);
|
|
11845
|
+
process.stderr.write(`warning: ${kind} "${from}" has been renamed to "${to}". The old name still works.
|
|
11846
|
+
`);
|
|
11847
|
+
}
|
|
11848
|
+
function warnRenamedCommand(from, to) {
|
|
11849
|
+
warn("command", `sim ${from}`, `sim ${to}`);
|
|
11850
|
+
}
|
|
11851
|
+
function warnRenamedFlag(from, to) {
|
|
11852
|
+
warn("flag", `--${from}`, `--${to}`);
|
|
11853
|
+
}
|
|
11854
|
+
|
|
11516
11855
|
// src/runtime/execute.ts
|
|
11517
11856
|
function cursorSlot(operationSpec) {
|
|
11518
11857
|
if (operationSpec.query && "cursor" in operationSpec.query)
|
|
@@ -11521,6 +11860,23 @@ function cursorSlot(operationSpec) {
|
|
|
11521
11860
|
return "body";
|
|
11522
11861
|
return null;
|
|
11523
11862
|
}
|
|
11863
|
+
function foldRenamedFlags(operation, commandSpec, flags) {
|
|
11864
|
+
for (const [field, flag] of Object.entries(commandSpec.flags ?? {})) {
|
|
11865
|
+
if (!flag.renamedFrom?.length)
|
|
11866
|
+
continue;
|
|
11867
|
+
const current = flagNameFor(operation, field);
|
|
11868
|
+
for (const previous of flag.renamedFrom) {
|
|
11869
|
+
const supplied = flags[camel(previous)];
|
|
11870
|
+
if (supplied === undefined)
|
|
11871
|
+
continue;
|
|
11872
|
+
if (flags[camel(current)] !== undefined) {
|
|
11873
|
+
throw new SimApiError(`--${previous} is the former name of --${current}; pass one, not both`, 0);
|
|
11874
|
+
}
|
|
11875
|
+
warnRenamedFlag(previous, current);
|
|
11876
|
+
flags[camel(current)] = supplied;
|
|
11877
|
+
}
|
|
11878
|
+
}
|
|
11879
|
+
}
|
|
11524
11880
|
async function executeOperation(operation, commandSpec, operationSpec, invocation) {
|
|
11525
11881
|
const host = invocation[invocation.length - 1];
|
|
11526
11882
|
const inheritedFlags = host.optsWithGlobals();
|
|
@@ -11535,6 +11891,7 @@ async function executeOperation(operation, commandSpec, operationSpec, invocatio
|
|
|
11535
11891
|
for (const [index, field] of (commandSpec.positionals ?? []).entries()) {
|
|
11536
11892
|
requestFlags[camel(flagNameFor(operation, field))] = invocation[pathPositionalCount + index];
|
|
11537
11893
|
}
|
|
11894
|
+
foldRenamedFlags(operation, commandSpec, requestFlags);
|
|
11538
11895
|
if (commandSpec.confirm && !requestFlags.yes) {
|
|
11539
11896
|
throw new SimApiError(`${commandSpec.confirm} Re-run with --yes to confirm.`, 0);
|
|
11540
11897
|
}
|
|
@@ -11555,16 +11912,23 @@ async function executeOperation(operation, commandSpec, operationSpec, invocatio
|
|
|
11555
11912
|
const pageSize = Math.min(Number.isFinite(limit) ? limit : DEFAULT_LIMIT, DEFAULT_LIMIT);
|
|
11556
11913
|
const pageLimit = "limit" in (operationSpec[paging] ?? {}) ? { limit: pageSize } : {};
|
|
11557
11914
|
const rows = [];
|
|
11915
|
+
const progress = pageProgress();
|
|
11558
11916
|
let cursor = null;
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
|
|
11567
|
-
|
|
11917
|
+
try {
|
|
11918
|
+
do {
|
|
11919
|
+
const page = await client.request(request.path, {
|
|
11920
|
+
method: operationSpec.method,
|
|
11921
|
+
query: paging === "query" ? { ...request.query, ...pageLimit, cursor } : request.query,
|
|
11922
|
+
body: paging === "body" ? { ...request.body ?? {}, ...pageLimit, ...cursor ? { cursor } : {} } : request.body
|
|
11923
|
+
});
|
|
11924
|
+
rows.push(...page.data);
|
|
11925
|
+
cursor = page.nextCursor;
|
|
11926
|
+
if (cursor && rows.length < limit)
|
|
11927
|
+
progress.advance(rows.length);
|
|
11928
|
+
} while (cursor && rows.length < limit);
|
|
11929
|
+
} finally {
|
|
11930
|
+
progress.finish();
|
|
11931
|
+
}
|
|
11568
11932
|
renderPage(profile.output, Number.isFinite(limit) ? rows.slice(0, limit) : rows, commandSpec);
|
|
11569
11933
|
return;
|
|
11570
11934
|
}
|
|
@@ -11693,6 +12057,19 @@ function configureOperation(command, operation, spec) {
|
|
|
11693
12057
|
function buildLeaf(operation, spec, leafName) {
|
|
11694
12058
|
return addMissingArgumentExample(configureOperation(new Command(leafName), operation, spec));
|
|
11695
12059
|
}
|
|
12060
|
+
function addRenamedCommand(groups, operation, spec, from, to) {
|
|
12061
|
+
const segments = from.split(" ");
|
|
12062
|
+
const [groupName, ...rest] = segments;
|
|
12063
|
+
if (rest.length === 0)
|
|
12064
|
+
throw new Error(`${operation}.renamedFrom "${from}" must include a verb`);
|
|
12065
|
+
let parent = groupFor(groups, groupName);
|
|
12066
|
+
for (const segment of rest.slice(0, -1)) {
|
|
12067
|
+
parent = nestedGroup(parent, segment, { hidden: true });
|
|
12068
|
+
}
|
|
12069
|
+
const leaf = buildLeaf(operation, spec, rest[rest.length - 1]);
|
|
12070
|
+
leaf.hook("preAction", () => warnRenamedCommand(from, to));
|
|
12071
|
+
parent.addCommand(leaf, { hidden: true });
|
|
12072
|
+
}
|
|
11696
12073
|
function groupFor(groups, name) {
|
|
11697
12074
|
const existing = groups.get(name);
|
|
11698
12075
|
if (existing)
|
|
@@ -11708,12 +12085,12 @@ function resourceLabel(name) {
|
|
|
11708
12085
|
const label = name.endsWith("s") ? name.slice(0, -1) : name;
|
|
11709
12086
|
return label.replaceAll("-", " ");
|
|
11710
12087
|
}
|
|
11711
|
-
function nestedGroup(parent, name) {
|
|
12088
|
+
function nestedGroup(parent, name, options = {}) {
|
|
11712
12089
|
const existing = parent.commands.find((candidate) => candidate.name() === name);
|
|
11713
12090
|
if (existing)
|
|
11714
12091
|
return existing;
|
|
11715
12092
|
const created = new Command(name).description(`Manage ${resourceLabel(parent.name())} ${name.replaceAll("-", " ")}`);
|
|
11716
|
-
parent.addCommand(created);
|
|
12093
|
+
parent.addCommand(created, { hidden: options.hidden });
|
|
11717
12094
|
return created;
|
|
11718
12095
|
}
|
|
11719
12096
|
function addLeafCommand(groups, operation, spec, segments) {
|
|
@@ -11742,6 +12119,7 @@ function variantCommandSpec(spec, variant) {
|
|
|
11742
12119
|
}
|
|
11743
12120
|
function buildGeneratedCommands() {
|
|
11744
12121
|
const groups = new Map;
|
|
12122
|
+
const renamed = [];
|
|
11745
12123
|
for (const operation of Object.keys(V2_OPERATIONS)) {
|
|
11746
12124
|
const spec = CLI_CONTRACT[operation] ?? {};
|
|
11747
12125
|
const operationSpec = V2_OPERATIONS[operation];
|
|
@@ -11764,6 +12142,12 @@ function buildGeneratedCommands() {
|
|
|
11764
12142
|
for (const variant of spec.variants ?? []) {
|
|
11765
12143
|
addLeafCommand(groups, operation, variantCommandSpec(spec, variant), variant.command.split(" "));
|
|
11766
12144
|
}
|
|
12145
|
+
for (const from of spec.renamedFrom ?? []) {
|
|
12146
|
+
renamed.push({ operation, spec, from, to: segments.join(" ") });
|
|
12147
|
+
}
|
|
12148
|
+
}
|
|
12149
|
+
for (const { operation, spec, from, to } of renamed) {
|
|
12150
|
+
addRenamedCommand(groups, operation, spec, from, to);
|
|
11767
12151
|
}
|
|
11768
12152
|
return [...groups.values()].sort((a, b) => a.name().localeCompare(b.name()));
|
|
11769
12153
|
}
|