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
package/src/lockfile/types.ts
CHANGED
|
@@ -39,6 +39,11 @@ export type ZyrkLockNode = {
|
|
|
39
39
|
origin: string;
|
|
40
40
|
/** Canonical source identity. */
|
|
41
41
|
source: string;
|
|
42
|
+
/** Exact archive and execution metadata for network-free reinstall. */
|
|
43
|
+
tarball?: string;
|
|
44
|
+
bin?: Record<string, string>;
|
|
45
|
+
platform?: { os?: string[]; cpu?: string[]; libc?: string[] };
|
|
46
|
+
engines?: Record<string, string>;
|
|
42
47
|
/** Dependency edge name -> occurrence key (alias-aware). */
|
|
43
48
|
edges: Record<string, string>;
|
|
44
49
|
/** Edge name -> prod | optional | peer. */
|
package/src/lockfile/write.ts
CHANGED
|
@@ -34,6 +34,7 @@ export type WriteLockOptions = {
|
|
|
34
34
|
version: string;
|
|
35
35
|
/** Declared direct dependencies for npm's root `dependencies` map. */
|
|
36
36
|
manifestDeps: Record<string, string>;
|
|
37
|
+
manifestDevDeps?: Record<string, string>;
|
|
37
38
|
existing?: NpmLockDoc | null;
|
|
38
39
|
resolution: WriteLockResolution;
|
|
39
40
|
fingerprints: { manifest: string; scripts: string; config: string };
|
|
@@ -85,6 +86,9 @@ export async function writeLockfile(
|
|
|
85
86
|
name: options.name,
|
|
86
87
|
version: options.version,
|
|
87
88
|
dependencies: { ...options.manifestDeps },
|
|
89
|
+
...(options.manifestDevDeps === undefined
|
|
90
|
+
? {}
|
|
91
|
+
: { devDependencies: options.manifestDevDeps }),
|
|
88
92
|
};
|
|
89
93
|
for (const root of [...options.resolution.roots].sort((a, b) =>
|
|
90
94
|
compareStrings(a.name, b.name),
|
|
@@ -107,6 +111,59 @@ export async function writeLockfile(
|
|
|
107
111
|
integrity: node.integrity,
|
|
108
112
|
};
|
|
109
113
|
}
|
|
114
|
+
const populate = (
|
|
115
|
+
location: string,
|
|
116
|
+
key: string,
|
|
117
|
+
ancestors: Map<string, string>,
|
|
118
|
+
): void => {
|
|
119
|
+
const node = byKey.get(key);
|
|
120
|
+
if (!node)
|
|
121
|
+
throw new ZyrkError(
|
|
122
|
+
"ZYRK_INTERNAL",
|
|
123
|
+
"Cannot serialize a missing dependency node.",
|
|
124
|
+
);
|
|
125
|
+
const dependencies: Record<string, string> = {};
|
|
126
|
+
for (const edge of node.edges) {
|
|
127
|
+
const child = byKey.get(edge.target);
|
|
128
|
+
if (!child)
|
|
129
|
+
throw new ZyrkError(
|
|
130
|
+
"ZYRK_INTERNAL",
|
|
131
|
+
"Cannot serialize a missing dependency edge.",
|
|
132
|
+
);
|
|
133
|
+
dependencies[edge.name] =
|
|
134
|
+
child.name === edge.name
|
|
135
|
+
? child.version
|
|
136
|
+
: `npm:${child.name}@${child.version}`;
|
|
137
|
+
}
|
|
138
|
+
packages[location] = {
|
|
139
|
+
...packages[location],
|
|
140
|
+
name: node.name,
|
|
141
|
+
version: node.version,
|
|
142
|
+
resolved: node.tarballUrl,
|
|
143
|
+
integrity: node.integrity,
|
|
144
|
+
dependencies,
|
|
145
|
+
...(Object.keys(node.bin).length ? { bin: node.bin } : {}),
|
|
146
|
+
};
|
|
147
|
+
const next = new Map(ancestors);
|
|
148
|
+
next.set(key, location);
|
|
149
|
+
for (const edge of node.edges) {
|
|
150
|
+
// An ancestor with this package slot satisfies a real cycle.
|
|
151
|
+
const ancestor = next.get(edge.target);
|
|
152
|
+
if (ancestor && ancestor.endsWith(`node_modules/${edge.name}`)) continue;
|
|
153
|
+
if (next.has(edge.target))
|
|
154
|
+
throw new ZyrkError(
|
|
155
|
+
"ZYRK_RESOLUTION_FAILED",
|
|
156
|
+
"Cannot encode an aliased dependency cycle in npm lock format.",
|
|
157
|
+
{
|
|
158
|
+
remedy:
|
|
159
|
+
"Use non-cyclic registry dependencies until alias-cycle lock support is available.",
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
populate(`${location}/node_modules/${edge.name}`, edge.target, next);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
for (const root of options.resolution.roots)
|
|
166
|
+
populate(`node_modules/${root.name}`, root.target, new Map());
|
|
110
167
|
const sortedPackages: Record<string, Record<string, unknown>> = {};
|
|
111
168
|
for (const location of Object.keys(packages).sort()) {
|
|
112
169
|
const entry = packages[location];
|
|
@@ -137,6 +194,10 @@ export async function writeLockfile(
|
|
|
137
194
|
integrity: node.integrity,
|
|
138
195
|
origin: node.origin,
|
|
139
196
|
source: node.sourceId,
|
|
197
|
+
tarball: node.tarballUrl,
|
|
198
|
+
bin: node.bin,
|
|
199
|
+
platform: node.platform,
|
|
200
|
+
...(node.engines === undefined ? {} : { engines: node.engines }),
|
|
140
201
|
edges,
|
|
141
202
|
edgeKinds,
|
|
142
203
|
peers,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
open,
|
|
3
|
+
lstat,
|
|
3
4
|
readFile,
|
|
4
5
|
readdir,
|
|
5
6
|
rename,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
symlink as fsSymlink,
|
|
8
9
|
} from "node:fs/promises";
|
|
9
10
|
import * as path from "node:path";
|
|
11
|
+
import { ZyrkError } from "../errors.ts";
|
|
10
12
|
import { acquireLock } from "../util/locks.ts";
|
|
11
13
|
import {
|
|
12
14
|
ensureDirWithMode,
|
|
@@ -50,6 +52,12 @@ type ProjectionEvent = {
|
|
|
50
52
|
at: string;
|
|
51
53
|
event: "staged" | "old-moved" | "published" | "done";
|
|
52
54
|
generationId: string;
|
|
55
|
+
metadataOnly?: boolean;
|
|
56
|
+
files?: Array<{
|
|
57
|
+
name: "package.json" | "package-lock.json";
|
|
58
|
+
before: string | null;
|
|
59
|
+
after: string;
|
|
60
|
+
}>;
|
|
53
61
|
};
|
|
54
62
|
|
|
55
63
|
function journalPath(projectDir: string): string {
|
|
@@ -87,9 +95,27 @@ async function readEvents(projectDir: string): Promise<ProjectionEvent[]> {
|
|
|
87
95
|
for (const line of text.split("\n")) {
|
|
88
96
|
if (line === "") continue;
|
|
89
97
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
const event = JSON.parse(line) as ProjectionEvent;
|
|
99
|
+
if (!/^gen1_[a-zA-Z0-9_-]+$/.test(event.generationId))
|
|
100
|
+
throw new Error("invalid generation");
|
|
101
|
+
if (
|
|
102
|
+
event.files?.some(
|
|
103
|
+
(file) =>
|
|
104
|
+
!["package.json", "package-lock.json"].includes(file.name) ||
|
|
105
|
+
(file.before !== null && typeof file.before !== "string") ||
|
|
106
|
+
typeof file.after !== "string",
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
throw new Error("invalid files");
|
|
110
|
+
events.push(event);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (!(error instanceof SyntaxError))
|
|
113
|
+
throw new ZyrkError(
|
|
114
|
+
"ZYRK_STORE_CORRUPT",
|
|
115
|
+
"Project recovery journal is malformed.",
|
|
116
|
+
{ remedy: "Restore the local journal before retrying installation." },
|
|
117
|
+
);
|
|
118
|
+
// A torn final append may be ignored; durable earlier events remain.
|
|
93
119
|
}
|
|
94
120
|
}
|
|
95
121
|
return events;
|
|
@@ -133,6 +159,37 @@ export async function recoverProjection(projectDir: string): Promise<void> {
|
|
|
133
159
|
await rmrfNoFollow(previous);
|
|
134
160
|
}
|
|
135
161
|
}
|
|
162
|
+
const staged = events.find(
|
|
163
|
+
(event) =>
|
|
164
|
+
event.generationId === last.generationId && event.files !== undefined,
|
|
165
|
+
);
|
|
166
|
+
if (staged?.files) {
|
|
167
|
+
let active: unknown;
|
|
168
|
+
try {
|
|
169
|
+
active = JSON.parse(
|
|
170
|
+
await readFile(
|
|
171
|
+
path.join(nodeModules, ".zyrk", "install-state.json"),
|
|
172
|
+
"utf8",
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
} catch {
|
|
176
|
+
active = null;
|
|
177
|
+
}
|
|
178
|
+
const published =
|
|
179
|
+
staged.metadataOnly === true
|
|
180
|
+
? last.event === "done"
|
|
181
|
+
: (active as { generationId?: unknown } | null)?.generationId ===
|
|
182
|
+
last.generationId;
|
|
183
|
+
for (const file of staged.files) {
|
|
184
|
+
const contents = published ? file.after : file.before;
|
|
185
|
+
if (contents === null)
|
|
186
|
+
await rm(path.join(projectDir, file.name), { force: true });
|
|
187
|
+
else
|
|
188
|
+
await writeFileAtomic(path.join(projectDir, file.name), contents, {
|
|
189
|
+
mode: 0o644,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
136
193
|
await rm(journalPath(projectDir), { force: true });
|
|
137
194
|
}
|
|
138
195
|
|
|
@@ -143,15 +200,26 @@ export async function recoverProjection(projectDir: string): Promise<void> {
|
|
|
143
200
|
export async function installProjection(
|
|
144
201
|
projectDir: string,
|
|
145
202
|
entries: ProjectionEntry[],
|
|
146
|
-
options: {
|
|
203
|
+
options: {
|
|
204
|
+
projectId?: string;
|
|
205
|
+
generationId?: string;
|
|
206
|
+
lockHeld?: boolean;
|
|
207
|
+
bins?: Record<string, string>;
|
|
208
|
+
files?: Array<{
|
|
209
|
+
name: "package.json" | "package-lock.json";
|
|
210
|
+
contents: string;
|
|
211
|
+
}>;
|
|
212
|
+
readiness?: "ready" | "intentionally-unbuilt";
|
|
213
|
+
metadata?: Record<string, unknown>;
|
|
214
|
+
} = {},
|
|
147
215
|
): Promise<ProjectionResult> {
|
|
148
|
-
const lock =
|
|
149
|
-
|
|
150
|
-
|
|
216
|
+
const lock = options.lockHeld
|
|
217
|
+
? undefined
|
|
218
|
+
: await acquireLock(lockPath(projectDir), { name: "project mutation" });
|
|
151
219
|
try {
|
|
152
220
|
await recoverProjection(projectDir);
|
|
153
221
|
|
|
154
|
-
const generationId = `gen1_${randomId("", 12)}`;
|
|
222
|
+
const generationId = options.generationId ?? `gen1_${randomId("", 12)}`;
|
|
155
223
|
const staging = path.join(
|
|
156
224
|
projectDir,
|
|
157
225
|
`node_modules.zyrk-staging-${generationId}`,
|
|
@@ -164,11 +232,51 @@ export async function installProjection(
|
|
|
164
232
|
|
|
165
233
|
await ensureDirWithMode(staging, 0o755);
|
|
166
234
|
for (const entry of entries) {
|
|
235
|
+
if (
|
|
236
|
+
!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(
|
|
237
|
+
entry.name,
|
|
238
|
+
) ||
|
|
239
|
+
entry.name === "node_modules"
|
|
240
|
+
)
|
|
241
|
+
throw new ZyrkError(
|
|
242
|
+
"ZYRK_MANIFEST_INVALID",
|
|
243
|
+
"Unsafe projection dependency name.",
|
|
244
|
+
{ remedy: "Use a valid registry package name." },
|
|
245
|
+
);
|
|
167
246
|
const target = path.join(staging, ...entry.name.split("/"));
|
|
168
247
|
await mkdirp(path.dirname(target));
|
|
169
248
|
await fsSymlink(entry.linkTarget, target);
|
|
170
249
|
}
|
|
171
250
|
|
|
251
|
+
if (Object.keys(options.bins ?? {}).length) {
|
|
252
|
+
await mkdirp(path.join(staging, ".bin"));
|
|
253
|
+
for (const [name, target] of Object.entries(options.bins ?? {})) {
|
|
254
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name))
|
|
255
|
+
throw new ZyrkError("ZYRK_MANIFEST_INVALID", "Unsafe binary name.", {
|
|
256
|
+
remedy: "Use a package with safe bin entries.",
|
|
257
|
+
});
|
|
258
|
+
const stat = await lstat(target);
|
|
259
|
+
if ((stat.mode & 0o111) !== 0)
|
|
260
|
+
await fsSymlink(target, path.join(staging, ".bin", name));
|
|
261
|
+
else {
|
|
262
|
+
const quote = (value: string): string =>
|
|
263
|
+
"'" + value.replaceAll("'", "'\\''") + "'";
|
|
264
|
+
const firstLine =
|
|
265
|
+
(await readFile(target, "utf8")).split("\n")[0] ?? "";
|
|
266
|
+
if (!firstLine.includes("node") && !/\.(?:[cm]?js|ts)$/.test(target))
|
|
267
|
+
throw new ZyrkError(
|
|
268
|
+
"ZYRK_MANIFEST_INVALID",
|
|
269
|
+
"Non-executable dependency bin has no Node entry point.",
|
|
270
|
+
{ remedy: "Select a package shipping executable bin files." },
|
|
271
|
+
);
|
|
272
|
+
await writeFileAtomic(
|
|
273
|
+
path.join(staging, ".bin", name),
|
|
274
|
+
`#!/bin/sh\nexec ${quote(process.execPath)} ${quote(target)} "$@"\n`,
|
|
275
|
+
{ mode: 0o755 },
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
172
280
|
await ensureDirWithMode(path.join(staging, ".zyrk"), 0o755);
|
|
173
281
|
const state = {
|
|
174
282
|
schemaVersion: 1,
|
|
@@ -178,7 +286,8 @@ export async function installProjection(
|
|
|
178
286
|
: { projectId: options.projectId }),
|
|
179
287
|
linker: "shared",
|
|
180
288
|
createdAt: new Date().toISOString(),
|
|
181
|
-
readiness: "ready",
|
|
289
|
+
readiness: options.readiness ?? "ready",
|
|
290
|
+
...options.metadata,
|
|
182
291
|
direct: entries.map((entry) => ({
|
|
183
292
|
name: entry.name,
|
|
184
293
|
target: entry.linkTarget,
|
|
@@ -191,11 +300,26 @@ export async function installProjection(
|
|
|
191
300
|
mode: 0o644,
|
|
192
301
|
},
|
|
193
302
|
);
|
|
303
|
+
const files = [];
|
|
304
|
+
for (const file of options.files ?? []) {
|
|
305
|
+
let before: string | null = null;
|
|
306
|
+
try {
|
|
307
|
+
before = await readFile(path.join(projectDir, file.name), "utf8");
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (!isErrno(error, "ENOENT")) throw error;
|
|
310
|
+
}
|
|
311
|
+
files.push({ name: file.name, before, after: file.contents });
|
|
312
|
+
}
|
|
194
313
|
await appendEvent(projectDir, {
|
|
195
314
|
at: new Date().toISOString(),
|
|
196
315
|
event: "staged",
|
|
197
316
|
generationId,
|
|
317
|
+
files,
|
|
198
318
|
});
|
|
319
|
+
for (const file of files)
|
|
320
|
+
await writeFileAtomic(path.join(projectDir, file.name), file.after, {
|
|
321
|
+
mode: 0o644,
|
|
322
|
+
});
|
|
199
323
|
|
|
200
324
|
const hadPrevious = await pathExists(nodeModules);
|
|
201
325
|
if (hadPrevious) {
|
|
@@ -223,6 +347,44 @@ export async function installProjection(
|
|
|
223
347
|
|
|
224
348
|
return { nodeModulesDir: nodeModules, generationId };
|
|
225
349
|
} finally {
|
|
226
|
-
await lock
|
|
350
|
+
await lock?.release();
|
|
227
351
|
}
|
|
228
352
|
}
|
|
353
|
+
|
|
354
|
+
/** Journal metadata-only changes while the caller holds the project lock. */
|
|
355
|
+
export async function installMetadata(
|
|
356
|
+
projectDir: string,
|
|
357
|
+
files: Array<{
|
|
358
|
+
name: "package.json" | "package-lock.json";
|
|
359
|
+
contents: string;
|
|
360
|
+
}>,
|
|
361
|
+
): Promise<void> {
|
|
362
|
+
const generationId = `gen1_${randomId("", 12)}`;
|
|
363
|
+
const snapshots = [];
|
|
364
|
+
for (const file of files) {
|
|
365
|
+
let before: string | null = null;
|
|
366
|
+
try {
|
|
367
|
+
before = await readFile(path.join(projectDir, file.name), "utf8");
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (!isErrno(error, "ENOENT")) throw error;
|
|
370
|
+
}
|
|
371
|
+
snapshots.push({ name: file.name, before, after: file.contents });
|
|
372
|
+
}
|
|
373
|
+
await appendEvent(projectDir, {
|
|
374
|
+
at: new Date().toISOString(),
|
|
375
|
+
event: "staged",
|
|
376
|
+
generationId,
|
|
377
|
+
metadataOnly: true,
|
|
378
|
+
files: snapshots,
|
|
379
|
+
});
|
|
380
|
+
for (const file of snapshots)
|
|
381
|
+
await writeFileAtomic(path.join(projectDir, file.name), file.after, {
|
|
382
|
+
mode: 0o644,
|
|
383
|
+
});
|
|
384
|
+
await appendEvent(projectDir, {
|
|
385
|
+
at: new Date().toISOString(),
|
|
386
|
+
event: "done",
|
|
387
|
+
generationId,
|
|
388
|
+
});
|
|
389
|
+
await rm(journalPath(projectDir), { force: true });
|
|
390
|
+
}
|