@schlenkr/ragents 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/apps/web/dist/help/guide-distributed.html +22 -8
- package/apps/web/dist/help/guide-distributed.md +28 -7
- package/apps/web/dist/host-web.json +1 -1
- package/apps/web/src/server-client.ts +165 -0
- package/bundles/ragents.activity/ragents-bundle.json +1 -1
- package/bundles/ragents.actor-programs/ragents-bundle.json +1 -1
- package/bundles/ragents.ask/ragents-bundle.json +1 -1
- package/bundles/ragents.browser/ragents-bundle.json +1 -1
- package/bundles/ragents.documents/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-fsharp/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-roslyn/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-typescript/ragents-bundle.json +1 -1
- package/bundles/ragents.model-relay/ragents-bundle.json +1 -1
- package/bundles/ragents.orchestration/ragents-bundle.json +1 -1
- package/bundles/ragents.overseer/ragents-bundle.json +1 -1
- package/bundles/ragents.processes/ragents-bundle.json +1 -1
- package/bundles/ragents.product/ragents-bundle.json +1 -1
- package/bundles/ragents.profile-distribution/ragents-bundle.json +1 -1
- package/bundles/ragents.reference/ragents-bundle.json +1 -1
- package/bundles/ragents.todo/ragents-bundle.json +1 -1
- package/bundles/ragents.transcript/ragents-bundle.json +1 -1
- package/bundles/ragents.watch/ragents-bundle.json +1 -1
- package/bundles/ragents.workspace/ragents-bundle.json +1 -1
- package/package.json +11 -2
- package/packages/ai/dist/api/openai-compatible-response.d.ts +2 -0
- package/packages/ai/package.json +1 -0
- package/packages/ai/src/api/ai-sdk-transport.ts +4 -2
- package/packages/ai/src/api/openai-compatible-response.ts +60 -0
- package/packages/workspace-executor/src/bundled-tools.ts +49 -0
- package/packages/workspace-executor/src/owner-lifetime.d.mts +18 -0
- package/packages/workspace-executor/src/owner-lifetime.mjs +102 -0
- package/plugins/ragents.workspace/client/transport.ts +9 -11
- package/plugins/ragents.workspace/client/workspace-client.ts +8 -1
- package/scripts/package/ragents.mjs +30 -4
- package/scripts/workspace-client/run-workspace-client.ts +51 -34
- package/scripts/package/build-package.ts +0 -319
- package/scripts/package/publish-package.ts +0 -144
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
-
import { copyFile, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { builtinModules } from "node:module";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { readHostVersion, readPackageVersion } from "../../apps/server/src/host-version.ts";
|
|
8
|
-
import { hostWebDirectory, hostWebProblem, isCheckout } from "../../apps/server/src/host-web.ts";
|
|
9
|
-
import { runtimeLibraries } from "../../apps/server/src/plugin-support/actor-programs/runtime-libraries.ts";
|
|
10
|
-
import { assertBundleRevision, readBundleManifest, sourceRevisionOf } from "../../apps/server/src/profile/bundle-manifest.ts";
|
|
11
|
-
import { builtInPluginFolders } from "../plugin/build-builtin-plugins.ts";
|
|
12
|
-
|
|
13
|
-
export const PACKAGE_NAME = "@schlenkr/ragents";
|
|
14
|
-
/** The folder under dist/; the package name has a scope, the folder name stays plain. */
|
|
15
|
-
export const PACKAGE_FOLDER = "ragents";
|
|
16
|
-
|
|
17
|
-
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
18
|
-
|
|
19
|
-
const SKIPPED_FOLDERS = new Set(["node_modules", "dist", ".git"]);
|
|
20
|
-
const SKIPPED_FILES = new Set([".DS_Store"]);
|
|
21
|
-
|
|
22
|
-
/** The subcommands' tests run in the checkout; in the package only their test material would be visible. */
|
|
23
|
-
const SKIPPED_IN_PACKAGE = /^scripts\/.*\.test\.tsx?$/;
|
|
24
|
-
|
|
25
|
-
const packagePath = (packageRoot: string, file: string): string => path.relative(packageRoot, file).split(path.sep).join("/");
|
|
26
|
-
|
|
27
|
-
/** The repository's neutral profiles; ragents start runs them from the package as from the checkout. */
|
|
28
|
-
export const PACKAGED_PROFILES = ["core", "developer", "showcase"] as const;
|
|
29
|
-
|
|
30
|
-
export const PACKAGE_LICENSE = "PolyForm-Shield-1.0.0";
|
|
31
|
-
export const PACKAGE_AUTHOR = "Ronald Schlenker";
|
|
32
|
-
export const REPOSITORY_URL = "https://github.com/SchlenkR/RAgents";
|
|
33
|
-
export const PACKAGE_KEYWORDS = ["agents", "llm", "actors", "typescript", "csharp", "fsharp", "vscode"] as const;
|
|
34
|
-
|
|
35
|
-
/** License and listing README are at the root of the package, their sources elsewhere in the repository. */
|
|
36
|
-
export const PACKAGE_ROOT_FILES: readonly (readonly [string, string])[] = [
|
|
37
|
-
["LICENSE", "LICENSE"],
|
|
38
|
-
["scripts/package/README.md", "README.md"],
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
const ROOT_FILE_SOURCES = new Set(PACKAGE_ROOT_FILES.map(([source]) => source));
|
|
42
|
-
|
|
43
|
-
const skippedInPackage = (relative: string): boolean => SKIPPED_IN_PACKAGE.test(relative) || ROOT_FILE_SOURCES.has(relative);
|
|
44
|
-
|
|
45
|
-
/** What the host loads at runtime: server, engine, the built web, the built-in bundles, plugin sources for contracts and the build tool, profiles and the scripts behind the subcommands; plus the types from declarationFolders. */
|
|
46
|
-
export const packageContents = (root = repositoryRoot): readonly string[] => [
|
|
47
|
-
"apps/server/package.json",
|
|
48
|
-
"apps/server/tsconfig.json",
|
|
49
|
-
"apps/server/tsconfig.plugin.json",
|
|
50
|
-
"apps/server/src",
|
|
51
|
-
"apps/web/package.json",
|
|
52
|
-
"apps/web/tsconfig.json",
|
|
53
|
-
"apps/web/tsconfig.plugin.json",
|
|
54
|
-
"apps/web/src",
|
|
55
|
-
"apps/web/dist",
|
|
56
|
-
...readdirSync(path.join(root, "packages"), { withFileTypes: true })
|
|
57
|
-
.filter((entry) => entry.isDirectory())
|
|
58
|
-
.flatMap((entry) => [`packages/${entry.name}/package.json`, `packages/${entry.name}/src`]),
|
|
59
|
-
"bundles",
|
|
60
|
-
"plugins",
|
|
61
|
-
...PACKAGED_PROFILES.map((profile) => `ragents.config.${profile}.ts`),
|
|
62
|
-
"scripts/agent",
|
|
63
|
-
"scripts/package",
|
|
64
|
-
"scripts/plugin",
|
|
65
|
-
"scripts/provision",
|
|
66
|
-
"scripts/remote",
|
|
67
|
-
"scripts/workspace-client",
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
/** The forked agent runtime runs from its sources, its types exist only built; the build tool checks third-party plugins against them. */
|
|
71
|
-
export const declarationFolders = (root = repositoryRoot): readonly string[] =>
|
|
72
|
-
readdirSync(path.join(root, "packages"), { withFileTypes: true })
|
|
73
|
-
.filter((entry) => entry.isDirectory())
|
|
74
|
-
.filter((entry) => {
|
|
75
|
-
const manifest = path.join(root, "packages", entry.name, "package.json");
|
|
76
|
-
const types = statSync(manifest, { throwIfNoEntry: false })?.isFile() ? (JSON.parse(readFileSync(manifest, "utf8")) as { types?: unknown }).types : undefined;
|
|
77
|
-
return typeof types === "string" && types.startsWith("./dist/");
|
|
78
|
-
})
|
|
79
|
-
.map((entry) => `packages/${entry.name}/dist`);
|
|
80
|
-
|
|
81
|
-
/** What the host executes, bundles into a mini-app or reads when building a plugin. */
|
|
82
|
-
const SCANNED = ["apps/server/src", "apps/web", "packages", "plugins", "scripts"];
|
|
83
|
-
const PACKAGE_NAME_PATTERN = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
84
|
-
const SCAN_SKIPPED = new Set(["node_modules", "dist"]);
|
|
85
|
-
const SCANNED_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".mjs", ".js", ".css"]);
|
|
86
|
-
const IMPORT_PATTERN = /\b(?:from|import|require(?:\.resolve)?)\s*\(?\s*["']([^"'\n]+)["']/g;
|
|
87
|
-
/** An executor contribution resolves packages from the host at runtime; the package must carry them too. */
|
|
88
|
-
const HOST_PACKAGE_PATTERN = /\bhostPackageFile\([^,()]*,\s*["']([^"'\n]+)["']/g;
|
|
89
|
-
|
|
90
|
-
const files = async (folder: string, skippedFolders: ReadonlySet<string> = SKIPPED_FOLDERS): Promise<readonly string[]> => {
|
|
91
|
-
const found: string[] = [];
|
|
92
|
-
for (const entry of (await readdir(folder, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
|
|
93
|
-
if (entry.isDirectory()) {
|
|
94
|
-
if (!skippedFolders.has(entry.name)) found.push(...await files(path.join(folder, entry.name), skippedFolders));
|
|
95
|
-
} else if (entry.isFile() && !SKIPPED_FILES.has(entry.name)) {
|
|
96
|
-
found.push(path.join(folder, entry.name));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return found;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const copyTo = async (source: string, target: string, packageRoot: string, skippedFolders?: ReadonlySet<string>): Promise<readonly string[]> => {
|
|
103
|
-
if (statSync(source).isFile()) {
|
|
104
|
-
if (skippedInPackage(packagePath(packageRoot, target))) return [];
|
|
105
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
106
|
-
await copyFile(source, target);
|
|
107
|
-
return [target];
|
|
108
|
-
}
|
|
109
|
-
const copied: string[] = [];
|
|
110
|
-
for (const file of await files(source, skippedFolders)) {
|
|
111
|
-
const destination = path.join(target, path.relative(source, file));
|
|
112
|
-
if (skippedInPackage(packagePath(packageRoot, destination))) continue;
|
|
113
|
-
await mkdir(path.dirname(destination), { recursive: true });
|
|
114
|
-
await copyFile(file, destination);
|
|
115
|
-
copied.push(destination);
|
|
116
|
-
}
|
|
117
|
-
return copied;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const scanRoots = (root: string): readonly string[] => SCANNED.map((entry) => path.join(root, entry));
|
|
121
|
-
|
|
122
|
-
const scannedFiles = (folder: string): readonly string[] => {
|
|
123
|
-
if (!statSync(folder, { throwIfNoEntry: false })?.isDirectory()) return [];
|
|
124
|
-
return readdirSync(folder, { withFileTypes: true }).flatMap((entry) => {
|
|
125
|
-
const absolute = path.join(folder, entry.name);
|
|
126
|
-
if (entry.isDirectory()) return SCAN_SKIPPED.has(entry.name) ? [] : scannedFiles(absolute);
|
|
127
|
-
return entry.isFile() && SCANNED_EXTENSIONS.has(path.extname(entry.name)) ? [absolute] : [];
|
|
128
|
-
});
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const packageNameOf = (specifier: string): string | undefined => {
|
|
132
|
-
if (/^(\.{1,2}\/|\/|node:|data:|@ragents\/|virtual:)/.test(specifier) || specifier === "vscode") return undefined;
|
|
133
|
-
const parts = specifier.split("/");
|
|
134
|
-
const name = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]!;
|
|
135
|
-
if (builtinModules.includes(name)) return undefined;
|
|
136
|
-
return PACKAGE_NAME_PATTERN.test(name) ? name : undefined;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const declaredWebDependencies = (packageDirectory: string): readonly string[] => {
|
|
140
|
-
const file = path.join(packageDirectory, "apps/web/package.json");
|
|
141
|
-
const declared = (JSON.parse(readFileSync(file, "utf8")) as { dependencies?: Record<string, string> }).dependencies ?? {};
|
|
142
|
-
return Object.keys(declared);
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
/** The packages that the executed part of the package actually imports, plus the libraries linked at runtime
|
|
146
|
-
* and those that the host's web offers to the web halves of third-party plugins. */
|
|
147
|
-
export const requiredPackages = (packageDirectory: string): ReadonlyMap<string, string> => {
|
|
148
|
-
const sources = new Map<string, string>();
|
|
149
|
-
for (const name of runtimeLibraries) sources.set(name, "plugins/ragents.actor-programs (linked at runtime)");
|
|
150
|
-
for (const name of declaredWebDependencies(packageDirectory)) sources.set(name, "apps/web/package.json (for the web halves of the plugins)");
|
|
151
|
-
for (const folder of scanRoots(packageDirectory)) {
|
|
152
|
-
for (const file of scannedFiles(folder)) {
|
|
153
|
-
const content = readFileSync(file, "utf8");
|
|
154
|
-
for (const match of [...content.matchAll(IMPORT_PATTERN), ...content.matchAll(HOST_PACKAGE_PATTERN)]) {
|
|
155
|
-
const name = packageNameOf(match[1]!);
|
|
156
|
-
if (name && !sources.has(name)) sources.set(name, path.relative(packageDirectory, file));
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return new Map([...sources].sort(([left], [right]) => left.localeCompare(right, "en")));
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const compareVersions = (left: string, right: string): number => {
|
|
164
|
-
const parts = (value: string) => value.split(/[.+-]/).map((part) => Number.isInteger(Number(part)) ? Number(part) : part);
|
|
165
|
-
const [first, second] = [parts(left), parts(right)];
|
|
166
|
-
for (let index = 0; index < Math.max(first.length, second.length); index += 1) {
|
|
167
|
-
const [one, other] = [first[index] ?? 0, second[index] ?? 0];
|
|
168
|
-
if (one === other) continue;
|
|
169
|
-
return typeof one === "number" && typeof other === "number" ? one - other : String(one).localeCompare(String(other), "en");
|
|
170
|
-
}
|
|
171
|
-
return 0;
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const anchors = (root: string): readonly string[] => [
|
|
175
|
-
path.join(root, "package.json"),
|
|
176
|
-
...["apps", "packages"].flatMap((group) => readdirSync(path.join(root, group), { withFileTypes: true })
|
|
177
|
-
.filter((entry) => entry.isDirectory())
|
|
178
|
-
.map((entry) => path.join(root, group, entry.name, "package.json"))
|
|
179
|
-
.filter((file) => statSync(file, { throwIfNoEntry: false })?.isFile() === true)),
|
|
180
|
-
];
|
|
181
|
-
|
|
182
|
-
/** The versions this checkout has installed for a package; the newest goes into the package. */
|
|
183
|
-
export const installedVersions = (name: string, root = repositoryRoot): readonly string[] => {
|
|
184
|
-
const found = new Set<string>();
|
|
185
|
-
for (const anchor of anchors(root)) {
|
|
186
|
-
for (let directory = path.dirname(anchor); ; directory = path.dirname(directory)) {
|
|
187
|
-
const file = path.join(directory, "node_modules", ...name.split("/"), "package.json");
|
|
188
|
-
if (statSync(file, { throwIfNoEntry: false })?.isFile()) {
|
|
189
|
-
found.add((JSON.parse(readFileSync(file, "utf8")) as { version: string }).version);
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
if (path.dirname(directory) === directory) break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
const versions = [...found].sort(compareVersions);
|
|
196
|
-
if (versions.length === 0) throw new Error(`The package ${name} is not installed in this checkout; without a version it cannot go into the package`);
|
|
197
|
-
return versions;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
export interface BuiltPackage {
|
|
201
|
-
readonly directory: string;
|
|
202
|
-
readonly manifest: Record<string, unknown>;
|
|
203
|
-
readonly fileCount: number;
|
|
204
|
-
readonly byteSize: number;
|
|
205
|
-
readonly flattened: readonly string[];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** The package ships the web prebuilt; it must be built and match its sources. */
|
|
209
|
-
const assertHostWeb = (root: string): void => {
|
|
210
|
-
const problem = hostWebProblem(hostWebDirectory(root), root, isCheckout(root));
|
|
211
|
-
if (problem) throw new Error(`${problem}; run pnpm build:web first`);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
/** Every built-in plugin must exist as a bundle and match its sources, otherwise the package starts no profile. */
|
|
215
|
-
const assertBuiltInBundles = (root: string): void => {
|
|
216
|
-
for (const folder of builtInPluginFolders(path.join(root, "plugins"))) {
|
|
217
|
-
const bundle = path.join(root, "bundles", path.basename(folder));
|
|
218
|
-
if (!statSync(bundle, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`The bundle ${bundle} is missing; run pnpm build:plugins first`);
|
|
219
|
-
if (readBundleManifest(bundle).sourceRevision !== sourceRevisionOf(folder)) throw new Error(`The bundle ${bundle} no longer matches ${folder}; run pnpm build:plugins first`);
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const copyDeclarations = async (root: string, target: string): Promise<readonly string[]> => {
|
|
224
|
-
const copied: string[] = [];
|
|
225
|
-
for (const folder of declarationFolders(root)) {
|
|
226
|
-
if (!statSync(path.join(root, folder), { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${folder} is missing; run pnpm build:agent first`);
|
|
227
|
-
for (const file of (await files(path.join(root, folder))).filter((name) => name.endsWith(".d.ts"))) {
|
|
228
|
-
const destination = path.join(target, path.relative(root, file));
|
|
229
|
-
await mkdir(path.dirname(destination), { recursive: true });
|
|
230
|
-
await copyFile(file, destination);
|
|
231
|
-
copied.push(destination);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return copied;
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
export const buildPackage = async (target: string, root = repositoryRoot): Promise<BuiltPackage> => {
|
|
238
|
-
assertBuiltInBundles(root);
|
|
239
|
-
assertHostWeb(root);
|
|
240
|
-
await rm(target, { recursive: true, force: true });
|
|
241
|
-
await mkdir(target, { recursive: true });
|
|
242
|
-
let fileCount = 0;
|
|
243
|
-
let byteSize = 0;
|
|
244
|
-
for (const entry of packageContents(root)) {
|
|
245
|
-
// A bundle is fully built and goes into the package completely, including an asset folder named dist.
|
|
246
|
-
const copied = await copyTo(path.join(root, entry), path.join(target, entry), target, entry === "bundles" ? new Set() : undefined);
|
|
247
|
-
fileCount += copied.length;
|
|
248
|
-
for (const file of copied) byteSize += statSync(file).size;
|
|
249
|
-
}
|
|
250
|
-
for (const folder of builtInPluginFolders(path.join(root, "plugins"))) {
|
|
251
|
-
const bundle = path.join(target, "bundles", path.basename(folder));
|
|
252
|
-
assertBundleRevision(bundle, readBundleManifest(bundle));
|
|
253
|
-
}
|
|
254
|
-
for (const file of await copyDeclarations(root, target)) {
|
|
255
|
-
fileCount += 1;
|
|
256
|
-
byteSize += statSync(file).size;
|
|
257
|
-
}
|
|
258
|
-
for (const [source, packaged] of PACKAGE_ROOT_FILES) {
|
|
259
|
-
const destination = path.join(target, packaged);
|
|
260
|
-
await mkdir(path.dirname(destination), { recursive: true });
|
|
261
|
-
await copyFile(path.join(root, source), destination);
|
|
262
|
-
fileCount += 1;
|
|
263
|
-
byteSize += statSync(destination).size;
|
|
264
|
-
}
|
|
265
|
-
const flattened: string[] = [];
|
|
266
|
-
const dependencies: Record<string, string> = {};
|
|
267
|
-
for (const [name] of requiredPackages(target)) {
|
|
268
|
-
const versions = installedVersions(name, root);
|
|
269
|
-
if (versions.length > 1) flattened.push(`${name}: ${versions.join(", ")} -> ${versions.at(-1)!}`);
|
|
270
|
-
dependencies[name] = versions.at(-1)!;
|
|
271
|
-
}
|
|
272
|
-
const manifest = {
|
|
273
|
-
name: PACKAGE_NAME,
|
|
274
|
-
version: readPackageVersion(root),
|
|
275
|
-
description: "RAgents host: server, engine, plugins and tools without a source checkout",
|
|
276
|
-
keywords: [...PACKAGE_KEYWORDS],
|
|
277
|
-
homepage: "https://schlenkr.github.io/RAgents/",
|
|
278
|
-
bugs: { url: `${REPOSITORY_URL}/issues` },
|
|
279
|
-
license: PACKAGE_LICENSE,
|
|
280
|
-
author: PACKAGE_AUTHOR,
|
|
281
|
-
repository: { type: "git", url: `git+${REPOSITORY_URL}.git` },
|
|
282
|
-
type: "module",
|
|
283
|
-
bin: { ragents: "scripts/package/ragents.mjs" },
|
|
284
|
-
engines: { node: ">=22.19.0" },
|
|
285
|
-
publishConfig: { access: "public" },
|
|
286
|
-
dependencies,
|
|
287
|
-
ragents: { hostVersion: readHostVersion(root) },
|
|
288
|
-
};
|
|
289
|
-
const manifestFile = path.join(target, "package.json");
|
|
290
|
-
await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
291
|
-
fileCount += 1;
|
|
292
|
-
byteSize += statSync(manifestFile).size;
|
|
293
|
-
return { directory: target, manifest, fileCount, byteSize, flattened };
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const main = async (): Promise<void> => {
|
|
297
|
-
const pack = process.argv.includes("--pack");
|
|
298
|
-
const unknown = process.argv.slice(2).filter((argument) => argument !== "--pack");
|
|
299
|
-
if (unknown.length) throw new Error(`Unknown argument: ${unknown.join(" ")} (allowed: --pack)`);
|
|
300
|
-
const target = path.join(repositoryRoot, "dist", PACKAGE_FOLDER);
|
|
301
|
-
const built = await buildPackage(target);
|
|
302
|
-
console.log(`== Package ${PACKAGE_NAME}@${built.manifest.version as string} in ${built.directory}`);
|
|
303
|
-
console.log(`== Host version ${(built.manifest.ragents as { hostVersion: string }).hostVersion}`);
|
|
304
|
-
console.log(`== ${built.fileCount} files, ${(built.byteSize / 1024 / 1024).toFixed(1)} MB, ${Object.keys(built.manifest.dependencies as object).length} dependencies`);
|
|
305
|
-
for (const [name, version] of Object.entries(built.manifest.dependencies as Record<string, string>)) console.log(` ${name}@${version}`);
|
|
306
|
-
for (const entry of built.flattened) console.log(`== Multiple versions flattened: ${entry}`);
|
|
307
|
-
if (!pack) return;
|
|
308
|
-
const packed = spawnSync("npm", ["pack", "--pack-destination", path.dirname(built.directory)], { cwd: built.directory, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
309
|
-
if (packed.status !== 0) throw new Error(`npm pack ended with code ${packed.status}`);
|
|
310
|
-
console.log(`== Archive ${path.join(path.dirname(built.directory), packed.stdout.trim().split("\n").at(-1) ?? "")}`);
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
const moduleUrl: string | undefined = import.meta.url;
|
|
314
|
-
if (moduleUrl && process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(moduleUrl)) {
|
|
315
|
-
main().catch((error: unknown) => {
|
|
316
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
317
|
-
process.exit(1);
|
|
318
|
-
});
|
|
319
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { compareVersions, readVersion, releaseVersion, versionLine, writeHostPackageVersion, writeVersion } from "../publish-version.ts";
|
|
5
|
-
import { buildPackage, PACKAGE_FOLDER, PACKAGE_NAME } from "./build-package.ts";
|
|
6
|
-
|
|
7
|
-
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
|
-
|
|
9
|
-
const usage = `Usage: pnpm publish:package [--dry-run]
|
|
10
|
-
Builds the package ${PACKAGE_NAME} and publishes it on npm. The token comes from the
|
|
11
|
-
environment variable npm_key. --dry-run passes the dry run through to npm and publishes nothing.
|
|
12
|
-
The version is in the root package.json; before building, the script raises the last place
|
|
13
|
-
above the last published version, in a dry run only in the output.`;
|
|
14
|
-
|
|
15
|
-
const REGISTRY_TOKEN_KEY = "npm_config_//registry.npmjs.org/:_authToken";
|
|
16
|
-
|
|
17
|
-
export interface NpmResult {
|
|
18
|
-
readonly status: number;
|
|
19
|
-
readonly stdout: string;
|
|
20
|
-
readonly stderr: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type NpmRunner = (args: readonly string[], options: { cwd: string; token?: string }) => NpmResult;
|
|
24
|
-
|
|
25
|
-
/** The token goes only as configuration into the child process's environment, never onto the command line. */
|
|
26
|
-
export const publishEnvironment = (token: string, base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv =>
|
|
27
|
-
({ ...base, [REGISTRY_TOKEN_KEY]: token });
|
|
28
|
-
|
|
29
|
-
/** Everything that comes from npm goes through this mask so that the token does not show up even in an error message. */
|
|
30
|
-
export const redacting = (token: string) => (text: string): string => token ? text.split(token).join("<npm_key>") : text;
|
|
31
|
-
|
|
32
|
-
/** An error message is one line; npm's output is therefore moved onto one. */
|
|
33
|
-
export const oneLine = (text: string): string => text.split("\n").map((line) => line.trim()).filter(Boolean).join("; ");
|
|
34
|
-
|
|
35
|
-
const runNpm: NpmRunner = (args, options) => {
|
|
36
|
-
const result = spawnSync("npm", [...args], {
|
|
37
|
-
cwd: options.cwd,
|
|
38
|
-
encoding: "utf8",
|
|
39
|
-
env: options.token ? publishEnvironment(options.token) : process.env,
|
|
40
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
41
|
-
});
|
|
42
|
-
if (result.error) throw new Error(`npm ${args[0]} could not be started: ${result.error.message}`);
|
|
43
|
-
return { status: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/** The versions already on npm; a package unknown there is the empty list, not an error. */
|
|
47
|
-
export const publishedVersions = (name: string, npm: NpmRunner): readonly string[] => {
|
|
48
|
-
const result = npm(["view", name, "versions", "--json"], { cwd: repositoryRoot });
|
|
49
|
-
if (result.status !== 0) {
|
|
50
|
-
if (/E404|404 Not Found/.test(`${result.stdout}\n${result.stderr}`)) return [];
|
|
51
|
-
throw new Error(`npm view ${name} versions ended with code ${result.status}: ${oneLine(result.stderr)}`);
|
|
52
|
-
}
|
|
53
|
-
const parsed = JSON.parse(result.stdout.trim() || "[]") as string | readonly string[];
|
|
54
|
-
return typeof parsed === "string" ? [parsed] : parsed;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/** The highest version on npm; a package unknown there has none. */
|
|
58
|
-
export const latestPublishedVersion = (npm: NpmRunner = runNpm, name = PACKAGE_NAME): string | undefined =>
|
|
59
|
-
[...publishedVersions(name, npm)].sort(compareVersions).at(-1);
|
|
60
|
-
|
|
61
|
-
export interface PublishPlan {
|
|
62
|
-
readonly name: string;
|
|
63
|
-
readonly version: string;
|
|
64
|
-
readonly hostVersion: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** What must be right before a publish: the built manifest and a version that does not yet exist on npm. */
|
|
68
|
-
export const publishPlan = (manifest: Record<string, unknown>, published: readonly string[], version: string): PublishPlan => {
|
|
69
|
-
const { name } = manifest as { name?: unknown };
|
|
70
|
-
const access = (manifest.publishConfig as { access?: unknown } | undefined)?.access;
|
|
71
|
-
const hostVersion = (manifest.ragents as { hostVersion?: unknown } | undefined)?.hostVersion;
|
|
72
|
-
if (name !== PACKAGE_NAME) throw new Error(`The built package is named ${String(name)}, but ${PACKAGE_NAME} is published`);
|
|
73
|
-
if (access !== "public") throw new Error(`${PACKAGE_NAME} has a scope and needs publishConfig.access "public", not ${JSON.stringify(access)}`);
|
|
74
|
-
if (!/^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?$/.test(version)) {
|
|
75
|
-
throw new Error(`The version ${JSON.stringify(version)} is not a version; it is the version in the root package.json`);
|
|
76
|
-
}
|
|
77
|
-
if (typeof hostVersion !== "string" || !/^[0-9a-f]{40}$/.test(hostVersion)) {
|
|
78
|
-
throw new Error("The package lacks ragents.hostVersion as a full Git commit; it is built only from a checkout");
|
|
79
|
-
}
|
|
80
|
-
if (published.includes(version)) {
|
|
81
|
-
throw new Error(`${PACKAGE_NAME}@${version} is already on npm (there: ${published.join(", ")}); `
|
|
82
|
-
+ "raise version in the root package.json and rebuild");
|
|
83
|
-
}
|
|
84
|
-
return { name: PACKAGE_NAME, version, hostVersion };
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
export interface PublishOptions {
|
|
88
|
-
readonly directory: string;
|
|
89
|
-
readonly version: string;
|
|
90
|
-
readonly token: string;
|
|
91
|
-
readonly dryRun: boolean;
|
|
92
|
-
readonly npm: NpmRunner;
|
|
93
|
-
readonly log: (line: string) => void;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** npm reports a missing organization as 404; without the hint you look for the error in the package. */
|
|
97
|
-
const scopeAdvice = (output: string): string => /Scope not found/i.test(output)
|
|
98
|
-
? ` The organization ${PACKAGE_NAME.split("/")[0]} does not exist on npm or the token may not use it: `
|
|
99
|
-
+ "create it on npmjs.com or grant the token access to the scope."
|
|
100
|
-
: "";
|
|
101
|
-
|
|
102
|
-
/** Publishes the built folder; exit 0 of npm publish is the confirmation. */
|
|
103
|
-
export const publishDirectory = (options: PublishOptions): void => {
|
|
104
|
-
const hide = redacting(options.token);
|
|
105
|
-
const published = options.npm(["publish", "--access", "public", ...options.dryRun ? ["--dry-run"] : []], { cwd: options.directory, token: options.token });
|
|
106
|
-
const output = `${published.stdout}${published.stderr}`;
|
|
107
|
-
for (const line of output.split("\n")) if (line.trim()) options.log(hide(line));
|
|
108
|
-
if (published.status !== 0) throw new Error(`npm publish ended with code ${published.status}.${scopeAdvice(output)}`);
|
|
109
|
-
if (options.dryRun) {
|
|
110
|
-
options.log("== Dry run: nothing published");
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
options.log(`== Published: ${PACKAGE_NAME}@${options.version}`);
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const main = async (): Promise<void> => {
|
|
117
|
-
const dryRun = process.argv.includes("--dry-run");
|
|
118
|
-
const unknown = process.argv.slice(2).filter((argument) => argument !== "--dry-run");
|
|
119
|
-
if (unknown.length) throw new Error(`Unknown argument: ${unknown.join(" ")}\n${usage}`);
|
|
120
|
-
const token = process.env.npm_key;
|
|
121
|
-
if (!token) throw new Error(`npm_key is missing: the npm token with write access to ${PACKAGE_NAME}.\n${usage}`);
|
|
122
|
-
const manifestFile = path.join(repositoryRoot, "package.json");
|
|
123
|
-
const published = publishedVersions(PACKAGE_NAME, runNpm);
|
|
124
|
-
const extensionManifestFile = path.join(repositoryRoot, "apps", "vscode", "package.json");
|
|
125
|
-
const next = releaseVersion([readVersion(manifestFile), readVersion(extensionManifestFile)], published);
|
|
126
|
-
console.log(versionLine(next));
|
|
127
|
-
if (!dryRun) {
|
|
128
|
-
writeVersion(manifestFile, next.version);
|
|
129
|
-
writeVersion(extensionManifestFile, next.version);
|
|
130
|
-
writeHostPackageVersion(extensionManifestFile, next.version);
|
|
131
|
-
}
|
|
132
|
-
const built = await buildPackage(path.join(repositoryRoot, "dist", PACKAGE_FOLDER));
|
|
133
|
-
const plan = publishPlan(built.manifest, published, next.version);
|
|
134
|
-
console.log(`== ${plan.name}@${plan.version} from host ${plan.hostVersion.slice(0, 12)}, ${built.fileCount} files, ${(built.byteSize / 1024 / 1024).toFixed(1)} MB`);
|
|
135
|
-
publishDirectory({ directory: built.directory, version: plan.version, token, dryRun, npm: runNpm, log: (line) => console.log(line) });
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const moduleUrl: string | undefined = import.meta.url;
|
|
139
|
-
if (moduleUrl && process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(moduleUrl)) {
|
|
140
|
-
main().catch((error: unknown) => {
|
|
141
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
142
|
-
process.exit(1);
|
|
143
|
-
});
|
|
144
|
-
}
|