@ekanos/cli 0.22.1 → 0.23.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/README.md +18 -4
- package/dist/commands/add.js +21 -10
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/contract.js +12 -2
- package/dist/commands/contract.js.map +1 -1
- package/dist/commands/dev.js +33 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/publish.d.ts +6 -0
- package/dist/commands/publish.js +80 -4
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/status.d.ts +44 -0
- package/dist/commands/status.js +79 -18
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/upgrade.js +287 -144
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/docs-links.js +5 -1
- package/dist/docs-links.js.map +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -1
- package/dist/exit-codes.d.ts +3 -0
- package/dist/exit-codes.js +3 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/experience/checks.js +2 -1
- package/dist/experience/checks.js.map +1 -1
- package/dist/experience/definition-checks.d.ts +7 -0
- package/dist/experience/definition-checks.js +36 -4
- package/dist/experience/definition-checks.js.map +1 -1
- package/dist/experience/init.js +10 -0
- package/dist/experience/init.js.map +1 -1
- package/dist/experience/load-experience.js +33 -2
- package/dist/experience/load-experience.js.map +1 -1
- package/dist/experience/pins.d.ts +10 -0
- package/dist/experience/pins.js +19 -1
- package/dist/experience/pins.js.map +1 -1
- package/dist/generated/phosphor-names.d.ts +3 -0
- package/dist/generated/phosphor-names.js +1541 -0
- package/dist/generated/phosphor-names.js.map +1 -0
- package/dist/harness-scaffold.d.ts +13 -0
- package/dist/harness-scaffold.js +19 -3
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/icon-findings.d.ts +12 -17
- package/dist/icon-findings.js +49 -74
- package/dist/icon-findings.js.map +1 -1
- package/dist/index.js +28 -5
- package/dist/index.js.map +1 -1
- package/dist/lockfile-instances.d.ts +60 -0
- package/dist/lockfile-instances.js +177 -0
- package/dist/lockfile-instances.js.map +1 -0
- package/dist/pack-hygiene.d.ts +8 -0
- package/dist/pack-hygiene.js +57 -0
- package/dist/pack-hygiene.js.map +1 -0
- package/dist/package-manager.d.ts +34 -6
- package/dist/package-manager.js +130 -20
- package/dist/package-manager.js.map +1 -1
- package/dist/project.js +11 -0
- package/dist/project.js.map +1 -1
- package/dist/repo/scaffold.js +4 -0
- package/dist/repo/scaffold.js.map +1 -1
- package/dist/validate-findings.js +10 -1
- package/dist/validate-findings.js.map +1 -1
- package/dist/workspace-root.d.ts +16 -0
- package/dist/workspace-root.js +50 -0
- package/dist/workspace-root.js.map +1 -0
- package/package.json +4 -4
- package/templates/AGENTS.md.tmpl +11 -15
- package/templates/experience/package.json.tmpl +8 -6
- package/templates/experience/src/widgets/hello-widget.test.tsx.tmpl +49 -0
- package/templates/experience/vitest.config.ts.tmpl +21 -0
- package/templates/harness/next.config.mjs.tmpl +7 -4
- package/templates/package.json.tmpl +5 -5
- package/templates/skills/build-integration/references/widget-design.md +2 -3
- package/templates/skills/polish-widgets/references/patterns.md +5 -5
- package/dist/generated/fa-free-names.d.ts +0 -9
- package/dist/generated/fa-free-names.js +0 -2599
- package/dist/generated/fa-free-names.js.map +0 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Finding } from '@ekanos/integration-schema';
|
|
2
|
+
/**
|
|
3
|
+
* Does the project's lockfile resolve `@ekanos/sdk` (or `@ekanos/ui`) more
|
|
4
|
+
* than once?
|
|
5
|
+
*
|
|
6
|
+
* Both create React contexts at module scope, so two resolved copies means a
|
|
7
|
+
* provider from one is invisible to hooks from the other — under `ekanos dev`
|
|
8
|
+
* that surfaces as "useOperation needs an <ExperienceRuntimeProvider>" with
|
|
9
|
+
* the harness's provider right there. pnpm makes the second copy whenever the
|
|
10
|
+
* SDK's peers (zod above all) resolve to two versions in one workspace: it
|
|
11
|
+
* builds one `@ekanos/sdk@x(zod@…)` variant per peer set.
|
|
12
|
+
*
|
|
13
|
+
* Read-only and offline: this parses the lockfile text and never runs a
|
|
14
|
+
* package manager.
|
|
15
|
+
*/
|
|
16
|
+
export declare const WATCHED_PACKAGES: readonly ['@ekanos/sdk', '@ekanos/ui'];
|
|
17
|
+
export type WatchedPackage = (typeof WATCHED_PACKAGES)[number];
|
|
18
|
+
export type LockfileKind = 'pnpm' | 'npm';
|
|
19
|
+
export type InstanceScan =
|
|
20
|
+
/** No lockfile at or above the project. */
|
|
21
|
+
{
|
|
22
|
+
status: 'no-lockfile';
|
|
23
|
+
}
|
|
24
|
+
/** A lockfile this check cannot read (yarn, bun); nothing is claimed. */
|
|
25
|
+
| {
|
|
26
|
+
status: 'unsupported';
|
|
27
|
+
lockfile: string;
|
|
28
|
+
}
|
|
29
|
+
/** A lockfile it should read but could not parse. */
|
|
30
|
+
| {
|
|
31
|
+
status: 'unreadable';
|
|
32
|
+
lockfile: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
} | {
|
|
35
|
+
status: 'ok';
|
|
36
|
+
lockfile: string;
|
|
37
|
+
kind: LockfileKind;
|
|
38
|
+
/** Each watched package resolved more than once, with every resolution. */
|
|
39
|
+
duplicates: Partial<Record<WatchedPackage, string[]>>;
|
|
40
|
+
};
|
|
41
|
+
/** The nearest lockfile at or above `dir` — a workspace package's is the workspace root's. */
|
|
42
|
+
export declare function findLockfile(dir: string): {
|
|
43
|
+
file: string;
|
|
44
|
+
kind: LockfileKind | null;
|
|
45
|
+
} | null;
|
|
46
|
+
/** Snapshot/package keys (`@ekanos/sdk@0.25.0(zod@4.6.7)`) per watched package in a pnpm lockfile. */
|
|
47
|
+
export declare function pnpmResolutions(text: string): Record<WatchedPackage, string[]>;
|
|
48
|
+
/** `node_modules/…/@ekanos/sdk` install paths per watched package in an npm lockfile (v2/v3). */
|
|
49
|
+
export declare function npmResolutions(text: string): Record<WatchedPackage, string[]>;
|
|
50
|
+
export declare function scanLockfileInstances(projectDir: string): InstanceScan;
|
|
51
|
+
export declare const DUPLICATE_INSTANCE_HINT: string;
|
|
52
|
+
/** One line per duplicated package, for `ekanos dev`'s warning and the finding's message. */
|
|
53
|
+
export declare function describeDuplicates(duplicates: Partial<Record<WatchedPackage, string[]>>): string[];
|
|
54
|
+
/**
|
|
55
|
+
* The `validate` finding: a warning when a watched package resolves more than
|
|
56
|
+
* once, an `info` when the lockfile could not be read. Never an error — the
|
|
57
|
+
* lockfile is the workspace's, and a duplicate breaks the harness, not the
|
|
58
|
+
* submission.
|
|
59
|
+
*/
|
|
60
|
+
export declare function collectDuplicateInstanceFindings(projectDir: string): Finding[];
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Does the project's lockfile resolve `@ekanos/sdk` (or `@ekanos/ui`) more
|
|
5
|
+
* than once?
|
|
6
|
+
*
|
|
7
|
+
* Both create React contexts at module scope, so two resolved copies means a
|
|
8
|
+
* provider from one is invisible to hooks from the other — under `ekanos dev`
|
|
9
|
+
* that surfaces as "useOperation needs an <ExperienceRuntimeProvider>" with
|
|
10
|
+
* the harness's provider right there. pnpm makes the second copy whenever the
|
|
11
|
+
* SDK's peers (zod above all) resolve to two versions in one workspace: it
|
|
12
|
+
* builds one `@ekanos/sdk@x(zod@…)` variant per peer set.
|
|
13
|
+
*
|
|
14
|
+
* Read-only and offline: this parses the lockfile text and never runs a
|
|
15
|
+
* package manager.
|
|
16
|
+
*/
|
|
17
|
+
export const WATCHED_PACKAGES = ['@ekanos/sdk', '@ekanos/ui'];
|
|
18
|
+
const LOCKFILES = [
|
|
19
|
+
{ name: 'pnpm-lock.yaml', kind: 'pnpm' },
|
|
20
|
+
{ name: 'package-lock.json', kind: 'npm' },
|
|
21
|
+
{ name: 'npm-shrinkwrap.json', kind: 'npm' },
|
|
22
|
+
{ name: 'yarn.lock', kind: null },
|
|
23
|
+
{ name: 'bun.lock', kind: null },
|
|
24
|
+
{ name: 'bun.lockb', kind: null },
|
|
25
|
+
];
|
|
26
|
+
/** The nearest lockfile at or above `dir` — a workspace package's is the workspace root's. */
|
|
27
|
+
export function findLockfile(dir) {
|
|
28
|
+
let current = path.resolve(dir);
|
|
29
|
+
for (;;) {
|
|
30
|
+
for (const { name, kind } of LOCKFILES) {
|
|
31
|
+
const file = path.join(current, name);
|
|
32
|
+
if (fs.existsSync(file))
|
|
33
|
+
return { file, kind };
|
|
34
|
+
}
|
|
35
|
+
const parent = path.dirname(current);
|
|
36
|
+
if (parent === current)
|
|
37
|
+
return null;
|
|
38
|
+
current = parent;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Snapshot/package keys (`@ekanos/sdk@0.25.0(zod@4.6.7)`) per watched package in a pnpm lockfile. */
|
|
42
|
+
export function pnpmResolutions(text) {
|
|
43
|
+
var _a, _b, _c, _d;
|
|
44
|
+
const bySection = new Map();
|
|
45
|
+
let section = '';
|
|
46
|
+
// ` '@ekanos/sdk@0.25.0(zod@4.6.7)':` — and a leaf snapshot writes its
|
|
47
|
+
// (empty) body on the same line: ` '@ekanos/ui@0.25.0': {}`.
|
|
48
|
+
const keyPattern = /^ {2}'?\/?(@ekanos\/(?:sdk|ui))@([^'\s:(]+(?:\([^'\n]*\))?)'?:(?:\s.*)?$/;
|
|
49
|
+
for (const line of text.split(/\r?\n/)) {
|
|
50
|
+
const top = /^([A-Za-z][\w-]*):/.exec(line);
|
|
51
|
+
if (top) {
|
|
52
|
+
section = top[1];
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const match = keyPattern.exec(line);
|
|
56
|
+
if (!match)
|
|
57
|
+
continue;
|
|
58
|
+
const name = match[1];
|
|
59
|
+
const entry = (_a = bySection.get(section)) !== null && _a !== void 0 ? _a : {
|
|
60
|
+
'@ekanos/sdk': new Set(),
|
|
61
|
+
'@ekanos/ui': new Set(),
|
|
62
|
+
};
|
|
63
|
+
entry[name].add(`${name}@${match[2]}`);
|
|
64
|
+
bySection.set(section, entry);
|
|
65
|
+
}
|
|
66
|
+
// Lockfile v9 lists each package once under `packages:` and once per peer
|
|
67
|
+
// variant under `snapshots:` — the variants are the copies. Older formats
|
|
68
|
+
// have only `packages:`, with the peer suffix in the key.
|
|
69
|
+
const source = (_b = bySection.get('snapshots')) !== null && _b !== void 0 ? _b : bySection.get('packages');
|
|
70
|
+
return {
|
|
71
|
+
'@ekanos/sdk': [...((_c = source === null || source === void 0 ? void 0 : source['@ekanos/sdk']) !== null && _c !== void 0 ? _c : [])].sort(),
|
|
72
|
+
'@ekanos/ui': [...((_d = source === null || source === void 0 ? void 0 : source['@ekanos/ui']) !== null && _d !== void 0 ? _d : [])].sort(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** `node_modules/…/@ekanos/sdk` install paths per watched package in an npm lockfile (v2/v3). */
|
|
76
|
+
export function npmResolutions(text) {
|
|
77
|
+
const parsed = JSON.parse(text);
|
|
78
|
+
const packages = typeof parsed === 'object' && parsed !== null
|
|
79
|
+
? parsed.packages
|
|
80
|
+
: undefined;
|
|
81
|
+
if (typeof packages !== 'object' || packages === null) {
|
|
82
|
+
throw new Error('has no "packages" map (lockfileVersion 1 is not supported).');
|
|
83
|
+
}
|
|
84
|
+
const result = {
|
|
85
|
+
'@ekanos/sdk': [],
|
|
86
|
+
'@ekanos/ui': [],
|
|
87
|
+
};
|
|
88
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
89
|
+
for (const name of WATCHED_PACKAGES) {
|
|
90
|
+
if (key !== `node_modules/${name}` &&
|
|
91
|
+
!key.endsWith(`/node_modules/${name}`)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const version = typeof value === 'object' && value !== null
|
|
95
|
+
? value.version
|
|
96
|
+
: undefined;
|
|
97
|
+
result[name].push(`${key}${typeof version === 'string' ? ` (${version})` : ''}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
'@ekanos/sdk': result['@ekanos/sdk'].sort(),
|
|
102
|
+
'@ekanos/ui': result['@ekanos/ui'].sort(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function scanLockfileInstances(projectDir) {
|
|
106
|
+
const found = findLockfile(projectDir);
|
|
107
|
+
if (!found)
|
|
108
|
+
return { status: 'no-lockfile' };
|
|
109
|
+
if (found.kind === null) {
|
|
110
|
+
return { status: 'unsupported', lockfile: found.file };
|
|
111
|
+
}
|
|
112
|
+
let resolutions;
|
|
113
|
+
try {
|
|
114
|
+
const text = fs.readFileSync(found.file, 'utf8');
|
|
115
|
+
resolutions =
|
|
116
|
+
found.kind === 'pnpm' ? pnpmResolutions(text) : npmResolutions(text);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return {
|
|
120
|
+
status: 'unreadable',
|
|
121
|
+
lockfile: found.file,
|
|
122
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const duplicates = {};
|
|
126
|
+
for (const name of WATCHED_PACKAGES) {
|
|
127
|
+
if (resolutions[name].length > 1)
|
|
128
|
+
duplicates[name] = resolutions[name];
|
|
129
|
+
}
|
|
130
|
+
return { status: 'ok', lockfile: found.file, kind: found.kind, duplicates };
|
|
131
|
+
}
|
|
132
|
+
export const DUPLICATE_INSTANCE_HINT = 'Give every package in the workspace the same zod range (or pin one: ' +
|
|
133
|
+
'"overrides": { "zod": "<one version>" } in pnpm-workspace.yaml, or ' +
|
|
134
|
+
'"pnpm.overrides" / npm "overrides" in the root package.json), reinstall, ' +
|
|
135
|
+
'and confirm with "pnpm why @ekanos/sdk" (npm: "npm ls @ekanos/sdk") that ' +
|
|
136
|
+
'one copy remains.';
|
|
137
|
+
/** One line per duplicated package, for `ekanos dev`'s warning and the finding's message. */
|
|
138
|
+
export function describeDuplicates(duplicates) {
|
|
139
|
+
return Object.entries(duplicates).map(([name, entries]) => `${name} resolves ${entries.length} times: ${entries.join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The `validate` finding: a warning when a watched package resolves more than
|
|
143
|
+
* once, an `info` when the lockfile could not be read. Never an error — the
|
|
144
|
+
* lockfile is the workspace's, and a duplicate breaks the harness, not the
|
|
145
|
+
* submission.
|
|
146
|
+
*/
|
|
147
|
+
export function collectDuplicateInstanceFindings(projectDir) {
|
|
148
|
+
const scan = scanLockfileInstances(projectDir);
|
|
149
|
+
if (scan.status === 'unreadable') {
|
|
150
|
+
return [
|
|
151
|
+
{
|
|
152
|
+
check: 'project.duplicate-sdk-instance',
|
|
153
|
+
severity: 'info',
|
|
154
|
+
file: scan.lockfile,
|
|
155
|
+
message: `Could not check the lockfile for duplicate @ekanos/sdk copies: ${scan.reason}`,
|
|
156
|
+
hint: 'Run "pnpm why @ekanos/sdk" (npm: "npm ls @ekanos/sdk") to check by hand.',
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
if (scan.status !== 'ok')
|
|
161
|
+
return [];
|
|
162
|
+
const lines = describeDuplicates(scan.duplicates);
|
|
163
|
+
if (lines.length === 0)
|
|
164
|
+
return [];
|
|
165
|
+
return [
|
|
166
|
+
{
|
|
167
|
+
check: 'project.duplicate-sdk-instance',
|
|
168
|
+
severity: 'warn',
|
|
169
|
+
file: scan.lockfile,
|
|
170
|
+
message: `${lines.join('; ')}. Each copy has its own React contexts, so a ` +
|
|
171
|
+
'provider from one is invisible to hooks from the other — under ' +
|
|
172
|
+
'"ekanos dev" this is "useOperation needs an <ExperienceRuntimeProvider>".',
|
|
173
|
+
hint: DUPLICATE_INSTANCE_HINT,
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=lockfile-instances.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lockfile-instances.js","sourceRoot":"","sources":["../src/lockfile-instances.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/**\n * Does the project's lockfile resolve `@ekanos/sdk` (or `@ekanos/ui`) more\n * than once?\n *\n * Both create React contexts at module scope, so two resolved copies means a\n * provider from one is invisible to hooks from the other — under `ekanos dev`\n * that surfaces as \"useOperation needs an <ExperienceRuntimeProvider>\" with\n * the harness's provider right there. pnpm makes the second copy whenever the\n * SDK's peers (zod above all) resolve to two versions in one workspace: it\n * builds one `@ekanos/sdk@x(zod@…)` variant per peer set.\n *\n * Read-only and offline: this parses the lockfile text and never runs a\n * package manager.\n */\n\nexport const WATCHED_PACKAGES = ['@ekanos/sdk', '@ekanos/ui'] as const;\nexport type WatchedPackage = (typeof WATCHED_PACKAGES)[number];\n\nexport type LockfileKind = 'pnpm' | 'npm';\n\nexport type InstanceScan =\n /** No lockfile at or above the project. */\n | { status: 'no-lockfile' }\n /** A lockfile this check cannot read (yarn, bun); nothing is claimed. */\n | { status: 'unsupported'; lockfile: string }\n /** A lockfile it should read but could not parse. */\n | { status: 'unreadable'; lockfile: string; reason: string }\n | {\n status: 'ok';\n lockfile: string;\n kind: LockfileKind;\n /** Each watched package resolved more than once, with every resolution. */\n duplicates: Partial<Record<WatchedPackage, string[]>>;\n };\n\nconst LOCKFILES: ReadonlyArray<{ name: string; kind: LockfileKind | null }> = [\n { name: 'pnpm-lock.yaml', kind: 'pnpm' },\n { name: 'package-lock.json', kind: 'npm' },\n { name: 'npm-shrinkwrap.json', kind: 'npm' },\n { name: 'yarn.lock', kind: null },\n { name: 'bun.lock', kind: null },\n { name: 'bun.lockb', kind: null },\n];\n\n/** The nearest lockfile at or above `dir` — a workspace package's is the workspace root's. */\nexport function findLockfile(\n dir: string,\n): { file: string; kind: LockfileKind | null } | null {\n let current = path.resolve(dir);\n for (;;) {\n for (const { name, kind } of LOCKFILES) {\n const file = path.join(current, name);\n if (fs.existsSync(file)) return { file, kind };\n }\n const parent = path.dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n}\n\n/** Snapshot/package keys (`@ekanos/sdk@0.25.0(zod@4.6.7)`) per watched package in a pnpm lockfile. */\nexport function pnpmResolutions(\n text: string,\n): Record<WatchedPackage, string[]> {\n const bySection = new Map<string, Record<WatchedPackage, Set<string>>>();\n let section = '';\n // ` '@ekanos/sdk@0.25.0(zod@4.6.7)':` — and a leaf snapshot writes its\n // (empty) body on the same line: ` '@ekanos/ui@0.25.0': {}`.\n const keyPattern =\n /^ {2}'?\\/?(@ekanos\\/(?:sdk|ui))@([^'\\s:(]+(?:\\([^'\\n]*\\))?)'?:(?:\\s.*)?$/;\n\n for (const line of text.split(/\\r?\\n/)) {\n const top = /^([A-Za-z][\\w-]*):/.exec(line);\n if (top) {\n section = top[1] as string;\n continue;\n }\n const match = keyPattern.exec(line);\n if (!match) continue;\n const name = match[1] as WatchedPackage;\n const entry = bySection.get(section) ?? {\n '@ekanos/sdk': new Set<string>(),\n '@ekanos/ui': new Set<string>(),\n };\n entry[name].add(`${name}@${match[2]}`);\n bySection.set(section, entry);\n }\n\n // Lockfile v9 lists each package once under `packages:` and once per peer\n // variant under `snapshots:` — the variants are the copies. Older formats\n // have only `packages:`, with the peer suffix in the key.\n const source = bySection.get('snapshots') ?? bySection.get('packages');\n\n return {\n '@ekanos/sdk': [...(source?.['@ekanos/sdk'] ?? [])].sort(),\n '@ekanos/ui': [...(source?.['@ekanos/ui'] ?? [])].sort(),\n };\n}\n\n/** `node_modules/…/@ekanos/sdk` install paths per watched package in an npm lockfile (v2/v3). */\nexport function npmResolutions(text: string): Record<WatchedPackage, string[]> {\n const parsed: unknown = JSON.parse(text);\n const packages =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { packages?: unknown }).packages\n : undefined;\n if (typeof packages !== 'object' || packages === null) {\n throw new Error(\n 'has no \"packages\" map (lockfileVersion 1 is not supported).',\n );\n }\n\n const result: Record<WatchedPackage, string[]> = {\n '@ekanos/sdk': [],\n '@ekanos/ui': [],\n };\n for (const [key, value] of Object.entries(packages)) {\n for (const name of WATCHED_PACKAGES) {\n if (\n key !== `node_modules/${name}` &&\n !key.endsWith(`/node_modules/${name}`)\n ) {\n continue;\n }\n const version =\n typeof value === 'object' && value !== null\n ? (value as { version?: unknown }).version\n : undefined;\n result[name].push(\n `${key}${typeof version === 'string' ? ` (${version})` : ''}`,\n );\n }\n }\n\n return {\n '@ekanos/sdk': result['@ekanos/sdk'].sort(),\n '@ekanos/ui': result['@ekanos/ui'].sort(),\n };\n}\n\nexport function scanLockfileInstances(projectDir: string): InstanceScan {\n const found = findLockfile(projectDir);\n if (!found) return { status: 'no-lockfile' };\n if (found.kind === null) {\n return { status: 'unsupported', lockfile: found.file };\n }\n\n let resolutions: Record<WatchedPackage, string[]>;\n try {\n const text = fs.readFileSync(found.file, 'utf8');\n resolutions =\n found.kind === 'pnpm' ? pnpmResolutions(text) : npmResolutions(text);\n } catch (error) {\n return {\n status: 'unreadable',\n lockfile: found.file,\n reason: error instanceof Error ? error.message : String(error),\n };\n }\n\n const duplicates: Partial<Record<WatchedPackage, string[]>> = {};\n for (const name of WATCHED_PACKAGES) {\n if (resolutions[name].length > 1) duplicates[name] = resolutions[name];\n }\n\n return { status: 'ok', lockfile: found.file, kind: found.kind, duplicates };\n}\n\nexport const DUPLICATE_INSTANCE_HINT =\n 'Give every package in the workspace the same zod range (or pin one: ' +\n '\"overrides\": { \"zod\": \"<one version>\" } in pnpm-workspace.yaml, or ' +\n '\"pnpm.overrides\" / npm \"overrides\" in the root package.json), reinstall, ' +\n 'and confirm with \"pnpm why @ekanos/sdk\" (npm: \"npm ls @ekanos/sdk\") that ' +\n 'one copy remains.';\n\n/** One line per duplicated package, for `ekanos dev`'s warning and the finding's message. */\nexport function describeDuplicates(\n duplicates: Partial<Record<WatchedPackage, string[]>>,\n): string[] {\n return Object.entries(duplicates).map(\n ([name, entries]) =>\n `${name} resolves ${entries.length} times: ${entries.join(', ')}`,\n );\n}\n\n/**\n * The `validate` finding: a warning when a watched package resolves more than\n * once, an `info` when the lockfile could not be read. Never an error — the\n * lockfile is the workspace's, and a duplicate breaks the harness, not the\n * submission.\n */\nexport function collectDuplicateInstanceFindings(\n projectDir: string,\n): Finding[] {\n const scan = scanLockfileInstances(projectDir);\n\n if (scan.status === 'unreadable') {\n return [\n {\n check: 'project.duplicate-sdk-instance',\n severity: 'info',\n file: scan.lockfile,\n message: `Could not check the lockfile for duplicate @ekanos/sdk copies: ${scan.reason}`,\n hint: 'Run \"pnpm why @ekanos/sdk\" (npm: \"npm ls @ekanos/sdk\") to check by hand.',\n },\n ];\n }\n if (scan.status !== 'ok') return [];\n\n const lines = describeDuplicates(scan.duplicates);\n if (lines.length === 0) return [];\n\n return [\n {\n check: 'project.duplicate-sdk-instance',\n severity: 'warn',\n file: scan.lockfile,\n message:\n `${lines.join('; ')}. Each copy has its own React contexts, so a ` +\n 'provider from one is invisible to hooks from the other — under ' +\n '\"ekanos dev\" this is \"useOperation needs an <ExperienceRuntimeProvider>\".',\n hint: DUPLICATE_INSTANCE_HINT,\n },\n ];\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Finding } from '@ekanos/integration-schema';
|
|
2
|
+
/**
|
|
3
|
+
* Warnings about what `publish` is about to pack. Packing takes every file
|
|
4
|
+
* under `src/`, so a stray empty file or a test goes into a submission that
|
|
5
|
+
* cannot be changed afterwards (a version is immutable). Nothing here changes
|
|
6
|
+
* what is packed; it only names what would be.
|
|
7
|
+
*/
|
|
8
|
+
export declare function collectPackHygieneFindings(projectDir: string): Finding[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { selectProjectFiles } from './pack.js';
|
|
4
|
+
const TEST_FILE = /\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
5
|
+
const TEST_DIRECTORY = /(^|\/)(__tests__|__fixtures__|__mocks__|fixtures)\//;
|
|
6
|
+
/** How many paths a message lists before it counts the rest. */
|
|
7
|
+
const LISTED = 8;
|
|
8
|
+
function list(files) {
|
|
9
|
+
const shown = files.slice(0, LISTED).join(', ');
|
|
10
|
+
return files.length > LISTED
|
|
11
|
+
? `${shown} and ${files.length - LISTED} more`
|
|
12
|
+
: shown;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Warnings about what `publish` is about to pack. Packing takes every file
|
|
16
|
+
* under `src/`, so a stray empty file or a test goes into a submission that
|
|
17
|
+
* cannot be changed afterwards (a version is immutable). Nothing here changes
|
|
18
|
+
* what is packed; it only names what would be.
|
|
19
|
+
*/
|
|
20
|
+
export function collectPackHygieneFindings(projectDir) {
|
|
21
|
+
const findings = [];
|
|
22
|
+
// Packing refuses a project without these and says so itself.
|
|
23
|
+
const packable = ['ekanos.json', 'package.json'].every((name) => fs.existsSync(path.join(projectDir, name)));
|
|
24
|
+
if (!packable)
|
|
25
|
+
return findings;
|
|
26
|
+
const files = selectProjectFiles(projectDir);
|
|
27
|
+
const empty = files.filter((file) => {
|
|
28
|
+
try {
|
|
29
|
+
return fs.statSync(path.join(projectDir, file)).size === 0;
|
|
30
|
+
}
|
|
31
|
+
catch (_a) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
if (empty.length > 0) {
|
|
36
|
+
findings.push({
|
|
37
|
+
check: 'pack.empty-file',
|
|
38
|
+
severity: 'warn',
|
|
39
|
+
file: empty[0],
|
|
40
|
+
message: `${empty.length} empty file${empty.length === 1 ? '' : 's'} would be packed into the submission: ${list(empty)}.`,
|
|
41
|
+
hint: 'Delete a file you did not mean to ship, or put something in it. A submitted version cannot be changed, only superseded by a version bump.',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const tests = files.filter((file) => file.startsWith('src/') &&
|
|
45
|
+
(TEST_FILE.test(file) || TEST_DIRECTORY.test(file)));
|
|
46
|
+
if (tests.length > 0) {
|
|
47
|
+
findings.push({
|
|
48
|
+
check: 'pack.test-file',
|
|
49
|
+
severity: 'warn',
|
|
50
|
+
file: tests[0],
|
|
51
|
+
message: `${tests.length} test or fixture file${tests.length === 1 ? '' : 's'} under src/ would be packed into the submission: ${list(tests)}.`,
|
|
52
|
+
hint: 'Everything under src/ is packed as it is. Tests are harmless but add weight to a version that cannot be changed once submitted.',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return findings;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=pack-hygiene.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pack-hygiene.js","sourceRoot":"","sources":["../src/pack-hygiene.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AAE5C,MAAM,SAAS,GAAG,8BAA8B,CAAC;AACjD,MAAM,cAAc,GAAG,qDAAqD,CAAC;AAE7E,gEAAgE;AAChE,MAAM,MAAM,GAAG,CAAC,CAAC;AAEjB,SAAS,IAAI,CAAC,KAAwB;IACpC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEhD,OAAO,KAAK,CAAC,MAAM,GAAG,MAAM;QAC1B,CAAC,CAAC,GAAG,KAAK,QAAQ,KAAK,CAAC,MAAM,GAAG,MAAM,OAAO;QAC9C,CAAC,CAAC,KAAK,CAAC;AACZ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,UAAkB;IAC3D,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,8DAA8D;IAC9D,MAAM,QAAQ,GAAG,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAC9D,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAC3C,CAAC;IACF,IAAI,CAAC,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC/B,MAAM,KAAK,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;IAE7C,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QAClC,IAAI,CAAC;YACH,OAAO,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC;QAC7D,CAAC;mBAAO,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;IACH,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,QAAQ,CAAC,IAAI,CAAC;YACZ,KAAK,EAAE,iBAAiB;YACxB,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;YACd,OAAO,EAAE,GAAG,KAAK,CAAC,MAAM,cAAc,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,yCAAyC,IAAI,CAAC,KAAK,CAAC,GAAG;YAC1H,IAAI,EAAE,2IAA2I;SAClJ,CAAC,CAAC;IACL,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CACxB,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QACvB,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CACtD,CAAC;IACF,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,QAAQ,CAAC,IAAI,CAAC;YACZ,KAAK,EAAE,gBAAgB;YACvB,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;YACd,OAAO,EAAE,GAAG,KAAK,CAAC,MAAM,wBAAwB,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,oDAAoD,IAAI,CAAC,KAAK,CAAC,GAAG;YAC/I,IAAI,EAAE,iIAAiI;SACxI,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { selectProjectFiles } from './pack';\n\nconst TEST_FILE = /\\.(test|spec)\\.[cm]?[jt]sx?$/;\nconst TEST_DIRECTORY = /(^|\\/)(__tests__|__fixtures__|__mocks__|fixtures)\\//;\n\n/** How many paths a message lists before it counts the rest. */\nconst LISTED = 8;\n\nfunction list(files: readonly string[]): string {\n const shown = files.slice(0, LISTED).join(', ');\n\n return files.length > LISTED\n ? `${shown} and ${files.length - LISTED} more`\n : shown;\n}\n\n/**\n * Warnings about what `publish` is about to pack. Packing takes every file\n * under `src/`, so a stray empty file or a test goes into a submission that\n * cannot be changed afterwards (a version is immutable). Nothing here changes\n * what is packed; it only names what would be.\n */\nexport function collectPackHygieneFindings(projectDir: string): Finding[] {\n const findings: Finding[] = [];\n // Packing refuses a project without these and says so itself.\n const packable = ['ekanos.json', 'package.json'].every((name) =>\n fs.existsSync(path.join(projectDir, name)),\n );\n if (!packable) return findings;\n const files = selectProjectFiles(projectDir);\n\n const empty = files.filter((file) => {\n try {\n return fs.statSync(path.join(projectDir, file)).size === 0;\n } catch {\n return false;\n }\n });\n if (empty.length > 0) {\n findings.push({\n check: 'pack.empty-file',\n severity: 'warn',\n file: empty[0],\n message: `${empty.length} empty file${empty.length === 1 ? '' : 's'} would be packed into the submission: ${list(empty)}.`,\n hint: 'Delete a file you did not mean to ship, or put something in it. A submitted version cannot be changed, only superseded by a version bump.',\n });\n }\n\n const tests = files.filter(\n (file) =>\n file.startsWith('src/') &&\n (TEST_FILE.test(file) || TEST_DIRECTORY.test(file)),\n );\n if (tests.length > 0) {\n findings.push({\n check: 'pack.test-file',\n severity: 'warn',\n file: tests[0],\n message: `${tests.length} test or fixture file${tests.length === 1 ? '' : 's'} under src/ would be packed into the submission: ${list(tests)}.`,\n hint: 'Everything under src/ is packed as it is. Tests are harmless but add weight to a version that cannot be changed once submitted.',\n });\n }\n\n return findings;\n}\n"]}
|
|
@@ -2,11 +2,21 @@ export type PackageManagerName = 'pnpm' | 'yarn' | 'bun' | 'npm';
|
|
|
2
2
|
export interface PackageManager {
|
|
3
3
|
name: PackageManagerName;
|
|
4
4
|
bin: string;
|
|
5
|
+
/**
|
|
6
|
+
* The directory the manager is anchored at: where its lockfile lives and
|
|
7
|
+
* where a frozen reinstall runs. The workspace root for a workspace
|
|
8
|
+
* package, else the project itself.
|
|
9
|
+
*/
|
|
10
|
+
root: string;
|
|
5
11
|
}
|
|
6
12
|
/**
|
|
7
|
-
* Detect the package manager
|
|
8
|
-
*
|
|
9
|
-
* `packageManager`
|
|
13
|
+
* Detect the package manager for `projectDir`. In each directory from the
|
|
14
|
+
* project up to its workspace root (`findWorkspaceRoot`), a lockfile decides
|
|
15
|
+
* first, then the corepack `packageManager` field, then a
|
|
16
|
+
* `pnpm-workspace.yaml`. A workspace package has no lockfile of its own — the
|
|
17
|
+
* workspace root holds it — so stopping at the project would answer npm for a
|
|
18
|
+
* pnpm workspace, and npm run inside pnpm's node_modules breaks it. Falls back
|
|
19
|
+
* to npm at the project when nothing says otherwise. `$PATH` is never probed.
|
|
10
20
|
*/
|
|
11
21
|
export declare function detectPackageManager(projectDir: string): PackageManager;
|
|
12
22
|
/**
|
|
@@ -29,7 +39,25 @@ export declare function installCommand(pm: PackageManager, packages: readonly st
|
|
|
29
39
|
* or a range. `dev` selects `dependencies` vs `devDependencies`, matching
|
|
30
40
|
* wherever the project already declares the package: `upgrade` moves a
|
|
31
41
|
* package to a new version, it never relocates it between the two.
|
|
42
|
+
*
|
|
43
|
+
* `workspaceRoot` marks a run at the root of a multi-package workspace: pnpm
|
|
44
|
+
* refuses `add` there without `-w` and yarn without `-W`. Run in a package
|
|
45
|
+
* directory, every manager scopes the add to that package by itself.
|
|
46
|
+
*/
|
|
47
|
+
export declare function installExactArgs(pm: PackageManager, packages: readonly string[], dev: boolean, workspaceRoot?: boolean): string[];
|
|
48
|
+
/**
|
|
49
|
+
* The argv that reinstalls `node_modules` exactly as the lockfile says, run at
|
|
50
|
+
* {@link PackageManager.root} — what a rollback needs once `package.json` and
|
|
51
|
+
* the lockfile are restored. Without a lockfile there is nothing to be frozen
|
|
52
|
+
* to, so it is a plain install.
|
|
53
|
+
*/
|
|
54
|
+
export declare function frozenInstallArgs(pm: PackageManager, hasLockfile: boolean): string[];
|
|
55
|
+
/** The lockfiles this package manager may write, at {@link PackageManager.root}. */
|
|
56
|
+
export declare function lockfileNames(pm: PackageManager): string[];
|
|
57
|
+
/**
|
|
58
|
+
* Whether `dir` is the root of a workspace with packages of its own, where
|
|
59
|
+
* pnpm and yarn need {@link installExactArgs}'s `workspaceRoot`. A standalone
|
|
60
|
+
* project's `pnpm-workspace.yaml` (build-script answers, no `packages:`) does
|
|
61
|
+
* not count.
|
|
32
62
|
*/
|
|
33
|
-
export declare function
|
|
34
|
-
/** The lockfile this package manager reads and writes, at the project root. */
|
|
35
|
-
export declare function lockfileName(pm: PackageManager): string;
|
|
63
|
+
export declare function isMultiPackageWorkspaceRoot(dir: string): boolean;
|
package/dist/package-manager.js
CHANGED
|
@@ -1,19 +1,80 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { findWorkspaceRoot } from './workspace-root.js';
|
|
4
|
+
const LOCKFILE_SIGNALS = [
|
|
5
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
6
|
+
['yarn.lock', 'yarn'],
|
|
7
|
+
['bun.lockb', 'bun'],
|
|
8
|
+
['bun.lock', 'bun'],
|
|
9
|
+
['package-lock.json', 'npm'],
|
|
10
|
+
['npm-shrinkwrap.json', 'npm'],
|
|
11
|
+
];
|
|
12
|
+
const MANAGER_NAMES = [
|
|
13
|
+
'pnpm',
|
|
14
|
+
'yarn',
|
|
15
|
+
'bun',
|
|
16
|
+
'npm',
|
|
17
|
+
];
|
|
18
|
+
/** `"pnpm@10.4.1"` → `pnpm`, from package.json's corepack `packageManager` field. */
|
|
19
|
+
function declaredManager(dir) {
|
|
20
|
+
var _a;
|
|
21
|
+
const manifestPath = path.join(dir, 'package.json');
|
|
22
|
+
if (!fs.existsSync(manifestPath))
|
|
23
|
+
return null;
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
27
|
+
}
|
|
28
|
+
catch (_b) {
|
|
29
|
+
// An unreadable package.json declares nothing; the command reading it for
|
|
30
|
+
// real reports the parse error.
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const field = typeof parsed === 'object' && parsed !== null
|
|
34
|
+
? parsed.packageManager
|
|
35
|
+
: undefined;
|
|
36
|
+
if (typeof field !== 'string')
|
|
37
|
+
return null;
|
|
38
|
+
const name = field.split('@')[0];
|
|
39
|
+
return (_a = MANAGER_NAMES.find((candidate) => candidate === name)) !== null && _a !== void 0 ? _a : null;
|
|
40
|
+
}
|
|
41
|
+
function managerAt(dir) {
|
|
42
|
+
for (const [file, name] of LOCKFILE_SIGNALS) {
|
|
43
|
+
if (fs.existsSync(path.join(dir, file)))
|
|
44
|
+
return name;
|
|
45
|
+
}
|
|
46
|
+
const declared = declaredManager(dir);
|
|
47
|
+
if (declared)
|
|
48
|
+
return declared;
|
|
49
|
+
if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml')))
|
|
50
|
+
return 'pnpm';
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
3
53
|
/**
|
|
4
|
-
* Detect the package manager
|
|
5
|
-
*
|
|
6
|
-
* `packageManager`
|
|
54
|
+
* Detect the package manager for `projectDir`. In each directory from the
|
|
55
|
+
* project up to its workspace root (`findWorkspaceRoot`), a lockfile decides
|
|
56
|
+
* first, then the corepack `packageManager` field, then a
|
|
57
|
+
* `pnpm-workspace.yaml`. A workspace package has no lockfile of its own — the
|
|
58
|
+
* workspace root holds it — so stopping at the project would answer npm for a
|
|
59
|
+
* pnpm workspace, and npm run inside pnpm's node_modules breaks it. Falls back
|
|
60
|
+
* to npm at the project when nothing says otherwise. `$PATH` is never probed.
|
|
7
61
|
*/
|
|
8
62
|
export function detectPackageManager(projectDir) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
63
|
+
const start = path.resolve(projectDir);
|
|
64
|
+
const top = findWorkspaceRoot(start);
|
|
65
|
+
let current = start;
|
|
66
|
+
for (;;) {
|
|
67
|
+
const name = managerAt(current);
|
|
68
|
+
if (name)
|
|
69
|
+
return { name, bin: name, root: current };
|
|
70
|
+
if (current === top)
|
|
71
|
+
break;
|
|
72
|
+
const parent = path.dirname(current);
|
|
73
|
+
if (parent === current)
|
|
74
|
+
break;
|
|
75
|
+
current = parent;
|
|
76
|
+
}
|
|
77
|
+
return { name: 'npm', bin: 'npm', root: start };
|
|
17
78
|
}
|
|
18
79
|
/**
|
|
19
80
|
* The argv for running a project-local binary through a package manager.
|
|
@@ -60,30 +121,79 @@ export function installCommand(pm, packages) {
|
|
|
60
121
|
* or a range. `dev` selects `dependencies` vs `devDependencies`, matching
|
|
61
122
|
* wherever the project already declares the package: `upgrade` moves a
|
|
62
123
|
* package to a new version, it never relocates it between the two.
|
|
124
|
+
*
|
|
125
|
+
* `workspaceRoot` marks a run at the root of a multi-package workspace: pnpm
|
|
126
|
+
* refuses `add` there without `-w` and yarn without `-W`. Run in a package
|
|
127
|
+
* directory, every manager scopes the add to that package by itself.
|
|
63
128
|
*/
|
|
64
|
-
export function installExactArgs(pm, packages, dev) {
|
|
129
|
+
export function installExactArgs(pm, packages, dev, workspaceRoot = false) {
|
|
65
130
|
switch (pm.name) {
|
|
66
131
|
case 'pnpm':
|
|
67
|
-
return [
|
|
132
|
+
return [
|
|
133
|
+
'add',
|
|
134
|
+
...(workspaceRoot ? ['-w'] : []),
|
|
135
|
+
...(dev ? ['-D'] : []),
|
|
136
|
+
...packages,
|
|
137
|
+
];
|
|
68
138
|
case 'yarn':
|
|
69
|
-
return [
|
|
139
|
+
return [
|
|
140
|
+
'add',
|
|
141
|
+
...(workspaceRoot ? ['-W'] : []),
|
|
142
|
+
...(dev ? ['-D'] : []),
|
|
143
|
+
...packages,
|
|
144
|
+
];
|
|
70
145
|
case 'bun':
|
|
71
146
|
return ['add', ...(dev ? ['-d'] : []), ...packages];
|
|
72
147
|
case 'npm':
|
|
73
148
|
return ['install', ...(dev ? ['-D'] : []), ...packages];
|
|
74
149
|
}
|
|
75
150
|
}
|
|
76
|
-
/**
|
|
77
|
-
|
|
151
|
+
/**
|
|
152
|
+
* The argv that reinstalls `node_modules` exactly as the lockfile says, run at
|
|
153
|
+
* {@link PackageManager.root} — what a rollback needs once `package.json` and
|
|
154
|
+
* the lockfile are restored. Without a lockfile there is nothing to be frozen
|
|
155
|
+
* to, so it is a plain install.
|
|
156
|
+
*/
|
|
157
|
+
export function frozenInstallArgs(pm, hasLockfile) {
|
|
158
|
+
if (!hasLockfile)
|
|
159
|
+
return ['install'];
|
|
78
160
|
switch (pm.name) {
|
|
79
161
|
case 'pnpm':
|
|
80
|
-
return 'pnpm-lock.yaml';
|
|
81
162
|
case 'yarn':
|
|
82
|
-
return 'yarn.lock';
|
|
83
163
|
case 'bun':
|
|
84
|
-
return '
|
|
164
|
+
return ['install', '--frozen-lockfile'];
|
|
85
165
|
case 'npm':
|
|
86
|
-
return '
|
|
166
|
+
return ['ci'];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** The lockfiles this package manager may write, at {@link PackageManager.root}. */
|
|
170
|
+
export function lockfileNames(pm) {
|
|
171
|
+
return LOCKFILE_SIGNALS.filter(([, name]) => name === pm.name).map(([file]) => file);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether `dir` is the root of a workspace with packages of its own, where
|
|
175
|
+
* pnpm and yarn need {@link installExactArgs}'s `workspaceRoot`. A standalone
|
|
176
|
+
* project's `pnpm-workspace.yaml` (build-script answers, no `packages:`) does
|
|
177
|
+
* not count.
|
|
178
|
+
*/
|
|
179
|
+
export function isMultiPackageWorkspaceRoot(dir) {
|
|
180
|
+
const workspaceFile = path.join(dir, 'pnpm-workspace.yaml');
|
|
181
|
+
if (fs.existsSync(workspaceFile) &&
|
|
182
|
+
/^packages:/m.test(fs.readFileSync(workspaceFile, 'utf8'))) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
const manifestPath = path.join(dir, 'package.json');
|
|
186
|
+
if (!fs.existsSync(manifestPath))
|
|
187
|
+
return false;
|
|
188
|
+
try {
|
|
189
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
190
|
+
return (typeof parsed === 'object' &&
|
|
191
|
+
parsed !== null &&
|
|
192
|
+
parsed.workspaces !== undefined);
|
|
193
|
+
}
|
|
194
|
+
catch (_a) {
|
|
195
|
+
// Unreadable: the caller's own read of package.json reports it.
|
|
196
|
+
return false;
|
|
87
197
|
}
|
|
88
198
|
}
|
|
89
199
|
//# sourceMappingURL=package-manager.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package-manager.js","sourceRoot":"","sources":["../src/package-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nexport type PackageManagerName = 'pnpm' | 'yarn' | 'bun' | 'npm';\n\nexport interface PackageManager {\n name: PackageManagerName;\n bin: string;\n}\n\n/**\n * Detect the package manager from the lockfile in the project root, falling\n * back to npm. Only the lockfile is authoritative — we do not parse\n * `packageManager` fields or probe $PATH.\n */\nexport function detectPackageManager(projectDir: string): PackageManager {\n const has = (file: string) => fs.existsSync(path.join(projectDir, file));\n if (has('pnpm-lock.yaml')) return { name: 'pnpm', bin: 'pnpm' };\n if (has('yarn.lock')) return { name: 'yarn', bin: 'yarn' };\n if (has('bun.lockb') || has('bun.lock')) return { name: 'bun', bin: 'bun' };\n return { name: 'npm', bin: 'npm' };\n}\n\n/**\n * The argv for running a project-local binary through a package manager.\n *\n * The harness shell has no `node_modules` of its own — `next` lives in the\n * partner's project root and resolves upward — so we cannot spawn the bin\n * directly by path without guessing a layout (pnpm's is not npm's). Every\n * package manager already knows how to find it; each just spells it\n * differently.\n */\nexport function execArgs(\n pm: PackageManager,\n bin: string,\n args: readonly string[],\n): string[] {\n switch (pm.name) {\n case 'pnpm':\n return ['exec', bin, ...args];\n case 'yarn':\n return [bin, ...args];\n case 'bun':\n return ['x', bin, ...args];\n case 'npm':\n return ['exec', '--', bin, ...args];\n }\n}\n\n/** The same invocation as a display string, for logs and the JSON envelope. */\nexport function formatCommand(bin: string, args: readonly string[]): string {\n return [bin, ...args].join(' ');\n}\n\n/** The install command that adds `packages` as dev dependencies. */\nexport function installCommand(\n pm: PackageManager,\n packages: readonly string[],\n): string {\n const spec = packages.join(' ');\n switch (pm.name) {\n case 'pnpm':\n return `pnpm add -D ${spec}`;\n case 'yarn':\n return `yarn add -D ${spec}`;\n case 'bun':\n return `bun add -d ${spec}`;\n case 'npm':\n return `npm install -D ${spec}`;\n }\n}\n\n/**\n * The argv (no shell) for adding `packages` at EXACT versions — used by\n * `ekanos upgrade`, which must land specific pinned versions, never \"latest\"\n * or a range. `dev` selects `dependencies` vs `devDependencies`, matching\n * wherever the project already declares the package: `upgrade` moves a\n * package to a new version, it never relocates it between the two.\n */\nexport function installExactArgs(\n pm: PackageManager,\n packages: readonly string[],\n dev: boolean,\n): string[] {\n switch (pm.name) {\n case 'pnpm':\n return ['add', ...(dev ? ['-D'] : []), ...packages];\n case 'yarn':\n return ['add', ...(dev ? ['-D'] : []), ...packages];\n case 'bun':\n return ['add', ...(dev ? ['-d'] : []), ...packages];\n case 'npm':\n return ['install', ...(dev ? ['-D'] : []), ...packages];\n }\n}\n\n/** The lockfile this package manager reads and writes, at the project root. */\nexport function lockfileName(pm: PackageManager): string {\n switch (pm.name) {\n case 'pnpm':\n return 'pnpm-lock.yaml';\n case 'yarn':\n return 'yarn.lock';\n case 'bun':\n return 'bun.lock';\n case 'npm':\n return 'package-lock.json';\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"package-manager.js","sourceRoot":"","sources":["../src/package-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { findWorkspaceRoot } from './workspace-root';\n\nexport type PackageManagerName = 'pnpm' | 'yarn' | 'bun' | 'npm';\n\nexport interface PackageManager {\n name: PackageManagerName;\n bin: string;\n /**\n * The directory the manager is anchored at: where its lockfile lives and\n * where a frozen reinstall runs. The workspace root for a workspace\n * package, else the project itself.\n */\n root: string;\n}\n\nconst LOCKFILE_SIGNALS: ReadonlyArray<[string, PackageManagerName]> = [\n ['pnpm-lock.yaml', 'pnpm'],\n ['yarn.lock', 'yarn'],\n ['bun.lockb', 'bun'],\n ['bun.lock', 'bun'],\n ['package-lock.json', 'npm'],\n ['npm-shrinkwrap.json', 'npm'],\n];\n\nconst MANAGER_NAMES: readonly PackageManagerName[] = [\n 'pnpm',\n 'yarn',\n 'bun',\n 'npm',\n];\n\n/** `\"pnpm@10.4.1\"` → `pnpm`, from package.json's corepack `packageManager` field. */\nfunction declaredManager(dir: string): PackageManagerName | null {\n const manifestPath = path.join(dir, 'package.json');\n if (!fs.existsSync(manifestPath)) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch {\n // An unreadable package.json declares nothing; the command reading it for\n // real reports the parse error.\n return null;\n }\n const field =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { packageManager?: unknown }).packageManager\n : undefined;\n if (typeof field !== 'string') return null;\n const name = field.split('@')[0];\n\n return MANAGER_NAMES.find((candidate) => candidate === name) ?? null;\n}\n\nfunction managerAt(dir: string): PackageManagerName | null {\n for (const [file, name] of LOCKFILE_SIGNALS) {\n if (fs.existsSync(path.join(dir, file))) return name;\n }\n const declared = declaredManager(dir);\n if (declared) return declared;\n if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) return 'pnpm';\n\n return null;\n}\n\n/**\n * Detect the package manager for `projectDir`. In each directory from the\n * project up to its workspace root (`findWorkspaceRoot`), a lockfile decides\n * first, then the corepack `packageManager` field, then a\n * `pnpm-workspace.yaml`. A workspace package has no lockfile of its own — the\n * workspace root holds it — so stopping at the project would answer npm for a\n * pnpm workspace, and npm run inside pnpm's node_modules breaks it. Falls back\n * to npm at the project when nothing says otherwise. `$PATH` is never probed.\n */\nexport function detectPackageManager(projectDir: string): PackageManager {\n const start = path.resolve(projectDir);\n const top = findWorkspaceRoot(start);\n let current = start;\n for (;;) {\n const name = managerAt(current);\n if (name) return { name, bin: name, root: current };\n if (current === top) break;\n const parent = path.dirname(current);\n if (parent === current) break;\n current = parent;\n }\n\n return { name: 'npm', bin: 'npm', root: start };\n}\n\n/**\n * The argv for running a project-local binary through a package manager.\n *\n * The harness shell has no `node_modules` of its own — `next` lives in the\n * partner's project root and resolves upward — so we cannot spawn the bin\n * directly by path without guessing a layout (pnpm's is not npm's). Every\n * package manager already knows how to find it; each just spells it\n * differently.\n */\nexport function execArgs(\n pm: PackageManager,\n bin: string,\n args: readonly string[],\n): string[] {\n switch (pm.name) {\n case 'pnpm':\n return ['exec', bin, ...args];\n case 'yarn':\n return [bin, ...args];\n case 'bun':\n return ['x', bin, ...args];\n case 'npm':\n return ['exec', '--', bin, ...args];\n }\n}\n\n/** The same invocation as a display string, for logs and the JSON envelope. */\nexport function formatCommand(bin: string, args: readonly string[]): string {\n return [bin, ...args].join(' ');\n}\n\n/** The install command that adds `packages` as dev dependencies. */\nexport function installCommand(\n pm: PackageManager,\n packages: readonly string[],\n): string {\n const spec = packages.join(' ');\n switch (pm.name) {\n case 'pnpm':\n return `pnpm add -D ${spec}`;\n case 'yarn':\n return `yarn add -D ${spec}`;\n case 'bun':\n return `bun add -d ${spec}`;\n case 'npm':\n return `npm install -D ${spec}`;\n }\n}\n\n/**\n * The argv (no shell) for adding `packages` at EXACT versions — used by\n * `ekanos upgrade`, which must land specific pinned versions, never \"latest\"\n * or a range. `dev` selects `dependencies` vs `devDependencies`, matching\n * wherever the project already declares the package: `upgrade` moves a\n * package to a new version, it never relocates it between the two.\n *\n * `workspaceRoot` marks a run at the root of a multi-package workspace: pnpm\n * refuses `add` there without `-w` and yarn without `-W`. Run in a package\n * directory, every manager scopes the add to that package by itself.\n */\nexport function installExactArgs(\n pm: PackageManager,\n packages: readonly string[],\n dev: boolean,\n workspaceRoot = false,\n): string[] {\n switch (pm.name) {\n case 'pnpm':\n return [\n 'add',\n ...(workspaceRoot ? ['-w'] : []),\n ...(dev ? ['-D'] : []),\n ...packages,\n ];\n case 'yarn':\n return [\n 'add',\n ...(workspaceRoot ? ['-W'] : []),\n ...(dev ? ['-D'] : []),\n ...packages,\n ];\n case 'bun':\n return ['add', ...(dev ? ['-d'] : []), ...packages];\n case 'npm':\n return ['install', ...(dev ? ['-D'] : []), ...packages];\n }\n}\n\n/**\n * The argv that reinstalls `node_modules` exactly as the lockfile says, run at\n * {@link PackageManager.root} — what a rollback needs once `package.json` and\n * the lockfile are restored. Without a lockfile there is nothing to be frozen\n * to, so it is a plain install.\n */\nexport function frozenInstallArgs(\n pm: PackageManager,\n hasLockfile: boolean,\n): string[] {\n if (!hasLockfile) return ['install'];\n switch (pm.name) {\n case 'pnpm':\n case 'yarn':\n case 'bun':\n return ['install', '--frozen-lockfile'];\n case 'npm':\n return ['ci'];\n }\n}\n\n/** The lockfiles this package manager may write, at {@link PackageManager.root}. */\nexport function lockfileNames(pm: PackageManager): string[] {\n return LOCKFILE_SIGNALS.filter(([, name]) => name === pm.name).map(\n ([file]) => file,\n );\n}\n\n/**\n * Whether `dir` is the root of a workspace with packages of its own, where\n * pnpm and yarn need {@link installExactArgs}'s `workspaceRoot`. A standalone\n * project's `pnpm-workspace.yaml` (build-script answers, no `packages:`) does\n * not count.\n */\nexport function isMultiPackageWorkspaceRoot(dir: string): boolean {\n const workspaceFile = path.join(dir, 'pnpm-workspace.yaml');\n if (\n fs.existsSync(workspaceFile) &&\n /^packages:/m.test(fs.readFileSync(workspaceFile, 'utf8'))\n ) {\n return true;\n }\n const manifestPath = path.join(dir, 'package.json');\n if (!fs.existsSync(manifestPath)) return false;\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n return (\n typeof parsed === 'object' &&\n parsed !== null &&\n (parsed as { workspaces?: unknown }).workspaces !== undefined\n );\n } catch {\n // Unreadable: the caller's own read of package.json reports it.\n return false;\n }\n}\n"]}
|