@remotedraw/cli 0.1.2 → 0.2.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/LICENSE +21 -0
- package/README.md +79 -2
- package/dist/cli.d.ts +15 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1212 -759
- package/dist/cloud.d.ts +13 -0
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +61 -14
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/generated/agent-skill.d.ts +2 -0
- package/dist/generated/agent-skill.d.ts.map +1 -0
- package/dist/generated/agent-skill.js +4 -0
- package/dist/i18n.d.ts +74 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +178 -0
- package/dist/index.js +35 -2
- package/dist/locales/catalog.d.ts +21 -0
- package/dist/locales/catalog.d.ts.map +1 -0
- package/dist/locales/catalog.js +1 -0
- package/dist/locales/en.d.ts +420 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/en.js +446 -0
- package/dist/locales/index.d.ts +436 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/index.js +27 -0
- package/dist/locales/nl.d.ts +10 -0
- package/dist/locales/nl.d.ts.map +1 -0
- package/dist/locales/nl.js +438 -0
- package/dist/setup-wizard.d.ts +10 -2
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +593 -102
- package/dist/telemetry.d.ts +73 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +145 -0
- package/dist/update.d.ts +53 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +240 -0
- package/package.json +11 -4
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash reporting for the published CLI.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately dependency-free: `remotedraw` is installed globally by customers,
|
|
5
|
+
* and pulling `@sentry/node` into it would add tens of megabytes and a native
|
|
6
|
+
* OpenTelemetry stack to what is otherwise a small package. Sentry's envelope
|
|
7
|
+
* endpoint is a plain POST, so the whole client is the function below.
|
|
8
|
+
*
|
|
9
|
+
* It only ever sends a crash — never a command's arguments, never file
|
|
10
|
+
* contents, never a project name. Anything that could carry a credential is
|
|
11
|
+
* scrubbed before it leaves the machine, and the whole path is opt-out.
|
|
12
|
+
*/
|
|
13
|
+
export type TelemetryEnvironment = {
|
|
14
|
+
env: Record<string, string | undefined>;
|
|
15
|
+
version: string;
|
|
16
|
+
platform: string;
|
|
17
|
+
nodeVersion: string;
|
|
18
|
+
/** Injected by the test suite so no suite run can reach the network. */
|
|
19
|
+
fetch?: typeof fetch | undefined;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Off whenever the user says so, and off in CI, where a crash report has no
|
|
23
|
+
* human behind it and the environment is usually a fresh container.
|
|
24
|
+
* DO_NOT_TRACK is the cross-vendor convention; we honor it without a flag.
|
|
25
|
+
*/
|
|
26
|
+
export declare function telemetryDisabled(env: Record<string, string | undefined>): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Removes the two things a CLI error message realistically leaks: credentials
|
|
29
|
+
* pasted into a flag or read from the environment, and the absolute path of the
|
|
30
|
+
* user's home directory, which usually contains their name.
|
|
31
|
+
*/
|
|
32
|
+
export declare function scrubTelemetryText(value: string, env?: Record<string, string | undefined>): string;
|
|
33
|
+
/**
|
|
34
|
+
* Builds the Sentry event for a crash. Exported so tests can assert exactly
|
|
35
|
+
* what would go over the wire without one leaving the machine.
|
|
36
|
+
*/
|
|
37
|
+
export declare function crashEvent(error: unknown, command: string, environment: TelemetryEnvironment): {
|
|
38
|
+
event_id: string;
|
|
39
|
+
timestamp: number;
|
|
40
|
+
platform: string;
|
|
41
|
+
level: string;
|
|
42
|
+
logger: string;
|
|
43
|
+
release: string;
|
|
44
|
+
environment: string;
|
|
45
|
+
transaction: string;
|
|
46
|
+
tags: {
|
|
47
|
+
command: string;
|
|
48
|
+
cli_version: string;
|
|
49
|
+
node_version: string;
|
|
50
|
+
os: string;
|
|
51
|
+
};
|
|
52
|
+
exception: {
|
|
53
|
+
values: {
|
|
54
|
+
type: string;
|
|
55
|
+
value: string;
|
|
56
|
+
mechanism: {
|
|
57
|
+
handled: boolean;
|
|
58
|
+
};
|
|
59
|
+
}[];
|
|
60
|
+
};
|
|
61
|
+
extra: {
|
|
62
|
+
stack: string;
|
|
63
|
+
} | undefined;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Reports a CLI crash. Never throws, never rejects, and never delays the exit
|
|
67
|
+
* by more than the timeout: a command's outcome must be identical whether or
|
|
68
|
+
* not Sentry is reachable.
|
|
69
|
+
*/
|
|
70
|
+
export declare function reportCliCrash(error: unknown, command: string, environment: TelemetryEnvironment): Promise<void>;
|
|
71
|
+
/** The environment description used by the real CLI process. */
|
|
72
|
+
export declare function nodeTelemetryEnvironment(version: string): TelemetryEnvironment;
|
|
73
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,KAAK,GAAG,SAAS,CAAC;CAClC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,WAOxE;AAMD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAM,UAW7C;AAmBD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsClC;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,oBAAoB,iBA+BlC;AAED,gEAAgE;AAChE,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,GACd,oBAAoB,CAOtB"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash reporting for the published CLI.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately dependency-free: `remotedraw` is installed globally by customers,
|
|
5
|
+
* and pulling `@sentry/node` into it would add tens of megabytes and a native
|
|
6
|
+
* OpenTelemetry stack to what is otherwise a small package. Sentry's envelope
|
|
7
|
+
* endpoint is a plain POST, so the whole client is the function below.
|
|
8
|
+
*
|
|
9
|
+
* It only ever sends a crash — never a command's arguments, never file
|
|
10
|
+
* contents, never a project name. Anything that could carry a credential is
|
|
11
|
+
* scrubbed before it leaves the machine, and the whole path is opt-out.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Filled in at release time. Sentry DSNs are public by design (browser bundles
|
|
15
|
+
* ship them), so a value here is not a secret; it only identifies the project
|
|
16
|
+
* that receives crashes. Override at runtime with REMOTEDRAW_SENTRY_DSN.
|
|
17
|
+
*/
|
|
18
|
+
const RELEASE_DSN = "";
|
|
19
|
+
/**
|
|
20
|
+
* Off whenever the user says so, and off in CI, where a crash report has no
|
|
21
|
+
* human behind it and the environment is usually a fresh container.
|
|
22
|
+
* DO_NOT_TRACK is the cross-vendor convention; we honor it without a flag.
|
|
23
|
+
*/
|
|
24
|
+
export function telemetryDisabled(env) {
|
|
25
|
+
return Boolean(env.REMOTEDRAW_TELEMETRY_DISABLED?.trim() ||
|
|
26
|
+
env.REMOTEDRAW_CLI_OFFLINE?.trim() ||
|
|
27
|
+
env.DO_NOT_TRACK?.trim() ||
|
|
28
|
+
env.CI?.trim());
|
|
29
|
+
}
|
|
30
|
+
function dsnFor(env) {
|
|
31
|
+
return env.REMOTEDRAW_SENTRY_DSN?.trim() || RELEASE_DSN;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Removes the two things a CLI error message realistically leaks: credentials
|
|
35
|
+
* pasted into a flag or read from the environment, and the absolute path of the
|
|
36
|
+
* user's home directory, which usually contains their name.
|
|
37
|
+
*/
|
|
38
|
+
export function scrubTelemetryText(value, env = {}) {
|
|
39
|
+
let scrubbed = value
|
|
40
|
+
.replace(/\brd_[a-z]+_[A-Za-z0-9_-]+/g, "rd_***")
|
|
41
|
+
.replace(/\bsk_[A-Za-z0-9_-]{8,}/g, "sk_***")
|
|
42
|
+
.replace(/\b(Bearer)\s+\S+/gi, "$1 ***");
|
|
43
|
+
const home = env.HOME ?? env.USERPROFILE;
|
|
44
|
+
if (home && home.length > 1) {
|
|
45
|
+
scrubbed = scrubbed.split(home).join("~");
|
|
46
|
+
}
|
|
47
|
+
return scrubbed;
|
|
48
|
+
}
|
|
49
|
+
function parseDsn(value) {
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL(value);
|
|
52
|
+
const projectId = url.pathname.replace(/^\//, "");
|
|
53
|
+
if (!url.username || !projectId)
|
|
54
|
+
return null;
|
|
55
|
+
return {
|
|
56
|
+
envelopeUrl: `${url.protocol}//${url.host}/api/${projectId}/envelope/`,
|
|
57
|
+
publicKey: url.username,
|
|
58
|
+
dsn: value,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Builds the Sentry event for a crash. Exported so tests can assert exactly
|
|
67
|
+
* what would go over the wire without one leaving the machine.
|
|
68
|
+
*/
|
|
69
|
+
export function crashEvent(error, command, environment) {
|
|
70
|
+
const isError = error instanceof Error;
|
|
71
|
+
const eventId = crypto.randomUUID().replace(/-/g, "");
|
|
72
|
+
return {
|
|
73
|
+
event_id: eventId,
|
|
74
|
+
timestamp: Date.now() / 1000,
|
|
75
|
+
platform: "node",
|
|
76
|
+
level: "error",
|
|
77
|
+
logger: "remotedraw.cli",
|
|
78
|
+
release: `remotedraw-cli@${environment.version}`,
|
|
79
|
+
environment: "production",
|
|
80
|
+
// The command name only; flags and values never leave the machine because
|
|
81
|
+
// any of them may be a key, a path, or a customer's project name.
|
|
82
|
+
transaction: command,
|
|
83
|
+
tags: {
|
|
84
|
+
command,
|
|
85
|
+
cli_version: environment.version,
|
|
86
|
+
node_version: environment.nodeVersion,
|
|
87
|
+
os: environment.platform,
|
|
88
|
+
},
|
|
89
|
+
exception: {
|
|
90
|
+
values: [
|
|
91
|
+
{
|
|
92
|
+
type: isError ? error.name || "Error" : "UnknownError",
|
|
93
|
+
value: scrubTelemetryText(isError ? error.message : String(error), environment.env),
|
|
94
|
+
mechanism: { handled: true },
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
extra: isError && error.stack
|
|
99
|
+
? { stack: scrubTelemetryText(error.stack, environment.env) }
|
|
100
|
+
: undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Reports a CLI crash. Never throws, never rejects, and never delays the exit
|
|
105
|
+
* by more than the timeout: a command's outcome must be identical whether or
|
|
106
|
+
* not Sentry is reachable.
|
|
107
|
+
*/
|
|
108
|
+
export async function reportCliCrash(error, command, environment) {
|
|
109
|
+
if (telemetryDisabled(environment.env))
|
|
110
|
+
return;
|
|
111
|
+
const target = parseDsn(dsnFor(environment.env));
|
|
112
|
+
if (!target)
|
|
113
|
+
return;
|
|
114
|
+
const event = crashEvent(error, command, environment);
|
|
115
|
+
const envelope = [
|
|
116
|
+
JSON.stringify({
|
|
117
|
+
event_id: event.event_id,
|
|
118
|
+
sent_at: new Date().toISOString(),
|
|
119
|
+
dsn: target.dsn,
|
|
120
|
+
}),
|
|
121
|
+
JSON.stringify({ type: "event" }),
|
|
122
|
+
JSON.stringify(event),
|
|
123
|
+
].join("\n");
|
|
124
|
+
const send = environment.fetch ?? fetch;
|
|
125
|
+
try {
|
|
126
|
+
await send(`${target.envelopeUrl}?sentry_key=${target.publicKey}&sentry_version=7`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/x-sentry-envelope" },
|
|
129
|
+
body: envelope,
|
|
130
|
+
signal: AbortSignal.timeout(2000),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// A crash report that fails to send is not itself worth reporting.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** The environment description used by the real CLI process. */
|
|
138
|
+
export function nodeTelemetryEnvironment(version) {
|
|
139
|
+
return {
|
|
140
|
+
env: process.env,
|
|
141
|
+
version,
|
|
142
|
+
platform: process.platform,
|
|
143
|
+
nodeVersion: process.versions.node,
|
|
144
|
+
};
|
|
145
|
+
}
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type CloudRuntime } from "./cloud.js";
|
|
2
|
+
export declare const CLI_PACKAGE_NAME = "@remotedraw/cli";
|
|
3
|
+
export type UpdateRuntime = CloudRuntime & {
|
|
4
|
+
/**
|
|
5
|
+
* Runs an installer. Absent in unit tests and in any runtime that must not
|
|
6
|
+
* spawn processes, which makes `remotedraw update` report the command instead
|
|
7
|
+
* of running it.
|
|
8
|
+
*/
|
|
9
|
+
spawnCommand?: (command: string, args: string[]) => Promise<{
|
|
10
|
+
exitCode: number;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
export type InstallMethod = {
|
|
14
|
+
/** Package manager the binary appears to have been installed with. */
|
|
15
|
+
manager: "npm" | "pnpm" | "yarn" | "bun";
|
|
16
|
+
/** True when the binary was resolved by npx/bunx, which never needs updating. */
|
|
17
|
+
ephemeral: boolean;
|
|
18
|
+
command: string;
|
|
19
|
+
args: string[];
|
|
20
|
+
};
|
|
21
|
+
export type UpdateCheck = {
|
|
22
|
+
current: string;
|
|
23
|
+
latest: string;
|
|
24
|
+
updateAvailable: boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare function parseVersion(value: string): {
|
|
27
|
+
numbers: number[];
|
|
28
|
+
prerelease: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function compareVersions(a: string, b: string): 1 | 0 | -1;
|
|
31
|
+
/**
|
|
32
|
+
* Derives the update command from where the running binary lives. Guessing
|
|
33
|
+
* wrong only prints the wrong suggestion, so the fallback is plain npm.
|
|
34
|
+
*/
|
|
35
|
+
export declare function detectInstallMethod(binPath: string): InstallMethod;
|
|
36
|
+
export declare function installMethodDisplay(method: InstallMethod): string;
|
|
37
|
+
export declare function updateCachePath(runtime: UpdateRuntime): string;
|
|
38
|
+
export declare function latestPublishedVersion(runtime: UpdateRuntime, timeoutMs?: number): Promise<string | undefined>;
|
|
39
|
+
/** True when the environment asked us not to make an update request at all. */
|
|
40
|
+
export declare function updateChecksDisabled(env: Record<string, string | undefined>): boolean;
|
|
41
|
+
export declare function checkForUpdate(runtime: UpdateRuntime, currentVersion: string, options?: {
|
|
42
|
+
force?: boolean;
|
|
43
|
+
now?: number;
|
|
44
|
+
}): Promise<UpdateCheck | undefined>;
|
|
45
|
+
export declare function updateNotice(check: UpdateCheck, method: InstallMethod): string;
|
|
46
|
+
export declare function runInstall(runtime: UpdateRuntime, method: InstallMethod): Promise<{
|
|
47
|
+
ran: false;
|
|
48
|
+
exitCode?: never;
|
|
49
|
+
} | {
|
|
50
|
+
ran: true;
|
|
51
|
+
exitCode: number;
|
|
52
|
+
}>;
|
|
53
|
+
//# sourceMappingURL=update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../src/update.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAGjE,eAAO,MAAM,gBAAgB,oBAAoB,CAAC;AAOlD,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG;IACzC;;;;OAIG;IACH,YAAY,CAAC,EAAE,CACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,sEAAsE;IACtE,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;IACzC,iFAAiF;IACjF,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC;AAiBF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM;;;EAczC;AA2BD,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,cASnD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAoClE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,UAEzD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,UAErD;AAgDD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,aAAa,EACtB,SAAS,SAAmB,+BAoC7B;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,WAO3E;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,aAAa,EACtB,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CA2BlC;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,UAWrE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa;;;;;;GAM7E"}
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { globalConfigPath } from "./cloud.js";
|
|
3
|
+
import { t } from "./i18n.js";
|
|
4
|
+
export const CLI_PACKAGE_NAME = "@remotedraw/cli";
|
|
5
|
+
const DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
6
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
7
|
+
/** The passive notice must never delay a command; an explicit check may wait. */
|
|
8
|
+
const CHECK_TIMEOUT_MS = 2_000;
|
|
9
|
+
const FORCED_CHECK_TIMEOUT_MS = 10_000;
|
|
10
|
+
/**
|
|
11
|
+
* npm registry semantics: a globally installed package is a pinned snapshot,
|
|
12
|
+
* so the CLI has to ask the registry itself to know it is stale.
|
|
13
|
+
*/
|
|
14
|
+
function registryUrl(env) {
|
|
15
|
+
const configured = env.REMOTEDRAW_CLI_REGISTRY?.trim() || env.npm_config_registry?.trim();
|
|
16
|
+
return (configured || DEFAULT_REGISTRY_URL).replace(/\/+$/, "");
|
|
17
|
+
}
|
|
18
|
+
export function parseVersion(value) {
|
|
19
|
+
const trimmed = value.trim().replace(/^v/, "");
|
|
20
|
+
const separator = trimmed.indexOf("-");
|
|
21
|
+
const core = separator === -1 ? trimmed : trimmed.slice(0, separator);
|
|
22
|
+
const prerelease = separator === -1 ? "" : trimmed.slice(separator + 1);
|
|
23
|
+
const numbers = core
|
|
24
|
+
.split(".")
|
|
25
|
+
.slice(0, 3)
|
|
26
|
+
.map((part) => {
|
|
27
|
+
const parsed = Number.parseInt(part, 10);
|
|
28
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
29
|
+
});
|
|
30
|
+
while (numbers.length < 3)
|
|
31
|
+
numbers.push(0);
|
|
32
|
+
return { numbers, prerelease };
|
|
33
|
+
}
|
|
34
|
+
function comparePrerelease(a, b) {
|
|
35
|
+
if (a === b)
|
|
36
|
+
return 0;
|
|
37
|
+
// A release always outranks any prerelease of the same core version.
|
|
38
|
+
if (!a)
|
|
39
|
+
return 1;
|
|
40
|
+
if (!b)
|
|
41
|
+
return -1;
|
|
42
|
+
const left = a.split(".");
|
|
43
|
+
const right = b.split(".");
|
|
44
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
45
|
+
const leftPart = left[index];
|
|
46
|
+
const rightPart = right[index];
|
|
47
|
+
if (leftPart == null)
|
|
48
|
+
return -1;
|
|
49
|
+
if (rightPart == null)
|
|
50
|
+
return 1;
|
|
51
|
+
const leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : undefined;
|
|
52
|
+
const rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : undefined;
|
|
53
|
+
if (leftNumber != null && rightNumber != null) {
|
|
54
|
+
if (leftNumber !== rightNumber)
|
|
55
|
+
return leftNumber < rightNumber ? -1 : 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (leftNumber != null)
|
|
59
|
+
return -1;
|
|
60
|
+
if (rightNumber != null)
|
|
61
|
+
return 1;
|
|
62
|
+
if (leftPart !== rightPart)
|
|
63
|
+
return leftPart < rightPart ? -1 : 1;
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
export function compareVersions(a, b) {
|
|
68
|
+
const left = parseVersion(a);
|
|
69
|
+
const right = parseVersion(b);
|
|
70
|
+
for (let index = 0; index < 3; index += 1) {
|
|
71
|
+
const leftNumber = left.numbers[index] ?? 0;
|
|
72
|
+
const rightNumber = right.numbers[index] ?? 0;
|
|
73
|
+
if (leftNumber !== rightNumber)
|
|
74
|
+
return leftNumber < rightNumber ? -1 : 1;
|
|
75
|
+
}
|
|
76
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Derives the update command from where the running binary lives. Guessing
|
|
80
|
+
* wrong only prints the wrong suggestion, so the fallback is plain npm.
|
|
81
|
+
*/
|
|
82
|
+
export function detectInstallMethod(binPath) {
|
|
83
|
+
const normalized = binPath.replace(/\\/g, "/");
|
|
84
|
+
const update = `${CLI_PACKAGE_NAME}@latest`;
|
|
85
|
+
if (/\/(_npx|\.npm\/_npx|\.bun\/install\/cache)\//.test(normalized)) {
|
|
86
|
+
return { manager: "npm", ephemeral: true, command: "npx", args: [update] };
|
|
87
|
+
}
|
|
88
|
+
if (/\/\.bun\//.test(normalized)) {
|
|
89
|
+
return {
|
|
90
|
+
manager: "bun",
|
|
91
|
+
ephemeral: false,
|
|
92
|
+
command: "bun",
|
|
93
|
+
args: ["add", "--global", update],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (/\/(\.pnpm|pnpm)\//.test(normalized)) {
|
|
97
|
+
return {
|
|
98
|
+
manager: "pnpm",
|
|
99
|
+
ephemeral: false,
|
|
100
|
+
command: "pnpm",
|
|
101
|
+
args: ["add", "--global", update],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (/\/(\.yarn|Yarn)\//.test(normalized)) {
|
|
105
|
+
return {
|
|
106
|
+
manager: "yarn",
|
|
107
|
+
ephemeral: false,
|
|
108
|
+
command: "yarn",
|
|
109
|
+
args: ["global", "add", update],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
manager: "npm",
|
|
114
|
+
ephemeral: false,
|
|
115
|
+
command: "npm",
|
|
116
|
+
args: ["install", "--global", update],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function installMethodDisplay(method) {
|
|
120
|
+
return [method.command, ...method.args].join(" ");
|
|
121
|
+
}
|
|
122
|
+
export function updateCachePath(runtime) {
|
|
123
|
+
return path.join(path.dirname(globalConfigPath(runtime)), "update-check.json");
|
|
124
|
+
}
|
|
125
|
+
async function readUpdateCache(runtime) {
|
|
126
|
+
const cachePath = updateCachePath(runtime);
|
|
127
|
+
if (!(await runtime.exists(cachePath)))
|
|
128
|
+
return undefined;
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(await runtime.readFile(cachePath));
|
|
131
|
+
if (typeof parsed !== "object" ||
|
|
132
|
+
parsed === null ||
|
|
133
|
+
typeof parsed.latest !== "string" ||
|
|
134
|
+
typeof parsed.checkedAt !== "number") {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// A corrupt cache is never fatal; the next check rewrites it.
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function writeUpdateCache(runtime, cache) {
|
|
145
|
+
try {
|
|
146
|
+
const cachePath = updateCachePath(runtime);
|
|
147
|
+
await runtime.mkdir(path.dirname(cachePath));
|
|
148
|
+
await runtime.writeFile(cachePath, `${JSON.stringify(cache)}\n`);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// A read-only config dir must not break the command the user actually ran.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function registryJson(runtime, url, accept, timeoutMs) {
|
|
155
|
+
const response = await runtime.fetch?.(url, {
|
|
156
|
+
headers: { Accept: accept },
|
|
157
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
158
|
+
});
|
|
159
|
+
if (!response?.ok)
|
|
160
|
+
return undefined;
|
|
161
|
+
return (await response.json());
|
|
162
|
+
}
|
|
163
|
+
export async function latestPublishedVersion(runtime, timeoutMs = CHECK_TIMEOUT_MS) {
|
|
164
|
+
if (!runtime.fetch)
|
|
165
|
+
return undefined;
|
|
166
|
+
const base = `${registryUrl(runtime.env)}/${CLI_PACKAGE_NAME}`;
|
|
167
|
+
try {
|
|
168
|
+
// `/latest` is one version manifest rather than the whole packument, so it
|
|
169
|
+
// stays small as releases accumulate. It rejects npm's abbreviated-packument
|
|
170
|
+
// Accept header with a 406, hence plain JSON here.
|
|
171
|
+
const manifest = await registryJson(runtime, `${base}/latest`, "application/json", timeoutMs);
|
|
172
|
+
if (typeof manifest?.version === "string")
|
|
173
|
+
return manifest.version;
|
|
174
|
+
// Registries and mirrors that do not serve `/latest` still serve dist-tags
|
|
175
|
+
// on the abbreviated packument.
|
|
176
|
+
const packument = await registryJson(runtime, base, "application/vnd.npm.install-v1+json", timeoutMs);
|
|
177
|
+
const distTags = packument?.["dist-tags"];
|
|
178
|
+
if (typeof distTags === "object" &&
|
|
179
|
+
distTags !== null &&
|
|
180
|
+
typeof distTags.latest === "string") {
|
|
181
|
+
return distTags.latest;
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** True when the environment asked us not to make an update request at all. */
|
|
190
|
+
export function updateChecksDisabled(env) {
|
|
191
|
+
return Boolean(env.REMOTEDRAW_CLI_OFFLINE === "1" ||
|
|
192
|
+
env.REMOTEDRAW_CLI_NO_UPDATE_CHECK === "1" ||
|
|
193
|
+
env.NO_UPDATE_NOTIFIER ||
|
|
194
|
+
env.CI);
|
|
195
|
+
}
|
|
196
|
+
export async function checkForUpdate(runtime, currentVersion, options = {}) {
|
|
197
|
+
if (currentVersion === "unknown")
|
|
198
|
+
return undefined;
|
|
199
|
+
if (!options.force && updateChecksDisabled(runtime.env))
|
|
200
|
+
return undefined;
|
|
201
|
+
const now = options.now ?? Date.now();
|
|
202
|
+
if (!options.force) {
|
|
203
|
+
const cached = await readUpdateCache(runtime);
|
|
204
|
+
if (cached && now - cached.checkedAt < CHECK_INTERVAL_MS) {
|
|
205
|
+
return {
|
|
206
|
+
current: currentVersion,
|
|
207
|
+
latest: cached.latest,
|
|
208
|
+
updateAvailable: compareVersions(currentVersion, cached.latest) < 0,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const latest = await latestPublishedVersion(runtime, options.force ? FORCED_CHECK_TIMEOUT_MS : CHECK_TIMEOUT_MS);
|
|
213
|
+
if (!latest)
|
|
214
|
+
return undefined;
|
|
215
|
+
await writeUpdateCache(runtime, { checkedAt: now, latest });
|
|
216
|
+
return {
|
|
217
|
+
current: currentVersion,
|
|
218
|
+
latest,
|
|
219
|
+
updateAvailable: compareVersions(currentVersion, latest) < 0,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export function updateNotice(check, method) {
|
|
223
|
+
return [
|
|
224
|
+
t("notice.update.available", {
|
|
225
|
+
package: CLI_PACKAGE_NAME,
|
|
226
|
+
current: check.current,
|
|
227
|
+
latest: check.latest,
|
|
228
|
+
}),
|
|
229
|
+
method.ephemeral
|
|
230
|
+
? t("notice.update.ephemeral", { package: CLI_PACKAGE_NAME })
|
|
231
|
+
: t("notice.update.install", { command: installMethodDisplay(method) }),
|
|
232
|
+
].join("\n");
|
|
233
|
+
}
|
|
234
|
+
export async function runInstall(runtime, method) {
|
|
235
|
+
if (!runtime.spawnCommand) {
|
|
236
|
+
return { ran: false };
|
|
237
|
+
}
|
|
238
|
+
const result = await runtime.spawnCommand(method.command, method.args);
|
|
239
|
+
return { ran: true, exitCode: result.exitCode };
|
|
240
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remotedraw/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Command-line tools for creating and inspecting RemoteDraw integrations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=20"
|
|
15
15
|
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/AxioSOzo/RemoteDraw.git",
|
|
20
|
+
"directory": "packages/cli"
|
|
21
|
+
},
|
|
16
22
|
"bugs": {
|
|
17
23
|
"email": "support@remotedraw.com"
|
|
18
24
|
},
|
|
@@ -27,12 +33,13 @@
|
|
|
27
33
|
"registry": "https://registry.npmjs.org/"
|
|
28
34
|
},
|
|
29
35
|
"scripts": {
|
|
30
|
-
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"build": "bun run generate:skill && tsc -p tsconfig.json",
|
|
31
37
|
"dev": "bun run src/index.ts",
|
|
32
38
|
"test": "bun test tests/*.test.ts",
|
|
33
39
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
34
|
-
"verify:publish": "bun run build && bun run typecheck && bun run test && node dist/index.js --help && node dist/index.js --version",
|
|
35
|
-
"prepack": "bun run verify:publish"
|
|
40
|
+
"verify:publish": "bun run generate:skill && bun run build && bun run typecheck && bun run test && node dist/index.js --help && node dist/index.js --version",
|
|
41
|
+
"prepack": "bun run verify:publish",
|
|
42
|
+
"generate:skill": "bun run scripts/generate-agent-skill.ts"
|
|
36
43
|
},
|
|
37
44
|
"devDependencies": {
|
|
38
45
|
"@types/node": "25.6.2",
|