@awak-app/simy-cli 0.2.3 → 0.4.0
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 +145 -1
- package/package.json +14 -5
- package/src/agent.js +740 -37
- package/src/bounded-local-task-subtask-pool.js +708 -0
- package/src/bounded-local-task-subtasks-contract.js +1189 -0
- package/src/cli-contract.js +42 -3
- package/src/console/app.js +212 -90
- package/src/desktop-executor.js +21 -2
- package/src/durable-local-task-steps-contract.js +1256 -0
- package/src/durable-local-task-worker.js +2607 -0
- package/src/execution-capability-contract.js +116 -0
- package/src/execution-guardrail.js +69 -12
- package/src/index.js +22 -7
- package/src/local-attachments.js +94 -113
- package/src/local-task-artifact-contract.js +266 -0
- package/src/local-task-attachment-store.js +447 -0
- package/src/local-task-file-capabilities.js +738 -0
- package/src/local-task-scenario-packs.js +681 -0
- package/src/local-task.js +1137 -0
- package/src/orchestrator/audit.js +42 -1
- package/src/orchestrator/loop.js +29 -3
- package/src/repository-inventory.js +37 -1
- package/src/runner.js +44 -0
- package/src/shutdown.js +63 -0
- package/src/sqm/bundle-store.js +249 -0
- package/src/sqm/canonical.js +45 -0
- package/src/sqm/checkers.js +299 -0
- package/src/sqm/command.js +149 -0
- package/src/sqm/evidence-client.js +12 -0
- package/src/sqm/index.js +141 -0
- package/src/sqm/proof.js +154 -0
- package/src/sqm/repository.js +163 -0
- package/src/sqm/session.js +58 -0
- package/src/sqm/validation.js +305 -0
- package/src/workspace-context.js +40 -7
package/src/sqm/proof.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign, verify } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { canonicalize } from "./canonical.js";
|
|
7
|
+
|
|
8
|
+
export function repositoryIdentity(repository) {
|
|
9
|
+
return {
|
|
10
|
+
repository: repository.repository,
|
|
11
|
+
base_ref: repository.base_ref,
|
|
12
|
+
base_commit: repository.base_commit ?? null,
|
|
13
|
+
head_commit: repository.head_commit ?? null,
|
|
14
|
+
committed_diff_digest: repository.committed_diff_digest ?? null,
|
|
15
|
+
working_tree_digest: repository.working_tree_digest ?? null,
|
|
16
|
+
diff_digest: repository.diff_digest ?? null,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sameRepositoryIdentity(left, right) {
|
|
21
|
+
return ["repository", "base_ref", "base_commit", "head_commit", "committed_diff_digest", "working_tree_digest", "diff_digest"]
|
|
22
|
+
.every((key) => left?.[key] === right?.[key]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function finalizeMinCheck(value) {
|
|
26
|
+
const result = structuredClone(value);
|
|
27
|
+
delete result.result_digest;
|
|
28
|
+
return { ...result, result_digest: taggedDigest(result) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function verifyMinCheck(value) {
|
|
32
|
+
if (!value || value.schema_version !== "1.0.0" || value.kind !== "sqm_min_check") throw new Error("SQM min-check result is invalid.");
|
|
33
|
+
const copy = structuredClone(value);
|
|
34
|
+
const claimed = copy.result_digest;
|
|
35
|
+
delete copy.result_digest;
|
|
36
|
+
if (claimed !== taggedDigest(copy)) throw new Error("SQM min-check result digest does not match its content.");
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildProof({ organizationId, repository, bundle, minCheck, evaluation, findings, cliVersion, executedAt = new Date().toISOString() }) {
|
|
41
|
+
const statuses = evaluation.modules.flatMap((module) => module.scenarios.map((scenario) => scenario.status));
|
|
42
|
+
const failedModules = evaluation.modules.filter((module) => module.scenarios.some((scenario) => scenario.status === "failed"));
|
|
43
|
+
const hasFailure = findings.length > 0 || failedModules.length > 0;
|
|
44
|
+
const shadowOnly = hasFailure && findings.every((finding) => finding.lifecycle === "shadow") && failedModules.every((module) => module.lifecycle === "shadow");
|
|
45
|
+
const requiresHuman = statuses.includes("requires_human");
|
|
46
|
+
const requiresPostDeploy = statuses.includes("requires_post_deploy");
|
|
47
|
+
const status = shadowOnly ? "shadow" : hasFailure ? "failed" : requiresHuman ? "requires_human" : requiresPostDeploy ? "requires_post_deploy" : evaluation.modules.length === 0 ? "skipped" : "passed";
|
|
48
|
+
const proof = {
|
|
49
|
+
schema_version: "1.0.0",
|
|
50
|
+
proof_id: `SQM-PROOF-${randomUUID()}`,
|
|
51
|
+
organization_id: organizationId,
|
|
52
|
+
repository_identity: repositoryIdentity(repository),
|
|
53
|
+
knowledge_bundle: {
|
|
54
|
+
id: bundle.bundle_id,
|
|
55
|
+
version: bundle.version,
|
|
56
|
+
digest: bundle.integrity.digest,
|
|
57
|
+
signing_key_id: bundle.integrity.key_id,
|
|
58
|
+
},
|
|
59
|
+
min_check: {
|
|
60
|
+
result_digest: minCheck.result_digest,
|
|
61
|
+
executed_at: minCheck.executed_at,
|
|
62
|
+
status: minCheck.status,
|
|
63
|
+
preliminary_finding_count: minCheck.preliminary_findings.length,
|
|
64
|
+
},
|
|
65
|
+
executions: evaluation.modules,
|
|
66
|
+
findings,
|
|
67
|
+
outcome: {
|
|
68
|
+
status,
|
|
69
|
+
reasons: [
|
|
70
|
+
...(hasFailure && !shadowOnly ? ["one_or_more_declarative_checks_failed"] : []),
|
|
71
|
+
...(shadowOnly ? ["declarative_checks_failed_in_shadow_mode"] : []),
|
|
72
|
+
...(evaluation.modules.length === 0 ? ["no_applicable_knowledge_module"] : []),
|
|
73
|
+
...(requiresHuman ? ["human_evidence_required"] : []),
|
|
74
|
+
...(requiresPostDeploy ? ["post_deploy_evidence_required"] : []),
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
provenance: { cli_version: cliVersion, executed_at: executedAt, executor: "simy-cli" },
|
|
78
|
+
};
|
|
79
|
+
return { ...proof, integrity: { algorithm: "sha256", digest: taggedDigest(proof) } };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function verifyProof(proof) {
|
|
83
|
+
const copy = structuredClone(proof);
|
|
84
|
+
const integrity = copy.integrity;
|
|
85
|
+
delete copy.integrity;
|
|
86
|
+
if (integrity?.algorithm !== "sha256" || integrity.digest !== taggedDigest(copy)) throw new Error("SQM proof digest verification failed.");
|
|
87
|
+
return integrity.digest;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function signProof(proof, {
|
|
91
|
+
deviceId,
|
|
92
|
+
accountId = null,
|
|
93
|
+
keyPath = process.env.SIMY_SQM_SIGNING_KEY_PATH || path.join(process.env.SIMY_HOME?.trim() || path.join(homedir(), ".simy"), "sqm", "run-signing-key.json"),
|
|
94
|
+
issuedAt = new Date().toISOString(),
|
|
95
|
+
} = {}) {
|
|
96
|
+
if (!deviceId) throw new Error("A trusted SIMY CLI device identity is required to sign SQM proof.");
|
|
97
|
+
const identity = await loadOrCreateSigningIdentity(keyPath);
|
|
98
|
+
const proofDigest = verifyProof(proof);
|
|
99
|
+
const signature = sign(null, Buffer.from(proofDigest.slice("sha256:".length), "hex"), identity.privateKey).toString("base64");
|
|
100
|
+
return {
|
|
101
|
+
schema_version: "1.0.0",
|
|
102
|
+
evidence_id: `SQM-EVIDENCE-${randomUUID()}`,
|
|
103
|
+
proof_id: proof.proof_id,
|
|
104
|
+
organization_id: proof.organization_id,
|
|
105
|
+
repository: proof.repository_identity.repository,
|
|
106
|
+
proof_digest: proofDigest,
|
|
107
|
+
signer: {
|
|
108
|
+
type: "simy_cli_device",
|
|
109
|
+
device_id: deviceId,
|
|
110
|
+
account_id: accountId,
|
|
111
|
+
key_id: identity.keyId,
|
|
112
|
+
public_key: identity.publicKeyDer.toString("base64"),
|
|
113
|
+
},
|
|
114
|
+
issued_at: issuedAt,
|
|
115
|
+
integrity: { algorithm: "ed25519", signature },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function verifySignedEvidence(proof, evidence) {
|
|
120
|
+
const proofDigest = verifyProof(proof);
|
|
121
|
+
if (evidence?.proof_id !== proof.proof_id || evidence?.proof_digest !== proofDigest || evidence?.organization_id !== proof.organization_id || evidence?.repository !== proof.repository_identity.repository) throw new Error("SQM signed evidence is not bound to this proof.");
|
|
122
|
+
if (evidence?.integrity?.algorithm !== "ed25519") throw new Error("SQM signed evidence uses an unsupported algorithm.");
|
|
123
|
+
const publicKeyDer = Buffer.from(String(evidence?.signer?.public_key || ""), "base64");
|
|
124
|
+
const keyId = `sha256:${createHash("sha256").update(publicKeyDer).digest("hex")}`;
|
|
125
|
+
if (evidence?.signer?.key_id !== keyId) throw new Error("SQM signed evidence key ID does not match its public key.");
|
|
126
|
+
const publicKey = createPublicKey({ key: publicKeyDer, format: "der", type: "spki" });
|
|
127
|
+
const signature = Buffer.from(String(evidence?.integrity?.signature || ""), "base64");
|
|
128
|
+
if (signature.length !== 64 || !verify(null, Buffer.from(proofDigest.slice("sha256:".length), "hex"), publicKey, signature)) throw new Error("SQM per-run signature verification failed.");
|
|
129
|
+
return { proof_digest: proofDigest, key_id: keyId, valid: true };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function loadOrCreateSigningIdentity(location) {
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(await readFile(location, "utf8"));
|
|
135
|
+
const privateKey = createPrivateKey({ key: Buffer.from(parsed.private_key, "base64"), format: "der", type: "pkcs8" });
|
|
136
|
+
const publicKeyDer = Buffer.from(parsed.public_key, "base64");
|
|
137
|
+
const keyId = `sha256:${createHash("sha256").update(publicKeyDer).digest("hex")}`;
|
|
138
|
+
if (parsed.key_id !== keyId) throw new Error("stored key ID mismatch");
|
|
139
|
+
return { privateKey, publicKeyDer, keyId };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error?.code !== "ENOENT") throw new Error(`SQM signing identity is invalid: ${error.message}`);
|
|
142
|
+
}
|
|
143
|
+
const pair = generateKeyPairSync("ed25519");
|
|
144
|
+
const privateKeyDer = pair.privateKey.export({ format: "der", type: "pkcs8" });
|
|
145
|
+
const publicKeyDer = pair.publicKey.export({ format: "der", type: "spki" });
|
|
146
|
+
const keyId = `sha256:${createHash("sha256").update(publicKeyDer).digest("hex")}`;
|
|
147
|
+
await mkdir(path.dirname(location), { recursive: true, mode: 0o700 });
|
|
148
|
+
await writeFile(location, `${JSON.stringify({ algorithm: "ed25519", key_id: keyId, private_key: privateKeyDer.toString("base64"), public_key: publicKeyDer.toString("base64"), created_at: new Date().toISOString() }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
149
|
+
return { privateKey: pair.privateKey, publicKeyDer, keyId };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function taggedDigest(value) {
|
|
153
|
+
return `sha256:${createHash("sha256").update(canonicalize(value), "utf8").digest("hex")}`;
|
|
154
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
import { normalizeGitHubRemote } from "../workspace-context.js";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const MAX_UNTRACKED_FILE_BYTES = 1024 * 1024;
|
|
11
|
+
const MAX_UNTRACKED_TOTAL_BYTES = 10 * 1024 * 1024;
|
|
12
|
+
const MAX_UNTRACKED_FILES = 1_000;
|
|
13
|
+
const MAX_UNTRACKED_LINES = 100_000;
|
|
14
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
15
|
+
|
|
16
|
+
export async function inspectRepository(cwd, { baseBranch = "dev", runCommand = execFileAsync } = {}) {
|
|
17
|
+
const root = await git(runCommand, ["rev-parse", "--show-toplevel"], cwd);
|
|
18
|
+
const remote = await git(runCommand, ["remote", "get-url", "origin"], root);
|
|
19
|
+
const repository = normalizeGitHubRemote(remote);
|
|
20
|
+
if (!repository) throw new Error("SQM requires a GitHub origin remote in owner/repository form.");
|
|
21
|
+
const baseRef = await resolveBaseRef(runCommand, root, baseBranch);
|
|
22
|
+
const baseCommit = await git(runCommand, ["merge-base", baseRef, "HEAD"], root);
|
|
23
|
+
const headCommit = await git(runCommand, ["rev-parse", "HEAD"], root);
|
|
24
|
+
const committed = await git(runCommand, ["diff", "--unified=0", "--no-color", `${baseRef}...HEAD`], root);
|
|
25
|
+
const working = await git(runCommand, ["diff", "--unified=0", "--no-color", "HEAD"], root);
|
|
26
|
+
const untracked = zeroSeparated(
|
|
27
|
+
await git(runCommand, ["ls-files", "-z", "--others", "--exclude-standard"], root),
|
|
28
|
+
);
|
|
29
|
+
const files = zeroSeparated(
|
|
30
|
+
await git(
|
|
31
|
+
runCommand,
|
|
32
|
+
["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
|
|
33
|
+
root,
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
const trackedDiff = parseUnifiedDiff(`${committed}\n${working}`);
|
|
37
|
+
const untrackedDiff = await untrackedAddedDiff(root, untracked);
|
|
38
|
+
const diff = [...trackedDiff, ...untrackedDiff];
|
|
39
|
+
const workingTreeDigest = digest({ working, untracked: untrackedDiff });
|
|
40
|
+
return {
|
|
41
|
+
root,
|
|
42
|
+
repository,
|
|
43
|
+
base_ref: baseRef,
|
|
44
|
+
base_commit: baseCommit,
|
|
45
|
+
head_commit: headCommit,
|
|
46
|
+
committed_diff_digest: digest(committed),
|
|
47
|
+
working_tree_digest: workingTreeDigest,
|
|
48
|
+
diff_digest: digest(diff),
|
|
49
|
+
diff,
|
|
50
|
+
paths: [...new Set(files)],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function digest(value) {
|
|
55
|
+
const encoded = typeof value === "string" ? value : JSON.stringify(value);
|
|
56
|
+
return `sha256:${createHash("sha256").update(encoded).digest("hex")}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function untrackedAddedDiff(root, files) {
|
|
60
|
+
const result = [];
|
|
61
|
+
let totalBytes = 0;
|
|
62
|
+
for (const relative of files.slice(0, MAX_UNTRACKED_FILES)) {
|
|
63
|
+
const resolved = pathInsideRoot(root, relative);
|
|
64
|
+
if (!resolved) continue;
|
|
65
|
+
let fileStat;
|
|
66
|
+
try {
|
|
67
|
+
fileStat = await lstat(resolved);
|
|
68
|
+
} catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
!fileStat.isFile() ||
|
|
73
|
+
fileStat.isSymbolicLink() ||
|
|
74
|
+
fileStat.size > MAX_UNTRACKED_FILE_BYTES ||
|
|
75
|
+
totalBytes + fileStat.size > MAX_UNTRACKED_TOTAL_BYTES
|
|
76
|
+
) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
let buffer;
|
|
80
|
+
let content;
|
|
81
|
+
try {
|
|
82
|
+
buffer = await readFile(resolved);
|
|
83
|
+
if (buffer.includes(0)) continue;
|
|
84
|
+
content = UTF8_DECODER.decode(buffer);
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const textLines = content.split(/\r?\n/);
|
|
89
|
+
if (textLines.length > MAX_UNTRACKED_LINES) continue;
|
|
90
|
+
if (textLines.at(-1) === "") textLines.pop();
|
|
91
|
+
totalBytes += buffer.length;
|
|
92
|
+
result.push({
|
|
93
|
+
path: relative,
|
|
94
|
+
lines: textLines.map((text, index) => ({ side: "added", line: index + 1, text })),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function parseUnifiedDiff(raw) {
|
|
101
|
+
const files = new Map();
|
|
102
|
+
let current = null;
|
|
103
|
+
let oldPath = null;
|
|
104
|
+
let oldLine = 0;
|
|
105
|
+
let newLine = 0;
|
|
106
|
+
for (const line of String(raw || "").split(/\r?\n/)) {
|
|
107
|
+
if (line.startsWith("--- ")) {
|
|
108
|
+
const candidate = line.slice(4).replace(/^a\//, "");
|
|
109
|
+
oldPath = candidate === "/dev/null" ? null : candidate;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (line.startsWith("+++ ")) {
|
|
113
|
+
const candidate = line.slice(4).replace(/^b\//, "");
|
|
114
|
+
current = candidate === "/dev/null" ? oldPath : candidate;
|
|
115
|
+
if (current && !files.has(current)) files.set(current, []);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const hunk = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
|
|
119
|
+
if (hunk) {
|
|
120
|
+
oldLine = Number(hunk[1]);
|
|
121
|
+
newLine = Number(hunk[2]);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!current || line.startsWith("diff --git ")) continue;
|
|
125
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
126
|
+
files.get(current).push({ side: "added", line: newLine, text: line.slice(1) });
|
|
127
|
+
newLine += 1;
|
|
128
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
129
|
+
files.get(current).push({ side: "removed", line: oldLine, text: line.slice(1) });
|
|
130
|
+
oldLine += 1;
|
|
131
|
+
} else if (!line.startsWith("\\")) {
|
|
132
|
+
oldLine += 1;
|
|
133
|
+
newLine += 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return [...files.entries()].map(([path, lines]) => ({ path, lines }));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function resolveBaseRef(runCommand, cwd, baseBranch) {
|
|
140
|
+
for (const ref of [`origin/${baseBranch}`, baseBranch]) {
|
|
141
|
+
try {
|
|
142
|
+
await git(runCommand, ["rev-parse", "--verify", ref], cwd);
|
|
143
|
+
return ref;
|
|
144
|
+
} catch {
|
|
145
|
+
// Try the local branch after origin/<branch>.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`SQM could not resolve base branch ${baseBranch}.`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function git(runCommand, args, cwd) {
|
|
152
|
+
const result = await runCommand("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
153
|
+
return String(result?.stdout || "").trimEnd();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function zeroSeparated(value) {
|
|
157
|
+
return String(value || "").split("\0").filter(Boolean);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function pathInsideRoot(root, relative) {
|
|
161
|
+
const resolved = path.resolve(root, relative);
|
|
162
|
+
return resolved !== root && resolved.startsWith(`${root}${path.sep}`) ? resolved : null;
|
|
163
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { startAgent } from "../agent.js";
|
|
2
|
+
import { openAuthorizationUrl } from "../browser.js";
|
|
3
|
+
import { isSessionValid, readSession } from "../session-store.js";
|
|
4
|
+
import { createAgentShutdown } from "../shutdown.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_AUTH_TIMEOUT_MS = 2 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
export async function ensureSqmSession(
|
|
9
|
+
webOrigin,
|
|
10
|
+
{
|
|
11
|
+
noOpen = false,
|
|
12
|
+
timeoutMs = DEFAULT_AUTH_TIMEOUT_MS,
|
|
13
|
+
logger = console,
|
|
14
|
+
now = Date.now,
|
|
15
|
+
sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
16
|
+
readSessionImpl = readSession,
|
|
17
|
+
isSessionValidImpl = isSessionValid,
|
|
18
|
+
startAgentImpl = startAgent,
|
|
19
|
+
openAuthorizationUrlImpl = openAuthorizationUrl,
|
|
20
|
+
createAgentShutdownImpl = createAgentShutdown,
|
|
21
|
+
} = {},
|
|
22
|
+
) {
|
|
23
|
+
let session = await readSessionImpl(webOrigin);
|
|
24
|
+
if (sqmSessionReady(session, webOrigin, now(), isSessionValidImpl)) return session;
|
|
25
|
+
|
|
26
|
+
const agent = await startAgentImpl({ requestedPort: 0, webOrigin, quiet: true });
|
|
27
|
+
const shutdown = createAgentShutdownImpl(agent);
|
|
28
|
+
try {
|
|
29
|
+
const loginUrl = agent.loginUrl;
|
|
30
|
+
if (!loginUrl) throw new Error("SIMY could not create an SQM authorization session.");
|
|
31
|
+
logger.log("SIMY sign-in is required before the SQM check can continue.");
|
|
32
|
+
if (noOpen) logger.log(`Open this URL to authorize SIMY CLI: ${loginUrl}`);
|
|
33
|
+
else await openAuthorizationUrlImpl(loginUrl, { logger });
|
|
34
|
+
logger.log("Waiting for browser authorization...");
|
|
35
|
+
|
|
36
|
+
const deadline = now() + timeoutMs;
|
|
37
|
+
while (now() < deadline) {
|
|
38
|
+
await sleep(Math.min(500, Math.max(1, deadline - now())));
|
|
39
|
+
session = await readSessionImpl(webOrigin);
|
|
40
|
+
if (sqmSessionReady(session, webOrigin, now(), isSessionValidImpl)) {
|
|
41
|
+
logger.log("SIMY CLI authorization completed. Continuing SQM check.");
|
|
42
|
+
return session;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new Error("SIMY authorization timed out. Complete the browser sign-in and rerun the SQM command.");
|
|
46
|
+
} finally {
|
|
47
|
+
await shutdown();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sqmSessionReady(session, webOrigin, now, isSessionValidImpl) {
|
|
52
|
+
return Boolean(
|
|
53
|
+
isSessionValidImpl(session, now, webOrigin) &&
|
|
54
|
+
(session?.organization_id || session?.org_id) &&
|
|
55
|
+
session?.token &&
|
|
56
|
+
session?.api_base_url,
|
|
57
|
+
);
|
|
58
|
+
}
|