@vectorplane/ctrl-cli 0.1.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/bin/vp.js +12 -0
  4. package/dist/commands/bootstrap.d.ts +1 -0
  5. package/dist/commands/bootstrap.js +40 -0
  6. package/dist/commands/config.d.ts +1 -0
  7. package/dist/commands/config.js +96 -0
  8. package/dist/commands/context.d.ts +1 -0
  9. package/dist/commands/context.js +46 -0
  10. package/dist/commands/doctor.d.ts +1 -0
  11. package/dist/commands/doctor.js +69 -0
  12. package/dist/commands/event.d.ts +1 -0
  13. package/dist/commands/event.js +65 -0
  14. package/dist/commands/login.d.ts +1 -0
  15. package/dist/commands/login.js +39 -0
  16. package/dist/commands/session.d.ts +1 -0
  17. package/dist/commands/session.js +110 -0
  18. package/dist/commands/status.d.ts +1 -0
  19. package/dist/commands/status.js +46 -0
  20. package/dist/commands/sync.d.ts +1 -0
  21. package/dist/commands/sync.js +108 -0
  22. package/dist/commands/whoami.d.ts +1 -0
  23. package/dist/commands/whoami.js +32 -0
  24. package/dist/commands/workspace.d.ts +1 -0
  25. package/dist/commands/workspace.js +66 -0
  26. package/dist/core/api.d.ts +24 -0
  27. package/dist/core/api.js +190 -0
  28. package/dist/core/auth.d.ts +16 -0
  29. package/dist/core/auth.js +71 -0
  30. package/dist/core/cli.d.ts +9 -0
  31. package/dist/core/cli.js +66 -0
  32. package/dist/core/config.d.ts +31 -0
  33. package/dist/core/config.js +263 -0
  34. package/dist/core/constants.d.ts +22 -0
  35. package/dist/core/constants.js +78 -0
  36. package/dist/core/env.d.ts +2 -0
  37. package/dist/core/env.js +10 -0
  38. package/dist/core/errors.d.ts +18 -0
  39. package/dist/core/errors.js +37 -0
  40. package/dist/core/git.d.ts +2 -0
  41. package/dist/core/git.js +66 -0
  42. package/dist/core/logger.d.ts +8 -0
  43. package/dist/core/logger.js +52 -0
  44. package/dist/core/machine.d.ts +4 -0
  45. package/dist/core/machine.js +87 -0
  46. package/dist/core/queue.d.ts +14 -0
  47. package/dist/core/queue.js +62 -0
  48. package/dist/core/runtime.d.ts +13 -0
  49. package/dist/core/runtime.js +16 -0
  50. package/dist/core/serializer.d.ts +7 -0
  51. package/dist/core/serializer.js +31 -0
  52. package/dist/core/server.d.ts +10 -0
  53. package/dist/core/server.js +84 -0
  54. package/dist/core/session.d.ts +16 -0
  55. package/dist/core/session.js +35 -0
  56. package/dist/core/workspace-binding.d.ts +15 -0
  57. package/dist/core/workspace-binding.js +41 -0
  58. package/dist/core/workspace.d.ts +2 -0
  59. package/dist/core/workspace.js +121 -0
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.js +97 -0
  62. package/dist/types/api.d.ts +61 -0
  63. package/dist/types/api.js +2 -0
  64. package/dist/types/auth.d.ts +36 -0
  65. package/dist/types/auth.js +2 -0
  66. package/dist/types/config.d.ts +58 -0
  67. package/dist/types/config.js +2 -0
  68. package/dist/types/machine.d.ts +49 -0
  69. package/dist/types/machine.js +2 -0
  70. package/dist/types/snapshot.d.ts +15 -0
  71. package/dist/types/snapshot.js +2 -0
  72. package/dist/types/workspace.d.ts +44 -0
  73. package/dist/types/workspace.js +2 -0
  74. package/package.json +61 -0
@@ -0,0 +1,66 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import path from "node:path";
4
+ const execFileAsync = promisify(execFile);
5
+ function sanitizeRemoteUrl(remote) {
6
+ if (!remote) {
7
+ return null;
8
+ }
9
+ if (remote.startsWith("git@")) {
10
+ const [left, right] = remote.split(":", 2);
11
+ const host = left.split("@")[1];
12
+ return `ssh://${host}/${right.replace(/\.git$/i, "")}`;
13
+ }
14
+ try {
15
+ const url = new URL(remote);
16
+ url.username = "";
17
+ url.password = "";
18
+ url.pathname = url.pathname.replace(/\.git$/i, "");
19
+ return url.toString();
20
+ }
21
+ catch {
22
+ return remote.replace(/:\/\/[^@]+@/, "://");
23
+ }
24
+ }
25
+ async function runGit(args, cwd) {
26
+ try {
27
+ const { stdout } = await execFileAsync("git", args, { cwd });
28
+ return stdout.trim();
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ export async function collectGitContext(cwd) {
35
+ const root = await runGit(["rev-parse", "--show-toplevel"], cwd);
36
+ if (!root) {
37
+ return {
38
+ isRepository: false,
39
+ rootPath: null,
40
+ branch: null,
41
+ commitHash: null,
42
+ remoteOrigin: null,
43
+ repositoryName: null,
44
+ hasLocalChanges: false,
45
+ statusSummary: [],
46
+ };
47
+ }
48
+ const [branch, commitHash, remoteOriginRaw, statusRaw] = await Promise.all([
49
+ runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd),
50
+ runGit(["rev-parse", "HEAD"], cwd),
51
+ runGit(["remote", "get-url", "origin"], cwd),
52
+ runGit(["status", "--short"], cwd),
53
+ ]);
54
+ const statusSummary = statusRaw ? statusRaw.split("\n").filter(Boolean).slice(0, 20) : [];
55
+ return {
56
+ isRepository: true,
57
+ rootPath: root,
58
+ branch,
59
+ commitHash,
60
+ remoteOrigin: sanitizeRemoteUrl(remoteOriginRaw),
61
+ repositoryName: path.basename(root),
62
+ hasLocalChanges: statusSummary.length > 0,
63
+ statusSummary,
64
+ };
65
+ }
66
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1,8 @@
1
+ export interface Logger {
2
+ info(message: string, meta?: unknown): void;
3
+ success(message: string, meta?: unknown): void;
4
+ warn(message: string, meta?: unknown): void;
5
+ error(message: string, meta?: unknown): void;
6
+ debug(message: string, meta?: unknown): void;
7
+ }
8
+ export declare function createLogger(debugEnabled: boolean): Logger;
@@ -0,0 +1,52 @@
1
+ import { PRODUCT_NAME, SENSITIVE_KEYS } from "./constants.js";
2
+ function maskValue(value) {
3
+ if (value.length <= 8) {
4
+ return "********";
5
+ }
6
+ return `${value.slice(0, 4)}...${value.slice(-2)}`;
7
+ }
8
+ function sanitizeUnknown(input) {
9
+ if (Array.isArray(input)) {
10
+ return input.map((item) => sanitizeUnknown(item));
11
+ }
12
+ if (!input || typeof input !== "object") {
13
+ return typeof input === "string" && input.length > 64 ? maskValue(input) : input;
14
+ }
15
+ const record = input;
16
+ const sanitized = {};
17
+ for (const [key, value] of Object.entries(record)) {
18
+ const sensitive = SENSITIVE_KEYS.some((item) => key.toLowerCase().includes(item));
19
+ sanitized[key] = sensitive && typeof value === "string" ? maskValue(value) : sanitizeUnknown(value);
20
+ }
21
+ return sanitized;
22
+ }
23
+ function writeLine(kind, message, meta) {
24
+ const stream = kind === "error" ? process.stderr : process.stdout;
25
+ stream.write(`${PRODUCT_NAME}: ${message}\n`);
26
+ if (meta && kind === "debug") {
27
+ stream.write(`${JSON.stringify(sanitizeUnknown(meta), null, 2)}\n`);
28
+ }
29
+ }
30
+ export function createLogger(debugEnabled) {
31
+ return {
32
+ info(message, meta) {
33
+ writeLine("info", message, meta);
34
+ },
35
+ success(message, meta) {
36
+ writeLine("success", message, meta);
37
+ },
38
+ warn(message, meta) {
39
+ writeLine("warn", message, meta);
40
+ },
41
+ error(message, meta) {
42
+ writeLine("error", message, meta);
43
+ },
44
+ debug(message, meta) {
45
+ if (!debugEnabled) {
46
+ return;
47
+ }
48
+ writeLine("debug", message, meta);
49
+ },
50
+ };
51
+ }
52
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1,4 @@
1
+ import type { CliConfig } from "../types/config.js";
2
+ import type { DeviceIdentity, MachineContext, RuntimeContext } from "../types/machine.js";
3
+ export declare function collectRuntimeContext(cliVersion: string, command: string, args: string[]): Promise<RuntimeContext>;
4
+ export declare function collectMachineContext(identity: DeviceIdentity, config: CliConfig): Promise<MachineContext>;
@@ -0,0 +1,87 @@
1
+ import os from "node:os";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { collectSafeEnvironmentVariables } from "./env.js";
5
+ const execFileAsync = promisify(execFile);
6
+ async function detectNpmVersion() {
7
+ try {
8
+ const { stdout } = await execFileAsync("npm", ["--version"]);
9
+ return stdout.trim();
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ async function detectPublicIp(timeoutMs) {
16
+ const controller = new AbortController();
17
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
18
+ try {
19
+ const response = await fetch("https://api.ipify.org?format=json", { signal: controller.signal });
20
+ if (!response.ok) {
21
+ return null;
22
+ }
23
+ const body = await response.json();
24
+ return typeof body.ip === "string" ? body.ip : null;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ finally {
30
+ clearTimeout(timer);
31
+ }
32
+ }
33
+ function collectNetworkInterfaces() {
34
+ const interfaces = os.networkInterfaces();
35
+ const results = [];
36
+ for (const [name, entries] of Object.entries(interfaces)) {
37
+ for (const entry of entries ?? []) {
38
+ results.push({
39
+ name,
40
+ family: entry.family,
41
+ mac: entry.mac,
42
+ address: entry.address,
43
+ internal: entry.internal,
44
+ cidr: entry.cidr ?? null,
45
+ });
46
+ }
47
+ }
48
+ return results.sort((left, right) => left.name.localeCompare(right.name) || left.address.localeCompare(right.address));
49
+ }
50
+ export async function collectRuntimeContext(cliVersion, command, args) {
51
+ return {
52
+ nodeVersion: process.version,
53
+ npmVersion: await detectNpmVersion(),
54
+ cliVersion,
55
+ pid: process.pid,
56
+ command,
57
+ args,
58
+ shell: process.env.SHELL ?? null,
59
+ safeEnv: collectSafeEnvironmentVariables(),
60
+ };
61
+ }
62
+ export async function collectMachineContext(identity, config) {
63
+ const cpus = os.cpus();
64
+ return {
65
+ machineId: identity.machineId,
66
+ hostname: os.hostname(),
67
+ platform: process.platform,
68
+ osRelease: os.release(),
69
+ architecture: os.arch(),
70
+ kernelVersion: os.version(),
71
+ username: os.userInfo().username,
72
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
73
+ locale: Intl.DateTimeFormat().resolvedOptions().locale,
74
+ shell: process.env.SHELL ?? null,
75
+ currentDirectory: process.cwd(),
76
+ homeDirectory: os.homedir(),
77
+ cpuModel: cpus[0]?.model ?? null,
78
+ cpuCount: cpus.length,
79
+ loadAverage: os.loadavg(),
80
+ memoryTotal: os.totalmem(),
81
+ memoryFree: os.freemem(),
82
+ uptimeSeconds: os.uptime(),
83
+ networkInterfaces: collectNetworkInterfaces(),
84
+ publicIp: await detectPublicIp(config.publicIpLookupTimeoutMs),
85
+ };
86
+ }
87
+ //# sourceMappingURL=machine.js.map
@@ -0,0 +1,14 @@
1
+ import type { Logger } from "./logger.js";
2
+ import { VectorPlaneApiClient } from "./api.js";
3
+ export declare function queueSync(profile: string, payload: Record<string, unknown>, error: unknown): Promise<void>;
4
+ export declare function queueEvent(profile: string, payload: Record<string, unknown>, error: unknown): Promise<void>;
5
+ export declare function flushQueue(params: {
6
+ profile: string;
7
+ accessToken: string;
8
+ apiClient: VectorPlaneApiClient;
9
+ logger: Logger;
10
+ queueMaxRetries: number;
11
+ }): Promise<{
12
+ delivered: number;
13
+ kept: number;
14
+ }>;
@@ -0,0 +1,62 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { enqueueItem, loadQueue, removeQueueItem, replaceQueueItem } from "./config.js";
3
+ import { ApiError } from "./errors.js";
4
+ export async function queueSync(profile, payload, error) {
5
+ await enqueueItem({
6
+ id: randomUUID(),
7
+ profile,
8
+ kind: "sync",
9
+ createdAt: new Date().toISOString(),
10
+ attempts: 0,
11
+ lastError: error instanceof Error ? error.message : String(error),
12
+ payload,
13
+ });
14
+ }
15
+ export async function queueEvent(profile, payload, error) {
16
+ await enqueueItem({
17
+ id: randomUUID(),
18
+ profile,
19
+ kind: "event",
20
+ createdAt: new Date().toISOString(),
21
+ attempts: 0,
22
+ lastError: error instanceof Error ? error.message : String(error),
23
+ payload,
24
+ });
25
+ }
26
+ export async function flushQueue(params) {
27
+ const queue = await loadQueue();
28
+ let delivered = 0;
29
+ let kept = 0;
30
+ for (const item of queue.items.filter((entry) => entry.profile === params.profile)) {
31
+ try {
32
+ if (item.kind === "sync") {
33
+ await params.apiClient.sync(params.accessToken, item.payload);
34
+ }
35
+ else {
36
+ await params.apiClient.sendEvent(params.accessToken, item.payload);
37
+ }
38
+ await removeQueueItem(item.id);
39
+ delivered += 1;
40
+ }
41
+ catch (error) {
42
+ const nextItem = {
43
+ ...item,
44
+ attempts: item.attempts + 1,
45
+ lastError: error instanceof Error ? error.message : String(error),
46
+ };
47
+ if (nextItem.attempts >= params.queueMaxRetries) {
48
+ await removeQueueItem(item.id);
49
+ params.logger.warn(`item da fila descartado após ${params.queueMaxRetries} tentativas.`);
50
+ }
51
+ else {
52
+ await replaceQueueItem(nextItem);
53
+ kept += 1;
54
+ }
55
+ if (!(error instanceof ApiError)) {
56
+ params.logger.debug("queue_flush_error", { error: nextItem.lastError, kind: item.kind });
57
+ }
58
+ }
59
+ }
60
+ return { delivered, kept };
61
+ }
62
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1,13 @@
1
+ import type { CliConfig, CliProfileConfig } from "../types/config.js";
2
+ import type { Logger } from "./logger.js";
3
+ import { loadOrCreateDeviceIdentity, loadState } from "./config.js";
4
+ export interface RuntimeServices {
5
+ config: CliConfig;
6
+ profile: CliProfileConfig;
7
+ logger: Logger;
8
+ }
9
+ export declare function loadRuntimeServices(): Promise<RuntimeServices>;
10
+ export declare function loadRuntimeStatus(): Promise<RuntimeServices & {
11
+ state: Awaited<ReturnType<typeof loadState>>;
12
+ device: Awaited<ReturnType<typeof loadOrCreateDeviceIdentity>>;
13
+ }>;
@@ -0,0 +1,16 @@
1
+ import { createLogger } from "./logger.js";
2
+ import { getActiveProfile, loadConfig, loadOrCreateDeviceIdentity, loadState } from "./config.js";
3
+ export async function loadRuntimeServices() {
4
+ const config = await loadConfig();
5
+ return {
6
+ config,
7
+ profile: getActiveProfile(config),
8
+ logger: createLogger(config.debug),
9
+ };
10
+ }
11
+ export async function loadRuntimeStatus() {
12
+ const runtime = await loadRuntimeServices();
13
+ const [state, device] = await Promise.all([loadState(), loadOrCreateDeviceIdentity()]);
14
+ return { ...runtime, state, device };
15
+ }
16
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1,7 @@
1
+ import type { SnapshotPayload } from "../types/snapshot.js";
2
+ export declare function serializeJson(value: unknown): string;
3
+ export declare function createStableHash(value: unknown): string;
4
+ export declare function buildSnapshotEnvelope(snapshot: SnapshotPayload): {
5
+ snapshot: SnapshotPayload;
6
+ hash: string;
7
+ };
@@ -0,0 +1,31 @@
1
+ import { createHash } from "node:crypto";
2
+ function normalize(value) {
3
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
4
+ return value;
5
+ }
6
+ if (Array.isArray(value)) {
7
+ return value.map((item) => normalize(item));
8
+ }
9
+ if (value instanceof Date) {
10
+ return value.toISOString();
11
+ }
12
+ if (!value || typeof value !== "object") {
13
+ return String(value);
14
+ }
15
+ const sortedEntries = Object.entries(value)
16
+ .filter(([, item]) => item !== undefined)
17
+ .sort(([left], [right]) => left.localeCompare(right))
18
+ .map(([key, item]) => [key, normalize(item)]);
19
+ return Object.fromEntries(sortedEntries);
20
+ }
21
+ export function serializeJson(value) {
22
+ return JSON.stringify(normalize(value));
23
+ }
24
+ export function createStableHash(value) {
25
+ return createHash("sha256").update(serializeJson(value)).digest("hex");
26
+ }
27
+ export function buildSnapshotEnvelope(snapshot) {
28
+ const hash = createStableHash(snapshot);
29
+ return { snapshot, hash };
30
+ }
31
+ //# sourceMappingURL=serializer.js.map
@@ -0,0 +1,10 @@
1
+ export interface CallbackResult {
2
+ code: string;
3
+ state: string;
4
+ }
5
+ export interface CallbackServerHandle {
6
+ waitForCallback(timeoutMs: number): Promise<CallbackResult>;
7
+ close(): Promise<void>;
8
+ callbackUrl: string;
9
+ }
10
+ export declare function createCallbackServer(host: string, port: number, callbackPath: string, expectedState: string): Promise<CallbackServerHandle>;
@@ -0,0 +1,84 @@
1
+ import http from "node:http";
2
+ import { AuthError } from "./errors.js";
3
+ export async function createCallbackServer(host, port, callbackPath, expectedState) {
4
+ let resolveCallback = null;
5
+ let rejectCallback = null;
6
+ let settled = false;
7
+ const server = http.createServer((request, response) => {
8
+ try {
9
+ const url = new URL(request.url ?? "/", `http://${host}:${port}`);
10
+ if (url.pathname !== callbackPath) {
11
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
12
+ response.end("VectorPlane callback not found.");
13
+ return;
14
+ }
15
+ const code = url.searchParams.get("code");
16
+ const state = url.searchParams.get("state");
17
+ if (!code || !state) {
18
+ response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
19
+ response.end("Missing code or state.");
20
+ if (!settled) {
21
+ settled = true;
22
+ rejectCallback?.(new AuthError("Callback inválido: parâmetros obrigatórios ausentes."));
23
+ }
24
+ return;
25
+ }
26
+ if (state !== expectedState) {
27
+ response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
28
+ response.end("State mismatch.");
29
+ if (!settled) {
30
+ settled = true;
31
+ rejectCallback?.(new AuthError("Falha de validação de segurança no login. O parâmetro state não confere."));
32
+ }
33
+ return;
34
+ }
35
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
36
+ response.end("<html><body><p>VectorPlane: autenticação recebida. Você já pode voltar ao terminal.</p></body></html>");
37
+ if (!settled) {
38
+ settled = true;
39
+ resolveCallback?.({ code, state });
40
+ }
41
+ }
42
+ catch (error) {
43
+ response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
44
+ response.end("VectorPlane callback error.");
45
+ if (!settled) {
46
+ settled = true;
47
+ rejectCallback?.(error);
48
+ }
49
+ }
50
+ });
51
+ await new Promise((resolve, reject) => {
52
+ server.once("error", (error) => reject(error));
53
+ server.listen(port, host, () => resolve());
54
+ }).catch((error) => {
55
+ const code = error.code;
56
+ if (code === "EADDRINUSE") {
57
+ throw new AuthError(`A porta ${port} já está em uso. Libere a porta e tente novamente.`);
58
+ }
59
+ throw new AuthError("Não foi possível iniciar o callback local de autenticação.", error);
60
+ });
61
+ const callbackUrl = `http://${host}:${server.address().port}${callbackPath}`;
62
+ return {
63
+ callbackUrl,
64
+ async waitForCallback(timeoutMs) {
65
+ return new Promise((resolve, reject) => {
66
+ resolveCallback = resolve;
67
+ rejectCallback = reject;
68
+ const timer = setTimeout(() => {
69
+ if (!settled) {
70
+ settled = true;
71
+ reject(new AuthError("Tempo esgotado aguardando a autenticação no navegador."));
72
+ }
73
+ }, timeoutMs);
74
+ server.once("close", () => clearTimeout(timer));
75
+ });
76
+ },
77
+ async close() {
78
+ await new Promise((resolve, reject) => {
79
+ server.close((error) => error ? reject(error) : resolve());
80
+ }).catch(() => undefined);
81
+ },
82
+ };
83
+ }
84
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,16 @@
1
+ import type { AuthSession } from "../types/auth.js";
2
+ import type { DeviceIdentity, MachineContext, RuntimeContext } from "../types/machine.js";
3
+ import type { CliProfileConfig } from "../types/config.js";
4
+ import type { Logger } from "./logger.js";
5
+ import { VectorPlaneApiClient } from "./api.js";
6
+ export declare function isSessionExpired(session: AuthSession, offsetMs?: number): boolean;
7
+ export declare function ensureFreshSession(params: {
8
+ profileName: string;
9
+ session: AuthSession;
10
+ machine: MachineContext;
11
+ runtime: RuntimeContext;
12
+ device: DeviceIdentity;
13
+ apiClient: VectorPlaneApiClient;
14
+ logger: Logger;
15
+ }): Promise<AuthSession>;
16
+ export declare function resolveWorkspaceSlug(profile: CliProfileConfig, session: AuthSession | null, explicit: string | undefined, bound: string | null, stateWorkspace: string | null): string | null;
@@ -0,0 +1,35 @@
1
+ import { AuthError } from "./errors.js";
2
+ import { CLI_CLIENT_ID } from "./constants.js";
3
+ import { saveSession } from "./config.js";
4
+ export function isSessionExpired(session, offsetMs = 60_000) {
5
+ return new Date(session.expiresAt).getTime() <= Date.now() + offsetMs;
6
+ }
7
+ export async function ensureFreshSession(params) {
8
+ if (!isSessionExpired(params.session)) {
9
+ return params.session;
10
+ }
11
+ params.logger.info("renovando sessão local...");
12
+ const refreshPayload = {
13
+ refresh_token: params.session.refreshToken,
14
+ client: CLI_CLIENT_ID,
15
+ };
16
+ const refreshed = await params.apiClient.refreshToken(refreshPayload);
17
+ if (!refreshed.access_token || !refreshed.refresh_token) {
18
+ throw new AuthError("A renovação de sessão retornou credenciais inválidas.");
19
+ }
20
+ const nextSession = {
21
+ accessToken: refreshed.access_token,
22
+ refreshToken: refreshed.refresh_token,
23
+ workspace: refreshed.workspace || params.session.workspace,
24
+ tokenType: refreshed.token_type,
25
+ expiresIn: refreshed.expires_in,
26
+ obtainedAt: new Date().toISOString(),
27
+ expiresAt: new Date(Date.now() + refreshed.expires_in * 1000).toISOString(),
28
+ };
29
+ await saveSession(nextSession, params.profileName);
30
+ return nextSession;
31
+ }
32
+ export function resolveWorkspaceSlug(profile, session, explicit, bound, stateWorkspace) {
33
+ return explicit ?? bound ?? profile.workspace ?? session?.workspace ?? stateWorkspace ?? null;
34
+ }
35
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1,15 @@
1
+ import type { CliProfileConfig, WorkspaceBindingStore } from "../types/config.js";
2
+ import type { GitContext } from "../types/workspace.js";
3
+ export declare function resolveWorkspaceRoot(cwd: string, git: GitContext): string;
4
+ export declare function deriveClientInstanceId(machineId: string, workspaceRoot: string, git: GitContext): string;
5
+ export declare function getBoundWorkspace(rootPath: string): Promise<string | null>;
6
+ export declare function getBindingStore(): Promise<WorkspaceBindingStore>;
7
+ export declare function bindWorkspaceToRoot(params: {
8
+ rootPath: string;
9
+ workspace: string;
10
+ repoUrl: string | null;
11
+ source: "manual" | "api-resolve" | "session";
12
+ }): Promise<void>;
13
+ export declare function clearBoundWorkspace(rootPath: string): Promise<void>;
14
+ export declare function resolveWorkspacePreference(profile: CliProfileConfig, boundWorkspace: string | null): string | null;
15
+ export declare function relativeRoot(rootPath: string): string;
@@ -0,0 +1,41 @@
1
+ import path from "node:path";
2
+ import { bindWorkspace, loadWorkspaceBindings, unbindWorkspace } from "./config.js";
3
+ import { createStableHash } from "./serializer.js";
4
+ export function resolveWorkspaceRoot(cwd, git) {
5
+ return git.rootPath ?? cwd;
6
+ }
7
+ export function deriveClientInstanceId(machineId, workspaceRoot, git) {
8
+ const hash = createStableHash({
9
+ machineId,
10
+ workspaceRoot,
11
+ branch: git.branch,
12
+ repositoryName: git.repositoryName,
13
+ });
14
+ return `vpci-${hash.slice(0, 24)}`;
15
+ }
16
+ export async function getBoundWorkspace(rootPath) {
17
+ const store = await loadWorkspaceBindings();
18
+ return store.bindings[rootPath]?.workspace ?? null;
19
+ }
20
+ export async function getBindingStore() {
21
+ return loadWorkspaceBindings();
22
+ }
23
+ export async function bindWorkspaceToRoot(params) {
24
+ await bindWorkspace({
25
+ rootPath: params.rootPath,
26
+ workspace: params.workspace,
27
+ repoUrl: params.repoUrl,
28
+ resolvedAt: new Date().toISOString(),
29
+ source: params.source,
30
+ });
31
+ }
32
+ export async function clearBoundWorkspace(rootPath) {
33
+ await unbindWorkspace(rootPath);
34
+ }
35
+ export function resolveWorkspacePreference(profile, boundWorkspace) {
36
+ return boundWorkspace ?? profile.workspace ?? null;
37
+ }
38
+ export function relativeRoot(rootPath) {
39
+ return path.basename(rootPath);
40
+ }
41
+ //# sourceMappingURL=workspace-binding.js.map
@@ -0,0 +1,2 @@
1
+ import type { GitContext, WorkspaceContext } from "../types/workspace.js";
2
+ export declare function collectWorkspaceContext(cwd: string, git: GitContext): Promise<WorkspaceContext>;