@pi-ohm/references 0.6.4-dev.27705031416.1.1d8069f

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.
@@ -0,0 +1,180 @@
1
+ import { parseRemoteRepositoryReference, repositoryCachePath, validateBranch } from "./repository.js";
2
+ import { defaultReferencesCacheRoot, ensureRepository } from "./cache.js";
3
+ import path from "node:path";
4
+ import { Result } from "better-result";
5
+ import os from "node:os";
6
+
7
+ //#region src/references.ts
8
+ function expandHome(value) {
9
+ if (value === "~") return os.homedir();
10
+ if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
11
+ return value;
12
+ }
13
+ function resolveLocalPath(cwd, value) {
14
+ const expanded = expandHome(value);
15
+ if (path.isAbsolute(expanded)) return path.normalize(expanded);
16
+ return path.resolve(cwd, expanded);
17
+ }
18
+ function stringEntryIsLocal(value) {
19
+ return value.startsWith(".") || value.startsWith("/") || value.startsWith("~");
20
+ }
21
+ function entryDescription(entry) {
22
+ return entry.description;
23
+ }
24
+ function entryHidden(entry) {
25
+ return entry.hidden;
26
+ }
27
+ function resolveEntry(input) {
28
+ if (typeof input.entry === "string" && stringEntryIsLocal(input.entry)) {
29
+ const source = {
30
+ type: "local",
31
+ path: resolveLocalPath(input.cwd, input.entry)
32
+ };
33
+ return Result.ok({
34
+ name: input.name,
35
+ path: source.path,
36
+ source
37
+ });
38
+ }
39
+ if (typeof input.entry !== "string" && "path" in input.entry) {
40
+ const source = {
41
+ type: "local",
42
+ path: resolveLocalPath(input.cwd, input.entry.path),
43
+ ...entryDescription(input.entry) ? { description: entryDescription(input.entry) } : {},
44
+ ...entryHidden(input.entry) !== undefined ? { hidden: entryHidden(input.entry) } : {}
45
+ };
46
+ return Result.ok({
47
+ name: input.name,
48
+ path: source.path,
49
+ ...source.description ? { description: source.description } : {},
50
+ ...source.hidden !== undefined ? { hidden: source.hidden } : {},
51
+ source
52
+ });
53
+ }
54
+ const repository = typeof input.entry === "string" ? input.entry : input.entry.repository;
55
+ const branch = typeof input.entry === "string" ? undefined : input.entry.branch;
56
+ const description = typeof input.entry === "string" ? undefined : input.entry.description;
57
+ const hidden = typeof input.entry === "string" ? undefined : input.entry.hidden;
58
+ const parsed = parseRemoteRepositoryReference(repository);
59
+ if (Result.isError(parsed)) {
60
+ return Result.err({
61
+ name: input.name,
62
+ message: parsed.error.message,
63
+ cause: parsed.error
64
+ });
65
+ }
66
+ if (branch) {
67
+ const validated = validateBranch(branch);
68
+ if (Result.isError(validated)) {
69
+ return Result.err({
70
+ name: input.name,
71
+ message: validated.error.message,
72
+ cause: validated.error
73
+ });
74
+ }
75
+ }
76
+ const source = {
77
+ type: "git",
78
+ repository,
79
+ ...branch ? { branch } : {},
80
+ ...description ? { description } : {},
81
+ ...hidden !== undefined ? { hidden } : {}
82
+ };
83
+ return Result.ok({
84
+ name: input.name,
85
+ path: repositoryCachePath(input.cacheRoot, parsed.value),
86
+ ...description ? { description } : {},
87
+ ...hidden !== undefined ? { hidden } : {},
88
+ source
89
+ });
90
+ }
91
+ function resolveConfiguredReferences(input) {
92
+ const cacheRoot = input.cacheRoot ?? defaultReferencesCacheRoot();
93
+ return Object.entries(input.config).map(([name, entry]) => resolveEntry({
94
+ cwd: input.cwd,
95
+ cacheRoot,
96
+ name,
97
+ entry
98
+ })).reduce((state, result) => {
99
+ if (Result.isOk(result)) {
100
+ return {
101
+ references: [...state.references, result.value],
102
+ diagnostics: state.diagnostics
103
+ };
104
+ }
105
+ return {
106
+ references: state.references,
107
+ diagnostics: [...state.diagnostics, result.error]
108
+ };
109
+ }, {
110
+ references: [],
111
+ diagnostics: []
112
+ });
113
+ }
114
+ function renderReferenceGuidance(references) {
115
+ const available = [...references].filter((reference) => reference.description !== undefined).sort((left, right) => left.name.localeCompare(right.name));
116
+ if (available.length === 0) return undefined;
117
+ return [
118
+ "Project references provide additional directories that can be accessed when relevant.",
119
+ "<available_references>",
120
+ ...available.flatMap((reference) => [
121
+ " <reference>",
122
+ ` <name>${reference.name}</name>`,
123
+ ` <path>${reference.path}</path>`,
124
+ ` <description>${reference.description}</description>`,
125
+ " </reference>"
126
+ ]),
127
+ "</available_references>"
128
+ ].join("\n");
129
+ }
130
+ async function materializeGitReferences(input) {
131
+ const cacheRoot = input.cacheRoot ?? defaultReferencesCacheRoot();
132
+ const materialized = await Promise.all(input.references.flatMap((reference) => {
133
+ if (reference.source.type !== "git") return [];
134
+ const source = reference.source;
135
+ return [(async () => {
136
+ const parsed = parseRemoteRepositoryReference(source.repository);
137
+ if (Result.isError(parsed)) {
138
+ return Result.err({
139
+ name: reference.name,
140
+ message: parsed.error.message,
141
+ cause: parsed.error
142
+ });
143
+ }
144
+ const result = await ensureRepository({
145
+ pi: input.pi,
146
+ reference: parsed.value,
147
+ branch: source.branch,
148
+ refresh: true,
149
+ root: cacheRoot,
150
+ ...input.signal ? { signal: input.signal } : {}
151
+ });
152
+ if (Result.isError(result)) {
153
+ return Result.err({
154
+ name: reference.name,
155
+ message: result.error.message,
156
+ cause: result.error
157
+ });
158
+ }
159
+ return Result.ok(result.value);
160
+ })()];
161
+ }));
162
+ return materialized.reduce((state, result) => {
163
+ if (Result.isOk(result)) {
164
+ return {
165
+ results: [...state.results, result.value],
166
+ diagnostics: state.diagnostics
167
+ };
168
+ }
169
+ return {
170
+ results: state.results,
171
+ diagnostics: [...state.diagnostics, result.error]
172
+ };
173
+ }, {
174
+ results: [],
175
+ diagnostics: []
176
+ });
177
+ }
178
+
179
+ //#endregion
180
+ export { materializeGitReferences, renderReferenceGuidance, resolveConfiguredReferences };
@@ -0,0 +1,40 @@
1
+ import { Result } from "better-result";
2
+
3
+ //#region src/repository.d.ts
4
+ type ReferenceErrorCode = "invalid_repository" | "unsupported_file_repository" | "invalid_branch" | "cache_operation_failed" | "git_command_failed";
5
+ declare const ReferencesError_base: import("better-result").TaggedErrorClass<"ReferencesError", {
6
+ readonly code: ReferenceErrorCode;
7
+ readonly message: string;
8
+ readonly repository?: string;
9
+ readonly branch?: string;
10
+ readonly path?: string;
11
+ readonly cause?: unknown;
12
+ }>;
13
+ declare class ReferencesError extends ReferencesError_base {}
14
+ interface BaseRepositoryReference {
15
+ readonly host: string;
16
+ readonly path: string;
17
+ readonly segments: readonly string[];
18
+ readonly owner?: string;
19
+ readonly repo: string;
20
+ readonly remote: string;
21
+ readonly label: string;
22
+ }
23
+ interface RemoteRepositoryReference extends BaseRepositoryReference {
24
+ readonly type: "remote";
25
+ readonly protocol?: string;
26
+ }
27
+ interface FileRepositoryReference extends BaseRepositoryReference {
28
+ readonly type: "file";
29
+ readonly host: "file";
30
+ readonly protocol: "file:";
31
+ }
32
+ type RepositoryReference = RemoteRepositoryReference | FileRepositoryReference;
33
+ declare function parseRepositoryReference(input: string): Result<RepositoryReference, ReferencesError>;
34
+ declare function parseRemoteRepositoryReference(input: string): Result<RemoteRepositoryReference, ReferencesError>;
35
+ declare function validateBranch(branch: string): Result<string, ReferencesError>;
36
+ declare function repositoryCachePath(root: string, reference: RepositoryReference): string;
37
+ declare function repositoryCacheIdentity(reference: RepositoryReference): string;
38
+ declare function sameRepositoryReference(left: RepositoryReference, right: RepositoryReference): boolean;
39
+ //#endregion
40
+ export { FileRepositoryReference, ReferenceErrorCode, ReferencesError, RemoteRepositoryReference, RepositoryReference, parseRemoteRepositoryReference, parseRepositoryReference, repositoryCacheIdentity, repositoryCachePath, sameRepositoryReference, validateBranch };
@@ -0,0 +1,165 @@
1
+ import path from "node:path";
2
+ import { Result, TaggedError } from "better-result";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ //#region src/repository.ts
6
+ var ReferencesError = class extends TaggedError("ReferencesError")() {};
7
+ function error(input) {
8
+ return new ReferencesError(input);
9
+ }
10
+ function normalizeInput(input) {
11
+ return input.trim().replace(/^git\+/, "").replace(/#.*$/, "").replace(/\/+$/, "");
12
+ }
13
+ function trimGitSuffix(input) {
14
+ return input.replace(/\.git$/, "");
15
+ }
16
+ function parts(input) {
17
+ return input.split("/").map((item) => trimGitSuffix(item.trim())).filter((item) => item.length > 0);
18
+ }
19
+ function safeHost(input) {
20
+ return input.length > 0 && !input.startsWith("-") && !/[\s/\\]/.test(input);
21
+ }
22
+ function safeSegment(input) {
23
+ return input !== "." && input !== ".." && !input.includes(":") && !/[\s/\\]/.test(input);
24
+ }
25
+ function hostLike(input) {
26
+ return input.includes(".") || input.includes(":") || input === "localhost";
27
+ }
28
+ function withSlash(input) {
29
+ if (input.endsWith("/")) return input;
30
+ return `${input}/`;
31
+ }
32
+ function githubRemote(pathname) {
33
+ const base = process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL;
34
+ if (!base) return `https://github.com/${pathname}.git`;
35
+ return new URL(`${pathname}.git`, withSlash(base)).href;
36
+ }
37
+ function invalidRepository(repository) {
38
+ return error({
39
+ code: "invalid_repository",
40
+ repository,
41
+ message: "Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand"
42
+ });
43
+ }
44
+ function buildRemote(input) {
45
+ const segments = input.segments.map(trimGitSuffix).filter((segment) => segment.length > 0);
46
+ if (!safeHost(input.host) || segments.length === 0 || segments.some((segment) => !safeSegment(segment))) {
47
+ return Result.err(invalidRepository(input.remote ?? input.host));
48
+ }
49
+ const repositoryPath = segments.join("/");
50
+ const host = input.host.toLowerCase();
51
+ const repo = segments.at(-1);
52
+ if (!repo) return Result.err(invalidRepository(input.remote ?? input.host));
53
+ return Result.ok({
54
+ type: "remote",
55
+ host,
56
+ path: repositoryPath,
57
+ segments,
58
+ ...segments.length === 2 ? { owner: segments[0] } : {},
59
+ repo,
60
+ remote: input.remote ?? (host === "github.com" ? githubRemote(repositoryPath) : `https://${host}/${repositoryPath}.git`),
61
+ label: host === "github.com" && segments.length === 2 ? repositoryPath : `${host}/${repositoryPath}`,
62
+ ...input.protocol ? { protocol: input.protocol } : {}
63
+ });
64
+ }
65
+ function buildFile(input) {
66
+ const filePath = path.normalize(fileURLToPath(input.url));
67
+ const segments = filePath.split(/[\\/]+/).filter((segment) => segment.length > 0);
68
+ const repo = segments.at(-1);
69
+ if (!repo) return Result.err(invalidRepository(input.remote));
70
+ return Result.ok({
71
+ type: "file",
72
+ host: "file",
73
+ path: filePath,
74
+ segments: segments.map((segment) => segment.replace(/:$/, "")),
75
+ repo: trimGitSuffix(repo),
76
+ remote: input.remote,
77
+ label: filePath,
78
+ protocol: "file:"
79
+ });
80
+ }
81
+ function parseRepositoryReference(input) {
82
+ const cleaned = normalizeInput(input);
83
+ if (cleaned.length === 0) return Result.err(invalidRepository(input));
84
+ const githubPrefixed = cleaned.match(/^github:([^/\s]+)\/([^/\s]+)$/);
85
+ const githubOwner = githubPrefixed?.[1];
86
+ const githubRepo = githubPrefixed?.[2];
87
+ if (githubOwner && githubRepo) {
88
+ return buildRemote({
89
+ host: "github.com",
90
+ segments: [githubOwner, githubRepo]
91
+ });
92
+ }
93
+ if (!cleaned.includes("://")) {
94
+ const scp = cleaned.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
95
+ const scpHost = scp?.[1];
96
+ const scpPath = scp?.[2];
97
+ if (scpHost && scpPath) {
98
+ return buildRemote({
99
+ host: scpHost,
100
+ segments: parts(scpPath),
101
+ remote: cleaned
102
+ });
103
+ }
104
+ const direct = parts(cleaned);
105
+ const first = direct[0];
106
+ if (direct.length >= 2 && first && hostLike(first)) {
107
+ return buildRemote({
108
+ host: first,
109
+ segments: direct.slice(1)
110
+ });
111
+ }
112
+ if (direct.length === 2) return buildRemote({
113
+ host: "github.com",
114
+ segments: direct
115
+ });
116
+ }
117
+ const parsed = Result.try({
118
+ try: () => new URL(cleaned),
119
+ catch: (cause) => cause
120
+ });
121
+ if (Result.isError(parsed)) return Result.err(invalidRepository(input));
122
+ if (parsed.value.protocol === "file:") return buildFile({
123
+ url: parsed.value,
124
+ remote: cleaned
125
+ });
126
+ const segments = parts(parsed.value.pathname);
127
+ return buildRemote({
128
+ host: parsed.value.host,
129
+ segments,
130
+ remote: parsed.value.host === "github.com" ? githubRemote(segments.join("/")) : cleaned,
131
+ protocol: parsed.value.protocol
132
+ });
133
+ }
134
+ function parseRemoteRepositoryReference(input) {
135
+ const reference = parseRepositoryReference(input);
136
+ if (Result.isError(reference)) return Result.err(reference.error);
137
+ if (reference.value.type === "remote") return Result.ok(reference.value);
138
+ return Result.err(error({
139
+ code: "unsupported_file_repository",
140
+ repository: input,
141
+ message: "Local file repositories are not supported through repository; use path instead"
142
+ }));
143
+ }
144
+ function validateBranch(branch) {
145
+ if (/^[A-Za-z0-9/_.-]+$/.test(branch) && !branch.startsWith("-") && !branch.includes("..")) {
146
+ return Result.ok(branch);
147
+ }
148
+ return Result.err(error({
149
+ code: "invalid_branch",
150
+ branch,
151
+ message: "Branch must contain only alphanumeric characters, /, _, ., and -, and cannot start with - or contain .."
152
+ }));
153
+ }
154
+ function repositoryCachePath(root, reference) {
155
+ return path.join(root, ...reference.host.split(":"), ...reference.segments);
156
+ }
157
+ function repositoryCacheIdentity(reference) {
158
+ return `${reference.host}/${reference.path}`;
159
+ }
160
+ function sameRepositoryReference(left, right) {
161
+ return repositoryCacheIdentity(left) === repositoryCacheIdentity(right);
162
+ }
163
+
164
+ //#endregion
165
+ export { ReferencesError, parseRemoteRepositoryReference, parseRepositoryReference, repositoryCacheIdentity, repositoryCachePath, sameRepositoryReference, validateBranch };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@pi-ohm/references",
3
+ "version": "0.6.4-dev.27705031416.1.1d8069f",
4
+ "homepage": "https://github.com/pi-ohm/pi-ohm/tree/dev/packages/references#readme",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/pi-ohm/pi-ohm.git",
8
+ "directory": "packages/references"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "README.md",
13
+ "SPEC.md"
14
+ ],
15
+ "type": "module",
16
+ "main": "dist/extension.js",
17
+ "types": "dist/extension.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/extension.d.ts",
21
+ "default": "./dist/extension.js"
22
+ },
23
+ "./config": {
24
+ "types": "./dist/config.d.ts",
25
+ "default": "./dist/config.js"
26
+ },
27
+ "./repository": {
28
+ "types": "./dist/repository.d.ts",
29
+ "default": "./dist/repository.js"
30
+ },
31
+ "./cache": {
32
+ "types": "./dist/cache.d.ts",
33
+ "default": "./dist/cache.js"
34
+ },
35
+ "./references": {
36
+ "types": "./dist/references.d.ts",
37
+ "default": "./dist/references.js"
38
+ }
39
+ },
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "provenance": true
43
+ },
44
+ "scripts": {
45
+ "build": "vp pack src/extension.ts src/config.ts src/repository.ts src/cache.ts src/references.ts"
46
+ },
47
+ "dependencies": {
48
+ "@earendil-works/pi-coding-agent": "0.79.4",
49
+ "@earendil-works/pi-tui": "0.79.4",
50
+ "@pi-ohm/core": "0.6.4-dev.27705031416.1.1d8069f",
51
+ "better-result": "2.9.2",
52
+ "typebox": "^1.0.68"
53
+ },
54
+ "peerDependencies": {
55
+ "@earendil-works/pi-coding-agent": "0.79.4",
56
+ "@earendil-works/pi-tui": "0.79.4"
57
+ },
58
+ "pi": {
59
+ "extensions": [
60
+ "./dist/extension.js"
61
+ ]
62
+ }
63
+ }