@stndrds/cli 1.0.0-alpha.292 → 1.0.0-alpha.294
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 +34 -0
- package/dist/bin.mjs +982 -153
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk10 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/client.ts
|
|
@@ -129,6 +129,13 @@ function getClientFromCommand(cmd) {
|
|
|
129
129
|
}
|
|
130
130
|
return createClient({ apiUrl: root.apiUrl, apiKey: root.apiKey, tenantId: root.tenant });
|
|
131
131
|
}
|
|
132
|
+
function requireTenant(cmd, message) {
|
|
133
|
+
const { tenant } = getGlobalOptions(cmd);
|
|
134
|
+
if (!tenant) {
|
|
135
|
+
throw new Error(message);
|
|
136
|
+
}
|
|
137
|
+
return tenant;
|
|
138
|
+
}
|
|
132
139
|
function messageOf(error) {
|
|
133
140
|
return error instanceof Error ? error.message : String(error);
|
|
134
141
|
}
|
|
@@ -301,6 +308,833 @@ function registerConnectorsCommand(program) {
|
|
|
301
308
|
});
|
|
302
309
|
}
|
|
303
310
|
|
|
311
|
+
// src/commands/device.ts
|
|
312
|
+
import { hostname } from "os";
|
|
313
|
+
import { SchemaError as SchemaError2, SchemaErrorCode as SchemaErrorCode2 } from "@stndrds/schema";
|
|
314
|
+
|
|
315
|
+
// src/config.ts
|
|
316
|
+
import { randomUUID } from "crypto";
|
|
317
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
318
|
+
import { homedir } from "os";
|
|
319
|
+
import { dirname, join } from "path";
|
|
320
|
+
var DEFAULT_API_URL = "http://localhost:4100/v1";
|
|
321
|
+
var ENV_PROFILE_NAME = "sandbox";
|
|
322
|
+
function getDefaultApiUrl() {
|
|
323
|
+
return DEFAULT_API_URL;
|
|
324
|
+
}
|
|
325
|
+
function getConfigPath() {
|
|
326
|
+
const configDir = process.env.STANDARDS_CONFIG_DIR ?? join(homedir(), ".standards");
|
|
327
|
+
return join(configDir, "config.json");
|
|
328
|
+
}
|
|
329
|
+
async function readConfig() {
|
|
330
|
+
try {
|
|
331
|
+
const raw = await readFile(getConfigPath(), "utf8");
|
|
332
|
+
const parsed = JSON.parse(raw);
|
|
333
|
+
return {
|
|
334
|
+
...parsed.installationId ? { installationId: parsed.installationId } : {},
|
|
335
|
+
currentProfile: parsed.currentProfile,
|
|
336
|
+
profiles: parsed.profiles ?? {}
|
|
337
|
+
};
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
340
|
+
return { profiles: {} };
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async function writeConfig(config) {
|
|
346
|
+
const path = getConfigPath();
|
|
347
|
+
const directory = dirname(path);
|
|
348
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
349
|
+
await chmod(directory, 448);
|
|
350
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}
|
|
351
|
+
`, { mode: 384 });
|
|
352
|
+
await chmod(path, 384);
|
|
353
|
+
}
|
|
354
|
+
function normalizeApiOrigin(apiUrl) {
|
|
355
|
+
try {
|
|
356
|
+
return new URL(apiUrl).origin;
|
|
357
|
+
} catch {
|
|
358
|
+
return apiUrl.replace(/\/+$/, "");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function upsertProfile(input2) {
|
|
362
|
+
const config = await readConfig();
|
|
363
|
+
const existingProfile = config.profiles[input2.name];
|
|
364
|
+
const device = existingProfile?.device && normalizeApiOrigin(existingProfile.apiUrl) === normalizeApiOrigin(input2.apiUrl) ? existingProfile.device : void 0;
|
|
365
|
+
config.profiles[input2.name] = {
|
|
366
|
+
apiUrl: input2.apiUrl,
|
|
367
|
+
apiKey: input2.apiKey,
|
|
368
|
+
...device ? { device } : {}
|
|
369
|
+
};
|
|
370
|
+
config.currentProfile = input2.name;
|
|
371
|
+
await writeConfig(config);
|
|
372
|
+
}
|
|
373
|
+
async function getOrCreateInstallationId() {
|
|
374
|
+
const config = await readConfig();
|
|
375
|
+
if (config.installationId) return config.installationId;
|
|
376
|
+
const installationId = randomUUID();
|
|
377
|
+
config.installationId = installationId;
|
|
378
|
+
await writeConfig(config);
|
|
379
|
+
return installationId;
|
|
380
|
+
}
|
|
381
|
+
async function getStoredDeviceCredential(profileName) {
|
|
382
|
+
const config = await readConfig();
|
|
383
|
+
return config.profiles[profileName]?.device ?? null;
|
|
384
|
+
}
|
|
385
|
+
async function storeDeviceCredential(profileName, credential) {
|
|
386
|
+
const config = await readConfig();
|
|
387
|
+
const profile = config.profiles[profileName];
|
|
388
|
+
if (!profile) return false;
|
|
389
|
+
profile.device = credential;
|
|
390
|
+
await writeConfig(config);
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
async function removeDeviceCredential(profileName) {
|
|
394
|
+
const config = await readConfig();
|
|
395
|
+
const profile = config.profiles[profileName];
|
|
396
|
+
if (!profile?.device) return false;
|
|
397
|
+
profile.device = void 0;
|
|
398
|
+
await writeConfig(config);
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
async function setCurrentProfile(name) {
|
|
402
|
+
const config = await readConfig();
|
|
403
|
+
if (!config.profiles[name]) {
|
|
404
|
+
throw new Error(`Unknown Standards instance "${name}"`);
|
|
405
|
+
}
|
|
406
|
+
config.currentProfile = name;
|
|
407
|
+
await writeConfig(config);
|
|
408
|
+
}
|
|
409
|
+
async function removeProfile(name) {
|
|
410
|
+
const config = await readConfig();
|
|
411
|
+
const profileName = name ?? config.currentProfile;
|
|
412
|
+
if (!profileName) return;
|
|
413
|
+
delete config.profiles[profileName];
|
|
414
|
+
if (config.currentProfile === profileName) {
|
|
415
|
+
config.currentProfile = void 0;
|
|
416
|
+
}
|
|
417
|
+
await writeConfig(config);
|
|
418
|
+
}
|
|
419
|
+
async function listProfiles() {
|
|
420
|
+
const config = await readConfig();
|
|
421
|
+
const profiles = Object.entries(config.profiles).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
|
|
422
|
+
name,
|
|
423
|
+
apiUrl: profile.apiUrl,
|
|
424
|
+
current: config.currentProfile === name
|
|
425
|
+
}));
|
|
426
|
+
const envProfile = getEnvProfile();
|
|
427
|
+
if (envProfile) {
|
|
428
|
+
profiles.push({
|
|
429
|
+
name: ENV_PROFILE_NAME,
|
|
430
|
+
apiUrl: envProfile.apiUrl,
|
|
431
|
+
current: !config.currentProfile,
|
|
432
|
+
source: "env"
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return profiles;
|
|
436
|
+
}
|
|
437
|
+
async function getActiveProfile() {
|
|
438
|
+
const config = await readConfig();
|
|
439
|
+
if (!config.currentProfile) {
|
|
440
|
+
const envProfile = getEnvProfile();
|
|
441
|
+
return envProfile ? { name: ENV_PROFILE_NAME, ...envProfile } : null;
|
|
442
|
+
}
|
|
443
|
+
const profile = config.profiles[config.currentProfile];
|
|
444
|
+
if (!profile) return null;
|
|
445
|
+
return { name: config.currentProfile, ...profile };
|
|
446
|
+
}
|
|
447
|
+
function getEnvProfile() {
|
|
448
|
+
if (!process.env.STANDARDS_API_KEY) return null;
|
|
449
|
+
return {
|
|
450
|
+
apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
|
|
451
|
+
apiKey: process.env.STANDARDS_API_KEY
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
async function resolveCliConfig(options) {
|
|
455
|
+
if (options.apiUrl || options.apiKey) {
|
|
456
|
+
return {
|
|
457
|
+
apiUrl: options.apiUrl ?? process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
|
|
458
|
+
apiKey: options.apiKey ?? process.env.STANDARDS_API_KEY
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const config = await readConfig();
|
|
462
|
+
const profileName = options.instance ?? config.currentProfile;
|
|
463
|
+
if (profileName) {
|
|
464
|
+
const profile = config.profiles[profileName];
|
|
465
|
+
if (!profile) throw new Error(`Unknown Standards instance "${profileName}"`);
|
|
466
|
+
return {
|
|
467
|
+
apiUrl: profile.apiUrl,
|
|
468
|
+
apiKey: profile.apiKey,
|
|
469
|
+
profileName
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
|
|
474
|
+
apiKey: process.env.STANDARDS_API_KEY
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/device/worker-client.ts
|
|
479
|
+
import { SchemaError, SchemaErrorCode } from "@stndrds/schema";
|
|
480
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
481
|
+
var DeviceWorkerRequestError = class extends SchemaError {
|
|
482
|
+
constructor(statusCode, message, code = SchemaErrorCode.REPOSITORY_QUERY_FAILED) {
|
|
483
|
+
super(message, code, { statusCode });
|
|
484
|
+
this.statusCode = statusCode;
|
|
485
|
+
this.name = "DeviceWorkerRequestError";
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var DeviceCredentialRejectedError = class extends DeviceWorkerRequestError {
|
|
489
|
+
constructor() {
|
|
490
|
+
super(
|
|
491
|
+
401,
|
|
492
|
+
"Device credential rejected; run standards device pair again",
|
|
493
|
+
SchemaErrorCode.ACCESS_DENIED
|
|
494
|
+
);
|
|
495
|
+
this.name = "DeviceCredentialRejectedError";
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
function isRecord(value) {
|
|
499
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
500
|
+
}
|
|
501
|
+
async function readJson(response) {
|
|
502
|
+
const text = await response.text();
|
|
503
|
+
if (!text) return void 0;
|
|
504
|
+
try {
|
|
505
|
+
return JSON.parse(text);
|
|
506
|
+
} catch {
|
|
507
|
+
return void 0;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function messageFrom(value, fallback) {
|
|
511
|
+
return isRecord(value) && typeof value.message === "string" ? value.message : fallback;
|
|
512
|
+
}
|
|
513
|
+
function parseLease(value) {
|
|
514
|
+
if (!isRecord(value)) throw new DeviceWorkerRequestError(200, "Invalid lease response");
|
|
515
|
+
const leaseToken = value.leaseToken;
|
|
516
|
+
if (!isRecord(value.command)) {
|
|
517
|
+
throw new DeviceWorkerRequestError(200, "Invalid lease response");
|
|
518
|
+
}
|
|
519
|
+
const leasedCommand = value.command;
|
|
520
|
+
const { id, intent, command, cwd, timeoutMs } = leasedCommand;
|
|
521
|
+
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)) {
|
|
522
|
+
throw new DeviceWorkerRequestError(200, "Invalid lease response");
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
id,
|
|
526
|
+
leaseToken,
|
|
527
|
+
intent,
|
|
528
|
+
command,
|
|
529
|
+
timeoutMs,
|
|
530
|
+
...typeof cwd === "string" ? { cwd } : {}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function parseHeartbeat(value) {
|
|
534
|
+
if (!isRecord(value)) {
|
|
535
|
+
throw new DeviceWorkerRequestError(200, "Invalid heartbeat response");
|
|
536
|
+
}
|
|
537
|
+
const { cancel } = value;
|
|
538
|
+
if (typeof cancel !== "boolean") {
|
|
539
|
+
throw new DeviceWorkerRequestError(200, "Invalid heartbeat response");
|
|
540
|
+
}
|
|
541
|
+
const reason = value.reason;
|
|
542
|
+
if (reason !== void 0 && reason !== "cancelled" && reason !== "revoked") {
|
|
543
|
+
throw new DeviceWorkerRequestError(200, "Invalid heartbeat response");
|
|
544
|
+
}
|
|
545
|
+
return { cancel, ...reason === void 0 ? {} : { reason } };
|
|
546
|
+
}
|
|
547
|
+
function isAbortError(error) {
|
|
548
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
549
|
+
}
|
|
550
|
+
var DeviceWorkerClient = class {
|
|
551
|
+
constructor(options) {
|
|
552
|
+
this.options = options;
|
|
553
|
+
this.fetch = options.fetch ?? fetch;
|
|
554
|
+
this.apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
555
|
+
}
|
|
556
|
+
async request(path, input2 = {}) {
|
|
557
|
+
const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
|
558
|
+
const signal = input2.signal ? AbortSignal.any([input2.signal, timeoutSignal]) : timeoutSignal;
|
|
559
|
+
let response;
|
|
560
|
+
try {
|
|
561
|
+
response = await this.fetch(`${this.apiUrl}/device-worker${path}`, {
|
|
562
|
+
method: "POST",
|
|
563
|
+
signal,
|
|
564
|
+
headers: {
|
|
565
|
+
authorization: `Device ${this.options.token}`,
|
|
566
|
+
"content-type": "application/json",
|
|
567
|
+
...input2.leaseToken ? { "x-device-lease-token": input2.leaseToken } : {}
|
|
568
|
+
},
|
|
569
|
+
...input2.body === void 0 ? {} : { body: JSON.stringify(input2.body) }
|
|
570
|
+
});
|
|
571
|
+
} catch (error) {
|
|
572
|
+
if (input2.signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
573
|
+
if (isAbortError(error)) throw error;
|
|
574
|
+
throw new DeviceWorkerRequestError(0, "Device worker request failed");
|
|
575
|
+
}
|
|
576
|
+
if (response.status === 401) throw new DeviceCredentialRejectedError();
|
|
577
|
+
if (!response.ok) {
|
|
578
|
+
const body = await readJson(response);
|
|
579
|
+
throw new DeviceWorkerRequestError(
|
|
580
|
+
response.status,
|
|
581
|
+
messageFrom(body, `Device worker request failed with status ${response.status}`),
|
|
582
|
+
response.status === 409 ? SchemaErrorCode.CONFLICT : SchemaErrorCode.REPOSITORY_QUERY_FAILED
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
return response;
|
|
586
|
+
}
|
|
587
|
+
async lease(signal) {
|
|
588
|
+
const response = await this.request("/commands/lease", {
|
|
589
|
+
body: { version: "1", capabilities: ["exec"] },
|
|
590
|
+
signal
|
|
591
|
+
});
|
|
592
|
+
if (response.status === 204) return null;
|
|
593
|
+
return parseLease(await readJson(response));
|
|
594
|
+
}
|
|
595
|
+
async markRunning(commandId, leaseToken) {
|
|
596
|
+
await this.request(`/commands/${commandId}/running`, { leaseToken });
|
|
597
|
+
}
|
|
598
|
+
async heartbeat(commandId, leaseToken, signal) {
|
|
599
|
+
const response = await this.request(`/commands/${commandId}/heartbeat`, {
|
|
600
|
+
leaseToken,
|
|
601
|
+
signal
|
|
602
|
+
});
|
|
603
|
+
return parseHeartbeat(await readJson(response));
|
|
604
|
+
}
|
|
605
|
+
async output(commandId, leaseToken, stdout, stderr) {
|
|
606
|
+
await this.request(`/commands/${commandId}/output`, {
|
|
607
|
+
leaseToken,
|
|
608
|
+
body: { stdout, stderr }
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
async complete(commandId, leaseToken, result) {
|
|
612
|
+
await this.request(`/commands/${commandId}/complete`, {
|
|
613
|
+
leaseToken,
|
|
614
|
+
body: {
|
|
615
|
+
exitCode: result.exitCode,
|
|
616
|
+
success: result.success,
|
|
617
|
+
...result.errorCode ? { errorCode: result.errorCode } : {}
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// src/device/process-runner.ts
|
|
624
|
+
import { spawn } from "child_process";
|
|
625
|
+
import { constants } from "fs";
|
|
626
|
+
import { access, stat } from "fs/promises";
|
|
627
|
+
import { isAbsolute } from "path";
|
|
628
|
+
import { StringDecoder } from "string_decoder";
|
|
629
|
+
var OUTPUT_LIMIT = 3e4;
|
|
630
|
+
var TERMINATION_GRACE_MS = 5e3;
|
|
631
|
+
var TERMINATION_POLL_MS = 50;
|
|
632
|
+
var BoundedTextBuffer = class {
|
|
633
|
+
constructor() {
|
|
634
|
+
this.decoder = new StringDecoder("utf8");
|
|
635
|
+
this.decoderFlushed = false;
|
|
636
|
+
this.value = "";
|
|
637
|
+
this.truncated = false;
|
|
638
|
+
}
|
|
639
|
+
append(chunk) {
|
|
640
|
+
if (this.truncated) return;
|
|
641
|
+
const text = typeof chunk === "string" ? chunk : this.decoder.write(Buffer.from(chunk));
|
|
642
|
+
this.appendText(text);
|
|
643
|
+
}
|
|
644
|
+
appendText(text) {
|
|
645
|
+
const remaining = OUTPUT_LIMIT - this.value.length;
|
|
646
|
+
if (text.length <= remaining) {
|
|
647
|
+
this.value += text;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
this.value += text.slice(0, Math.max(remaining, 0));
|
|
651
|
+
this.truncated = true;
|
|
652
|
+
}
|
|
653
|
+
toString() {
|
|
654
|
+
if (!this.decoderFlushed) {
|
|
655
|
+
this.decoderFlushed = true;
|
|
656
|
+
this.appendText(this.decoder.end());
|
|
657
|
+
}
|
|
658
|
+
return this.truncated ? `${this.value}
|
|
659
|
+
... output truncated to ${OUTPUT_LIMIT} chars` : this.value;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
function isMissingProcess(error) {
|
|
663
|
+
return error instanceof Error && "code" in error && error.code === "ESRCH";
|
|
664
|
+
}
|
|
665
|
+
function signalProcessGroup(pid, signal, kill) {
|
|
666
|
+
try {
|
|
667
|
+
kill(-pid, signal);
|
|
668
|
+
return true;
|
|
669
|
+
} catch (error) {
|
|
670
|
+
if (isMissingProcess(error)) return false;
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function beginProcessGroupTermination(pid, deps = {}) {
|
|
675
|
+
const kill = deps.kill ?? process.kill.bind(process);
|
|
676
|
+
const setTimer = deps.setTimer ?? setTimeout;
|
|
677
|
+
const clearTimer = deps.clearTimer ?? clearTimeout;
|
|
678
|
+
return new Promise((resolve, reject) => {
|
|
679
|
+
let settled = false;
|
|
680
|
+
let escalationTimer;
|
|
681
|
+
let pollTimer;
|
|
682
|
+
function clearTimers() {
|
|
683
|
+
if (escalationTimer) clearTimer(escalationTimer);
|
|
684
|
+
if (pollTimer) clearTimer(pollTimer);
|
|
685
|
+
}
|
|
686
|
+
function finish() {
|
|
687
|
+
if (settled) return;
|
|
688
|
+
settled = true;
|
|
689
|
+
clearTimers();
|
|
690
|
+
resolve();
|
|
691
|
+
}
|
|
692
|
+
function fail(error) {
|
|
693
|
+
if (settled) return;
|
|
694
|
+
settled = true;
|
|
695
|
+
clearTimers();
|
|
696
|
+
reject(error);
|
|
697
|
+
}
|
|
698
|
+
function poll() {
|
|
699
|
+
if (settled) return;
|
|
700
|
+
try {
|
|
701
|
+
if (!signalProcessGroup(pid, 0, kill)) {
|
|
702
|
+
finish();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
pollTimer = setTimer(poll, TERMINATION_POLL_MS);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
fail(error);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
if (!signalProcessGroup(pid, "SIGTERM", kill)) {
|
|
712
|
+
finish();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
escalationTimer = setTimer(() => {
|
|
716
|
+
try {
|
|
717
|
+
if (!signalProcessGroup(pid, 0, kill)) {
|
|
718
|
+
finish();
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
signalProcessGroup(pid, "SIGKILL", kill);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
fail(error);
|
|
724
|
+
}
|
|
725
|
+
}, TERMINATION_GRACE_MS);
|
|
726
|
+
pollTimer = setTimer(poll, TERMINATION_POLL_MS);
|
|
727
|
+
} catch (error) {
|
|
728
|
+
fail(error);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
async function resolveShell(shell = process.env.SHELL) {
|
|
733
|
+
if (!(shell && isAbsolute(shell))) return "/bin/sh";
|
|
734
|
+
try {
|
|
735
|
+
const metadata = await stat(shell);
|
|
736
|
+
if (!metadata.isFile()) return "/bin/sh";
|
|
737
|
+
await access(shell, constants.X_OK);
|
|
738
|
+
return shell;
|
|
739
|
+
} catch {
|
|
740
|
+
return "/bin/sh";
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function terminalResult(reason, stdout, stderr) {
|
|
744
|
+
return {
|
|
745
|
+
stdout: stdout.toString(),
|
|
746
|
+
stderr: stderr.toString(),
|
|
747
|
+
exitCode: reason === "timed_out" ? 124 : 130,
|
|
748
|
+
success: false,
|
|
749
|
+
errorCode: reason
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
async function runDeviceProcess(input2) {
|
|
753
|
+
if (input2.signal?.aborted) {
|
|
754
|
+
return { stdout: "", stderr: "", exitCode: 130, success: false, errorCode: "cancelled" };
|
|
755
|
+
}
|
|
756
|
+
const shell = await resolveShell();
|
|
757
|
+
if (input2.signal?.aborted) {
|
|
758
|
+
return { stdout: "", stderr: "", exitCode: 130, success: false, errorCode: "cancelled" };
|
|
759
|
+
}
|
|
760
|
+
const stdout = new BoundedTextBuffer();
|
|
761
|
+
const stderr = new BoundedTextBuffer();
|
|
762
|
+
return new Promise((resolve) => {
|
|
763
|
+
let settled = false;
|
|
764
|
+
let terminationReason;
|
|
765
|
+
let termination;
|
|
766
|
+
let child;
|
|
767
|
+
function finish(result) {
|
|
768
|
+
if (settled) return;
|
|
769
|
+
settled = true;
|
|
770
|
+
clearTimeout(timeout);
|
|
771
|
+
input2.signal?.removeEventListener("abort", cancel);
|
|
772
|
+
resolve(result);
|
|
773
|
+
}
|
|
774
|
+
function finishTerminationFailure(error) {
|
|
775
|
+
finish({
|
|
776
|
+
stdout: stdout.toString(),
|
|
777
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
778
|
+
exitCode: 1,
|
|
779
|
+
success: false,
|
|
780
|
+
errorCode: "execution_failed"
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
function terminate(reason) {
|
|
784
|
+
if (settled || terminationReason || !child?.pid) return;
|
|
785
|
+
terminationReason = reason;
|
|
786
|
+
termination = beginProcessGroupTermination(child.pid);
|
|
787
|
+
termination.catch(finishTerminationFailure);
|
|
788
|
+
}
|
|
789
|
+
function cancel() {
|
|
790
|
+
terminate("cancelled");
|
|
791
|
+
}
|
|
792
|
+
const timeout = setTimeout(() => terminate("timed_out"), input2.timeoutMs);
|
|
793
|
+
try {
|
|
794
|
+
const spawned2 = spawn(shell, ["-lc", input2.command], {
|
|
795
|
+
cwd: input2.cwd,
|
|
796
|
+
env: process.env,
|
|
797
|
+
detached: true,
|
|
798
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
799
|
+
});
|
|
800
|
+
child = spawned2;
|
|
801
|
+
} catch (error) {
|
|
802
|
+
finish({
|
|
803
|
+
stdout: "",
|
|
804
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
805
|
+
exitCode: 1,
|
|
806
|
+
success: false,
|
|
807
|
+
errorCode: "execution_failed"
|
|
808
|
+
});
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
input2.signal?.addEventListener("abort", cancel, { once: true });
|
|
812
|
+
if (input2.signal?.aborted) cancel();
|
|
813
|
+
const spawned = child;
|
|
814
|
+
spawned.stdout.on("data", (chunk) => stdout.append(chunk));
|
|
815
|
+
spawned.stderr.on("data", (chunk) => stderr.append(chunk));
|
|
816
|
+
spawned.once("error", (error) => {
|
|
817
|
+
finish({
|
|
818
|
+
stdout: stdout.toString(),
|
|
819
|
+
stderr: error.message,
|
|
820
|
+
exitCode: 1,
|
|
821
|
+
success: false,
|
|
822
|
+
errorCode: "execution_failed"
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
spawned.once("close", (code) => {
|
|
826
|
+
if (terminationReason) {
|
|
827
|
+
const reason = terminationReason;
|
|
828
|
+
(termination ?? Promise.resolve()).then(
|
|
829
|
+
() => finish(terminalResult(reason, stdout, stderr)),
|
|
830
|
+
finishTerminationFailure
|
|
831
|
+
);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const exitCode = code ?? 1;
|
|
835
|
+
finish({
|
|
836
|
+
stdout: stdout.toString(),
|
|
837
|
+
stderr: stderr.toString(),
|
|
838
|
+
exitCode,
|
|
839
|
+
success: exitCode === 0,
|
|
840
|
+
...code === null ? { errorCode: "execution_failed" } : {}
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/device/worker-loop.ts
|
|
847
|
+
var HEARTBEAT_INTERVAL_MS = 2e3;
|
|
848
|
+
var INITIAL_RETRY_MS = 250;
|
|
849
|
+
var MAX_RETRY_MS = 1e4;
|
|
850
|
+
var RETRY_JITTER_MS = 250;
|
|
851
|
+
function computeRetryDelay(attempt, random = Math.random) {
|
|
852
|
+
const exponential = Math.min(INITIAL_RETRY_MS * 2 ** attempt, MAX_RETRY_MS);
|
|
853
|
+
return exponential + Math.floor(random() * RETRY_JITTER_MS);
|
|
854
|
+
}
|
|
855
|
+
function sleepWithAbort(milliseconds, signal) {
|
|
856
|
+
if (signal?.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
857
|
+
return new Promise((resolve, reject) => {
|
|
858
|
+
function abort() {
|
|
859
|
+
clearTimeout(timer);
|
|
860
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
861
|
+
}
|
|
862
|
+
const timer = setTimeout(() => {
|
|
863
|
+
signal?.removeEventListener("abort", abort);
|
|
864
|
+
resolve();
|
|
865
|
+
}, milliseconds);
|
|
866
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
function isAbortError2(error) {
|
|
870
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
871
|
+
}
|
|
872
|
+
function isRetryableIdleError(error) {
|
|
873
|
+
return error instanceof DeviceWorkerRequestError && (error.statusCode === 0 || error.statusCode >= 500);
|
|
874
|
+
}
|
|
875
|
+
function isLeaseConflict(error) {
|
|
876
|
+
return error instanceof DeviceWorkerRequestError && error.statusCode === 409;
|
|
877
|
+
}
|
|
878
|
+
async function executeLease(command, input2, runProcess, sleep) {
|
|
879
|
+
try {
|
|
880
|
+
await input2.client.markRunning(command.id, command.leaseToken);
|
|
881
|
+
} catch (error) {
|
|
882
|
+
if (error instanceof DeviceCredentialRejectedError) throw error;
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
input2.onEvent?.({ type: "running", commandId: command.id, intent: command.intent });
|
|
886
|
+
const commandController = new AbortController();
|
|
887
|
+
const heartbeatController = new AbortController();
|
|
888
|
+
let completionAllowed = true;
|
|
889
|
+
let heartbeatFailure;
|
|
890
|
+
function abortCommand() {
|
|
891
|
+
commandController.abort();
|
|
892
|
+
}
|
|
893
|
+
input2.signal.addEventListener("abort", abortCommand, { once: true });
|
|
894
|
+
const processPromise = runProcess({
|
|
895
|
+
command: command.command,
|
|
896
|
+
cwd: command.cwd,
|
|
897
|
+
timeoutMs: command.timeoutMs,
|
|
898
|
+
signal: commandController.signal
|
|
899
|
+
});
|
|
900
|
+
const heartbeatPromise = (async () => {
|
|
901
|
+
while (!heartbeatController.signal.aborted) {
|
|
902
|
+
try {
|
|
903
|
+
await sleep(HEARTBEAT_INTERVAL_MS, heartbeatController.signal);
|
|
904
|
+
const heartbeat = await input2.client.heartbeat(
|
|
905
|
+
command.id,
|
|
906
|
+
command.leaseToken,
|
|
907
|
+
heartbeatController.signal
|
|
908
|
+
);
|
|
909
|
+
if (heartbeat.cancel) {
|
|
910
|
+
if (heartbeat.reason === "revoked") completionAllowed = false;
|
|
911
|
+
commandController.abort();
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
} catch (error) {
|
|
915
|
+
if (heartbeatController.signal.aborted && isAbortError2(error)) return;
|
|
916
|
+
heartbeatFailure = error;
|
|
917
|
+
completionAllowed = false;
|
|
918
|
+
commandController.abort();
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
})();
|
|
923
|
+
let result;
|
|
924
|
+
try {
|
|
925
|
+
result = await processPromise;
|
|
926
|
+
} catch (error) {
|
|
927
|
+
result = {
|
|
928
|
+
stdout: "",
|
|
929
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
930
|
+
exitCode: 1,
|
|
931
|
+
success: false,
|
|
932
|
+
errorCode: "execution_failed"
|
|
933
|
+
};
|
|
934
|
+
} finally {
|
|
935
|
+
heartbeatController.abort();
|
|
936
|
+
await heartbeatPromise;
|
|
937
|
+
input2.signal.removeEventListener("abort", abortCommand);
|
|
938
|
+
}
|
|
939
|
+
if (heartbeatFailure instanceof DeviceCredentialRejectedError) throw heartbeatFailure;
|
|
940
|
+
if (!completionAllowed || isLeaseConflict(heartbeatFailure)) return;
|
|
941
|
+
try {
|
|
942
|
+
await input2.client.output(command.id, command.leaseToken, result.stdout, result.stderr);
|
|
943
|
+
await input2.client.complete(command.id, command.leaseToken, result);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
if (error instanceof DeviceCredentialRejectedError) throw error;
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
input2.onEvent?.({ type: "completed", commandId: command.id, exitCode: result.exitCode });
|
|
949
|
+
}
|
|
950
|
+
async function runDeviceWorker(input2) {
|
|
951
|
+
const runProcess = input2.runProcess ?? runDeviceProcess;
|
|
952
|
+
const sleep = input2.sleep ?? sleepWithAbort;
|
|
953
|
+
const random = input2.random ?? Math.random;
|
|
954
|
+
let retryAttempt = 0;
|
|
955
|
+
while (!input2.signal.aborted) {
|
|
956
|
+
let command;
|
|
957
|
+
try {
|
|
958
|
+
command = await input2.client.lease(input2.signal);
|
|
959
|
+
retryAttempt = 0;
|
|
960
|
+
} catch (error) {
|
|
961
|
+
if (input2.signal.aborted) return;
|
|
962
|
+
if (error instanceof DeviceCredentialRejectedError) throw error;
|
|
963
|
+
if (!isRetryableIdleError(error)) throw error;
|
|
964
|
+
try {
|
|
965
|
+
await sleep(computeRetryDelay(retryAttempt, random), input2.signal);
|
|
966
|
+
} catch (sleepError) {
|
|
967
|
+
if (input2.signal.aborted && isAbortError2(sleepError)) return;
|
|
968
|
+
throw sleepError;
|
|
969
|
+
}
|
|
970
|
+
retryAttempt += 1;
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (!command) continue;
|
|
974
|
+
await executeLease(command, input2, runProcess, sleep);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/commands/device.ts
|
|
979
|
+
var NAMED_PROFILE_REQUIRED = "Device pairing requires a named Standards profile";
|
|
980
|
+
var SUPPORTED_PLATFORMS_REQUIRED = "Device workers support macOS and Linux only";
|
|
981
|
+
var DeviceCommandError = class extends SchemaError2 {
|
|
982
|
+
constructor(message) {
|
|
983
|
+
super(message, SchemaErrorCode2.VALIDATION_FAILED);
|
|
984
|
+
this.name = "DeviceCommandError";
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
function requireSupportedPlatform(platform) {
|
|
988
|
+
if (platform === "darwin" || platform === "linux") return platform;
|
|
989
|
+
throw new DeviceCommandError(SUPPORTED_PLATFORMS_REQUIRED);
|
|
990
|
+
}
|
|
991
|
+
function requireNamedProfile(command) {
|
|
992
|
+
const options = getGlobalOptions(command);
|
|
993
|
+
if (!(options.profileName && options.apiUrl && options.apiKey)) {
|
|
994
|
+
throw new DeviceCommandError(NAMED_PROFILE_REQUIRED);
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
apiUrl: options.apiUrl,
|
|
998
|
+
apiKey: options.apiKey,
|
|
999
|
+
profileName: options.profileName,
|
|
1000
|
+
tenantId: options.tenant
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function isRecord2(value) {
|
|
1004
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1005
|
+
}
|
|
1006
|
+
function parseDeviceView(value) {
|
|
1007
|
+
if (!isRecord2(value)) throw new DeviceCommandError("Invalid device response");
|
|
1008
|
+
const { id, name, platform, arch, hostname: deviceHostname } = value;
|
|
1009
|
+
const { status } = value;
|
|
1010
|
+
if (typeof id !== "string" || typeof name !== "string" || platform !== "darwin" && platform !== "linux" || typeof arch !== "string" || typeof deviceHostname !== "string" || status !== "online" && status !== "offline" && status !== "revoked") {
|
|
1011
|
+
throw new DeviceCommandError("Invalid device response");
|
|
1012
|
+
}
|
|
1013
|
+
return { id, name, platform, arch, hostname: deviceHostname, status };
|
|
1014
|
+
}
|
|
1015
|
+
function parsePairDeviceResponse(value) {
|
|
1016
|
+
if (!isRecord2(value) || typeof value.token !== "string") {
|
|
1017
|
+
throw new DeviceCommandError("Invalid device pairing response");
|
|
1018
|
+
}
|
|
1019
|
+
return { device: parseDeviceView(value.device), token: value.token };
|
|
1020
|
+
}
|
|
1021
|
+
function parseDeviceList(value) {
|
|
1022
|
+
if (!Array.isArray(value)) throw new DeviceCommandError("Invalid device list response");
|
|
1023
|
+
return value.map(parseDeviceView);
|
|
1024
|
+
}
|
|
1025
|
+
async function pairDevice(name, command) {
|
|
1026
|
+
const platform = requireSupportedPlatform(process.platform);
|
|
1027
|
+
const profile = requireNamedProfile(command);
|
|
1028
|
+
const installationId = await getOrCreateInstallationId();
|
|
1029
|
+
const client = createClient({
|
|
1030
|
+
apiUrl: profile.apiUrl,
|
|
1031
|
+
apiKey: profile.apiKey,
|
|
1032
|
+
tenantId: profile.tenantId
|
|
1033
|
+
});
|
|
1034
|
+
const response = parsePairDeviceResponse(
|
|
1035
|
+
await client.post("/devices/pair", {
|
|
1036
|
+
installationId,
|
|
1037
|
+
name,
|
|
1038
|
+
platform,
|
|
1039
|
+
arch: process.arch,
|
|
1040
|
+
hostname: hostname()
|
|
1041
|
+
})
|
|
1042
|
+
);
|
|
1043
|
+
const stored = await storeDeviceCredential(profile.profileName, {
|
|
1044
|
+
id: response.device.id,
|
|
1045
|
+
name: response.device.name,
|
|
1046
|
+
token: response.token
|
|
1047
|
+
});
|
|
1048
|
+
if (!stored) throw new DeviceCommandError(NAMED_PROFILE_REQUIRED);
|
|
1049
|
+
process.stdout.write(`Paired ${response.device.name} (${response.device.id})
|
|
1050
|
+
`);
|
|
1051
|
+
}
|
|
1052
|
+
async function showDeviceStatus(command) {
|
|
1053
|
+
const profile = requireNamedProfile(command);
|
|
1054
|
+
const credential = await getStoredDeviceCredential(profile.profileName);
|
|
1055
|
+
if (!credential) {
|
|
1056
|
+
throw new DeviceCommandError("No paired device; run standards device pair first");
|
|
1057
|
+
}
|
|
1058
|
+
const client = createClient({
|
|
1059
|
+
apiUrl: profile.apiUrl,
|
|
1060
|
+
apiKey: profile.apiKey,
|
|
1061
|
+
tenantId: profile.tenantId
|
|
1062
|
+
});
|
|
1063
|
+
const devices = parseDeviceList(await client.get("/devices"));
|
|
1064
|
+
const pairedDevice = devices.find((device) => device.id === credential.id);
|
|
1065
|
+
formatOutput(
|
|
1066
|
+
pairedDevice ? [pairedDevice] : [{ id: credential.id, name: credential.name, status: "revoked" }],
|
|
1067
|
+
getFormat(command)
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
async function revokeDevice(command) {
|
|
1071
|
+
const profile = requireNamedProfile(command);
|
|
1072
|
+
const credential = await getStoredDeviceCredential(profile.profileName);
|
|
1073
|
+
if (!credential) {
|
|
1074
|
+
throw new DeviceCommandError("No paired device; run standards device pair first");
|
|
1075
|
+
}
|
|
1076
|
+
const client = createClient({
|
|
1077
|
+
apiUrl: profile.apiUrl,
|
|
1078
|
+
apiKey: profile.apiKey,
|
|
1079
|
+
tenantId: profile.tenantId
|
|
1080
|
+
});
|
|
1081
|
+
await client.delete(`/devices/${credential.id}`);
|
|
1082
|
+
await removeDeviceCredential(profile.profileName);
|
|
1083
|
+
}
|
|
1084
|
+
async function serveDevice(command) {
|
|
1085
|
+
const platform = requireSupportedPlatform(process.platform);
|
|
1086
|
+
const profile = requireNamedProfile(command);
|
|
1087
|
+
const credential = await getStoredDeviceCredential(profile.profileName);
|
|
1088
|
+
if (!credential) {
|
|
1089
|
+
throw new DeviceCommandError("No paired device; run standards device pair first");
|
|
1090
|
+
}
|
|
1091
|
+
const userClient = createClient({
|
|
1092
|
+
apiUrl: profile.apiUrl,
|
|
1093
|
+
apiKey: profile.apiKey,
|
|
1094
|
+
tenantId: profile.tenantId
|
|
1095
|
+
});
|
|
1096
|
+
const devices = parseDeviceList(await userClient.get("/devices"));
|
|
1097
|
+
const device = devices.find((candidate) => candidate.id === credential.id);
|
|
1098
|
+
if (!device) throw new DeviceCommandError("Device is unavailable");
|
|
1099
|
+
if (device.platform !== platform) {
|
|
1100
|
+
throw new DeviceCommandError("Device platform does not match paired device");
|
|
1101
|
+
}
|
|
1102
|
+
const controller = new AbortController();
|
|
1103
|
+
const stop = () => controller.abort();
|
|
1104
|
+
process.once("SIGINT", stop);
|
|
1105
|
+
process.once("SIGTERM", stop);
|
|
1106
|
+
process.stdout.write(`Connected as ${credential.name}
|
|
1107
|
+
`);
|
|
1108
|
+
process.stdout.write("Waiting for commands \u2014 press Ctrl+C to disconnect\n");
|
|
1109
|
+
try {
|
|
1110
|
+
await runDeviceWorker({
|
|
1111
|
+
client: new DeviceWorkerClient({ apiUrl: profile.apiUrl, token: credential.token }),
|
|
1112
|
+
signal: controller.signal,
|
|
1113
|
+
onEvent: (event) => {
|
|
1114
|
+
if (event.type === "running") {
|
|
1115
|
+
process.stdout.write(`Running ${event.commandId}: ${event.intent}
|
|
1116
|
+
`);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
process.stdout.write(`Completed ${event.commandId} with exit code ${event.exitCode}
|
|
1120
|
+
`);
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
} finally {
|
|
1124
|
+
process.removeListener("SIGINT", stop);
|
|
1125
|
+
process.removeListener("SIGTERM", stop);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function registerDeviceCommand(program) {
|
|
1129
|
+
const device = program.command("device").description("Pair and run this computer as a device");
|
|
1130
|
+
device.command("pair").description("Pair this computer with Standards").requiredOption("--name <name>", "device name").action(
|
|
1131
|
+
async (options, command) => pairDevice(options.name, command)
|
|
1132
|
+
);
|
|
1133
|
+
device.command("status").description("Show the paired device status").action(async (_options, command) => showDeviceStatus(command));
|
|
1134
|
+
device.command("revoke").description("Revoke this computer's device credential").action(async (_options, command) => revokeDevice(command));
|
|
1135
|
+
device.command("serve").description("Run the foreground device worker").action(async (_options, command) => serveDevice(command));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
304
1138
|
// src/commands/documents.ts
|
|
305
1139
|
import { formatByteSize } from "@stndrds/schema";
|
|
306
1140
|
function toFileRow(file) {
|
|
@@ -379,8 +1213,24 @@ function registerDocumentsCommand(program) {
|
|
|
379
1213
|
});
|
|
380
1214
|
}
|
|
381
1215
|
|
|
1216
|
+
// src/commands/embeddings.ts
|
|
1217
|
+
import chalk4 from "chalk";
|
|
1218
|
+
function registerEmbeddingsCommand(program) {
|
|
1219
|
+
const embeddings = program.command("embeddings").description("Maintain record embeddings");
|
|
1220
|
+
embeddings.command("drain").description("Embed every pending memory and skill record of the tenant").action(async (_opts, cmd) => {
|
|
1221
|
+
const tenant = requireTenant(
|
|
1222
|
+
cmd,
|
|
1223
|
+
'An embedding drain names its tenant explicitly. Pass "--tenant <id>".'
|
|
1224
|
+
);
|
|
1225
|
+
const client = getClientFromCommand(cmd);
|
|
1226
|
+
await client.post("/admin/embeddings/drain", { tenantId: tenant });
|
|
1227
|
+
process.stdout.write(`${chalk4.green("\u2713")} Embedding drain accepted for tenant ${tenant}.
|
|
1228
|
+
`);
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
382
1232
|
// src/commands/folders.ts
|
|
383
|
-
import { readFile } from "fs/promises";
|
|
1233
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
384
1234
|
function registerFoldersCommand(program) {
|
|
385
1235
|
const folders = program.command("folders").description("Manage record drive folders");
|
|
386
1236
|
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 +1244,7 @@ function registerFoldersCommand(program) {
|
|
|
394
1244
|
).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
1245
|
const paths = [...opts.path];
|
|
396
1246
|
if (opts.pathsFile) {
|
|
397
|
-
const content = await
|
|
1247
|
+
const content = await readFile2(opts.pathsFile, "utf-8");
|
|
398
1248
|
const filePaths = content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
399
1249
|
paths.push(...filePaths);
|
|
400
1250
|
}
|
|
@@ -412,7 +1262,7 @@ function registerFoldersCommand(program) {
|
|
|
412
1262
|
}
|
|
413
1263
|
|
|
414
1264
|
// src/commands/keys.ts
|
|
415
|
-
import
|
|
1265
|
+
import chalk5 from "chalk";
|
|
416
1266
|
function registerKeysCommand(program) {
|
|
417
1267
|
const keys = program.command("keys").description("Manage Standards API keys");
|
|
418
1268
|
keys.command("list").description("List API keys").action(async (_opts, cmd) => {
|
|
@@ -439,14 +1289,14 @@ function registerKeysCommand(program) {
|
|
|
439
1289
|
}
|
|
440
1290
|
const client = getClientFromCommand(cmd);
|
|
441
1291
|
await client.delete(`/api-keys/${id}`);
|
|
442
|
-
process.stdout.write(`${
|
|
1292
|
+
process.stdout.write(`${chalk5.green("\u2713")} API key ${id} revoked.
|
|
443
1293
|
`);
|
|
444
1294
|
});
|
|
445
1295
|
}
|
|
446
1296
|
|
|
447
1297
|
// src/commands/mcp.ts
|
|
448
1298
|
import { RESOURCE_VISIBILITIES, ValidationError as ValidationError2 } from "@stndrds/schema";
|
|
449
|
-
import
|
|
1299
|
+
import chalk6 from "chalk";
|
|
450
1300
|
var AUTH_TYPES = ["none", "header"];
|
|
451
1301
|
function buildAuth(opts) {
|
|
452
1302
|
const type = opts.authType ?? "none";
|
|
@@ -509,7 +1359,7 @@ function registerMcpCommand(program) {
|
|
|
509
1359
|
const client = getClientFromCommand(cmd);
|
|
510
1360
|
await client.post(`/mcp-servers/${id}/trust`, { trusted });
|
|
511
1361
|
process.stdout.write(
|
|
512
|
-
`${
|
|
1362
|
+
`${chalk6.green("\u2713")} MCP server ${id} ${trusted ? "trusted" : "untrusted"}.
|
|
513
1363
|
`
|
|
514
1364
|
);
|
|
515
1365
|
});
|
|
@@ -522,13 +1372,18 @@ function registerMcpCommand(program) {
|
|
|
522
1372
|
}
|
|
523
1373
|
const client = getClientFromCommand(cmd);
|
|
524
1374
|
await client.delete(`/mcp-servers/${id}`);
|
|
525
|
-
process.stdout.write(`${
|
|
1375
|
+
process.stdout.write(`${chalk6.green("\u2713")} MCP server ${id} disconnected.
|
|
526
1376
|
`);
|
|
527
1377
|
});
|
|
528
1378
|
}
|
|
529
1379
|
|
|
530
1380
|
// src/commands/meetings.ts
|
|
531
|
-
import {
|
|
1381
|
+
import { createHmac } from "crypto";
|
|
1382
|
+
import {
|
|
1383
|
+
FAKE_MEETING_BOT_SIGNATURE_HEADER,
|
|
1384
|
+
RECORDING_OVERRIDES,
|
|
1385
|
+
ValidationError as ValidationError3
|
|
1386
|
+
} from "@stndrds/schema";
|
|
532
1387
|
var ORDERS = ["asc", "desc"];
|
|
533
1388
|
function parseOrder(order) {
|
|
534
1389
|
if (ORDERS.includes(order)) return order;
|
|
@@ -553,6 +1408,43 @@ function buildQuery(opts) {
|
|
|
553
1408
|
if (opts.offset !== void 0) query.offset = parseCount("--offset", opts.offset);
|
|
554
1409
|
return query;
|
|
555
1410
|
}
|
|
1411
|
+
function parseOverride(opts) {
|
|
1412
|
+
const chosen = RECORDING_OVERRIDES.filter(
|
|
1413
|
+
(override) => opts[override]
|
|
1414
|
+
);
|
|
1415
|
+
if (opts.auto) chosen.push(null);
|
|
1416
|
+
if (chosen.length !== 1) {
|
|
1417
|
+
throw new ValidationError3("record needs exactly one of --force, --skip or --auto", []);
|
|
1418
|
+
}
|
|
1419
|
+
return chosen[0] ?? null;
|
|
1420
|
+
}
|
|
1421
|
+
var SIMULATED_LINE_MS = 4e3;
|
|
1422
|
+
var DEFAULT_SIMULATED_LINES = 12;
|
|
1423
|
+
var SIMULATED_SENTENCES = [
|
|
1424
|
+
"Bonjour \xE0 tous, merci d'\xEAtre l\xE0, on commence par le point d'avancement.",
|
|
1425
|
+
"De mon c\xF4t\xE9 la maquette est valid\xE9e, il reste les retours du client.",
|
|
1426
|
+
"On garde le planning initial, la livraison est pr\xE9vue pour vendredi.",
|
|
1427
|
+
"Est-ce que quelqu'un a des questions sur le budget de la phase deux ?",
|
|
1428
|
+
"Je propose qu'on fasse un point rapide jeudi pour v\xE9rifier les tests.",
|
|
1429
|
+
"Tr\xE8s bien, je vous envoie le compte rendu dans la journ\xE9e, bonne journ\xE9e."
|
|
1430
|
+
];
|
|
1431
|
+
function simulatedSpeakers(meeting) {
|
|
1432
|
+
const speakers = meeting.participants.map(
|
|
1433
|
+
(participant) => participant.name?.trim() || participant.address.split("@")[0] || participant.address
|
|
1434
|
+
);
|
|
1435
|
+
return speakers.length > 0 ? speakers : ["Speaker"];
|
|
1436
|
+
}
|
|
1437
|
+
function buildSimulatedSegments(speakers, lines) {
|
|
1438
|
+
return Array.from({ length: lines }, (_, index) => ({
|
|
1439
|
+
speaker: speakers[index % speakers.length] ?? null,
|
|
1440
|
+
startsAtMs: index * SIMULATED_LINE_MS,
|
|
1441
|
+
endsAtMs: (index + 1) * SIMULATED_LINE_MS,
|
|
1442
|
+
text: SIMULATED_SENTENCES[index % SIMULATED_SENTENCES.length] ?? ""
|
|
1443
|
+
}));
|
|
1444
|
+
}
|
|
1445
|
+
function signFakeWebhook(secret, body) {
|
|
1446
|
+
return createHmac("sha256", secret).update(body).digest("hex");
|
|
1447
|
+
}
|
|
556
1448
|
function withQueryOptions(command) {
|
|
557
1449
|
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
1450
|
}
|
|
@@ -589,10 +1481,69 @@ function registerMeetingsCommand(program) {
|
|
|
589
1481
|
formatOutput(result, getFormat(cmd));
|
|
590
1482
|
}
|
|
591
1483
|
);
|
|
1484
|
+
meetings.command("transcript").description("Read a meeting's completed transcript").argument("<id>", "meeting ID").action(async (id, _opts, cmd) => {
|
|
1485
|
+
const client = getClientFromCommand(cmd);
|
|
1486
|
+
const result = await client.get(`/meetings/${id}/transcript`);
|
|
1487
|
+
formatOutput(result, getFormat(cmd));
|
|
1488
|
+
});
|
|
1489
|
+
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) => {
|
|
1490
|
+
const override = parseOverride(opts);
|
|
1491
|
+
const client = getClientFromCommand(cmd);
|
|
1492
|
+
const result = await client.put(`/meetings/${id}/recording`, { override });
|
|
1493
|
+
formatOutput(result, getFormat(cmd));
|
|
1494
|
+
});
|
|
1495
|
+
meetings.command("simulate-transcript").description("Complete a fake-provider bot by posting a generated transcript to its webhook").argument("<id>", "meeting ID").option(
|
|
1496
|
+
"--lines <n>",
|
|
1497
|
+
"number of transcript lines to generate",
|
|
1498
|
+
String(DEFAULT_SIMULATED_LINES)
|
|
1499
|
+
).action(async (id, opts, cmd) => {
|
|
1500
|
+
const secret = process.env.MEETING_BOT_FAKE_SECRET;
|
|
1501
|
+
if (!secret) {
|
|
1502
|
+
throw new ValidationError3("MEETING_BOT_FAKE_SECRET is not set in this shell", []);
|
|
1503
|
+
}
|
|
1504
|
+
const lines = parseCount("--lines", opts.lines);
|
|
1505
|
+
if (lines < 1) throw new ValidationError3("--lines must be at least 1", []);
|
|
1506
|
+
const client = getClientFromCommand(cmd);
|
|
1507
|
+
const meeting = await client.get(`/meetings/${id}`);
|
|
1508
|
+
const { status, botId } = meeting.recording;
|
|
1509
|
+
if (status !== "scheduled" && status !== "recording" || botId === null) {
|
|
1510
|
+
throw new ValidationError3(`Meeting ${id} has no bot to complete (status: ${status})`, []);
|
|
1511
|
+
}
|
|
1512
|
+
const segments = buildSimulatedSegments(simulatedSpeakers(meeting), lines);
|
|
1513
|
+
const startedAt = meeting.startsAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1514
|
+
const endedAt = new Date(
|
|
1515
|
+
new Date(startedAt).getTime() + lines * SIMULATED_LINE_MS
|
|
1516
|
+
).toISOString();
|
|
1517
|
+
const body = JSON.stringify({
|
|
1518
|
+
botId,
|
|
1519
|
+
transcript: {
|
|
1520
|
+
fullText: segments.map((segment) => segment.text).join("\n"),
|
|
1521
|
+
segments,
|
|
1522
|
+
language: "fr",
|
|
1523
|
+
startedAt,
|
|
1524
|
+
endedAt
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
const response = await fetch(
|
|
1528
|
+
`${getGlobalOptions(cmd).apiUrl}/meetings/transcripts/webhook/fake`,
|
|
1529
|
+
{
|
|
1530
|
+
method: "POST",
|
|
1531
|
+
headers: {
|
|
1532
|
+
"content-type": "application/json",
|
|
1533
|
+
[FAKE_MEETING_BOT_SIGNATURE_HEADER]: signFakeWebhook(secret, body)
|
|
1534
|
+
},
|
|
1535
|
+
body
|
|
1536
|
+
}
|
|
1537
|
+
);
|
|
1538
|
+
if (!response.ok) {
|
|
1539
|
+
throw new ValidationError3(`webhook answered ${response.status}`, []);
|
|
1540
|
+
}
|
|
1541
|
+
formatOutput({ botId, segments: segments.length }, getFormat(cmd));
|
|
1542
|
+
});
|
|
592
1543
|
}
|
|
593
1544
|
|
|
594
1545
|
// src/commands/pull.ts
|
|
595
|
-
import
|
|
1546
|
+
import chalk7 from "chalk";
|
|
596
1547
|
|
|
597
1548
|
// src/drift/reconciliation-prompt.ts
|
|
598
1549
|
var PREAMBLE = [
|
|
@@ -711,7 +1662,7 @@ function registerPullCommand(program) {
|
|
|
711
1662
|
try {
|
|
712
1663
|
client = getClientFromCommand(cmd);
|
|
713
1664
|
} catch (error) {
|
|
714
|
-
console.error(
|
|
1665
|
+
console.error(chalk7.red(`\u2717 ${messageOf(error)}`));
|
|
715
1666
|
process.exit(2);
|
|
716
1667
|
return;
|
|
717
1668
|
}
|
|
@@ -721,14 +1672,14 @@ function registerPullCommand(program) {
|
|
|
721
1672
|
console.info(JSON.stringify(state, null, 2));
|
|
722
1673
|
process.exit(0);
|
|
723
1674
|
} catch (error) {
|
|
724
|
-
console.error(
|
|
1675
|
+
console.error(chalk7.red(`\u2717 ${messageOf(error)}`));
|
|
725
1676
|
process.exit(2);
|
|
726
1677
|
}
|
|
727
1678
|
return;
|
|
728
1679
|
}
|
|
729
1680
|
const result = await runPullCommand(client);
|
|
730
1681
|
if (result.exitCode === 2) {
|
|
731
|
-
console.error(
|
|
1682
|
+
console.error(chalk7.red(`\u2717 ${result.errorMessage}`));
|
|
732
1683
|
process.exit(2);
|
|
733
1684
|
return;
|
|
734
1685
|
}
|
|
@@ -738,7 +1689,7 @@ function registerPullCommand(program) {
|
|
|
738
1689
|
}
|
|
739
1690
|
|
|
740
1691
|
// src/commands/records.ts
|
|
741
|
-
import { readFile as
|
|
1692
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
742
1693
|
import { basename } from "path";
|
|
743
1694
|
function parseSortFlag(sort) {
|
|
744
1695
|
const [attribute, direction = "asc"] = sort.split(":");
|
|
@@ -804,7 +1755,7 @@ function registerRecordsCommand(program) {
|
|
|
804
1755
|
formatOutput(result, getFormat(cmd));
|
|
805
1756
|
});
|
|
806
1757
|
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
|
|
1758
|
+
const fileContent = await readFile3(opts.file);
|
|
808
1759
|
const fileName = basename(opts.file);
|
|
809
1760
|
const title = opts.title ?? fileName;
|
|
810
1761
|
const form = new FormData();
|
|
@@ -824,129 +1775,7 @@ function registerRecordsCommand(program) {
|
|
|
824
1775
|
// src/commands/root.ts
|
|
825
1776
|
import { stdin as input, stdout as output } from "process";
|
|
826
1777
|
import { createInterface } from "readline/promises";
|
|
827
|
-
import
|
|
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
|
|
1778
|
+
import chalk8 from "chalk";
|
|
950
1779
|
async function promptForMissingValue(label) {
|
|
951
1780
|
const rl = createInterface({ input, output });
|
|
952
1781
|
try {
|
|
@@ -964,7 +1793,7 @@ function registerRootCommands(program) {
|
|
|
964
1793
|
await client.get("/api-keys");
|
|
965
1794
|
await upsertProfile({ name: opts.name, apiUrl: opts.url, apiKey });
|
|
966
1795
|
process.stdout.write(
|
|
967
|
-
`${
|
|
1796
|
+
`${chalk8.green("\u2713")} Standards instance "${opts.name}" saved and selected.
|
|
968
1797
|
`
|
|
969
1798
|
);
|
|
970
1799
|
process.stdout.write(` API URL: ${opts.url}
|
|
@@ -976,7 +1805,7 @@ function registerRootCommands(program) {
|
|
|
976
1805
|
});
|
|
977
1806
|
program.command("use").description("Select the active Standards instance").argument("<name>", "instance name").action(async (name) => {
|
|
978
1807
|
await setCurrentProfile(name);
|
|
979
|
-
process.stdout.write(`${
|
|
1808
|
+
process.stdout.write(`${chalk8.green("\u2713")} Standards instance "${name}" selected.
|
|
980
1809
|
`);
|
|
981
1810
|
});
|
|
982
1811
|
program.command("instances").description("List configured Standards instances").action(async (_opts, cmd) => {
|
|
@@ -995,7 +1824,7 @@ function registerRootCommands(program) {
|
|
|
995
1824
|
});
|
|
996
1825
|
program.command("logout").description("Remove a Standards instance from local CLI config").argument("[name]", "instance name, defaults to current").action(async (name) => {
|
|
997
1826
|
await removeProfile(name);
|
|
998
|
-
process.stdout.write(`${
|
|
1827
|
+
process.stdout.write(`${chalk8.green("\u2713")} Standards instance removed.
|
|
999
1828
|
`);
|
|
1000
1829
|
});
|
|
1001
1830
|
}
|
|
@@ -1016,7 +1845,7 @@ function registerSchemaCommand(program) {
|
|
|
1016
1845
|
}
|
|
1017
1846
|
|
|
1018
1847
|
// src/commands/search.ts
|
|
1019
|
-
import
|
|
1848
|
+
import chalk9 from "chalk";
|
|
1020
1849
|
function registerSearchCommand(program) {
|
|
1021
1850
|
const search = program.command("search").description("Maintain the Standards search index");
|
|
1022
1851
|
search.command("reindex").description("Clear the tenant search index and re-inject every record").option("--yes", "reindex without confirmation").action(async (opts, cmd) => {
|
|
@@ -1025,16 +1854,14 @@ function registerSearchCommand(program) {
|
|
|
1025
1854
|
'A full reindex clears the index before refilling it. Re-run with "--yes" to confirm.'
|
|
1026
1855
|
);
|
|
1027
1856
|
}
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
);
|
|
1033
|
-
}
|
|
1857
|
+
const tenant = requireTenant(
|
|
1858
|
+
cmd,
|
|
1859
|
+
'A full reindex names its tenant explicitly. Pass "--tenant <id>" with the tenant you are authenticated in.'
|
|
1860
|
+
);
|
|
1034
1861
|
const client = getClientFromCommand(cmd);
|
|
1035
1862
|
await client.post("/admin/search/full-reindex", { tenantId: tenant });
|
|
1036
1863
|
process.stdout.write(
|
|
1037
|
-
`${
|
|
1864
|
+
`${chalk9.green("\u2713")} Full reindex accepted for tenant ${tenant}. It runs in the background \u2014 follow the server logs for progress.
|
|
1038
1865
|
`
|
|
1039
1866
|
);
|
|
1040
1867
|
});
|
|
@@ -1054,6 +1881,7 @@ function createProgram() {
|
|
|
1054
1881
|
registerSchemaCommand(program);
|
|
1055
1882
|
registerPullCommand(program);
|
|
1056
1883
|
registerDocumentsCommand(program);
|
|
1884
|
+
registerDeviceCommand(program);
|
|
1057
1885
|
registerFoldersCommand(program);
|
|
1058
1886
|
registerKeysCommand(program);
|
|
1059
1887
|
registerAuthCommand(program);
|
|
@@ -1062,6 +1890,7 @@ function createProgram() {
|
|
|
1062
1890
|
registerMcpCommand(program);
|
|
1063
1891
|
registerMeetingsCommand(program);
|
|
1064
1892
|
registerSearchCommand(program);
|
|
1893
|
+
registerEmbeddingsCommand(program);
|
|
1065
1894
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
1066
1895
|
const raw = program.opts();
|
|
1067
1896
|
const resolved = await resolveCliConfig({
|
|
@@ -1075,7 +1904,7 @@ function createProgram() {
|
|
|
1075
1904
|
program.setOptionValue("tenant", raw.tenant);
|
|
1076
1905
|
if (!(resolved.apiKey || isPublicCommand(actionCommand))) {
|
|
1077
1906
|
console.error(
|
|
1078
|
-
|
|
1907
|
+
chalk10.red(
|
|
1079
1908
|
`\u2717 Error: No Standards instance configured. Run "standards login" or pass --api-key.`
|
|
1080
1909
|
)
|
|
1081
1910
|
);
|
|
@@ -1089,12 +1918,12 @@ async function runProgram(argv = process.argv) {
|
|
|
1089
1918
|
await program.parseAsync(argv).catch((error) => {
|
|
1090
1919
|
if (error instanceof ApiClientError) {
|
|
1091
1920
|
if (error.statusCode > 0) {
|
|
1092
|
-
console.error(
|
|
1921
|
+
console.error(chalk10.red(`\u2717 Error (${error.statusCode}): ${error.message}`));
|
|
1093
1922
|
} else {
|
|
1094
|
-
console.error(
|
|
1923
|
+
console.error(chalk10.red(`\u2717 Error: ${error.message}`));
|
|
1095
1924
|
}
|
|
1096
1925
|
} else if (error instanceof Error) {
|
|
1097
|
-
console.error(
|
|
1926
|
+
console.error(chalk10.red(`\u2717 Error: ${error.message}`));
|
|
1098
1927
|
}
|
|
1099
1928
|
process.exit(1);
|
|
1100
1929
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stndrds/cli",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.294",
|
|
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.
|
|
16
|
+
"@stndrds/schema": "1.0.0-alpha.294"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "^25.6.0",
|