@stndrds/cli 1.0.0-alpha.291 → 1.0.0-alpha.293

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.
Files changed (3) hide show
  1. package/README.md +34 -0
  2. package/dist/bin.mjs +934 -127
  3. package/package.json +2 -2
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # Standards CLI
2
+
3
+ The Standards CLI connects a terminal to a Standards instance. See the public
4
+ [CLI guide](../../content/docs/07-cli.mdx) for installation and the full command surface.
5
+
6
+ ## Run this computer as a paired device
7
+
8
+ Remote device execution is an optional server capability. Once an operator has enabled it,
9
+ create a named profile, pair the current computer, and keep the foreground worker running:
10
+
11
+ ```bash
12
+ standards login --name production --url https://standards.example/v1 --key "$STANDARDS_API_KEY"
13
+ standards device pair --name "Build Mac"
14
+ standards device serve
15
+ ```
16
+
17
+ The worker supports macOS and Linux and makes outbound HTTPS requests only. It executes each
18
+ approved command in the foreground using the user's local account and permissions. There is no
19
+ privilege escalation, background service installation, PTY, file transfer, or GUI control.
20
+
21
+ Commands time out after 10 minutes by default and may not exceed 60 minutes. Standard output and
22
+ standard error are each limited to 30,000 characters. Stop the worker with `Ctrl+C`; inspect it
23
+ with `standards device status`; revoke its credential with `standards device revoke`.
24
+
25
+ Interactive harnesses are outside the V0 contract. Invoke installed tools in their
26
+ non-interactive mode, for example:
27
+
28
+ ```bash
29
+ codex exec --json "Run the repository checks"
30
+ claude -p "Summarize the failing tests"
31
+ ```
32
+
33
+ Every agent command targeting a device requires explicit user approval. Calls to `exec_command`
34
+ without a `deviceId` continue to use the agent sandbox.
package/dist/bin.mjs CHANGED
@@ -301,6 +301,833 @@ function registerConnectorsCommand(program) {
301
301
  });
302
302
  }
303
303
 
304
+ // src/commands/device.ts
305
+ import { hostname } from "os";
306
+ import { SchemaError as SchemaError2, SchemaErrorCode as SchemaErrorCode2 } from "@stndrds/schema";
307
+
308
+ // src/config.ts
309
+ import { randomUUID } from "crypto";
310
+ import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
311
+ import { homedir } from "os";
312
+ import { dirname, join } from "path";
313
+ var DEFAULT_API_URL = "http://localhost:4100/v1";
314
+ var ENV_PROFILE_NAME = "sandbox";
315
+ function getDefaultApiUrl() {
316
+ return DEFAULT_API_URL;
317
+ }
318
+ function getConfigPath() {
319
+ const configDir = process.env.STANDARDS_CONFIG_DIR ?? join(homedir(), ".standards");
320
+ return join(configDir, "config.json");
321
+ }
322
+ async function readConfig() {
323
+ try {
324
+ const raw = await readFile(getConfigPath(), "utf8");
325
+ const parsed = JSON.parse(raw);
326
+ return {
327
+ ...parsed.installationId ? { installationId: parsed.installationId } : {},
328
+ currentProfile: parsed.currentProfile,
329
+ profiles: parsed.profiles ?? {}
330
+ };
331
+ } catch (error) {
332
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
333
+ return { profiles: {} };
334
+ }
335
+ throw error;
336
+ }
337
+ }
338
+ async function writeConfig(config) {
339
+ const path = getConfigPath();
340
+ const directory = dirname(path);
341
+ await mkdir(directory, { recursive: true, mode: 448 });
342
+ await chmod(directory, 448);
343
+ await writeFile(path, `${JSON.stringify(config, null, 2)}
344
+ `, { mode: 384 });
345
+ await chmod(path, 384);
346
+ }
347
+ function normalizeApiOrigin(apiUrl) {
348
+ try {
349
+ return new URL(apiUrl).origin;
350
+ } catch {
351
+ return apiUrl.replace(/\/+$/, "");
352
+ }
353
+ }
354
+ async function upsertProfile(input2) {
355
+ const config = await readConfig();
356
+ const existingProfile = config.profiles[input2.name];
357
+ const device = existingProfile?.device && normalizeApiOrigin(existingProfile.apiUrl) === normalizeApiOrigin(input2.apiUrl) ? existingProfile.device : void 0;
358
+ config.profiles[input2.name] = {
359
+ apiUrl: input2.apiUrl,
360
+ apiKey: input2.apiKey,
361
+ ...device ? { device } : {}
362
+ };
363
+ config.currentProfile = input2.name;
364
+ await writeConfig(config);
365
+ }
366
+ async function getOrCreateInstallationId() {
367
+ const config = await readConfig();
368
+ if (config.installationId) return config.installationId;
369
+ const installationId = randomUUID();
370
+ config.installationId = installationId;
371
+ await writeConfig(config);
372
+ return installationId;
373
+ }
374
+ async function getStoredDeviceCredential(profileName) {
375
+ const config = await readConfig();
376
+ return config.profiles[profileName]?.device ?? null;
377
+ }
378
+ async function storeDeviceCredential(profileName, credential) {
379
+ const config = await readConfig();
380
+ const profile = config.profiles[profileName];
381
+ if (!profile) return false;
382
+ profile.device = credential;
383
+ await writeConfig(config);
384
+ return true;
385
+ }
386
+ async function removeDeviceCredential(profileName) {
387
+ const config = await readConfig();
388
+ const profile = config.profiles[profileName];
389
+ if (!profile?.device) return false;
390
+ profile.device = void 0;
391
+ await writeConfig(config);
392
+ return true;
393
+ }
394
+ async function setCurrentProfile(name) {
395
+ const config = await readConfig();
396
+ if (!config.profiles[name]) {
397
+ throw new Error(`Unknown Standards instance "${name}"`);
398
+ }
399
+ config.currentProfile = name;
400
+ await writeConfig(config);
401
+ }
402
+ async function removeProfile(name) {
403
+ const config = await readConfig();
404
+ const profileName = name ?? config.currentProfile;
405
+ if (!profileName) return;
406
+ delete config.profiles[profileName];
407
+ if (config.currentProfile === profileName) {
408
+ config.currentProfile = void 0;
409
+ }
410
+ await writeConfig(config);
411
+ }
412
+ async function listProfiles() {
413
+ const config = await readConfig();
414
+ const profiles = Object.entries(config.profiles).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
415
+ name,
416
+ apiUrl: profile.apiUrl,
417
+ current: config.currentProfile === name
418
+ }));
419
+ const envProfile = getEnvProfile();
420
+ if (envProfile) {
421
+ profiles.push({
422
+ name: ENV_PROFILE_NAME,
423
+ apiUrl: envProfile.apiUrl,
424
+ current: !config.currentProfile,
425
+ source: "env"
426
+ });
427
+ }
428
+ return profiles;
429
+ }
430
+ async function getActiveProfile() {
431
+ const config = await readConfig();
432
+ if (!config.currentProfile) {
433
+ const envProfile = getEnvProfile();
434
+ return envProfile ? { name: ENV_PROFILE_NAME, ...envProfile } : null;
435
+ }
436
+ const profile = config.profiles[config.currentProfile];
437
+ if (!profile) return null;
438
+ return { name: config.currentProfile, ...profile };
439
+ }
440
+ function getEnvProfile() {
441
+ if (!process.env.STANDARDS_API_KEY) return null;
442
+ return {
443
+ apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
444
+ apiKey: process.env.STANDARDS_API_KEY
445
+ };
446
+ }
447
+ async function resolveCliConfig(options) {
448
+ if (options.apiUrl || options.apiKey) {
449
+ return {
450
+ apiUrl: options.apiUrl ?? process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
451
+ apiKey: options.apiKey ?? process.env.STANDARDS_API_KEY
452
+ };
453
+ }
454
+ const config = await readConfig();
455
+ const profileName = options.instance ?? config.currentProfile;
456
+ if (profileName) {
457
+ const profile = config.profiles[profileName];
458
+ if (!profile) throw new Error(`Unknown Standards instance "${profileName}"`);
459
+ return {
460
+ apiUrl: profile.apiUrl,
461
+ apiKey: profile.apiKey,
462
+ profileName
463
+ };
464
+ }
465
+ return {
466
+ apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
467
+ apiKey: process.env.STANDARDS_API_KEY
468
+ };
469
+ }
470
+
471
+ // src/device/worker-client.ts
472
+ import { SchemaError, SchemaErrorCode } from "@stndrds/schema";
473
+ var REQUEST_TIMEOUT_MS = 3e4;
474
+ var DeviceWorkerRequestError = class extends SchemaError {
475
+ constructor(statusCode, message, code = SchemaErrorCode.REPOSITORY_QUERY_FAILED) {
476
+ super(message, code, { statusCode });
477
+ this.statusCode = statusCode;
478
+ this.name = "DeviceWorkerRequestError";
479
+ }
480
+ };
481
+ var DeviceCredentialRejectedError = class extends DeviceWorkerRequestError {
482
+ constructor() {
483
+ super(
484
+ 401,
485
+ "Device credential rejected; run standards device pair again",
486
+ SchemaErrorCode.ACCESS_DENIED
487
+ );
488
+ this.name = "DeviceCredentialRejectedError";
489
+ }
490
+ };
491
+ function isRecord(value) {
492
+ return value !== null && typeof value === "object" && !Array.isArray(value);
493
+ }
494
+ async function readJson(response) {
495
+ const text = await response.text();
496
+ if (!text) return void 0;
497
+ try {
498
+ return JSON.parse(text);
499
+ } catch {
500
+ return void 0;
501
+ }
502
+ }
503
+ function messageFrom(value, fallback) {
504
+ return isRecord(value) && typeof value.message === "string" ? value.message : fallback;
505
+ }
506
+ function parseLease(value) {
507
+ if (!isRecord(value)) throw new DeviceWorkerRequestError(200, "Invalid lease response");
508
+ const leaseToken = value.leaseToken;
509
+ if (!isRecord(value.command)) {
510
+ throw new DeviceWorkerRequestError(200, "Invalid lease response");
511
+ }
512
+ const leasedCommand = value.command;
513
+ const { id, intent, command, cwd, timeoutMs } = leasedCommand;
514
+ if (typeof id !== "string" || typeof leaseToken !== "string" || typeof intent !== "string" || typeof command !== "string" || cwd !== void 0 && cwd !== null && typeof cwd !== "string" || typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs)) {
515
+ throw new DeviceWorkerRequestError(200, "Invalid lease response");
516
+ }
517
+ return {
518
+ id,
519
+ leaseToken,
520
+ intent,
521
+ command,
522
+ timeoutMs,
523
+ ...typeof cwd === "string" ? { cwd } : {}
524
+ };
525
+ }
526
+ function parseHeartbeat(value) {
527
+ if (!isRecord(value)) {
528
+ throw new DeviceWorkerRequestError(200, "Invalid heartbeat response");
529
+ }
530
+ const { cancel } = value;
531
+ if (typeof cancel !== "boolean") {
532
+ throw new DeviceWorkerRequestError(200, "Invalid heartbeat response");
533
+ }
534
+ const reason = value.reason;
535
+ if (reason !== void 0 && reason !== "cancelled" && reason !== "revoked") {
536
+ throw new DeviceWorkerRequestError(200, "Invalid heartbeat response");
537
+ }
538
+ return { cancel, ...reason === void 0 ? {} : { reason } };
539
+ }
540
+ function isAbortError(error) {
541
+ return error instanceof DOMException && error.name === "AbortError";
542
+ }
543
+ var DeviceWorkerClient = class {
544
+ constructor(options) {
545
+ this.options = options;
546
+ this.fetch = options.fetch ?? fetch;
547
+ this.apiUrl = options.apiUrl.replace(/\/+$/, "");
548
+ }
549
+ async request(path, input2 = {}) {
550
+ const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
551
+ const signal = input2.signal ? AbortSignal.any([input2.signal, timeoutSignal]) : timeoutSignal;
552
+ let response;
553
+ try {
554
+ response = await this.fetch(`${this.apiUrl}/device-worker${path}`, {
555
+ method: "POST",
556
+ signal,
557
+ headers: {
558
+ authorization: `Device ${this.options.token}`,
559
+ "content-type": "application/json",
560
+ ...input2.leaseToken ? { "x-device-lease-token": input2.leaseToken } : {}
561
+ },
562
+ ...input2.body === void 0 ? {} : { body: JSON.stringify(input2.body) }
563
+ });
564
+ } catch (error) {
565
+ if (input2.signal?.aborted) throw new DOMException("Aborted", "AbortError");
566
+ if (isAbortError(error)) throw error;
567
+ throw new DeviceWorkerRequestError(0, "Device worker request failed");
568
+ }
569
+ if (response.status === 401) throw new DeviceCredentialRejectedError();
570
+ if (!response.ok) {
571
+ const body = await readJson(response);
572
+ throw new DeviceWorkerRequestError(
573
+ response.status,
574
+ messageFrom(body, `Device worker request failed with status ${response.status}`),
575
+ response.status === 409 ? SchemaErrorCode.CONFLICT : SchemaErrorCode.REPOSITORY_QUERY_FAILED
576
+ );
577
+ }
578
+ return response;
579
+ }
580
+ async lease(signal) {
581
+ const response = await this.request("/commands/lease", {
582
+ body: { version: "1", capabilities: ["exec"] },
583
+ signal
584
+ });
585
+ if (response.status === 204) return null;
586
+ return parseLease(await readJson(response));
587
+ }
588
+ async markRunning(commandId, leaseToken) {
589
+ await this.request(`/commands/${commandId}/running`, { leaseToken });
590
+ }
591
+ async heartbeat(commandId, leaseToken, signal) {
592
+ const response = await this.request(`/commands/${commandId}/heartbeat`, {
593
+ leaseToken,
594
+ signal
595
+ });
596
+ return parseHeartbeat(await readJson(response));
597
+ }
598
+ async output(commandId, leaseToken, stdout, stderr) {
599
+ await this.request(`/commands/${commandId}/output`, {
600
+ leaseToken,
601
+ body: { stdout, stderr }
602
+ });
603
+ }
604
+ async complete(commandId, leaseToken, result) {
605
+ await this.request(`/commands/${commandId}/complete`, {
606
+ leaseToken,
607
+ body: {
608
+ exitCode: result.exitCode,
609
+ success: result.success,
610
+ ...result.errorCode ? { errorCode: result.errorCode } : {}
611
+ }
612
+ });
613
+ }
614
+ };
615
+
616
+ // src/device/process-runner.ts
617
+ import { spawn } from "child_process";
618
+ import { constants } from "fs";
619
+ import { access, stat } from "fs/promises";
620
+ import { isAbsolute } from "path";
621
+ import { StringDecoder } from "string_decoder";
622
+ var OUTPUT_LIMIT = 3e4;
623
+ var TERMINATION_GRACE_MS = 5e3;
624
+ var TERMINATION_POLL_MS = 50;
625
+ var BoundedTextBuffer = class {
626
+ constructor() {
627
+ this.decoder = new StringDecoder("utf8");
628
+ this.decoderFlushed = false;
629
+ this.value = "";
630
+ this.truncated = false;
631
+ }
632
+ append(chunk) {
633
+ if (this.truncated) return;
634
+ const text = typeof chunk === "string" ? chunk : this.decoder.write(Buffer.from(chunk));
635
+ this.appendText(text);
636
+ }
637
+ appendText(text) {
638
+ const remaining = OUTPUT_LIMIT - this.value.length;
639
+ if (text.length <= remaining) {
640
+ this.value += text;
641
+ return;
642
+ }
643
+ this.value += text.slice(0, Math.max(remaining, 0));
644
+ this.truncated = true;
645
+ }
646
+ toString() {
647
+ if (!this.decoderFlushed) {
648
+ this.decoderFlushed = true;
649
+ this.appendText(this.decoder.end());
650
+ }
651
+ return this.truncated ? `${this.value}
652
+ ... output truncated to ${OUTPUT_LIMIT} chars` : this.value;
653
+ }
654
+ };
655
+ function isMissingProcess(error) {
656
+ return error instanceof Error && "code" in error && error.code === "ESRCH";
657
+ }
658
+ function signalProcessGroup(pid, signal, kill) {
659
+ try {
660
+ kill(-pid, signal);
661
+ return true;
662
+ } catch (error) {
663
+ if (isMissingProcess(error)) return false;
664
+ throw error;
665
+ }
666
+ }
667
+ function beginProcessGroupTermination(pid, deps = {}) {
668
+ const kill = deps.kill ?? process.kill.bind(process);
669
+ const setTimer = deps.setTimer ?? setTimeout;
670
+ const clearTimer = deps.clearTimer ?? clearTimeout;
671
+ return new Promise((resolve, reject) => {
672
+ let settled = false;
673
+ let escalationTimer;
674
+ let pollTimer;
675
+ function clearTimers() {
676
+ if (escalationTimer) clearTimer(escalationTimer);
677
+ if (pollTimer) clearTimer(pollTimer);
678
+ }
679
+ function finish() {
680
+ if (settled) return;
681
+ settled = true;
682
+ clearTimers();
683
+ resolve();
684
+ }
685
+ function fail(error) {
686
+ if (settled) return;
687
+ settled = true;
688
+ clearTimers();
689
+ reject(error);
690
+ }
691
+ function poll() {
692
+ if (settled) return;
693
+ try {
694
+ if (!signalProcessGroup(pid, 0, kill)) {
695
+ finish();
696
+ return;
697
+ }
698
+ pollTimer = setTimer(poll, TERMINATION_POLL_MS);
699
+ } catch (error) {
700
+ fail(error);
701
+ }
702
+ }
703
+ try {
704
+ if (!signalProcessGroup(pid, "SIGTERM", kill)) {
705
+ finish();
706
+ return;
707
+ }
708
+ escalationTimer = setTimer(() => {
709
+ try {
710
+ if (!signalProcessGroup(pid, 0, kill)) {
711
+ finish();
712
+ return;
713
+ }
714
+ signalProcessGroup(pid, "SIGKILL", kill);
715
+ } catch (error) {
716
+ fail(error);
717
+ }
718
+ }, TERMINATION_GRACE_MS);
719
+ pollTimer = setTimer(poll, TERMINATION_POLL_MS);
720
+ } catch (error) {
721
+ fail(error);
722
+ }
723
+ });
724
+ }
725
+ async function resolveShell(shell = process.env.SHELL) {
726
+ if (!(shell && isAbsolute(shell))) return "/bin/sh";
727
+ try {
728
+ const metadata = await stat(shell);
729
+ if (!metadata.isFile()) return "/bin/sh";
730
+ await access(shell, constants.X_OK);
731
+ return shell;
732
+ } catch {
733
+ return "/bin/sh";
734
+ }
735
+ }
736
+ function terminalResult(reason, stdout, stderr) {
737
+ return {
738
+ stdout: stdout.toString(),
739
+ stderr: stderr.toString(),
740
+ exitCode: reason === "timed_out" ? 124 : 130,
741
+ success: false,
742
+ errorCode: reason
743
+ };
744
+ }
745
+ async function runDeviceProcess(input2) {
746
+ if (input2.signal?.aborted) {
747
+ return { stdout: "", stderr: "", exitCode: 130, success: false, errorCode: "cancelled" };
748
+ }
749
+ const shell = await resolveShell();
750
+ if (input2.signal?.aborted) {
751
+ return { stdout: "", stderr: "", exitCode: 130, success: false, errorCode: "cancelled" };
752
+ }
753
+ const stdout = new BoundedTextBuffer();
754
+ const stderr = new BoundedTextBuffer();
755
+ return new Promise((resolve) => {
756
+ let settled = false;
757
+ let terminationReason;
758
+ let termination;
759
+ let child;
760
+ function finish(result) {
761
+ if (settled) return;
762
+ settled = true;
763
+ clearTimeout(timeout);
764
+ input2.signal?.removeEventListener("abort", cancel);
765
+ resolve(result);
766
+ }
767
+ function finishTerminationFailure(error) {
768
+ finish({
769
+ stdout: stdout.toString(),
770
+ stderr: error instanceof Error ? error.message : String(error),
771
+ exitCode: 1,
772
+ success: false,
773
+ errorCode: "execution_failed"
774
+ });
775
+ }
776
+ function terminate(reason) {
777
+ if (settled || terminationReason || !child?.pid) return;
778
+ terminationReason = reason;
779
+ termination = beginProcessGroupTermination(child.pid);
780
+ termination.catch(finishTerminationFailure);
781
+ }
782
+ function cancel() {
783
+ terminate("cancelled");
784
+ }
785
+ const timeout = setTimeout(() => terminate("timed_out"), input2.timeoutMs);
786
+ try {
787
+ const spawned2 = spawn(shell, ["-lc", input2.command], {
788
+ cwd: input2.cwd,
789
+ env: process.env,
790
+ detached: true,
791
+ stdio: ["ignore", "pipe", "pipe"]
792
+ });
793
+ child = spawned2;
794
+ } catch (error) {
795
+ finish({
796
+ stdout: "",
797
+ stderr: error instanceof Error ? error.message : String(error),
798
+ exitCode: 1,
799
+ success: false,
800
+ errorCode: "execution_failed"
801
+ });
802
+ return;
803
+ }
804
+ input2.signal?.addEventListener("abort", cancel, { once: true });
805
+ if (input2.signal?.aborted) cancel();
806
+ const spawned = child;
807
+ spawned.stdout.on("data", (chunk) => stdout.append(chunk));
808
+ spawned.stderr.on("data", (chunk) => stderr.append(chunk));
809
+ spawned.once("error", (error) => {
810
+ finish({
811
+ stdout: stdout.toString(),
812
+ stderr: error.message,
813
+ exitCode: 1,
814
+ success: false,
815
+ errorCode: "execution_failed"
816
+ });
817
+ });
818
+ spawned.once("close", (code) => {
819
+ if (terminationReason) {
820
+ const reason = terminationReason;
821
+ (termination ?? Promise.resolve()).then(
822
+ () => finish(terminalResult(reason, stdout, stderr)),
823
+ finishTerminationFailure
824
+ );
825
+ return;
826
+ }
827
+ const exitCode = code ?? 1;
828
+ finish({
829
+ stdout: stdout.toString(),
830
+ stderr: stderr.toString(),
831
+ exitCode,
832
+ success: exitCode === 0,
833
+ ...code === null ? { errorCode: "execution_failed" } : {}
834
+ });
835
+ });
836
+ });
837
+ }
838
+
839
+ // src/device/worker-loop.ts
840
+ var HEARTBEAT_INTERVAL_MS = 2e3;
841
+ var INITIAL_RETRY_MS = 250;
842
+ var MAX_RETRY_MS = 1e4;
843
+ var RETRY_JITTER_MS = 250;
844
+ function computeRetryDelay(attempt, random = Math.random) {
845
+ const exponential = Math.min(INITIAL_RETRY_MS * 2 ** attempt, MAX_RETRY_MS);
846
+ return exponential + Math.floor(random() * RETRY_JITTER_MS);
847
+ }
848
+ function sleepWithAbort(milliseconds, signal) {
849
+ if (signal?.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
850
+ return new Promise((resolve, reject) => {
851
+ function abort() {
852
+ clearTimeout(timer);
853
+ reject(new DOMException("Aborted", "AbortError"));
854
+ }
855
+ const timer = setTimeout(() => {
856
+ signal?.removeEventListener("abort", abort);
857
+ resolve();
858
+ }, milliseconds);
859
+ signal?.addEventListener("abort", abort, { once: true });
860
+ });
861
+ }
862
+ function isAbortError2(error) {
863
+ return error instanceof DOMException && error.name === "AbortError";
864
+ }
865
+ function isRetryableIdleError(error) {
866
+ return error instanceof DeviceWorkerRequestError && (error.statusCode === 0 || error.statusCode >= 500);
867
+ }
868
+ function isLeaseConflict(error) {
869
+ return error instanceof DeviceWorkerRequestError && error.statusCode === 409;
870
+ }
871
+ async function executeLease(command, input2, runProcess, sleep) {
872
+ try {
873
+ await input2.client.markRunning(command.id, command.leaseToken);
874
+ } catch (error) {
875
+ if (error instanceof DeviceCredentialRejectedError) throw error;
876
+ return;
877
+ }
878
+ input2.onEvent?.({ type: "running", commandId: command.id, intent: command.intent });
879
+ const commandController = new AbortController();
880
+ const heartbeatController = new AbortController();
881
+ let completionAllowed = true;
882
+ let heartbeatFailure;
883
+ function abortCommand() {
884
+ commandController.abort();
885
+ }
886
+ input2.signal.addEventListener("abort", abortCommand, { once: true });
887
+ const processPromise = runProcess({
888
+ command: command.command,
889
+ cwd: command.cwd,
890
+ timeoutMs: command.timeoutMs,
891
+ signal: commandController.signal
892
+ });
893
+ const heartbeatPromise = (async () => {
894
+ while (!heartbeatController.signal.aborted) {
895
+ try {
896
+ await sleep(HEARTBEAT_INTERVAL_MS, heartbeatController.signal);
897
+ const heartbeat = await input2.client.heartbeat(
898
+ command.id,
899
+ command.leaseToken,
900
+ heartbeatController.signal
901
+ );
902
+ if (heartbeat.cancel) {
903
+ if (heartbeat.reason === "revoked") completionAllowed = false;
904
+ commandController.abort();
905
+ return;
906
+ }
907
+ } catch (error) {
908
+ if (heartbeatController.signal.aborted && isAbortError2(error)) return;
909
+ heartbeatFailure = error;
910
+ completionAllowed = false;
911
+ commandController.abort();
912
+ return;
913
+ }
914
+ }
915
+ })();
916
+ let result;
917
+ try {
918
+ result = await processPromise;
919
+ } catch (error) {
920
+ result = {
921
+ stdout: "",
922
+ stderr: error instanceof Error ? error.message : String(error),
923
+ exitCode: 1,
924
+ success: false,
925
+ errorCode: "execution_failed"
926
+ };
927
+ } finally {
928
+ heartbeatController.abort();
929
+ await heartbeatPromise;
930
+ input2.signal.removeEventListener("abort", abortCommand);
931
+ }
932
+ if (heartbeatFailure instanceof DeviceCredentialRejectedError) throw heartbeatFailure;
933
+ if (!completionAllowed || isLeaseConflict(heartbeatFailure)) return;
934
+ try {
935
+ await input2.client.output(command.id, command.leaseToken, result.stdout, result.stderr);
936
+ await input2.client.complete(command.id, command.leaseToken, result);
937
+ } catch (error) {
938
+ if (error instanceof DeviceCredentialRejectedError) throw error;
939
+ return;
940
+ }
941
+ input2.onEvent?.({ type: "completed", commandId: command.id, exitCode: result.exitCode });
942
+ }
943
+ async function runDeviceWorker(input2) {
944
+ const runProcess = input2.runProcess ?? runDeviceProcess;
945
+ const sleep = input2.sleep ?? sleepWithAbort;
946
+ const random = input2.random ?? Math.random;
947
+ let retryAttempt = 0;
948
+ while (!input2.signal.aborted) {
949
+ let command;
950
+ try {
951
+ command = await input2.client.lease(input2.signal);
952
+ retryAttempt = 0;
953
+ } catch (error) {
954
+ if (input2.signal.aborted) return;
955
+ if (error instanceof DeviceCredentialRejectedError) throw error;
956
+ if (!isRetryableIdleError(error)) throw error;
957
+ try {
958
+ await sleep(computeRetryDelay(retryAttempt, random), input2.signal);
959
+ } catch (sleepError) {
960
+ if (input2.signal.aborted && isAbortError2(sleepError)) return;
961
+ throw sleepError;
962
+ }
963
+ retryAttempt += 1;
964
+ continue;
965
+ }
966
+ if (!command) continue;
967
+ await executeLease(command, input2, runProcess, sleep);
968
+ }
969
+ }
970
+
971
+ // src/commands/device.ts
972
+ var NAMED_PROFILE_REQUIRED = "Device pairing requires a named Standards profile";
973
+ var SUPPORTED_PLATFORMS_REQUIRED = "Device workers support macOS and Linux only";
974
+ var DeviceCommandError = class extends SchemaError2 {
975
+ constructor(message) {
976
+ super(message, SchemaErrorCode2.VALIDATION_FAILED);
977
+ this.name = "DeviceCommandError";
978
+ }
979
+ };
980
+ function requireSupportedPlatform(platform) {
981
+ if (platform === "darwin" || platform === "linux") return platform;
982
+ throw new DeviceCommandError(SUPPORTED_PLATFORMS_REQUIRED);
983
+ }
984
+ function requireNamedProfile(command) {
985
+ const options = getGlobalOptions(command);
986
+ if (!(options.profileName && options.apiUrl && options.apiKey)) {
987
+ throw new DeviceCommandError(NAMED_PROFILE_REQUIRED);
988
+ }
989
+ return {
990
+ apiUrl: options.apiUrl,
991
+ apiKey: options.apiKey,
992
+ profileName: options.profileName,
993
+ tenantId: options.tenant
994
+ };
995
+ }
996
+ function isRecord2(value) {
997
+ return value !== null && typeof value === "object" && !Array.isArray(value);
998
+ }
999
+ function parseDeviceView(value) {
1000
+ if (!isRecord2(value)) throw new DeviceCommandError("Invalid device response");
1001
+ const { id, name, platform, arch, hostname: deviceHostname } = value;
1002
+ const { status } = value;
1003
+ if (typeof id !== "string" || typeof name !== "string" || platform !== "darwin" && platform !== "linux" || typeof arch !== "string" || typeof deviceHostname !== "string" || status !== "online" && status !== "offline" && status !== "revoked") {
1004
+ throw new DeviceCommandError("Invalid device response");
1005
+ }
1006
+ return { id, name, platform, arch, hostname: deviceHostname, status };
1007
+ }
1008
+ function parsePairDeviceResponse(value) {
1009
+ if (!isRecord2(value) || typeof value.token !== "string") {
1010
+ throw new DeviceCommandError("Invalid device pairing response");
1011
+ }
1012
+ return { device: parseDeviceView(value.device), token: value.token };
1013
+ }
1014
+ function parseDeviceList(value) {
1015
+ if (!Array.isArray(value)) throw new DeviceCommandError("Invalid device list response");
1016
+ return value.map(parseDeviceView);
1017
+ }
1018
+ async function pairDevice(name, command) {
1019
+ const platform = requireSupportedPlatform(process.platform);
1020
+ const profile = requireNamedProfile(command);
1021
+ const installationId = await getOrCreateInstallationId();
1022
+ const client = createClient({
1023
+ apiUrl: profile.apiUrl,
1024
+ apiKey: profile.apiKey,
1025
+ tenantId: profile.tenantId
1026
+ });
1027
+ const response = parsePairDeviceResponse(
1028
+ await client.post("/devices/pair", {
1029
+ installationId,
1030
+ name,
1031
+ platform,
1032
+ arch: process.arch,
1033
+ hostname: hostname()
1034
+ })
1035
+ );
1036
+ const stored = await storeDeviceCredential(profile.profileName, {
1037
+ id: response.device.id,
1038
+ name: response.device.name,
1039
+ token: response.token
1040
+ });
1041
+ if (!stored) throw new DeviceCommandError(NAMED_PROFILE_REQUIRED);
1042
+ process.stdout.write(`Paired ${response.device.name} (${response.device.id})
1043
+ `);
1044
+ }
1045
+ async function showDeviceStatus(command) {
1046
+ const profile = requireNamedProfile(command);
1047
+ const credential = await getStoredDeviceCredential(profile.profileName);
1048
+ if (!credential) {
1049
+ throw new DeviceCommandError("No paired device; run standards device pair first");
1050
+ }
1051
+ const client = createClient({
1052
+ apiUrl: profile.apiUrl,
1053
+ apiKey: profile.apiKey,
1054
+ tenantId: profile.tenantId
1055
+ });
1056
+ const devices = parseDeviceList(await client.get("/devices"));
1057
+ const pairedDevice = devices.find((device) => device.id === credential.id);
1058
+ formatOutput(
1059
+ pairedDevice ? [pairedDevice] : [{ id: credential.id, name: credential.name, status: "revoked" }],
1060
+ getFormat(command)
1061
+ );
1062
+ }
1063
+ async function revokeDevice(command) {
1064
+ const profile = requireNamedProfile(command);
1065
+ const credential = await getStoredDeviceCredential(profile.profileName);
1066
+ if (!credential) {
1067
+ throw new DeviceCommandError("No paired device; run standards device pair first");
1068
+ }
1069
+ const client = createClient({
1070
+ apiUrl: profile.apiUrl,
1071
+ apiKey: profile.apiKey,
1072
+ tenantId: profile.tenantId
1073
+ });
1074
+ await client.delete(`/devices/${credential.id}`);
1075
+ await removeDeviceCredential(profile.profileName);
1076
+ }
1077
+ async function serveDevice(command) {
1078
+ const platform = requireSupportedPlatform(process.platform);
1079
+ const profile = requireNamedProfile(command);
1080
+ const credential = await getStoredDeviceCredential(profile.profileName);
1081
+ if (!credential) {
1082
+ throw new DeviceCommandError("No paired device; run standards device pair first");
1083
+ }
1084
+ const userClient = createClient({
1085
+ apiUrl: profile.apiUrl,
1086
+ apiKey: profile.apiKey,
1087
+ tenantId: profile.tenantId
1088
+ });
1089
+ const devices = parseDeviceList(await userClient.get("/devices"));
1090
+ const device = devices.find((candidate) => candidate.id === credential.id);
1091
+ if (!device) throw new DeviceCommandError("Device is unavailable");
1092
+ if (device.platform !== platform) {
1093
+ throw new DeviceCommandError("Device platform does not match paired device");
1094
+ }
1095
+ const controller = new AbortController();
1096
+ const stop = () => controller.abort();
1097
+ process.once("SIGINT", stop);
1098
+ process.once("SIGTERM", stop);
1099
+ process.stdout.write(`Connected as ${credential.name}
1100
+ `);
1101
+ process.stdout.write("Waiting for commands \u2014 press Ctrl+C to disconnect\n");
1102
+ try {
1103
+ await runDeviceWorker({
1104
+ client: new DeviceWorkerClient({ apiUrl: profile.apiUrl, token: credential.token }),
1105
+ signal: controller.signal,
1106
+ onEvent: (event) => {
1107
+ if (event.type === "running") {
1108
+ process.stdout.write(`Running ${event.commandId}: ${event.intent}
1109
+ `);
1110
+ return;
1111
+ }
1112
+ process.stdout.write(`Completed ${event.commandId} with exit code ${event.exitCode}
1113
+ `);
1114
+ }
1115
+ });
1116
+ } finally {
1117
+ process.removeListener("SIGINT", stop);
1118
+ process.removeListener("SIGTERM", stop);
1119
+ }
1120
+ }
1121
+ function registerDeviceCommand(program) {
1122
+ const device = program.command("device").description("Pair and run this computer as a device");
1123
+ device.command("pair").description("Pair this computer with Standards").requiredOption("--name <name>", "device name").action(
1124
+ async (options, command) => pairDevice(options.name, command)
1125
+ );
1126
+ device.command("status").description("Show the paired device status").action(async (_options, command) => showDeviceStatus(command));
1127
+ device.command("revoke").description("Revoke this computer's device credential").action(async (_options, command) => revokeDevice(command));
1128
+ device.command("serve").description("Run the foreground device worker").action(async (_options, command) => serveDevice(command));
1129
+ }
1130
+
304
1131
  // src/commands/documents.ts
305
1132
  import { formatByteSize } from "@stndrds/schema";
306
1133
  function toFileRow(file) {
@@ -380,7 +1207,7 @@ function registerDocumentsCommand(program) {
380
1207
  }
381
1208
 
382
1209
  // src/commands/folders.ts
383
- import { readFile } from "fs/promises";
1210
+ import { readFile as readFile2 } from "fs/promises";
384
1211
  function registerFoldersCommand(program) {
385
1212
  const folders = program.command("folders").description("Manage record drive folders");
386
1213
  folders.command("reconcile-paths").description("Create a folder tree on a record drive from paths").argument("<object>", "object name (e.g. contacts)").argument("<recordId>", "record ID owning the drive").option(
@@ -394,7 +1221,7 @@ function registerFoldersCommand(program) {
394
1221
  ).option("--paths-file <file>", "file containing one path per line").option("--mode <mode>", "reconcile mode: repair or dryRun", "repair").action(async (objectName, recordId, opts, cmd) => {
395
1222
  const paths = [...opts.path];
396
1223
  if (opts.pathsFile) {
397
- const content = await readFile(opts.pathsFile, "utf-8");
1224
+ const content = await readFile2(opts.pathsFile, "utf-8");
398
1225
  const filePaths = content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
399
1226
  paths.push(...filePaths);
400
1227
  }
@@ -528,7 +1355,12 @@ function registerMcpCommand(program) {
528
1355
  }
529
1356
 
530
1357
  // src/commands/meetings.ts
531
- import { ValidationError as ValidationError3 } from "@stndrds/schema";
1358
+ import { createHmac } from "crypto";
1359
+ import {
1360
+ FAKE_MEETING_BOT_SIGNATURE_HEADER,
1361
+ RECORDING_OVERRIDES,
1362
+ ValidationError as ValidationError3
1363
+ } from "@stndrds/schema";
532
1364
  var ORDERS = ["asc", "desc"];
533
1365
  function parseOrder(order) {
534
1366
  if (ORDERS.includes(order)) return order;
@@ -553,6 +1385,43 @@ function buildQuery(opts) {
553
1385
  if (opts.offset !== void 0) query.offset = parseCount("--offset", opts.offset);
554
1386
  return query;
555
1387
  }
1388
+ function parseOverride(opts) {
1389
+ const chosen = RECORDING_OVERRIDES.filter(
1390
+ (override) => opts[override]
1391
+ );
1392
+ if (opts.auto) chosen.push(null);
1393
+ if (chosen.length !== 1) {
1394
+ throw new ValidationError3("record needs exactly one of --force, --skip or --auto", []);
1395
+ }
1396
+ return chosen[0] ?? null;
1397
+ }
1398
+ var SIMULATED_LINE_MS = 4e3;
1399
+ var DEFAULT_SIMULATED_LINES = 12;
1400
+ var SIMULATED_SENTENCES = [
1401
+ "Bonjour \xE0 tous, merci d'\xEAtre l\xE0, on commence par le point d'avancement.",
1402
+ "De mon c\xF4t\xE9 la maquette est valid\xE9e, il reste les retours du client.",
1403
+ "On garde le planning initial, la livraison est pr\xE9vue pour vendredi.",
1404
+ "Est-ce que quelqu'un a des questions sur le budget de la phase deux ?",
1405
+ "Je propose qu'on fasse un point rapide jeudi pour v\xE9rifier les tests.",
1406
+ "Tr\xE8s bien, je vous envoie le compte rendu dans la journ\xE9e, bonne journ\xE9e."
1407
+ ];
1408
+ function simulatedSpeakers(meeting) {
1409
+ const speakers = meeting.participants.map(
1410
+ (participant) => participant.name?.trim() || participant.address.split("@")[0] || participant.address
1411
+ );
1412
+ return speakers.length > 0 ? speakers : ["Speaker"];
1413
+ }
1414
+ function buildSimulatedSegments(speakers, lines) {
1415
+ return Array.from({ length: lines }, (_, index) => ({
1416
+ speaker: speakers[index % speakers.length] ?? null,
1417
+ startsAtMs: index * SIMULATED_LINE_MS,
1418
+ endsAtMs: (index + 1) * SIMULATED_LINE_MS,
1419
+ text: SIMULATED_SENTENCES[index % SIMULATED_SENTENCES.length] ?? ""
1420
+ }));
1421
+ }
1422
+ function signFakeWebhook(secret, body) {
1423
+ return createHmac("sha256", secret).update(body).digest("hex");
1424
+ }
556
1425
  function withQueryOptions(command) {
557
1426
  return command.option("--search <text>", "match title, description and location").option("--since <iso>", "inclusive ISO lower bound on the start").option("--before <iso>", "exclusive ISO upper bound on the start").option("--addresses <emails>", "comma-separated participant addresses to restrict to").option("--record <id>", "only meetings linked to this record").option("--order <order>", "asc for what is coming next, desc to read history").option("--limit <n>", "max meetings to return").option("--offset <n>", "number of meetings to skip");
558
1427
  }
@@ -589,6 +1458,65 @@ function registerMeetingsCommand(program) {
589
1458
  formatOutput(result, getFormat(cmd));
590
1459
  }
591
1460
  );
1461
+ meetings.command("transcript").description("Read a meeting's completed transcript").argument("<id>", "meeting ID").action(async (id, _opts, cmd) => {
1462
+ const client = getClientFromCommand(cmd);
1463
+ const result = await client.get(`/meetings/${id}/transcript`);
1464
+ formatOutput(result, getFormat(cmd));
1465
+ });
1466
+ meetings.command("record").description("Force, skip, or hand back to the workspace policy the recording of a meeting").argument("<id>", "meeting ID").option("--force", "record this meeting whatever the workspace policy says").option("--skip", "never record this meeting").option("--auto", "clear the override and follow the workspace policy").action(async (id, opts, cmd) => {
1467
+ const override = parseOverride(opts);
1468
+ const client = getClientFromCommand(cmd);
1469
+ const result = await client.put(`/meetings/${id}/recording`, { override });
1470
+ formatOutput(result, getFormat(cmd));
1471
+ });
1472
+ meetings.command("simulate-transcript").description("Complete a fake-provider bot by posting a generated transcript to its webhook").argument("<id>", "meeting ID").option(
1473
+ "--lines <n>",
1474
+ "number of transcript lines to generate",
1475
+ String(DEFAULT_SIMULATED_LINES)
1476
+ ).action(async (id, opts, cmd) => {
1477
+ const secret = process.env.MEETING_BOT_FAKE_SECRET;
1478
+ if (!secret) {
1479
+ throw new ValidationError3("MEETING_BOT_FAKE_SECRET is not set in this shell", []);
1480
+ }
1481
+ const lines = parseCount("--lines", opts.lines);
1482
+ if (lines < 1) throw new ValidationError3("--lines must be at least 1", []);
1483
+ const client = getClientFromCommand(cmd);
1484
+ const meeting = await client.get(`/meetings/${id}`);
1485
+ const { status, botId } = meeting.recording;
1486
+ if (status !== "scheduled" && status !== "recording" || botId === null) {
1487
+ throw new ValidationError3(`Meeting ${id} has no bot to complete (status: ${status})`, []);
1488
+ }
1489
+ const segments = buildSimulatedSegments(simulatedSpeakers(meeting), lines);
1490
+ const startedAt = meeting.startsAt ?? (/* @__PURE__ */ new Date()).toISOString();
1491
+ const endedAt = new Date(
1492
+ new Date(startedAt).getTime() + lines * SIMULATED_LINE_MS
1493
+ ).toISOString();
1494
+ const body = JSON.stringify({
1495
+ botId,
1496
+ transcript: {
1497
+ fullText: segments.map((segment) => segment.text).join("\n"),
1498
+ segments,
1499
+ language: "fr",
1500
+ startedAt,
1501
+ endedAt
1502
+ }
1503
+ });
1504
+ const response = await fetch(
1505
+ `${getGlobalOptions(cmd).apiUrl}/meetings/transcripts/webhook/fake`,
1506
+ {
1507
+ method: "POST",
1508
+ headers: {
1509
+ "content-type": "application/json",
1510
+ [FAKE_MEETING_BOT_SIGNATURE_HEADER]: signFakeWebhook(secret, body)
1511
+ },
1512
+ body
1513
+ }
1514
+ );
1515
+ if (!response.ok) {
1516
+ throw new ValidationError3(`webhook answered ${response.status}`, []);
1517
+ }
1518
+ formatOutput({ botId, segments: segments.length }, getFormat(cmd));
1519
+ });
592
1520
  }
593
1521
 
594
1522
  // src/commands/pull.ts
@@ -738,7 +1666,7 @@ function registerPullCommand(program) {
738
1666
  }
739
1667
 
740
1668
  // src/commands/records.ts
741
- import { readFile as readFile2 } from "fs/promises";
1669
+ import { readFile as readFile3 } from "fs/promises";
742
1670
  import { basename } from "path";
743
1671
  function parseSortFlag(sort) {
744
1672
  const [attribute, direction = "asc"] = sort.split(":");
@@ -804,7 +1732,7 @@ function registerRecordsCommand(program) {
804
1732
  formatOutput(result, getFormat(cmd));
805
1733
  });
806
1734
  records.command("attach-document").description("Upload and attach a local file to a record").argument("<object>", "object name (e.g. contacts)").argument("<recordId>", "record ID").requiredOption("--file <path>", "local file path to upload").option("--attribute <attr>", "attribute name to attach the document to").option("--parent-id <folderId>", "parent folder ID").option("--title <title>", "document title (defaults to filename)").action(async (objectName, recordId, opts, cmd) => {
807
- const fileContent = await readFile2(opts.file);
1735
+ const fileContent = await readFile3(opts.file);
808
1736
  const fileName = basename(opts.file);
809
1737
  const title = opts.title ?? fileName;
810
1738
  const form = new FormData();
@@ -825,128 +1753,6 @@ function registerRecordsCommand(program) {
825
1753
  import { stdin as input, stdout as output } from "process";
826
1754
  import { createInterface } from "readline/promises";
827
1755
  import chalk7 from "chalk";
828
-
829
- // src/config.ts
830
- import { mkdir, readFile as readFile3, rm, writeFile } from "fs/promises";
831
- import { homedir } from "os";
832
- import { dirname, join } from "path";
833
- var DEFAULT_API_URL = "http://localhost:4100/v1";
834
- var ENV_PROFILE_NAME = "sandbox";
835
- function getDefaultApiUrl() {
836
- return DEFAULT_API_URL;
837
- }
838
- function getConfigPath() {
839
- const configDir = process.env.STANDARDS_CONFIG_DIR ?? join(homedir(), ".standards");
840
- return join(configDir, "config.json");
841
- }
842
- async function readConfig() {
843
- try {
844
- const raw = await readFile3(getConfigPath(), "utf8");
845
- const parsed = JSON.parse(raw);
846
- return {
847
- currentProfile: parsed.currentProfile,
848
- profiles: parsed.profiles ?? {}
849
- };
850
- } catch (error) {
851
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
852
- return { profiles: {} };
853
- }
854
- throw error;
855
- }
856
- }
857
- async function writeConfig(config) {
858
- const path = getConfigPath();
859
- await mkdir(dirname(path), { recursive: true, mode: 448 });
860
- await writeFile(path, `${JSON.stringify(config, null, 2)}
861
- `, { mode: 384 });
862
- }
863
- async function upsertProfile(input2) {
864
- const config = await readConfig();
865
- config.profiles[input2.name] = {
866
- apiUrl: input2.apiUrl,
867
- apiKey: input2.apiKey
868
- };
869
- config.currentProfile = input2.name;
870
- await writeConfig(config);
871
- }
872
- async function setCurrentProfile(name) {
873
- const config = await readConfig();
874
- if (!config.profiles[name]) {
875
- throw new Error(`Unknown Standards instance "${name}"`);
876
- }
877
- config.currentProfile = name;
878
- await writeConfig(config);
879
- }
880
- async function removeProfile(name) {
881
- const config = await readConfig();
882
- const profileName = name ?? config.currentProfile;
883
- if (!profileName) return;
884
- delete config.profiles[profileName];
885
- if (config.currentProfile === profileName) {
886
- config.currentProfile = void 0;
887
- }
888
- await writeConfig(config);
889
- }
890
- async function listProfiles() {
891
- const config = await readConfig();
892
- const profiles = Object.entries(config.profiles).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
893
- name,
894
- apiUrl: profile.apiUrl,
895
- current: config.currentProfile === name
896
- }));
897
- const envProfile = getEnvProfile();
898
- if (envProfile) {
899
- profiles.push({
900
- name: ENV_PROFILE_NAME,
901
- apiUrl: envProfile.apiUrl,
902
- current: !config.currentProfile,
903
- source: "env"
904
- });
905
- }
906
- return profiles;
907
- }
908
- async function getActiveProfile() {
909
- const config = await readConfig();
910
- if (!config.currentProfile) {
911
- const envProfile = getEnvProfile();
912
- return envProfile ? { name: ENV_PROFILE_NAME, ...envProfile } : null;
913
- }
914
- const profile = config.profiles[config.currentProfile];
915
- if (!profile) return null;
916
- return { name: config.currentProfile, ...profile };
917
- }
918
- function getEnvProfile() {
919
- if (!process.env.STANDARDS_API_KEY) return null;
920
- return {
921
- apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
922
- apiKey: process.env.STANDARDS_API_KEY
923
- };
924
- }
925
- async function resolveCliConfig(options) {
926
- if (options.apiUrl || options.apiKey) {
927
- return {
928
- apiUrl: options.apiUrl ?? process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
929
- apiKey: options.apiKey ?? process.env.STANDARDS_API_KEY
930
- };
931
- }
932
- const config = await readConfig();
933
- const profileName = options.instance ?? config.currentProfile;
934
- if (profileName) {
935
- const profile = config.profiles[profileName];
936
- if (!profile) throw new Error(`Unknown Standards instance "${profileName}"`);
937
- return {
938
- apiUrl: profile.apiUrl,
939
- apiKey: profile.apiKey,
940
- profileName
941
- };
942
- }
943
- return {
944
- apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
945
- apiKey: process.env.STANDARDS_API_KEY
946
- };
947
- }
948
-
949
- // src/commands/root.ts
950
1756
  async function promptForMissingValue(label) {
951
1757
  const rl = createInterface({ input, output });
952
1758
  try {
@@ -1054,6 +1860,7 @@ function createProgram() {
1054
1860
  registerSchemaCommand(program);
1055
1861
  registerPullCommand(program);
1056
1862
  registerDocumentsCommand(program);
1863
+ registerDeviceCommand(program);
1057
1864
  registerFoldersCommand(program);
1058
1865
  registerKeysCommand(program);
1059
1866
  registerAuthCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stndrds/cli",
3
- "version": "1.0.0-alpha.291",
3
+ "version": "1.0.0-alpha.293",
4
4
  "description": "CLI tool to interact with Standards API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "chalk": "^5.4.1",
14
14
  "cli-table3": "^0.6.5",
15
15
  "commander": "^13.1.0",
16
- "@stndrds/schema": "1.0.0-alpha.291"
16
+ "@stndrds/schema": "1.0.0-alpha.293"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^25.6.0",