zyrk 0.2.3 → 0.3.7
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 +75 -22
- package/VERSION +1 -1
- package/package.json +4 -4
- package/src/cli/main.ts +87 -2
- package/src/commands/dependencies.ts +154 -0
- package/src/commands/deploy.ts +777 -0
- package/src/commands/maintenance.ts +260 -0
- package/src/commands/patch.ts +408 -0
- package/src/commands/project.ts +598 -0
- package/src/commands/store.ts +689 -0
- package/src/install/import-npm.ts +177 -0
- package/src/install/lock.ts +176 -0
- package/src/install/metadata.ts +122 -0
- package/src/install/registry.ts +88 -0
- package/src/install/run.ts +760 -0
- package/src/lockfile/types.ts +5 -0
- package/src/lockfile/write.ts +61 -0
- package/src/project/projection.ts +172 -10
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** Adopt npm v2/v3 exact selections without asking the registry to pick versions. */
|
|
2
|
+
import { valid, satisfies } from "semver";
|
|
3
|
+
import type { NpmLockDoc } from "../lockfile/types.ts";
|
|
4
|
+
import type {
|
|
5
|
+
FinalizedGraph,
|
|
6
|
+
ResolvedOccurrence,
|
|
7
|
+
} from "../resolve/resolver.ts";
|
|
8
|
+
import { parseRequirement } from "../resolve/spec.ts";
|
|
9
|
+
import { sourceIdOf, sha512Hex } from "../util/hash.ts";
|
|
10
|
+
import { cleanUrl, lockMismatch, packageName } from "./lock.ts";
|
|
11
|
+
|
|
12
|
+
export function importNpmLock(
|
|
13
|
+
doc: NpmLockDoc,
|
|
14
|
+
requirements: Record<string, string>,
|
|
15
|
+
origin: string,
|
|
16
|
+
): FinalizedGraph {
|
|
17
|
+
const packages = doc.packages;
|
|
18
|
+
if (!packages || !packages[""])
|
|
19
|
+
throw lockMismatch("npm lockfile lacks an importer manifest");
|
|
20
|
+
const locations = new Map<string, ResolvedOccurrence>();
|
|
21
|
+
const find = (from: string, name: string): string | undefined => {
|
|
22
|
+
let current = from;
|
|
23
|
+
for (;;) {
|
|
24
|
+
if (!current.endsWith("node_modules")) {
|
|
25
|
+
const candidate = `${current ? `${current}/` : ""}node_modules/${name}`;
|
|
26
|
+
if (packages[candidate]) return candidate;
|
|
27
|
+
}
|
|
28
|
+
if (!current) return undefined;
|
|
29
|
+
current = current.includes("/")
|
|
30
|
+
? current.slice(0, current.lastIndexOf("/"))
|
|
31
|
+
: "";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const load = (
|
|
35
|
+
location: string,
|
|
36
|
+
edgeName: string,
|
|
37
|
+
spec: string,
|
|
38
|
+
): ResolvedOccurrence => {
|
|
39
|
+
const existing = locations.get(location);
|
|
40
|
+
const entry = packages[location];
|
|
41
|
+
if (!entry || entry.link === true || entry.inBundle === true)
|
|
42
|
+
throw lockMismatch(
|
|
43
|
+
"npm lock contains an unsupported linked or bundled selection",
|
|
44
|
+
);
|
|
45
|
+
const request = parseRequirement(edgeName, spec);
|
|
46
|
+
const name = typeof entry.name === "string" ? entry.name : request.realName;
|
|
47
|
+
if (
|
|
48
|
+
!packageName(name) ||
|
|
49
|
+
name !== request.realName ||
|
|
50
|
+
typeof entry.version !== "string" ||
|
|
51
|
+
valid(entry.version) === null ||
|
|
52
|
+
typeof entry.resolved !== "string" ||
|
|
53
|
+
!cleanUrl(entry.resolved) ||
|
|
54
|
+
typeof entry.integrity !== "string" ||
|
|
55
|
+
!/^sha512-[A-Za-z0-9+/]{86}==$/.test(entry.integrity)
|
|
56
|
+
)
|
|
57
|
+
throw lockMismatch(
|
|
58
|
+
"npm selection lacks a valid name, version, archive, or SHA-512 integrity",
|
|
59
|
+
);
|
|
60
|
+
if (
|
|
61
|
+
request.targetKind !== "tag" &&
|
|
62
|
+
!satisfies(entry.version, request.targetSpec)
|
|
63
|
+
)
|
|
64
|
+
throw lockMismatch("npm selection disagrees with a manifest requirement");
|
|
65
|
+
// Every incoming edge must satisfy its requirement, including a cycle or
|
|
66
|
+
// a location already selected by a different dependency.
|
|
67
|
+
if (existing) return existing;
|
|
68
|
+
const node: ResolvedOccurrence = {
|
|
69
|
+
key: `${name}@${entry.version}#${sha512Hex(location).slice(0, 16)}`,
|
|
70
|
+
name,
|
|
71
|
+
version: entry.version,
|
|
72
|
+
origin,
|
|
73
|
+
integrity: entry.integrity,
|
|
74
|
+
sourceId: sourceIdOf({
|
|
75
|
+
sourceType: "registry",
|
|
76
|
+
name,
|
|
77
|
+
version: entry.version,
|
|
78
|
+
origin,
|
|
79
|
+
integrity: entry.integrity,
|
|
80
|
+
}),
|
|
81
|
+
tarballUrl: entry.resolved,
|
|
82
|
+
bin: {},
|
|
83
|
+
platform: {},
|
|
84
|
+
edges: [],
|
|
85
|
+
peerBindings: [],
|
|
86
|
+
};
|
|
87
|
+
if (entry.engines !== undefined) {
|
|
88
|
+
if (
|
|
89
|
+
typeof entry.engines !== "object" ||
|
|
90
|
+
entry.engines === null ||
|
|
91
|
+
Array.isArray(entry.engines) ||
|
|
92
|
+
Object.values(entry.engines).some((value) => typeof value !== "string")
|
|
93
|
+
)
|
|
94
|
+
throw lockMismatch("invalid npm engine constraint");
|
|
95
|
+
node.engines = entry.engines as Record<string, string>;
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
typeof entry.bin === "object" &&
|
|
99
|
+
entry.bin !== null &&
|
|
100
|
+
!Array.isArray(entry.bin)
|
|
101
|
+
) {
|
|
102
|
+
for (const [key, value] of Object.entries(entry.bin)) {
|
|
103
|
+
if (typeof value !== "string")
|
|
104
|
+
throw lockMismatch("invalid npm binary entry");
|
|
105
|
+
node.bin[key] = value;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const field of ["os", "cpu", "libc"] as const) {
|
|
109
|
+
const value = entry[field];
|
|
110
|
+
if (value !== undefined) {
|
|
111
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === "string"))
|
|
112
|
+
throw lockMismatch("invalid npm platform constraint");
|
|
113
|
+
node.platform[field] = value as string[];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
locations.set(location, node);
|
|
117
|
+
const dependencies: Record<
|
|
118
|
+
string,
|
|
119
|
+
{ spec: string; kind: "prod" | "optional" | "peer" }
|
|
120
|
+
> = Object.create(null) as Record<
|
|
121
|
+
string,
|
|
122
|
+
{ spec: string; kind: "prod" | "optional" | "peer" }
|
|
123
|
+
>;
|
|
124
|
+
for (const [group, kind] of [
|
|
125
|
+
["dependencies", "prod"],
|
|
126
|
+
["optionalDependencies", "optional"],
|
|
127
|
+
["peerDependencies", "peer"],
|
|
128
|
+
] as const) {
|
|
129
|
+
const entries = entry[group];
|
|
130
|
+
if (entries === undefined) continue;
|
|
131
|
+
if (
|
|
132
|
+
typeof entries !== "object" ||
|
|
133
|
+
entries === null ||
|
|
134
|
+
Array.isArray(entries)
|
|
135
|
+
)
|
|
136
|
+
throw lockMismatch("invalid npm dependency declarations");
|
|
137
|
+
for (const [dep, range] of Object.entries(entries)) {
|
|
138
|
+
if (!packageName(dep) || typeof range !== "string")
|
|
139
|
+
throw lockMismatch("invalid npm dependency requirement");
|
|
140
|
+
dependencies[dep] = { spec: range, kind };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const [dep, requirement] of Object.entries(dependencies)) {
|
|
144
|
+
const targetLocation = find(location, dep);
|
|
145
|
+
const meta = entry.peerDependenciesMeta as
|
|
146
|
+
Record<string, { optional?: boolean }> | undefined;
|
|
147
|
+
const optionalPeer =
|
|
148
|
+
requirement.kind === "peer" && meta?.[dep]?.optional === true;
|
|
149
|
+
if (!targetLocation) {
|
|
150
|
+
if (requirement.kind === "optional" || optionalPeer) {
|
|
151
|
+
if (optionalPeer) node.peerBindings.push({ name: dep, target: null });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
throw lockMismatch("npm lock is missing a required dependency");
|
|
155
|
+
}
|
|
156
|
+
const target = load(targetLocation, dep, requirement.spec);
|
|
157
|
+
node.edges.push({
|
|
158
|
+
name: dep,
|
|
159
|
+
target: target.key,
|
|
160
|
+
kind: requirement.kind,
|
|
161
|
+
spec: requirement.spec,
|
|
162
|
+
});
|
|
163
|
+
if (requirement.kind === "peer")
|
|
164
|
+
node.peerBindings.push({ name: dep, target: target.key });
|
|
165
|
+
}
|
|
166
|
+
return node;
|
|
167
|
+
};
|
|
168
|
+
const roots = Object.entries(requirements).map(([name, spec]) => {
|
|
169
|
+
const location = find("", name);
|
|
170
|
+
if (!location) throw lockMismatch("npm lock is missing a direct selection");
|
|
171
|
+
return { name, target: load(location, name, spec).key };
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
nodes: [...locations.values()].sort((a, b) => a.key.localeCompare(b.key)),
|
|
175
|
+
roots,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/** Validate locked graph data before it can address any store/project paths. */
|
|
2
|
+
import { valid } from "semver";
|
|
3
|
+
import { ZyrkError } from "../errors.ts";
|
|
4
|
+
import type { NpmLockDoc } from "../lockfile/types.ts";
|
|
5
|
+
import { ZYRK_RESOLVER_POLICY } from "../lockfile/types.ts";
|
|
6
|
+
import type {
|
|
7
|
+
FinalizedGraph,
|
|
8
|
+
ResolvedOccurrence,
|
|
9
|
+
} from "../resolve/resolver.ts";
|
|
10
|
+
import { sourceIdOf } from "../util/hash.ts";
|
|
11
|
+
import { CONTEXT_CANONICALIZATION_VERSION } from "../version.ts";
|
|
12
|
+
import { createGraphDoc } from "../store/graph.ts";
|
|
13
|
+
|
|
14
|
+
export function packageName(name: string): boolean {
|
|
15
|
+
return (
|
|
16
|
+
/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name) &&
|
|
17
|
+
!["node_modules", "__proto__", "constructor", "prototype"].includes(name) &&
|
|
18
|
+
name.length <= 214
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
export function cleanUrl(value: string): boolean {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(value);
|
|
24
|
+
return (
|
|
25
|
+
["https:", "http:"].includes(url.protocol) &&
|
|
26
|
+
!url.username &&
|
|
27
|
+
!url.password &&
|
|
28
|
+
!url.search &&
|
|
29
|
+
!url.hash
|
|
30
|
+
);
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function lockMismatch(why: string): ZyrkError {
|
|
36
|
+
return new ZyrkError(
|
|
37
|
+
"ZYRK_LOCK_MISMATCH",
|
|
38
|
+
`cannot install the locked graph (${why}).`,
|
|
39
|
+
{
|
|
40
|
+
remedy:
|
|
41
|
+
"Restore a compatible package-lock.json, or refresh it intentionally with zyrk install after reviewing dependency changes.",
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
export function lockedGraph(doc: NpmLockDoc): FinalizedGraph {
|
|
49
|
+
const ext = doc.zyrk;
|
|
50
|
+
if (
|
|
51
|
+
!ext ||
|
|
52
|
+
ext.resolver !== ZYRK_RESOLVER_POLICY ||
|
|
53
|
+
ext.contextCanonicalization !== CONTEXT_CANONICALIZATION_VERSION ||
|
|
54
|
+
!record(ext.nodes) ||
|
|
55
|
+
!record(ext.roots) ||
|
|
56
|
+
!record(ext.roots["."])
|
|
57
|
+
)
|
|
58
|
+
throw lockMismatch("unsupported graph metadata");
|
|
59
|
+
const nodes: ResolvedOccurrence[] = [];
|
|
60
|
+
for (const [key, raw] of Object.entries(ext.nodes)) {
|
|
61
|
+
if (
|
|
62
|
+
!record(raw) ||
|
|
63
|
+
typeof raw.name !== "string" ||
|
|
64
|
+
!packageName(raw.name) ||
|
|
65
|
+
typeof raw.version !== "string" ||
|
|
66
|
+
valid(raw.version) === null ||
|
|
67
|
+
typeof raw.integrity !== "string" ||
|
|
68
|
+
!/^sha512-[A-Za-z0-9+/]{86}==$/.test(raw.integrity) ||
|
|
69
|
+
typeof raw.origin !== "string" ||
|
|
70
|
+
!cleanUrl(raw.origin) ||
|
|
71
|
+
typeof raw.tarball !== "string" ||
|
|
72
|
+
!cleanUrl(raw.tarball) ||
|
|
73
|
+
!record(raw.edges) ||
|
|
74
|
+
!record(raw.edgeKinds) ||
|
|
75
|
+
!record(raw.peers) ||
|
|
76
|
+
!record(raw.bin) ||
|
|
77
|
+
!record(raw.platform)
|
|
78
|
+
)
|
|
79
|
+
throw lockMismatch("invalid node record");
|
|
80
|
+
const identity = {
|
|
81
|
+
sourceType: "registry" as const,
|
|
82
|
+
name: raw.name,
|
|
83
|
+
version: raw.version,
|
|
84
|
+
origin: raw.origin,
|
|
85
|
+
integrity: raw.integrity,
|
|
86
|
+
};
|
|
87
|
+
if (sourceIdOf(identity) !== raw.source)
|
|
88
|
+
throw lockMismatch("source identity disagrees with locked inputs");
|
|
89
|
+
const edges: ResolvedOccurrence["edges"] = [];
|
|
90
|
+
for (const [name, target] of Object.entries(raw.edges)) {
|
|
91
|
+
const kind = raw.edgeKinds[name];
|
|
92
|
+
if (
|
|
93
|
+
!packageName(name) ||
|
|
94
|
+
typeof target !== "string" ||
|
|
95
|
+
!["prod", "optional", "peer"].includes(String(kind))
|
|
96
|
+
)
|
|
97
|
+
throw lockMismatch("invalid dependency edge");
|
|
98
|
+
edges.push({
|
|
99
|
+
name,
|
|
100
|
+
target,
|
|
101
|
+
kind: kind as "prod" | "optional" | "peer",
|
|
102
|
+
spec: "",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const peers: ResolvedOccurrence["peerBindings"] = [];
|
|
106
|
+
for (const [name, target] of Object.entries(raw.peers)) {
|
|
107
|
+
if (
|
|
108
|
+
!packageName(name) ||
|
|
109
|
+
(target !== null && typeof target !== "string") ||
|
|
110
|
+
(target !== null && raw.edges[name] !== target)
|
|
111
|
+
)
|
|
112
|
+
throw lockMismatch("invalid peer binding");
|
|
113
|
+
peers.push({ name, target });
|
|
114
|
+
}
|
|
115
|
+
const bin: Record<string, string> = Object.create(null) as Record<
|
|
116
|
+
string,
|
|
117
|
+
string
|
|
118
|
+
>;
|
|
119
|
+
for (const [name, target] of Object.entries(raw.bin)) {
|
|
120
|
+
if (
|
|
121
|
+
!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name) ||
|
|
122
|
+
typeof target !== "string"
|
|
123
|
+
)
|
|
124
|
+
throw lockMismatch("invalid binary entry");
|
|
125
|
+
bin[name] = target;
|
|
126
|
+
}
|
|
127
|
+
const platform: ResolvedOccurrence["platform"] = {};
|
|
128
|
+
for (const field of ["os", "cpu", "libc"] as const) {
|
|
129
|
+
const list = raw.platform[field];
|
|
130
|
+
if (list !== undefined) {
|
|
131
|
+
if (
|
|
132
|
+
!Array.isArray(list) ||
|
|
133
|
+
!list.every((item: unknown) => typeof item === "string")
|
|
134
|
+
)
|
|
135
|
+
throw lockMismatch("invalid platform constraints");
|
|
136
|
+
platform[field] = list as string[];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const engines: Record<string, string> = {};
|
|
140
|
+
if (raw.engines !== undefined) {
|
|
141
|
+
if (!record(raw.engines))
|
|
142
|
+
throw lockMismatch("invalid engine constraints");
|
|
143
|
+
for (const [name, value] of Object.entries(raw.engines)) {
|
|
144
|
+
if (typeof value !== "string")
|
|
145
|
+
throw lockMismatch("invalid engine constraint");
|
|
146
|
+
engines[name] = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
nodes.push({
|
|
150
|
+
key,
|
|
151
|
+
...identity,
|
|
152
|
+
sourceId: raw.source as string,
|
|
153
|
+
tarballUrl: raw.tarball,
|
|
154
|
+
bin,
|
|
155
|
+
engines,
|
|
156
|
+
platform,
|
|
157
|
+
edges,
|
|
158
|
+
peerBindings: peers,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const roots = Object.entries(ext.roots["."]).map(([name, target]) => {
|
|
162
|
+
if (
|
|
163
|
+
!packageName(name) ||
|
|
164
|
+
typeof target !== "string" ||
|
|
165
|
+
!nodes.some((node) => node.key === target)
|
|
166
|
+
)
|
|
167
|
+
throw lockMismatch("invalid importer root");
|
|
168
|
+
return { name, target: target as string };
|
|
169
|
+
});
|
|
170
|
+
try {
|
|
171
|
+
createGraphDoc(nodes.map((n) => ({ ...n, deps: n.edges })));
|
|
172
|
+
} catch {
|
|
173
|
+
throw lockMismatch("invalid dependency graph");
|
|
174
|
+
}
|
|
175
|
+
return { nodes, roots };
|
|
176
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** Durable project roots and exact physical instances (SPEC 14.4, 18, 30). */
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
openStoreDb,
|
|
6
|
+
recordSource,
|
|
7
|
+
recordObject,
|
|
8
|
+
withTransaction,
|
|
9
|
+
} from "../store/db.ts";
|
|
10
|
+
import { Store } from "../store/layout.ts";
|
|
11
|
+
import { readSourceManifest } from "../store/manifests.ts";
|
|
12
|
+
import { contextIdFor, graphHash, type GraphDoc } from "../store/graph.ts";
|
|
13
|
+
import type { RealizeResult } from "../store/realize.ts";
|
|
14
|
+
import { sha512Hex } from "../util/hash.ts";
|
|
15
|
+
import { STORE_FORMAT_VERSION } from "../version.ts";
|
|
16
|
+
|
|
17
|
+
export async function registerGeneration(options: {
|
|
18
|
+
store: Store;
|
|
19
|
+
project: string;
|
|
20
|
+
projectId: string;
|
|
21
|
+
generationId: string;
|
|
22
|
+
graph: GraphDoc;
|
|
23
|
+
realized: RealizeResult;
|
|
24
|
+
roots: readonly string[];
|
|
25
|
+
readiness: "ready" | "intentionally-unbuilt";
|
|
26
|
+
}): Promise<{
|
|
27
|
+
instances: string[];
|
|
28
|
+
contexts: string[];
|
|
29
|
+
activate(): void;
|
|
30
|
+
close(): void;
|
|
31
|
+
}> {
|
|
32
|
+
const { store, project, projectId, generationId, graph, realized } = options;
|
|
33
|
+
const db = openStoreDb(store.paths.dbFile);
|
|
34
|
+
try {
|
|
35
|
+
for (const node of graph.nodes) {
|
|
36
|
+
const doc = await readSourceManifest(store, node.sourceId);
|
|
37
|
+
const m = doc.manifest;
|
|
38
|
+
recordSource(db, {
|
|
39
|
+
sourceId: m.sourceId,
|
|
40
|
+
sourceType: "registry",
|
|
41
|
+
name: m.name,
|
|
42
|
+
version: m.version,
|
|
43
|
+
origin: m.origin,
|
|
44
|
+
integrity: m.integrity,
|
|
45
|
+
manifestHash: doc.manifestHash,
|
|
46
|
+
extractionPolicyVersion: m.extractionPolicyVersion,
|
|
47
|
+
logicalBytes: m.totals.logicalBytes,
|
|
48
|
+
fileCount: m.totals.files,
|
|
49
|
+
entryCount: m.entries.length,
|
|
50
|
+
verifiedAt: doc.provenance.verifiedAt,
|
|
51
|
+
});
|
|
52
|
+
for (const entry of m.entries)
|
|
53
|
+
if (entry.type === "file")
|
|
54
|
+
recordObject(db, {
|
|
55
|
+
objectId: entry.objectId,
|
|
56
|
+
innerDigest: entry.innerDigest,
|
|
57
|
+
modeClass: entry.mode,
|
|
58
|
+
bytes: entry.size,
|
|
59
|
+
committedAt: doc.provenance.verifiedAt,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const now = new Date().toISOString();
|
|
63
|
+
const fs = await stat(project);
|
|
64
|
+
const contexts: string[] = [];
|
|
65
|
+
const instances: string[] = [];
|
|
66
|
+
withTransaction(db, () => {
|
|
67
|
+
db.prepare(
|
|
68
|
+
"INSERT INTO projects(project_id,canonical_path,fs_dev,fs_ino,created_at,last_seen_at) VALUES(?,?,?,?,?,?) ON CONFLICT(project_id) DO UPDATE SET last_seen_at=excluded.last_seen_at",
|
|
69
|
+
).run(projectId, project, String(fs.dev), String(fs.ino), now, now);
|
|
70
|
+
db.prepare(
|
|
71
|
+
"INSERT INTO generations(generation_id,project_id,graph_digest,readiness,state,created_at) VALUES(?,?,?,?,'pending',?)",
|
|
72
|
+
).run(generationId, projectId, graphHash(graph), options.readiness, now);
|
|
73
|
+
for (const node of graph.nodes) {
|
|
74
|
+
const context = contextIdFor(graph, node.key);
|
|
75
|
+
const directory = realized.realizationDirByKey.get(node.key)!;
|
|
76
|
+
const instance = `inst1_${sha512Hex(directory).slice(0, 32)}`;
|
|
77
|
+
contexts.push(context);
|
|
78
|
+
instances.push(instance);
|
|
79
|
+
db.prepare(
|
|
80
|
+
"INSERT INTO realizations(context_id,descriptor_hash,source_id,readiness,store_format,created_at) VALUES(?,?,?,'ready',?,?) ON CONFLICT(context_id) DO NOTHING",
|
|
81
|
+
).run(context, context, node.sourceId, STORE_FORMAT_VERSION, now);
|
|
82
|
+
db.prepare(
|
|
83
|
+
"INSERT INTO realization_instances(instance_id,context_id,path,verification_state,created_at) VALUES(?,?,?,'verified',?) ON CONFLICT(instance_id) DO NOTHING",
|
|
84
|
+
).run(instance, context, directory, now);
|
|
85
|
+
db.prepare(
|
|
86
|
+
"INSERT INTO generation_instances(generation_id,context_id,instance_id,direct_link) VALUES(?,?,?,?) ON CONFLICT(generation_id,context_id) DO NOTHING",
|
|
87
|
+
).run(
|
|
88
|
+
generationId,
|
|
89
|
+
context,
|
|
90
|
+
instance,
|
|
91
|
+
options.roots.includes(node.key) ? 1 : 0,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
db.prepare(
|
|
95
|
+
"INSERT INTO roots(root_id,project_id,generation_id,kind,state,created_at) VALUES(?,?,?,'pending','active',?)",
|
|
96
|
+
).run(`root1_${generationId}`, projectId, generationId, now);
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
contexts,
|
|
100
|
+
instances,
|
|
101
|
+
activate() {
|
|
102
|
+
withTransaction(db, () => {
|
|
103
|
+
db.prepare(
|
|
104
|
+
"UPDATE generations SET state='rollback' WHERE project_id=? AND state='active' AND generation_id<>?",
|
|
105
|
+
).run(projectId, generationId);
|
|
106
|
+
db.prepare(
|
|
107
|
+
"UPDATE generations SET state='active',activated_at=? WHERE generation_id=?",
|
|
108
|
+
).run(new Date().toISOString(), generationId);
|
|
109
|
+
db.prepare(
|
|
110
|
+
"UPDATE roots SET kind='generation' WHERE generation_id=?",
|
|
111
|
+
).run(generationId);
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
close() {
|
|
115
|
+
db.close();
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
} catch (cause) {
|
|
119
|
+
db.close();
|
|
120
|
+
throw cause;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Fail closed on registry settings the current single-registry adapter cannot honor. */
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { ZyrkError } from "../errors.ts";
|
|
6
|
+
import { RegistryClient, DEFAULT_REGISTRY } from "../fetch/registry.ts";
|
|
7
|
+
import { isErrno } from "../util/fsx.ts";
|
|
8
|
+
import { cleanUrl } from "./lock.ts";
|
|
9
|
+
|
|
10
|
+
export async function installRegistry(options: {
|
|
11
|
+
cwd: string;
|
|
12
|
+
env: NodeJS.ProcessEnv;
|
|
13
|
+
version: string;
|
|
14
|
+
registry?: string;
|
|
15
|
+
}): Promise<RegistryClient> {
|
|
16
|
+
let registry = DEFAULT_REGISTRY;
|
|
17
|
+
const home = options.env.HOME ?? os.homedir();
|
|
18
|
+
const files = [
|
|
19
|
+
options.env.npm_config_userconfig ?? path.join(home, ".npmrc"),
|
|
20
|
+
path.join(options.cwd, ".npmrc"),
|
|
21
|
+
];
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
let text: string;
|
|
24
|
+
try {
|
|
25
|
+
text = await readFile(file, "utf8");
|
|
26
|
+
} catch (cause) {
|
|
27
|
+
if (isErrno(cause, "ENOENT")) continue;
|
|
28
|
+
throw new ZyrkError(
|
|
29
|
+
"ZYRK_POLICY_DENIED",
|
|
30
|
+
"Cannot read npm registry configuration.",
|
|
31
|
+
{
|
|
32
|
+
remedy: "Make the user and project .npmrc readable before retrying.",
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
for (const line of text.split(/\r?\n/)) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";"))
|
|
39
|
+
continue;
|
|
40
|
+
const index = trimmed.indexOf("=");
|
|
41
|
+
const key = trimmed.slice(0, index).trim();
|
|
42
|
+
const value = trimmed.slice(index + 1).trim();
|
|
43
|
+
if (key === "registry") registry = value;
|
|
44
|
+
if (/^@.+:registry$/.test(key))
|
|
45
|
+
throw new ZyrkError(
|
|
46
|
+
"ZYRK_POLICY_DENIED",
|
|
47
|
+
"Scoped registry routing is not supported by project install yet.",
|
|
48
|
+
{
|
|
49
|
+
remedy:
|
|
50
|
+
"Use npm for mixed-registry projects; zyrk will not silently fetch a private scope from a public registry.",
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
if (key === "strict-ssl" && value === "false")
|
|
54
|
+
throw new ZyrkError(
|
|
55
|
+
"ZYRK_POLICY_DENIED",
|
|
56
|
+
"Disabling registry TLS verification is not supported.",
|
|
57
|
+
{
|
|
58
|
+
remedy:
|
|
59
|
+
"Configure a trusted HTTPS registry with valid TLS certificates.",
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
registry = options.registry ?? options.env.npm_config_registry ?? registry;
|
|
65
|
+
if (!cleanUrl(registry))
|
|
66
|
+
throw new ZyrkError(
|
|
67
|
+
"ZYRK_POLICY_DENIED",
|
|
68
|
+
"Registry URL must be HTTP(S) without credentials, query parameters, or fragments.",
|
|
69
|
+
{
|
|
70
|
+
remedy:
|
|
71
|
+
"Use a credential-free registry URL. Authenticated private registries are not supported by project install yet.",
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
const url = new URL(registry);
|
|
75
|
+
if (
|
|
76
|
+
url.protocol !== "https:" &&
|
|
77
|
+
!["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)
|
|
78
|
+
)
|
|
79
|
+
throw new ZyrkError(
|
|
80
|
+
"ZYRK_POLICY_DENIED",
|
|
81
|
+
"Remote registries require HTTPS.",
|
|
82
|
+
{
|
|
83
|
+
remedy:
|
|
84
|
+
"Select an HTTPS registry; HTTP is limited to loopback development registries.",
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
return new RegistryClient({ registry, userAgent: `zyrk/${options.version}` });
|
|
88
|
+
}
|