@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.
Files changed (76) hide show
  1. package/README.md +18 -4
  2. package/dist/commands/add.js +21 -10
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/contract.js +12 -2
  5. package/dist/commands/contract.js.map +1 -1
  6. package/dist/commands/dev.js +33 -2
  7. package/dist/commands/dev.js.map +1 -1
  8. package/dist/commands/publish.d.ts +6 -0
  9. package/dist/commands/publish.js +80 -4
  10. package/dist/commands/publish.js.map +1 -1
  11. package/dist/commands/status.d.ts +44 -0
  12. package/dist/commands/status.js +79 -18
  13. package/dist/commands/status.js.map +1 -1
  14. package/dist/commands/upgrade.js +287 -144
  15. package/dist/commands/upgrade.js.map +1 -1
  16. package/dist/docs-links.js +5 -1
  17. package/dist/docs-links.js.map +1 -1
  18. package/dist/errors.d.ts +2 -0
  19. package/dist/errors.js +9 -0
  20. package/dist/errors.js.map +1 -1
  21. package/dist/exit-codes.d.ts +3 -0
  22. package/dist/exit-codes.js +3 -0
  23. package/dist/exit-codes.js.map +1 -1
  24. package/dist/experience/checks.js +2 -1
  25. package/dist/experience/checks.js.map +1 -1
  26. package/dist/experience/definition-checks.d.ts +7 -0
  27. package/dist/experience/definition-checks.js +36 -4
  28. package/dist/experience/definition-checks.js.map +1 -1
  29. package/dist/experience/init.js +10 -0
  30. package/dist/experience/init.js.map +1 -1
  31. package/dist/experience/load-experience.js +33 -2
  32. package/dist/experience/load-experience.js.map +1 -1
  33. package/dist/experience/pins.d.ts +10 -0
  34. package/dist/experience/pins.js +19 -1
  35. package/dist/experience/pins.js.map +1 -1
  36. package/dist/generated/phosphor-names.d.ts +3 -0
  37. package/dist/generated/phosphor-names.js +1541 -0
  38. package/dist/generated/phosphor-names.js.map +1 -0
  39. package/dist/harness-scaffold.d.ts +13 -0
  40. package/dist/harness-scaffold.js +19 -3
  41. package/dist/harness-scaffold.js.map +1 -1
  42. package/dist/icon-findings.d.ts +12 -17
  43. package/dist/icon-findings.js +49 -74
  44. package/dist/icon-findings.js.map +1 -1
  45. package/dist/index.js +28 -5
  46. package/dist/index.js.map +1 -1
  47. package/dist/lockfile-instances.d.ts +60 -0
  48. package/dist/lockfile-instances.js +177 -0
  49. package/dist/lockfile-instances.js.map +1 -0
  50. package/dist/pack-hygiene.d.ts +8 -0
  51. package/dist/pack-hygiene.js +57 -0
  52. package/dist/pack-hygiene.js.map +1 -0
  53. package/dist/package-manager.d.ts +34 -6
  54. package/dist/package-manager.js +130 -20
  55. package/dist/package-manager.js.map +1 -1
  56. package/dist/project.js +11 -0
  57. package/dist/project.js.map +1 -1
  58. package/dist/repo/scaffold.js +4 -0
  59. package/dist/repo/scaffold.js.map +1 -1
  60. package/dist/validate-findings.js +10 -1
  61. package/dist/validate-findings.js.map +1 -1
  62. package/dist/workspace-root.d.ts +16 -0
  63. package/dist/workspace-root.js +50 -0
  64. package/dist/workspace-root.js.map +1 -0
  65. package/package.json +4 -4
  66. package/templates/AGENTS.md.tmpl +11 -15
  67. package/templates/experience/package.json.tmpl +8 -6
  68. package/templates/experience/src/widgets/hello-widget.test.tsx.tmpl +49 -0
  69. package/templates/experience/vitest.config.ts.tmpl +21 -0
  70. package/templates/harness/next.config.mjs.tmpl +7 -4
  71. package/templates/package.json.tmpl +5 -5
  72. package/templates/skills/build-integration/references/widget-design.md +2 -3
  73. package/templates/skills/polish-widgets/references/patterns.md +5 -5
  74. package/dist/generated/fa-free-names.d.ts +0 -9
  75. package/dist/generated/fa-free-names.js +0 -2599
  76. 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 from the lockfile in the project root, falling
8
- * back to npm. Only the lockfile is authoritative — we do not parse
9
- * `packageManager` fields or probe $PATH.
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 installExactArgs(pm: PackageManager, packages: readonly string[], dev: boolean): string[];
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;
@@ -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 from the lockfile in the project root, falling
5
- * back to npm. Only the lockfile is authoritative — we do not parse
6
- * `packageManager` fields or probe $PATH.
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 has = (file) => fs.existsSync(path.join(projectDir, file));
10
- if (has('pnpm-lock.yaml'))
11
- return { name: 'pnpm', bin: 'pnpm' };
12
- if (has('yarn.lock'))
13
- return { name: 'yarn', bin: 'yarn' };
14
- if (has('bun.lockb') || has('bun.lock'))
15
- return { name: 'bun', bin: 'bun' };
16
- return { name: 'npm', bin: 'npm' };
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 ['add', ...(dev ? ['-D'] : []), ...packages];
132
+ return [
133
+ 'add',
134
+ ...(workspaceRoot ? ['-w'] : []),
135
+ ...(dev ? ['-D'] : []),
136
+ ...packages,
137
+ ];
68
138
  case 'yarn':
69
- return ['add', ...(dev ? ['-D'] : []), ...packages];
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
- /** The lockfile this package manager reads and writes, at the project root. */
77
- export function lockfileName(pm) {
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 'bun.lock';
164
+ return ['install', '--frozen-lockfile'];
85
165
  case 'npm':
86
- return 'package-lock.json';
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"]}