@standards/cli 1.0.0-alpha.409 → 1.0.0-alpha.411
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 +5 -28
- package/dist/bin.mjs +274 -976
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -3,35 +3,12 @@
|
|
|
3
3
|
The Standards CLI connects a terminal to a Standards instance. See the public
|
|
4
4
|
[CLI guide](../../content/docs/13-cli/index.mdx) for installation and the full command surface.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Devices
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
8
|
+
The CLI no longer runs a device worker. A machine your agents run commands on is enrolled with
|
|
9
|
+
the `standards-device` worker and an enrollment token from Settings → Devices (**Enroll your
|
|
10
|
+
computer** for your own machine, **Enroll machines** for a managed fleet): see the
|
|
11
|
+
[device fleet guide](../../content/docs/10-agents/07-device-fleet.mdx).
|
|
35
12
|
|
|
36
13
|
## Move to the `@standards` scope
|
|
37
14
|
|
package/dist/bin.mjs
CHANGED
|
@@ -5,22 +5,20 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import { styleText } from "node:util";
|
|
6
6
|
import { BundleConflictError, StandardsRequestError, createTransport, syncBundle } from "@standards/client";
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import { BUNDLE_LOCALES, COUNT_MODES,
|
|
8
|
+
import { BUNDLE_LOCALES, COUNT_MODES, MEETING_RESPONSES, RECORDING_OVERRIDES, RECORD_ACCESS_ROLES, RESOURCE_VISIBILITIES, ValidationError, canonicalStringify, diffSchemaState, formatByteSize, parseRecordAccessRole, parseSchemaState, schemaStateSourcePayload } from "@standards/schema";
|
|
9
9
|
import Table from "cli-table3";
|
|
10
10
|
import { createAppClient } from "@standards/client/app";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { basename, dirname,
|
|
14
|
-
import {
|
|
15
|
-
import { REMOTE_DEVICE_PLATFORMS, buildOperationPath, recordsOperations } from "@standards/schema/api";
|
|
16
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
17
|
-
import { WebSocket } from "ws";
|
|
18
|
-
import { spawn } from "node:child_process";
|
|
19
|
-
import { StringDecoder } from "node:string_decoder";
|
|
11
|
+
import { createWriteStream } from "node:fs";
|
|
12
|
+
import { chmod, mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
13
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
20
15
|
import { Readable } from "node:stream";
|
|
21
16
|
import { pipeline } from "node:stream/promises";
|
|
17
|
+
import { buildOperationPath, recordsOperations } from "@standards/schema/api";
|
|
22
18
|
import { stdin, stdout } from "node:process";
|
|
23
19
|
import { createInterface } from "node:readline/promises";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { isSafeSkillFilePath, skillFileText } from "@standards/constants";
|
|
24
22
|
//#region src/output.ts
|
|
25
23
|
const MAX_CELL_WIDTH = 50;
|
|
26
24
|
function truncate(value, maxLength) {
|
|
@@ -366,971 +364,6 @@ function registerConnectorsCommand(program) {
|
|
|
366
364
|
});
|
|
367
365
|
}
|
|
368
366
|
//#endregion
|
|
369
|
-
//#region src/config.ts
|
|
370
|
-
const DEFAULT_API_URL = "http://localhost:4100/v1";
|
|
371
|
-
const ENV_PROFILE_NAME = "sandbox";
|
|
372
|
-
function getDefaultApiUrl() {
|
|
373
|
-
return DEFAULT_API_URL;
|
|
374
|
-
}
|
|
375
|
-
function getConfigPath() {
|
|
376
|
-
const configDir = process.env.STANDARDS_CONFIG_DIR ?? join(homedir(), ".standards");
|
|
377
|
-
return join(configDir, "config.json");
|
|
378
|
-
}
|
|
379
|
-
async function readConfig() {
|
|
380
|
-
try {
|
|
381
|
-
const raw = await readFile(getConfigPath(), "utf8");
|
|
382
|
-
const parsed = JSON.parse(raw);
|
|
383
|
-
return {
|
|
384
|
-
...parsed.installationId ? { installationId: parsed.installationId } : {},
|
|
385
|
-
currentProfile: parsed.currentProfile,
|
|
386
|
-
profiles: parsed.profiles ?? {}
|
|
387
|
-
};
|
|
388
|
-
} catch (error) {
|
|
389
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") return { profiles: {} };
|
|
390
|
-
throw error;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
async function writeConfig(config) {
|
|
394
|
-
const path = getConfigPath();
|
|
395
|
-
const directory = dirname(path);
|
|
396
|
-
await mkdir(directory, {
|
|
397
|
-
recursive: true,
|
|
398
|
-
mode: 448
|
|
399
|
-
});
|
|
400
|
-
await chmod(directory, 448);
|
|
401
|
-
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
|
|
402
|
-
await chmod(path, 384);
|
|
403
|
-
}
|
|
404
|
-
function normalizeApiOrigin(apiUrl) {
|
|
405
|
-
try {
|
|
406
|
-
return new URL(apiUrl).origin;
|
|
407
|
-
} catch {
|
|
408
|
-
return apiUrl.replace(/\/+$/, "");
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
async function upsertProfile(input) {
|
|
412
|
-
const config = await readConfig();
|
|
413
|
-
const existingProfile = config.profiles[input.name];
|
|
414
|
-
const device = existingProfile?.device && normalizeApiOrigin(existingProfile.apiUrl) === normalizeApiOrigin(input.apiUrl) ? existingProfile.device : void 0;
|
|
415
|
-
config.profiles[input.name] = {
|
|
416
|
-
apiUrl: input.apiUrl,
|
|
417
|
-
apiKey: input.apiKey,
|
|
418
|
-
...device ? { device } : {}
|
|
419
|
-
};
|
|
420
|
-
config.currentProfile = input.name;
|
|
421
|
-
await writeConfig(config);
|
|
422
|
-
}
|
|
423
|
-
async function getOrCreateInstallationId() {
|
|
424
|
-
const config = await readConfig();
|
|
425
|
-
if (config.installationId) return config.installationId;
|
|
426
|
-
const installationId = randomUUID();
|
|
427
|
-
config.installationId = installationId;
|
|
428
|
-
await writeConfig(config);
|
|
429
|
-
return installationId;
|
|
430
|
-
}
|
|
431
|
-
async function getStoredDeviceCredential(profileName) {
|
|
432
|
-
return (await readConfig()).profiles[profileName]?.device ?? null;
|
|
433
|
-
}
|
|
434
|
-
async function storeDeviceCredential(profileName, credential) {
|
|
435
|
-
const config = await readConfig();
|
|
436
|
-
const profile = config.profiles[profileName];
|
|
437
|
-
if (!profile) return false;
|
|
438
|
-
profile.device = credential;
|
|
439
|
-
await writeConfig(config);
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
|
-
async function removeDeviceCredential(profileName) {
|
|
443
|
-
const config = await readConfig();
|
|
444
|
-
const profile = config.profiles[profileName];
|
|
445
|
-
if (!profile?.device) return false;
|
|
446
|
-
profile.device = void 0;
|
|
447
|
-
await writeConfig(config);
|
|
448
|
-
return true;
|
|
449
|
-
}
|
|
450
|
-
async function setCurrentProfile(name) {
|
|
451
|
-
const config = await readConfig();
|
|
452
|
-
if (!config.profiles[name]) throw new Error(`Unknown Standards instance "${name}"`);
|
|
453
|
-
config.currentProfile = name;
|
|
454
|
-
await writeConfig(config);
|
|
455
|
-
}
|
|
456
|
-
async function removeProfile(name) {
|
|
457
|
-
const config = await readConfig();
|
|
458
|
-
const profileName = name ?? config.currentProfile;
|
|
459
|
-
if (!profileName) return;
|
|
460
|
-
delete config.profiles[profileName];
|
|
461
|
-
if (config.currentProfile === profileName) config.currentProfile = void 0;
|
|
462
|
-
await writeConfig(config);
|
|
463
|
-
}
|
|
464
|
-
async function listProfiles() {
|
|
465
|
-
const config = await readConfig();
|
|
466
|
-
const profiles = Object.entries(config.profiles).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
|
|
467
|
-
name,
|
|
468
|
-
apiUrl: profile.apiUrl,
|
|
469
|
-
current: config.currentProfile === name
|
|
470
|
-
}));
|
|
471
|
-
const envProfile = getEnvProfile();
|
|
472
|
-
if (envProfile) profiles.push({
|
|
473
|
-
name: ENV_PROFILE_NAME,
|
|
474
|
-
apiUrl: envProfile.apiUrl,
|
|
475
|
-
current: !config.currentProfile,
|
|
476
|
-
source: "env"
|
|
477
|
-
});
|
|
478
|
-
return profiles;
|
|
479
|
-
}
|
|
480
|
-
async function getActiveProfile() {
|
|
481
|
-
const config = await readConfig();
|
|
482
|
-
if (!config.currentProfile) {
|
|
483
|
-
const envProfile = getEnvProfile();
|
|
484
|
-
return envProfile ? {
|
|
485
|
-
name: ENV_PROFILE_NAME,
|
|
486
|
-
...envProfile
|
|
487
|
-
} : null;
|
|
488
|
-
}
|
|
489
|
-
const profile = config.profiles[config.currentProfile];
|
|
490
|
-
if (!profile) return null;
|
|
491
|
-
return {
|
|
492
|
-
name: config.currentProfile,
|
|
493
|
-
...profile
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
function getEnvProfile() {
|
|
497
|
-
if (!process.env.STANDARDS_API_KEY) return null;
|
|
498
|
-
return {
|
|
499
|
-
apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
|
|
500
|
-
apiKey: process.env.STANDARDS_API_KEY
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
async function resolveCliConfig(options) {
|
|
504
|
-
if (options.apiUrl || options.apiKey) return {
|
|
505
|
-
apiUrl: options.apiUrl ?? process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
|
|
506
|
-
apiKey: options.apiKey ?? process.env.STANDARDS_API_KEY
|
|
507
|
-
};
|
|
508
|
-
const config = await readConfig();
|
|
509
|
-
const profileName = options.instance ?? config.currentProfile;
|
|
510
|
-
if (profileName) {
|
|
511
|
-
const profile = config.profiles[profileName];
|
|
512
|
-
if (!profile) throw new Error(`Unknown Standards instance "${profileName}"`);
|
|
513
|
-
return {
|
|
514
|
-
apiUrl: profile.apiUrl,
|
|
515
|
-
apiKey: profile.apiKey,
|
|
516
|
-
profileName
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
return {
|
|
520
|
-
apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
|
|
521
|
-
apiKey: process.env.STANDARDS_API_KEY
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
//#endregion
|
|
525
|
-
//#region src/device/worker-client.ts
|
|
526
|
-
/** How long a connection may wait for `ready` after sending `authenticate`. */
|
|
527
|
-
const REQUEST_TIMEOUT_MS = 3e4;
|
|
528
|
-
/** The code the SERVER closes an authenticated-but-not-ready socket with. */
|
|
529
|
-
const SERVER_POLICY_VIOLATION_CLOSE_CODE = 1008;
|
|
530
|
-
const NORMAL_CLOSE_CODE = 1e3;
|
|
531
|
-
const EMPTY_SHELL_COMMAND_REFUSED = "Shell command frame carries no command";
|
|
532
|
-
var DeviceWorkerRequestError = class extends SchemaError {
|
|
533
|
-
constructor(statusCode, message, code = SchemaErrorCode.REPOSITORY_QUERY_FAILED) {
|
|
534
|
-
super(message, code, { statusCode });
|
|
535
|
-
this.statusCode = statusCode;
|
|
536
|
-
this.name = "DeviceWorkerRequestError";
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
|
-
var DeviceCredentialRejectedError = class extends DeviceWorkerRequestError {
|
|
540
|
-
constructor() {
|
|
541
|
-
super(401, "Device credential rejected; run standards device pair again", SchemaErrorCode.ACCESS_DENIED);
|
|
542
|
-
this.name = "DeviceCredentialRejectedError";
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
/** The lease poll's own curve: 250 ms doubling up to 10 s, well under the channel's. */
|
|
546
|
-
const LEASE_RETRY = {
|
|
547
|
-
initialMs: 250,
|
|
548
|
-
maxMs: 1e4
|
|
549
|
-
};
|
|
550
|
-
function computeRetryDelay$1(attempt, random = Math.random) {
|
|
551
|
-
return computeRetryDelay(attempt, random, LEASE_RETRY);
|
|
552
|
-
}
|
|
553
|
-
function abortError() {
|
|
554
|
-
return new DOMException("Aborted", "AbortError");
|
|
555
|
-
}
|
|
556
|
-
function isAbortError$1(error) {
|
|
557
|
-
return error instanceof DOMException && error.name === "AbortError";
|
|
558
|
-
}
|
|
559
|
-
function sleepWithAbort(milliseconds, signal) {
|
|
560
|
-
if (signal?.aborted) return Promise.reject(abortError());
|
|
561
|
-
return new Promise((resolve, reject) => {
|
|
562
|
-
function abort() {
|
|
563
|
-
clearTimeout(timer);
|
|
564
|
-
reject(abortError());
|
|
565
|
-
}
|
|
566
|
-
const timer = setTimeout(() => {
|
|
567
|
-
signal?.removeEventListener("abort", abort);
|
|
568
|
-
resolve();
|
|
569
|
-
}, milliseconds);
|
|
570
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
function withAbort(promise, signal) {
|
|
574
|
-
if (!signal) return promise;
|
|
575
|
-
if (signal.aborted) return Promise.reject(abortError());
|
|
576
|
-
return new Promise((resolve, reject) => {
|
|
577
|
-
const abort = () => reject(abortError());
|
|
578
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
579
|
-
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
function transmit(socket, frame) {
|
|
583
|
-
return new Promise((resolve, reject) => {
|
|
584
|
-
socket.send(JSON.stringify(frame), (error) => {
|
|
585
|
-
if (error) reject(new DeviceWorkerRequestError(0, "Device channel send failed"));
|
|
586
|
-
else resolve();
|
|
587
|
-
});
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
function leasedCommand(frame, command) {
|
|
591
|
-
return {
|
|
592
|
-
id: frame.id,
|
|
593
|
-
leaseToken: frame.leaseToken,
|
|
594
|
-
intent: frame.intent,
|
|
595
|
-
command,
|
|
596
|
-
timeoutMs: frame.timeoutMs,
|
|
597
|
-
...typeof frame.cwd === "string" ? { cwd: frame.cwd } : {}
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* One WebSocket to the device channel. The first frame authenticates the
|
|
602
|
-
* device; commands are queued as they arrive and handed to `lease()`.
|
|
603
|
-
* A dropped connection reconnects with backoff; a credential rejection
|
|
604
|
-
* (`revoked`, or close 1008 before `ready`) is terminal.
|
|
605
|
-
*/
|
|
606
|
-
var DeviceWorkerClient = class {
|
|
607
|
-
constructor(options) {
|
|
608
|
-
this.lifecycle = new AbortController();
|
|
609
|
-
this.queue = [];
|
|
610
|
-
this.waiters = /* @__PURE__ */ new Set();
|
|
611
|
-
this.cancellations = /* @__PURE__ */ new Map();
|
|
612
|
-
this.socket = null;
|
|
613
|
-
this.ready = false;
|
|
614
|
-
this.connection = null;
|
|
615
|
-
this.rejection = null;
|
|
616
|
-
this.closed = false;
|
|
617
|
-
this.url = deviceChannelUrl(options.apiUrl);
|
|
618
|
-
this.credential = `Device ${options.token}`;
|
|
619
|
-
this.sleep = options.sleep ?? sleepWithAbort;
|
|
620
|
-
this.random = options.random ?? Math.random;
|
|
621
|
-
this.now = options.now ?? Date.now;
|
|
622
|
-
}
|
|
623
|
-
/** Stops reconnecting and closes the socket; pending and later leases reject with an AbortError. */
|
|
624
|
-
close() {
|
|
625
|
-
this.closed = true;
|
|
626
|
-
this.lifecycle.abort();
|
|
627
|
-
this.socket?.close(NORMAL_CLOSE_CODE);
|
|
628
|
-
this.failWaiters(abortError());
|
|
629
|
-
}
|
|
630
|
-
lease(signal) {
|
|
631
|
-
if (this.rejection) return Promise.reject(this.rejection);
|
|
632
|
-
if (this.closed || signal?.aborted) return Promise.reject(abortError());
|
|
633
|
-
this.connection ??= this.maintainConnection().catch((error) => this.failWaiters(error));
|
|
634
|
-
const queued = this.queue.shift();
|
|
635
|
-
if (queued) return Promise.resolve(queued);
|
|
636
|
-
return new Promise((resolve, reject) => {
|
|
637
|
-
const settle = () => {
|
|
638
|
-
this.waiters.delete(waiter);
|
|
639
|
-
signal?.removeEventListener("abort", abort);
|
|
640
|
-
};
|
|
641
|
-
const waiter = {
|
|
642
|
-
resolve: (command) => {
|
|
643
|
-
settle();
|
|
644
|
-
resolve(command);
|
|
645
|
-
},
|
|
646
|
-
reject: (error) => {
|
|
647
|
-
settle();
|
|
648
|
-
reject(error);
|
|
649
|
-
}
|
|
650
|
-
};
|
|
651
|
-
const abort = () => waiter.reject(abortError());
|
|
652
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
653
|
-
this.waiters.add(waiter);
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
async markRunning(commandId, leaseToken) {
|
|
657
|
-
await this.send({
|
|
658
|
-
type: "running",
|
|
659
|
-
id: commandId,
|
|
660
|
-
leaseToken
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
async heartbeat(commandId, leaseToken, signal) {
|
|
664
|
-
const cancelled = this.cancellations.get(commandId);
|
|
665
|
-
if (cancelled) return {
|
|
666
|
-
cancel: true,
|
|
667
|
-
reason: cancelled
|
|
668
|
-
};
|
|
669
|
-
await withAbort(this.send({
|
|
670
|
-
type: "heartbeat",
|
|
671
|
-
id: commandId,
|
|
672
|
-
leaseToken
|
|
673
|
-
}), signal);
|
|
674
|
-
const reason = this.cancellations.get(commandId);
|
|
675
|
-
return reason ? {
|
|
676
|
-
cancel: true,
|
|
677
|
-
reason
|
|
678
|
-
} : { cancel: false };
|
|
679
|
-
}
|
|
680
|
-
async output(commandId, leaseToken, stdout, stderr) {
|
|
681
|
-
if (!(stdout || stderr)) return;
|
|
682
|
-
await this.send({
|
|
683
|
-
type: "output",
|
|
684
|
-
id: commandId,
|
|
685
|
-
leaseToken,
|
|
686
|
-
...stdout ? { stdout } : {},
|
|
687
|
-
...stderr ? { stderr } : {}
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
async complete(commandId, leaseToken, result) {
|
|
691
|
-
await this.send({
|
|
692
|
-
type: "complete",
|
|
693
|
-
id: commandId,
|
|
694
|
-
leaseToken,
|
|
695
|
-
result: {
|
|
696
|
-
stdout: result.stdout,
|
|
697
|
-
stderr: result.stderr,
|
|
698
|
-
exitCode: result.exitCode,
|
|
699
|
-
success: result.success,
|
|
700
|
-
...result.errorCode ? { errorCode: result.errorCode } : {}
|
|
701
|
-
}
|
|
702
|
-
});
|
|
703
|
-
this.cancellations.delete(commandId);
|
|
704
|
-
}
|
|
705
|
-
send(frame) {
|
|
706
|
-
const socket = this.socket;
|
|
707
|
-
if (!(socket && this.ready && socket.readyState === WebSocket.OPEN)) return Promise.reject(new DeviceWorkerRequestError(0, "Device channel is not connected"));
|
|
708
|
-
return transmit(socket, frame);
|
|
709
|
-
}
|
|
710
|
-
reject(error) {
|
|
711
|
-
this.rejection = error;
|
|
712
|
-
this.failWaiters(error);
|
|
713
|
-
}
|
|
714
|
-
failWaiters(error) {
|
|
715
|
-
for (const waiter of [...this.waiters]) waiter.reject(error);
|
|
716
|
-
}
|
|
717
|
-
deliver(command) {
|
|
718
|
-
const [waiter] = this.waiters;
|
|
719
|
-
if (waiter) waiter.resolve(command);
|
|
720
|
-
else this.queue.push(command);
|
|
721
|
-
}
|
|
722
|
-
async maintainConnection() {
|
|
723
|
-
const signal = this.lifecycle.signal;
|
|
724
|
-
let attempt = 0;
|
|
725
|
-
let lastDelay = 0;
|
|
726
|
-
while (!(signal.aborted || this.rejection)) {
|
|
727
|
-
const { ready, readyAt } = await this.connectOnce();
|
|
728
|
-
if (signal.aborted || this.rejection) return;
|
|
729
|
-
if (ready && readyAt !== null && this.now() - readyAt > lastDelay) attempt = 0;
|
|
730
|
-
lastDelay = computeReconnectDelay(attempt, this.random);
|
|
731
|
-
try {
|
|
732
|
-
await this.sleep(lastDelay, signal);
|
|
733
|
-
} catch (error) {
|
|
734
|
-
if (isAbortError$1(error)) return;
|
|
735
|
-
throw error;
|
|
736
|
-
}
|
|
737
|
-
attempt += 1;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
/** Opens one socket and resolves once it has closed, telling whether and when `ready` was seen. */
|
|
741
|
-
connectOnce() {
|
|
742
|
-
return new Promise((resolve) => {
|
|
743
|
-
const signal = this.lifecycle.signal;
|
|
744
|
-
const socket = new WebSocket(this.url);
|
|
745
|
-
let authenticated = false;
|
|
746
|
-
let ready = false;
|
|
747
|
-
let readyAt = null;
|
|
748
|
-
const stop = () => socket.close(NORMAL_CLOSE_CODE);
|
|
749
|
-
const readyTimer = setTimeout(stop, REQUEST_TIMEOUT_MS);
|
|
750
|
-
signal.addEventListener("abort", stop, { once: true });
|
|
751
|
-
this.socket = socket;
|
|
752
|
-
socket.on("open", () => {
|
|
753
|
-
authenticated = true;
|
|
754
|
-
transmit(socket, {
|
|
755
|
-
type: "authenticate",
|
|
756
|
-
credential: this.credential
|
|
757
|
-
}).catch(stop);
|
|
758
|
-
});
|
|
759
|
-
socket.on("error", () => void 0);
|
|
760
|
-
socket.on("message", (data) => {
|
|
761
|
-
let frame;
|
|
762
|
-
try {
|
|
763
|
-
frame = parseServerFrame(data.toString());
|
|
764
|
-
} catch {
|
|
765
|
-
socket.close(DEVICE_CHANNEL_WORKER_CLOSE_PROTOCOL_VIOLATION, "DEVICE_CHANNEL_PROTOCOL");
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
if (frame.type === "ready") {
|
|
769
|
-
clearTimeout(readyTimer);
|
|
770
|
-
ready = true;
|
|
771
|
-
readyAt = this.now();
|
|
772
|
-
this.ready = true;
|
|
773
|
-
return;
|
|
774
|
-
}
|
|
775
|
-
this.handleFrame(socket, frame);
|
|
776
|
-
});
|
|
777
|
-
socket.on("close", (code) => {
|
|
778
|
-
clearTimeout(readyTimer);
|
|
779
|
-
signal.removeEventListener("abort", stop);
|
|
780
|
-
this.socket = null;
|
|
781
|
-
this.ready = false;
|
|
782
|
-
if (code === SERVER_POLICY_VIOLATION_CLOSE_CODE && authenticated && !ready) this.reject(new DeviceCredentialRejectedError());
|
|
783
|
-
resolve({
|
|
784
|
-
ready,
|
|
785
|
-
readyAt
|
|
786
|
-
});
|
|
787
|
-
});
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
handleFrame(socket, frame) {
|
|
791
|
-
switch (frame.type) {
|
|
792
|
-
case "command":
|
|
793
|
-
this.receiveCommand(socket, frame);
|
|
794
|
-
return;
|
|
795
|
-
case "cancel":
|
|
796
|
-
this.cancellations.set(frame.id, frame.reason);
|
|
797
|
-
return;
|
|
798
|
-
case "reauthenticate":
|
|
799
|
-
transmit(socket, {
|
|
800
|
-
type: "authenticate",
|
|
801
|
-
credential: this.credential
|
|
802
|
-
}).catch(() => socket.close(NORMAL_CLOSE_CODE));
|
|
803
|
-
return;
|
|
804
|
-
case "revoked":
|
|
805
|
-
this.reject(new DeviceCredentialRejectedError());
|
|
806
|
-
socket.close(NORMAL_CLOSE_CODE);
|
|
807
|
-
return;
|
|
808
|
-
case "ping":
|
|
809
|
-
transmit(socket, { type: "pong" }).catch(() => socket.close(NORMAL_CLOSE_CODE));
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
receiveCommand(socket, frame) {
|
|
814
|
-
if (frame.command === void 0) {
|
|
815
|
-
this.refuse(socket, frame, EMPTY_SHELL_COMMAND_REFUSED);
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
this.deliver(leasedCommand(frame, frame.command));
|
|
819
|
-
}
|
|
820
|
-
/** Completes a command the shell worker cannot run, without handing it to the loop. */
|
|
821
|
-
refuse(socket, frame, stderr) {
|
|
822
|
-
const { id, leaseToken } = frame;
|
|
823
|
-
transmit(socket, {
|
|
824
|
-
type: "running",
|
|
825
|
-
id,
|
|
826
|
-
leaseToken
|
|
827
|
-
}).then(() => transmit(socket, {
|
|
828
|
-
type: "complete",
|
|
829
|
-
id,
|
|
830
|
-
leaseToken,
|
|
831
|
-
result: {
|
|
832
|
-
stdout: "",
|
|
833
|
-
stderr,
|
|
834
|
-
exitCode: 1,
|
|
835
|
-
success: false,
|
|
836
|
-
errorCode: "execution_failed"
|
|
837
|
-
}
|
|
838
|
-
})).catch(() => void 0);
|
|
839
|
-
}
|
|
840
|
-
};
|
|
841
|
-
//#endregion
|
|
842
|
-
//#region src/device/process-runner.ts
|
|
843
|
-
const OUTPUT_LIMIT = 3e4;
|
|
844
|
-
const TERMINATION_GRACE_MS = 5e3;
|
|
845
|
-
const TERMINATION_POLL_MS = 50;
|
|
846
|
-
var BoundedTextBuffer = class {
|
|
847
|
-
constructor() {
|
|
848
|
-
this.decoder = new StringDecoder("utf8");
|
|
849
|
-
this.decoderFlushed = false;
|
|
850
|
-
this.value = "";
|
|
851
|
-
this.truncated = false;
|
|
852
|
-
}
|
|
853
|
-
append(chunk) {
|
|
854
|
-
if (this.truncated) return;
|
|
855
|
-
const text = typeof chunk === "string" ? chunk : this.decoder.write(Buffer.from(chunk));
|
|
856
|
-
this.appendText(text);
|
|
857
|
-
}
|
|
858
|
-
appendText(text) {
|
|
859
|
-
const remaining = OUTPUT_LIMIT - this.value.length;
|
|
860
|
-
if (text.length <= remaining) {
|
|
861
|
-
this.value += text;
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
this.value += text.slice(0, Math.max(remaining, 0));
|
|
865
|
-
this.truncated = true;
|
|
866
|
-
}
|
|
867
|
-
toString() {
|
|
868
|
-
if (!this.decoderFlushed) {
|
|
869
|
-
this.decoderFlushed = true;
|
|
870
|
-
this.appendText(this.decoder.end());
|
|
871
|
-
}
|
|
872
|
-
return this.truncated ? `${this.value}\n... output truncated to ${OUTPUT_LIMIT} chars` : this.value;
|
|
873
|
-
}
|
|
874
|
-
};
|
|
875
|
-
function isMissingProcess(error) {
|
|
876
|
-
return error instanceof Error && "code" in error && error.code === "ESRCH";
|
|
877
|
-
}
|
|
878
|
-
function signalProcessGroup(pid, signal, kill) {
|
|
879
|
-
try {
|
|
880
|
-
kill(-pid, signal);
|
|
881
|
-
return true;
|
|
882
|
-
} catch (error) {
|
|
883
|
-
if (isMissingProcess(error)) return false;
|
|
884
|
-
throw error;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
function beginProcessGroupTermination(pid, deps = {}) {
|
|
888
|
-
const kill = deps.kill ?? process.kill.bind(process);
|
|
889
|
-
const setTimer = deps.setTimer ?? setTimeout;
|
|
890
|
-
const clearTimer = deps.clearTimer ?? clearTimeout;
|
|
891
|
-
return new Promise((resolve, reject) => {
|
|
892
|
-
let settled = false;
|
|
893
|
-
let escalationTimer;
|
|
894
|
-
let pollTimer;
|
|
895
|
-
function clearTimers() {
|
|
896
|
-
if (escalationTimer) clearTimer(escalationTimer);
|
|
897
|
-
if (pollTimer) clearTimer(pollTimer);
|
|
898
|
-
}
|
|
899
|
-
function finish() {
|
|
900
|
-
if (settled) return;
|
|
901
|
-
settled = true;
|
|
902
|
-
clearTimers();
|
|
903
|
-
resolve();
|
|
904
|
-
}
|
|
905
|
-
function fail(error) {
|
|
906
|
-
if (settled) return;
|
|
907
|
-
settled = true;
|
|
908
|
-
clearTimers();
|
|
909
|
-
reject(error);
|
|
910
|
-
}
|
|
911
|
-
function poll() {
|
|
912
|
-
if (settled) return;
|
|
913
|
-
try {
|
|
914
|
-
if (!signalProcessGroup(pid, 0, kill)) {
|
|
915
|
-
finish();
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
pollTimer = setTimer(poll, TERMINATION_POLL_MS);
|
|
919
|
-
} catch (error) {
|
|
920
|
-
fail(error);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
try {
|
|
924
|
-
if (!signalProcessGroup(pid, "SIGTERM", kill)) {
|
|
925
|
-
finish();
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
escalationTimer = setTimer(() => {
|
|
929
|
-
try {
|
|
930
|
-
if (!signalProcessGroup(pid, 0, kill)) {
|
|
931
|
-
finish();
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
signalProcessGroup(pid, "SIGKILL", kill);
|
|
935
|
-
} catch (error) {
|
|
936
|
-
fail(error);
|
|
937
|
-
}
|
|
938
|
-
}, TERMINATION_GRACE_MS);
|
|
939
|
-
pollTimer = setTimer(poll, TERMINATION_POLL_MS);
|
|
940
|
-
} catch (error) {
|
|
941
|
-
fail(error);
|
|
942
|
-
}
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
async function resolveShell(shell = process.env.SHELL) {
|
|
946
|
-
if (!(shell && isAbsolute(shell))) return "/bin/sh";
|
|
947
|
-
try {
|
|
948
|
-
if (!(await stat(shell)).isFile()) return "/bin/sh";
|
|
949
|
-
await access(shell, constants.X_OK);
|
|
950
|
-
return shell;
|
|
951
|
-
} catch {
|
|
952
|
-
return "/bin/sh";
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
function terminalResult(reason, stdout, stderr) {
|
|
956
|
-
return {
|
|
957
|
-
stdout: stdout.toString(),
|
|
958
|
-
stderr: stderr.toString(),
|
|
959
|
-
exitCode: reason === "timed_out" ? 124 : 130,
|
|
960
|
-
success: false,
|
|
961
|
-
errorCode: reason
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
async function runDeviceProcess(input) {
|
|
965
|
-
if (input.signal?.aborted) return {
|
|
966
|
-
stdout: "",
|
|
967
|
-
stderr: "",
|
|
968
|
-
exitCode: 130,
|
|
969
|
-
success: false,
|
|
970
|
-
errorCode: "cancelled"
|
|
971
|
-
};
|
|
972
|
-
const shell = await resolveShell();
|
|
973
|
-
if (input.signal?.aborted) return {
|
|
974
|
-
stdout: "",
|
|
975
|
-
stderr: "",
|
|
976
|
-
exitCode: 130,
|
|
977
|
-
success: false,
|
|
978
|
-
errorCode: "cancelled"
|
|
979
|
-
};
|
|
980
|
-
const stdout = new BoundedTextBuffer();
|
|
981
|
-
const stderr = new BoundedTextBuffer();
|
|
982
|
-
return new Promise((resolve) => {
|
|
983
|
-
let settled = false;
|
|
984
|
-
let terminationReason;
|
|
985
|
-
let termination;
|
|
986
|
-
let child;
|
|
987
|
-
function finish(result) {
|
|
988
|
-
if (settled) return;
|
|
989
|
-
settled = true;
|
|
990
|
-
clearTimeout(timeout);
|
|
991
|
-
input.signal?.removeEventListener("abort", cancel);
|
|
992
|
-
resolve(result);
|
|
993
|
-
}
|
|
994
|
-
function finishTerminationFailure(error) {
|
|
995
|
-
finish({
|
|
996
|
-
stdout: stdout.toString(),
|
|
997
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
998
|
-
exitCode: 1,
|
|
999
|
-
success: false,
|
|
1000
|
-
errorCode: "execution_failed"
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
function terminate(reason) {
|
|
1004
|
-
if (settled || terminationReason || !child?.pid) return;
|
|
1005
|
-
terminationReason = reason;
|
|
1006
|
-
termination = beginProcessGroupTermination(child.pid);
|
|
1007
|
-
termination.catch(finishTerminationFailure);
|
|
1008
|
-
}
|
|
1009
|
-
function cancel() {
|
|
1010
|
-
terminate("cancelled");
|
|
1011
|
-
}
|
|
1012
|
-
const timeout = setTimeout(() => terminate("timed_out"), input.timeoutMs);
|
|
1013
|
-
try {
|
|
1014
|
-
child = spawn(shell, ["-lc", input.command], {
|
|
1015
|
-
cwd: input.cwd,
|
|
1016
|
-
env: process.env,
|
|
1017
|
-
detached: true,
|
|
1018
|
-
stdio: [
|
|
1019
|
-
"ignore",
|
|
1020
|
-
"pipe",
|
|
1021
|
-
"pipe"
|
|
1022
|
-
]
|
|
1023
|
-
});
|
|
1024
|
-
} catch (error) {
|
|
1025
|
-
finish({
|
|
1026
|
-
stdout: "",
|
|
1027
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
1028
|
-
exitCode: 1,
|
|
1029
|
-
success: false,
|
|
1030
|
-
errorCode: "execution_failed"
|
|
1031
|
-
});
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
input.signal?.addEventListener("abort", cancel, { once: true });
|
|
1035
|
-
if (input.signal?.aborted) cancel();
|
|
1036
|
-
const spawned = child;
|
|
1037
|
-
spawned.stdout.on("data", (chunk) => stdout.append(chunk));
|
|
1038
|
-
spawned.stderr.on("data", (chunk) => stderr.append(chunk));
|
|
1039
|
-
spawned.once("error", (error) => {
|
|
1040
|
-
finish({
|
|
1041
|
-
stdout: stdout.toString(),
|
|
1042
|
-
stderr: error.message,
|
|
1043
|
-
exitCode: 1,
|
|
1044
|
-
success: false,
|
|
1045
|
-
errorCode: "execution_failed"
|
|
1046
|
-
});
|
|
1047
|
-
});
|
|
1048
|
-
spawned.once("close", (code) => {
|
|
1049
|
-
if (terminationReason) {
|
|
1050
|
-
const reason = terminationReason;
|
|
1051
|
-
(termination ?? Promise.resolve()).then(() => finish(terminalResult(reason, stdout, stderr)), finishTerminationFailure);
|
|
1052
|
-
return;
|
|
1053
|
-
}
|
|
1054
|
-
const exitCode = code ?? 1;
|
|
1055
|
-
finish({
|
|
1056
|
-
stdout: stdout.toString(),
|
|
1057
|
-
stderr: stderr.toString(),
|
|
1058
|
-
exitCode,
|
|
1059
|
-
success: exitCode === 0,
|
|
1060
|
-
...code === null ? { errorCode: "execution_failed" } : {}
|
|
1061
|
-
});
|
|
1062
|
-
});
|
|
1063
|
-
});
|
|
1064
|
-
}
|
|
1065
|
-
//#endregion
|
|
1066
|
-
//#region src/device/worker-loop.ts
|
|
1067
|
-
const HEARTBEAT_INTERVAL_MS = 2e3;
|
|
1068
|
-
function isAbortError(error) {
|
|
1069
|
-
return error instanceof DOMException && error.name === "AbortError";
|
|
1070
|
-
}
|
|
1071
|
-
/** The channel client reconnects on its own; only a transport failure (status 0) is retried here. */
|
|
1072
|
-
function isRetryableIdleError(error) {
|
|
1073
|
-
return error instanceof DeviceWorkerRequestError && error.statusCode === 0;
|
|
1074
|
-
}
|
|
1075
|
-
function isLeaseConflict(error) {
|
|
1076
|
-
return error instanceof DeviceWorkerRequestError && error.statusCode === 409;
|
|
1077
|
-
}
|
|
1078
|
-
async function executeLease(command, input, runProcess, sleep) {
|
|
1079
|
-
try {
|
|
1080
|
-
await input.client.markRunning(command.id, command.leaseToken);
|
|
1081
|
-
} catch (error) {
|
|
1082
|
-
if (error instanceof DeviceCredentialRejectedError) throw error;
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
input.onEvent?.({
|
|
1086
|
-
type: "running",
|
|
1087
|
-
commandId: command.id,
|
|
1088
|
-
intent: command.intent
|
|
1089
|
-
});
|
|
1090
|
-
const commandController = new AbortController();
|
|
1091
|
-
const heartbeatController = new AbortController();
|
|
1092
|
-
let completionAllowed = true;
|
|
1093
|
-
let heartbeatFailure;
|
|
1094
|
-
function abortCommand() {
|
|
1095
|
-
commandController.abort();
|
|
1096
|
-
}
|
|
1097
|
-
input.signal.addEventListener("abort", abortCommand, { once: true });
|
|
1098
|
-
const processPromise = runProcess({
|
|
1099
|
-
command: command.command,
|
|
1100
|
-
cwd: command.cwd,
|
|
1101
|
-
timeoutMs: command.timeoutMs,
|
|
1102
|
-
signal: commandController.signal
|
|
1103
|
-
});
|
|
1104
|
-
const heartbeatPromise = (async () => {
|
|
1105
|
-
while (!heartbeatController.signal.aborted) try {
|
|
1106
|
-
await sleep(HEARTBEAT_INTERVAL_MS, heartbeatController.signal);
|
|
1107
|
-
const heartbeat = await input.client.heartbeat(command.id, command.leaseToken, heartbeatController.signal);
|
|
1108
|
-
if (heartbeat.cancel) {
|
|
1109
|
-
if (heartbeat.reason === "revoked") completionAllowed = false;
|
|
1110
|
-
commandController.abort();
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
} catch (error) {
|
|
1114
|
-
if (heartbeatController.signal.aborted && isAbortError(error)) return;
|
|
1115
|
-
heartbeatFailure = error;
|
|
1116
|
-
completionAllowed = false;
|
|
1117
|
-
commandController.abort();
|
|
1118
|
-
return;
|
|
1119
|
-
}
|
|
1120
|
-
})();
|
|
1121
|
-
let result;
|
|
1122
|
-
try {
|
|
1123
|
-
result = await processPromise;
|
|
1124
|
-
} catch (error) {
|
|
1125
|
-
result = {
|
|
1126
|
-
stdout: "",
|
|
1127
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
1128
|
-
exitCode: 1,
|
|
1129
|
-
success: false,
|
|
1130
|
-
errorCode: "execution_failed"
|
|
1131
|
-
};
|
|
1132
|
-
} finally {
|
|
1133
|
-
heartbeatController.abort();
|
|
1134
|
-
await heartbeatPromise;
|
|
1135
|
-
input.signal.removeEventListener("abort", abortCommand);
|
|
1136
|
-
}
|
|
1137
|
-
if (heartbeatFailure instanceof DeviceCredentialRejectedError) throw heartbeatFailure;
|
|
1138
|
-
if (!completionAllowed || isLeaseConflict(heartbeatFailure)) return;
|
|
1139
|
-
try {
|
|
1140
|
-
await input.client.output(command.id, command.leaseToken, result.stdout, result.stderr);
|
|
1141
|
-
await input.client.complete(command.id, command.leaseToken, result);
|
|
1142
|
-
} catch (error) {
|
|
1143
|
-
if (error instanceof DeviceCredentialRejectedError) throw error;
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
input.onEvent?.({
|
|
1147
|
-
type: "completed",
|
|
1148
|
-
commandId: command.id,
|
|
1149
|
-
exitCode: result.exitCode
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
async function runDeviceWorker(input) {
|
|
1153
|
-
const runProcess = input.runProcess ?? runDeviceProcess;
|
|
1154
|
-
const sleep = input.sleep ?? sleepWithAbort;
|
|
1155
|
-
const random = input.random ?? Math.random;
|
|
1156
|
-
let retryAttempt = 0;
|
|
1157
|
-
while (!input.signal.aborted) {
|
|
1158
|
-
let command;
|
|
1159
|
-
try {
|
|
1160
|
-
command = await input.client.lease(input.signal);
|
|
1161
|
-
retryAttempt = 0;
|
|
1162
|
-
} catch (error) {
|
|
1163
|
-
if (input.signal.aborted) return;
|
|
1164
|
-
if (error instanceof DeviceCredentialRejectedError) throw error;
|
|
1165
|
-
if (!isRetryableIdleError(error)) throw error;
|
|
1166
|
-
try {
|
|
1167
|
-
await sleep(computeRetryDelay$1(retryAttempt, random), input.signal);
|
|
1168
|
-
} catch (sleepError) {
|
|
1169
|
-
if (input.signal.aborted && isAbortError(sleepError)) return;
|
|
1170
|
-
throw sleepError;
|
|
1171
|
-
}
|
|
1172
|
-
retryAttempt += 1;
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
|
-
if (!command) continue;
|
|
1176
|
-
await executeLease(command, input, runProcess, sleep);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
//#endregion
|
|
1180
|
-
//#region src/commands/device.ts
|
|
1181
|
-
const NAMED_PROFILE_REQUIRED = "Device pairing requires a named Standards profile";
|
|
1182
|
-
const SUPPORTED_PLATFORMS_REQUIRED = "Device workers support macOS and Linux only";
|
|
1183
|
-
var DeviceCommandError = class extends SchemaError {
|
|
1184
|
-
constructor(message) {
|
|
1185
|
-
super(message, SchemaErrorCode.VALIDATION_FAILED);
|
|
1186
|
-
this.name = "DeviceCommandError";
|
|
1187
|
-
}
|
|
1188
|
-
};
|
|
1189
|
-
function requireSupportedPlatform(platform) {
|
|
1190
|
-
if (platform === "darwin" || platform === "linux") return platform;
|
|
1191
|
-
throw new DeviceCommandError(SUPPORTED_PLATFORMS_REQUIRED);
|
|
1192
|
-
}
|
|
1193
|
-
function requireNamedProfile(command) {
|
|
1194
|
-
const options = getGlobalOptions(command);
|
|
1195
|
-
if (!(options.profileName && options.apiUrl && options.apiKey)) throw new DeviceCommandError(NAMED_PROFILE_REQUIRED);
|
|
1196
|
-
return {
|
|
1197
|
-
apiUrl: options.apiUrl,
|
|
1198
|
-
apiKey: options.apiKey,
|
|
1199
|
-
profileName: options.profileName,
|
|
1200
|
-
tenantId: options.tenant
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1203
|
-
function isRemoteDevicePlatform(value) {
|
|
1204
|
-
return REMOTE_DEVICE_PLATFORMS.some((platform) => platform === value);
|
|
1205
|
-
}
|
|
1206
|
-
function isRecord(value) {
|
|
1207
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1208
|
-
}
|
|
1209
|
-
function parseDeviceView(value) {
|
|
1210
|
-
if (!isRecord(value)) throw new DeviceCommandError("Invalid device response");
|
|
1211
|
-
const { id, name, platform, arch, hostname: deviceHostname } = value;
|
|
1212
|
-
const { status } = value;
|
|
1213
|
-
if (typeof id !== "string" || typeof name !== "string" || !isRemoteDevicePlatform(platform) || typeof arch !== "string" || typeof deviceHostname !== "string" || status !== "online" && status !== "offline" && status !== "revoked") throw new DeviceCommandError("Invalid device response");
|
|
1214
|
-
return {
|
|
1215
|
-
id,
|
|
1216
|
-
name,
|
|
1217
|
-
platform,
|
|
1218
|
-
arch,
|
|
1219
|
-
hostname: deviceHostname,
|
|
1220
|
-
status
|
|
1221
|
-
};
|
|
1222
|
-
}
|
|
1223
|
-
function parsePairDeviceResponse(value) {
|
|
1224
|
-
if (!isRecord(value) || typeof value.token !== "string") throw new DeviceCommandError("Invalid device pairing response");
|
|
1225
|
-
return {
|
|
1226
|
-
device: parseDeviceView(value.device),
|
|
1227
|
-
token: value.token
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
function parseDeviceList(value) {
|
|
1231
|
-
if (!Array.isArray(value)) throw new DeviceCommandError("Invalid device list response");
|
|
1232
|
-
return value.map(parseDeviceView);
|
|
1233
|
-
}
|
|
1234
|
-
async function pairDevice(name, command) {
|
|
1235
|
-
const platform = requireSupportedPlatform(process.platform);
|
|
1236
|
-
const profile = requireNamedProfile(command);
|
|
1237
|
-
const installationId = await getOrCreateInstallationId();
|
|
1238
|
-
const client = createClient({
|
|
1239
|
-
apiUrl: profile.apiUrl,
|
|
1240
|
-
apiKey: profile.apiKey,
|
|
1241
|
-
tenantId: profile.tenantId
|
|
1242
|
-
});
|
|
1243
|
-
const response = parsePairDeviceResponse(await createAppClient(client).devices.pair({
|
|
1244
|
-
installationId,
|
|
1245
|
-
name,
|
|
1246
|
-
platform,
|
|
1247
|
-
arch: process.arch,
|
|
1248
|
-
hostname: hostname()
|
|
1249
|
-
}));
|
|
1250
|
-
if (!await storeDeviceCredential(profile.profileName, {
|
|
1251
|
-
id: response.device.id,
|
|
1252
|
-
name: response.device.name,
|
|
1253
|
-
token: response.token
|
|
1254
|
-
})) throw new DeviceCommandError(NAMED_PROFILE_REQUIRED);
|
|
1255
|
-
process.stdout.write(`Paired ${response.device.name} (${response.device.id})\n`);
|
|
1256
|
-
}
|
|
1257
|
-
async function showDeviceStatus(command) {
|
|
1258
|
-
const profile = requireNamedProfile(command);
|
|
1259
|
-
const credential = await getStoredDeviceCredential(profile.profileName);
|
|
1260
|
-
if (!credential) throw new DeviceCommandError("No paired device; run standards device pair first");
|
|
1261
|
-
const client = createClient({
|
|
1262
|
-
apiUrl: profile.apiUrl,
|
|
1263
|
-
apiKey: profile.apiKey,
|
|
1264
|
-
tenantId: profile.tenantId
|
|
1265
|
-
});
|
|
1266
|
-
const pairedDevice = parseDeviceList(await createAppClient(client).devices.list({})).find((device) => device.id === credential.id);
|
|
1267
|
-
formatOutput(pairedDevice ? [pairedDevice] : [{
|
|
1268
|
-
id: credential.id,
|
|
1269
|
-
name: credential.name,
|
|
1270
|
-
status: "revoked"
|
|
1271
|
-
}], getFormat(command));
|
|
1272
|
-
}
|
|
1273
|
-
async function revokeDevice(command) {
|
|
1274
|
-
const profile = requireNamedProfile(command);
|
|
1275
|
-
const credential = await getStoredDeviceCredential(profile.profileName);
|
|
1276
|
-
if (!credential) throw new DeviceCommandError("No paired device; run standards device pair first");
|
|
1277
|
-
const client = createClient({
|
|
1278
|
-
apiUrl: profile.apiUrl,
|
|
1279
|
-
apiKey: profile.apiKey,
|
|
1280
|
-
tenantId: profile.tenantId
|
|
1281
|
-
});
|
|
1282
|
-
await createAppClient(client).devices.revoke({ id: credential.id });
|
|
1283
|
-
await removeDeviceCredential(profile.profileName);
|
|
1284
|
-
}
|
|
1285
|
-
async function serveDevice(command) {
|
|
1286
|
-
const platform = requireSupportedPlatform(process.platform);
|
|
1287
|
-
const profile = requireNamedProfile(command);
|
|
1288
|
-
const credential = await getStoredDeviceCredential(profile.profileName);
|
|
1289
|
-
if (!credential) throw new DeviceCommandError("No paired device; run standards device pair first");
|
|
1290
|
-
const userClient = createClient({
|
|
1291
|
-
apiUrl: profile.apiUrl,
|
|
1292
|
-
apiKey: profile.apiKey,
|
|
1293
|
-
tenantId: profile.tenantId
|
|
1294
|
-
});
|
|
1295
|
-
const device = parseDeviceList(await createAppClient(userClient).devices.list({})).find((candidate) => candidate.id === credential.id);
|
|
1296
|
-
if (!device) throw new DeviceCommandError("Device is unavailable");
|
|
1297
|
-
if (device.platform !== platform) throw new DeviceCommandError("Device platform does not match paired device");
|
|
1298
|
-
const controller = new AbortController();
|
|
1299
|
-
const stop = () => controller.abort();
|
|
1300
|
-
process.once("SIGINT", stop);
|
|
1301
|
-
process.once("SIGTERM", stop);
|
|
1302
|
-
process.stdout.write(`Connected as ${credential.name}\n`);
|
|
1303
|
-
process.stdout.write("Waiting for commands — press Ctrl+C to disconnect\n");
|
|
1304
|
-
const client = new DeviceWorkerClient({
|
|
1305
|
-
apiUrl: profile.apiUrl,
|
|
1306
|
-
token: credential.token
|
|
1307
|
-
});
|
|
1308
|
-
try {
|
|
1309
|
-
await runDeviceWorker({
|
|
1310
|
-
client,
|
|
1311
|
-
signal: controller.signal,
|
|
1312
|
-
onEvent: (event) => {
|
|
1313
|
-
if (event.type === "running") {
|
|
1314
|
-
process.stdout.write(`Running ${event.commandId}: ${event.intent}\n`);
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
process.stdout.write(`Completed ${event.commandId} with exit code ${event.exitCode}\n`);
|
|
1318
|
-
}
|
|
1319
|
-
});
|
|
1320
|
-
} finally {
|
|
1321
|
-
client.close();
|
|
1322
|
-
process.removeListener("SIGINT", stop);
|
|
1323
|
-
process.removeListener("SIGTERM", stop);
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
function registerDeviceCommand(program) {
|
|
1327
|
-
const device = program.command("device").description("Pair and run this computer as a device");
|
|
1328
|
-
device.command("pair").description("Pair this computer with Standards").requiredOption("--name <name>", "device name").action(async (options, command) => pairDevice(options.name, command));
|
|
1329
|
-
device.command("status").description("Show the paired device status").action(async (_options, command) => showDeviceStatus(command));
|
|
1330
|
-
device.command("revoke").description("Revoke this computer's device credential").action(async (_options, command) => revokeDevice(command));
|
|
1331
|
-
device.command("serve").description("Run the foreground device worker").action(async (_options, command) => serveDevice(command));
|
|
1332
|
-
}
|
|
1333
|
-
//#endregion
|
|
1334
367
|
//#region src/drift/shared-state.ts
|
|
1335
368
|
async function readSchemaState(client) {
|
|
1336
369
|
const state = parseSchemaState(await client.schemaState.get({}));
|
|
@@ -1917,6 +950,124 @@ function registerRecordsCommand(program) {
|
|
|
1917
950
|
});
|
|
1918
951
|
}
|
|
1919
952
|
//#endregion
|
|
953
|
+
//#region src/config.ts
|
|
954
|
+
const DEFAULT_API_URL = "http://localhost:4100/v1";
|
|
955
|
+
const ENV_PROFILE_NAME = "sandbox";
|
|
956
|
+
function getDefaultApiUrl() {
|
|
957
|
+
return DEFAULT_API_URL;
|
|
958
|
+
}
|
|
959
|
+
function getConfigPath() {
|
|
960
|
+
const configDir = process.env.STANDARDS_CONFIG_DIR ?? join(homedir(), ".standards");
|
|
961
|
+
return join(configDir, "config.json");
|
|
962
|
+
}
|
|
963
|
+
async function readConfig() {
|
|
964
|
+
try {
|
|
965
|
+
const raw = await readFile(getConfigPath(), "utf8");
|
|
966
|
+
const parsed = JSON.parse(raw);
|
|
967
|
+
return {
|
|
968
|
+
currentProfile: parsed.currentProfile,
|
|
969
|
+
profiles: parsed.profiles ?? {}
|
|
970
|
+
};
|
|
971
|
+
} catch (error) {
|
|
972
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return { profiles: {} };
|
|
973
|
+
throw error;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async function writeConfig(config) {
|
|
977
|
+
const path = getConfigPath();
|
|
978
|
+
const directory = dirname(path);
|
|
979
|
+
await mkdir(directory, {
|
|
980
|
+
recursive: true,
|
|
981
|
+
mode: 448
|
|
982
|
+
});
|
|
983
|
+
await chmod(directory, 448);
|
|
984
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
|
|
985
|
+
await chmod(path, 384);
|
|
986
|
+
}
|
|
987
|
+
async function upsertProfile(input) {
|
|
988
|
+
const config = await readConfig();
|
|
989
|
+
config.profiles[input.name] = {
|
|
990
|
+
apiUrl: input.apiUrl,
|
|
991
|
+
apiKey: input.apiKey
|
|
992
|
+
};
|
|
993
|
+
config.currentProfile = input.name;
|
|
994
|
+
await writeConfig(config);
|
|
995
|
+
}
|
|
996
|
+
async function setCurrentProfile(name) {
|
|
997
|
+
const config = await readConfig();
|
|
998
|
+
if (!config.profiles[name]) throw new Error(`Unknown Standards instance "${name}"`);
|
|
999
|
+
config.currentProfile = name;
|
|
1000
|
+
await writeConfig(config);
|
|
1001
|
+
}
|
|
1002
|
+
async function removeProfile(name) {
|
|
1003
|
+
const config = await readConfig();
|
|
1004
|
+
const profileName = name ?? config.currentProfile;
|
|
1005
|
+
if (!profileName) return;
|
|
1006
|
+
delete config.profiles[profileName];
|
|
1007
|
+
if (config.currentProfile === profileName) config.currentProfile = void 0;
|
|
1008
|
+
await writeConfig(config);
|
|
1009
|
+
}
|
|
1010
|
+
async function listProfiles() {
|
|
1011
|
+
const config = await readConfig();
|
|
1012
|
+
const profiles = Object.entries(config.profiles).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
|
|
1013
|
+
name,
|
|
1014
|
+
apiUrl: profile.apiUrl,
|
|
1015
|
+
current: config.currentProfile === name
|
|
1016
|
+
}));
|
|
1017
|
+
const envProfile = getEnvProfile();
|
|
1018
|
+
if (envProfile) profiles.push({
|
|
1019
|
+
name: ENV_PROFILE_NAME,
|
|
1020
|
+
apiUrl: envProfile.apiUrl,
|
|
1021
|
+
current: !config.currentProfile,
|
|
1022
|
+
source: "env"
|
|
1023
|
+
});
|
|
1024
|
+
return profiles;
|
|
1025
|
+
}
|
|
1026
|
+
async function getActiveProfile() {
|
|
1027
|
+
const config = await readConfig();
|
|
1028
|
+
if (!config.currentProfile) {
|
|
1029
|
+
const envProfile = getEnvProfile();
|
|
1030
|
+
return envProfile ? {
|
|
1031
|
+
name: ENV_PROFILE_NAME,
|
|
1032
|
+
...envProfile
|
|
1033
|
+
} : null;
|
|
1034
|
+
}
|
|
1035
|
+
const profile = config.profiles[config.currentProfile];
|
|
1036
|
+
if (!profile) return null;
|
|
1037
|
+
return {
|
|
1038
|
+
name: config.currentProfile,
|
|
1039
|
+
...profile
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
function getEnvProfile() {
|
|
1043
|
+
if (!process.env.STANDARDS_API_KEY) return null;
|
|
1044
|
+
return {
|
|
1045
|
+
apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
|
|
1046
|
+
apiKey: process.env.STANDARDS_API_KEY
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
async function resolveCliConfig(options) {
|
|
1050
|
+
if (options.apiUrl || options.apiKey) return {
|
|
1051
|
+
apiUrl: options.apiUrl ?? process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
|
|
1052
|
+
apiKey: options.apiKey ?? process.env.STANDARDS_API_KEY
|
|
1053
|
+
};
|
|
1054
|
+
const config = await readConfig();
|
|
1055
|
+
const profileName = options.instance ?? config.currentProfile;
|
|
1056
|
+
if (profileName) {
|
|
1057
|
+
const profile = config.profiles[profileName];
|
|
1058
|
+
if (!profile) throw new Error(`Unknown Standards instance "${profileName}"`);
|
|
1059
|
+
return {
|
|
1060
|
+
apiUrl: profile.apiUrl,
|
|
1061
|
+
apiKey: profile.apiKey,
|
|
1062
|
+
profileName
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
|
|
1067
|
+
apiKey: process.env.STANDARDS_API_KEY
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
//#endregion
|
|
1920
1071
|
//#region src/commands/root.ts
|
|
1921
1072
|
async function promptForMissingValue(label) {
|
|
1922
1073
|
const rl = createInterface({
|
|
@@ -2006,6 +1157,153 @@ function registerSearchCommand(program) {
|
|
|
2006
1157
|
});
|
|
2007
1158
|
}
|
|
2008
1159
|
//#endregion
|
|
1160
|
+
//#region src/commands/skills.ts
|
|
1161
|
+
const SKILL_FILE = "SKILL.md";
|
|
1162
|
+
/** A folder name: one path segment under the rule (`isSafeSkillFilePath`). */
|
|
1163
|
+
function isSafeSegment(segment) {
|
|
1164
|
+
return !segment.includes("/") && isSafeSkillFilePath(segment);
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* A path the server answered, before it lands on this disk: the skill path
|
|
1168
|
+
* rule, and no drive letter (`C:`), which Windows would read as absolute.
|
|
1169
|
+
*/
|
|
1170
|
+
function isSafeRelativePath(path) {
|
|
1171
|
+
return !/^[A-Za-z]:/.test(path) && isSafeSkillFilePath(path);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Hidden entries (`.git`, `.env`, `.DS_Store`…) and symbolic links are not
|
|
1175
|
+
* sent: a link could point anywhere on the machine. Each one is reported.
|
|
1176
|
+
*/
|
|
1177
|
+
function isSkipped(entry) {
|
|
1178
|
+
return entry.name.startsWith(".") || entry.isSymbolicLink();
|
|
1179
|
+
}
|
|
1180
|
+
async function isDirectory(path) {
|
|
1181
|
+
return (await stat(path)).isDirectory();
|
|
1182
|
+
}
|
|
1183
|
+
async function listFiles(dir, skipped) {
|
|
1184
|
+
const files = [];
|
|
1185
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
1186
|
+
const path = join(dir, entry.name);
|
|
1187
|
+
if (isSkipped(entry)) skipped.push(path);
|
|
1188
|
+
else if (entry.isDirectory()) files.push(...await listFiles(path, skipped));
|
|
1189
|
+
else if (entry.isFile()) files.push(path);
|
|
1190
|
+
}
|
|
1191
|
+
return files.sort();
|
|
1192
|
+
}
|
|
1193
|
+
/** A directory holding `SKILL.md` is one folder; any other is a directory of folders. */
|
|
1194
|
+
async function foldersIn(dir, skipped) {
|
|
1195
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1196
|
+
if (entries.some((entry) => entry.isFile() && entry.name === SKILL_FILE)) return [dir];
|
|
1197
|
+
const folders = [];
|
|
1198
|
+
for (const entry of entries) if (isSkipped(entry)) skipped.push(join(dir, entry.name));
|
|
1199
|
+
else if (entry.isDirectory()) folders.push(join(dir, entry.name));
|
|
1200
|
+
return folders.sort();
|
|
1201
|
+
}
|
|
1202
|
+
/** One folder as read: text as `content`, any other bytes uploaded and sent by id. */
|
|
1203
|
+
async function readFolder(client, dir, skipped) {
|
|
1204
|
+
const files = [];
|
|
1205
|
+
for (const absolute of await listFiles(dir, skipped)) {
|
|
1206
|
+
const path = relative(dir, absolute).split(sep).join("/");
|
|
1207
|
+
const bytes = await readFile(absolute);
|
|
1208
|
+
const content = skillFileText(bytes);
|
|
1209
|
+
if (content !== void 0) {
|
|
1210
|
+
files.push({
|
|
1211
|
+
path,
|
|
1212
|
+
content
|
|
1213
|
+
});
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
const [stored] = await client.files.upload({ files: [new File([bytes], basename(absolute))] });
|
|
1217
|
+
if (!stored) throw new Error(`The upload of ${absolute} answered no file`);
|
|
1218
|
+
files.push({
|
|
1219
|
+
path,
|
|
1220
|
+
fileId: stored.id
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
return {
|
|
1224
|
+
name: basename(resolve(dir)),
|
|
1225
|
+
files
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* `standards skills push`: every folder sent as read in one `skills.import`.
|
|
1230
|
+
* The CLI judges nothing: the server answers every problem, printed whole.
|
|
1231
|
+
*/
|
|
1232
|
+
async function runSkillsPush(client, dirs, options = {}) {
|
|
1233
|
+
try {
|
|
1234
|
+
const folders = [];
|
|
1235
|
+
const skipped = [];
|
|
1236
|
+
for (const dir of dirs) {
|
|
1237
|
+
if (!await isDirectory(dir)) throw new Error(`${dir} is not a directory`);
|
|
1238
|
+
for (const folder of await foldersIn(dir, skipped)) folders.push(await readFolder(client, folder, skipped));
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
exitCode: 0,
|
|
1242
|
+
result: await client.skills.import({
|
|
1243
|
+
folders,
|
|
1244
|
+
onConflict: options.replace === true ? "replace" : "fail"
|
|
1245
|
+
}),
|
|
1246
|
+
skipped
|
|
1247
|
+
};
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
return {
|
|
1250
|
+
exitCode: 1,
|
|
1251
|
+
errorMessage: messageOf(error)
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
/** The bytes behind an exported file: its text, or its signed URL fetched. */
|
|
1256
|
+
async function exportedBytes(file) {
|
|
1257
|
+
if ("content" in file) return new TextEncoder().encode(file.content);
|
|
1258
|
+
const response = await fetch(file.url);
|
|
1259
|
+
if (!response.ok) throw new Error(`Downloading ${file.url} answered ${response.status}`);
|
|
1260
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1261
|
+
}
|
|
1262
|
+
/** `standards skills pull`: each skill written as `<dir>/<name>/`, its files at their paths. */
|
|
1263
|
+
async function runSkillsPull(client, dir, names) {
|
|
1264
|
+
const { folders } = await client.skills.export(names && names.length > 0 ? { names: [...names] } : {});
|
|
1265
|
+
const refused = folders.flatMap((folder) => isSafeSegment(folder.name) ? folder.files.filter((file) => !isSafeRelativePath(file.path)).map((file) => `skill ${JSON.stringify(folder.name)}: file path ${JSON.stringify(file.path)}`) : [`skill name ${JSON.stringify(folder.name)}`]);
|
|
1266
|
+
if (refused.length > 0) throw new Error(`Pull refused, nothing was written: each name must be one folder name and each path relative, inside it:\n${refused.map((line) => `- ${line}`).join("\n")}`);
|
|
1267
|
+
const target = resolve(dir);
|
|
1268
|
+
const written = [];
|
|
1269
|
+
for (const folder of folders) {
|
|
1270
|
+
const root = resolve(target, folder.name);
|
|
1271
|
+
for (const file of folder.files) {
|
|
1272
|
+
const destination = resolve(root, file.path);
|
|
1273
|
+
if (!(dirname(root) === target && destination.startsWith(`${root}${sep}`))) throw new Error(`Skill ${folder.name}: file path ${file.path} leaves ${target}`);
|
|
1274
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
1275
|
+
await writeFile(destination, await exportedBytes(file));
|
|
1276
|
+
}
|
|
1277
|
+
written.push(root);
|
|
1278
|
+
}
|
|
1279
|
+
return written;
|
|
1280
|
+
}
|
|
1281
|
+
function collectName(value, names) {
|
|
1282
|
+
return [...names, value];
|
|
1283
|
+
}
|
|
1284
|
+
/** Skills as Agent Skills folders: pull them to disk, push folders back. */
|
|
1285
|
+
function registerSkillsCommand(program) {
|
|
1286
|
+
const skills = program.command("skills").description("Pull and push skills as Agent Skills folders (SKILL.md and its files)");
|
|
1287
|
+
skills.command("pull").description("Write each skill as a folder <dir>/<name>/ (every skill you can open by default)").argument("[dir]", "directory to write the folders into", ".").option("--name <name>", "a skill to pull (repeat for several)", collectName, []).action(async (dir, opts, cmd) => {
|
|
1288
|
+
try {
|
|
1289
|
+
formatOutput({ written: await runSkillsPull(getAppClientFromCommand(cmd), dir, opts.name) }, getFormat(cmd));
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
console.error(styleText("red", `✗ ${messageOf(error)}`));
|
|
1292
|
+
process.exit(1);
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
skills.command("push").description("Import skill folders: a folder holding SKILL.md, or a directory of such folders. All or nothing").argument("<dirs...>", "skill folders, or directories of skill folders").option("--replace", "rewrite the skill a name already reads instead of refusing it").action(async (dirs, opts, cmd) => {
|
|
1296
|
+
const outcome = await runSkillsPush(getAppClientFromCommand(cmd), dirs, opts);
|
|
1297
|
+
if (outcome.exitCode === 1) {
|
|
1298
|
+
console.error(styleText("red", `✗ ${outcome.errorMessage}`));
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
for (const path of outcome.skipped) console.warn(styleText("yellow", `! skipped ${path} (hidden or a symbolic link)`));
|
|
1302
|
+
for (const warning of outcome.result.warnings) console.warn(styleText("yellow", `! ${warning}`));
|
|
1303
|
+
formatOutput(outcome.result, getFormat(cmd));
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
//#endregion
|
|
2009
1307
|
//#region src/version.ts
|
|
2010
1308
|
/**
|
|
2011
1309
|
* The installed package's version, read from its own manifest: `src/` and the
|
|
@@ -2044,7 +1342,6 @@ function createProgram() {
|
|
|
2044
1342
|
registerPullCommand(program);
|
|
2045
1343
|
registerDiffCommand(program);
|
|
2046
1344
|
registerDocumentsCommand(program);
|
|
2047
|
-
registerDeviceCommand(program);
|
|
2048
1345
|
registerFoldersCommand(program);
|
|
2049
1346
|
registerKeysCommand(program);
|
|
2050
1347
|
registerInvitationsCommand(program);
|
|
@@ -2055,6 +1352,7 @@ function createProgram() {
|
|
|
2055
1352
|
registerMeetingsCommand(program);
|
|
2056
1353
|
registerMemoryCommand(program);
|
|
2057
1354
|
registerSearchCommand(program);
|
|
1355
|
+
registerSkillsCommand(program);
|
|
2058
1356
|
registerEmbeddingsCommand(program);
|
|
2059
1357
|
registerAiCommand(program);
|
|
2060
1358
|
registerQuotasCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@standards/cli",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.411",
|
|
4
4
|
"description": "CLI tool to interact with Standards API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,14 +26,12 @@
|
|
|
26
26
|
"cli-table3": "^0.6.5",
|
|
27
27
|
"commander": "^13.1.0",
|
|
28
28
|
"jiti": "2.7.0",
|
|
29
|
-
"
|
|
30
|
-
"@standards/
|
|
31
|
-
"@standards/
|
|
32
|
-
"@standards/schema": "1.0.0-alpha.409"
|
|
29
|
+
"@standards/client": "1.0.0-alpha.411",
|
|
30
|
+
"@standards/constants": "1.0.0-alpha.411",
|
|
31
|
+
"@standards/schema": "1.0.0-alpha.411"
|
|
33
32
|
},
|
|
34
33
|
"devDependencies": {
|
|
35
34
|
"@types/node": "^25.6.0",
|
|
36
|
-
"@types/ws": "8.18.1",
|
|
37
35
|
"jsdom": "^25.0.1",
|
|
38
36
|
"tsdown": "^0.23.0",
|
|
39
37
|
"typescript": "^5.6.3",
|