@_linked/cli 1.46.1 → 1.47.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 (167) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +48 -36
  3. package/hooks/drift.cjs +169 -0
  4. package/hooks/git-hook.cjs +170 -0
  5. package/hooks/hook-shim.cjs +63 -0
  6. package/hooks/lock-at-rest.cjs +268 -0
  7. package/hooks/manifests.cjs +216 -0
  8. package/hooks/platform.cjs +25 -0
  9. package/lib/esm/app-release/build-backend.d.ts.map +1 -1
  10. package/lib/esm/app-release/build-backend.js +2 -1
  11. package/lib/esm/app-release/build-backend.js.map +1 -1
  12. package/lib/esm/cli-methods.d.ts.map +1 -1
  13. package/lib/esm/cli-methods.js +166 -150
  14. package/lib/esm/cli-methods.js.map +1 -1
  15. package/lib/esm/cli.js +20 -22
  16. package/lib/esm/cli.js.map +1 -1
  17. package/lib/esm/commands/build-app.d.ts.map +1 -1
  18. package/lib/esm/commands/build-app.js +2 -1
  19. package/lib/esm/commands/build-app.js.map +1 -1
  20. package/lib/esm/commands/build-package.d.ts.map +1 -1
  21. package/lib/esm/commands/build-package.js +6 -2
  22. package/lib/esm/commands/build-package.js.map +1 -1
  23. package/lib/esm/commands/localize.d.ts +9 -15
  24. package/lib/esm/commands/localize.d.ts.map +1 -1
  25. package/lib/esm/commands/localize.js +43 -74
  26. package/lib/esm/commands/localize.js.map +1 -1
  27. package/lib/esm/commands/setup-publish.d.ts.map +1 -1
  28. package/lib/esm/commands/setup-publish.js +22 -15
  29. package/lib/esm/commands/setup-publish.js.map +1 -1
  30. package/lib/esm/commands/start.d.ts +8 -0
  31. package/lib/esm/commands/start.d.ts.map +1 -1
  32. package/lib/esm/commands/start.js +20 -8
  33. package/lib/esm/commands/start.js.map +1 -1
  34. package/lib/esm/installed-packages.d.ts +5 -13
  35. package/lib/esm/installed-packages.d.ts.map +1 -1
  36. package/lib/esm/installed-packages.js +5 -38
  37. package/lib/esm/installed-packages.js.map +1 -1
  38. package/lib/esm/lifecycle.d.ts +35 -22
  39. package/lib/esm/lifecycle.d.ts.map +1 -1
  40. package/lib/esm/lifecycle.js +62 -36
  41. package/lib/esm/lifecycle.js.map +1 -1
  42. package/lib/esm/localize/adopt.d.ts +5 -5
  43. package/lib/esm/localize/adopt.d.ts.map +1 -1
  44. package/lib/esm/localize/adopt.js +46 -55
  45. package/lib/esm/localize/adopt.js.map +1 -1
  46. package/lib/esm/localize/app-root.d.ts +16 -0
  47. package/lib/esm/localize/app-root.d.ts.map +1 -0
  48. package/lib/esm/localize/app-root.js +94 -0
  49. package/lib/esm/localize/app-root.js.map +1 -0
  50. package/lib/esm/localize/app-setup.d.ts +21 -0
  51. package/lib/esm/localize/app-setup.d.ts.map +1 -0
  52. package/lib/esm/localize/app-setup.js +178 -0
  53. package/lib/esm/localize/app-setup.js.map +1 -0
  54. package/lib/esm/localize/delocalize.d.ts.map +1 -1
  55. package/lib/esm/localize/delocalize.js +169 -104
  56. package/lib/esm/localize/delocalize.js.map +1 -1
  57. package/lib/esm/localize/drift-hook.d.ts +18 -0
  58. package/lib/esm/localize/drift-hook.d.ts.map +1 -0
  59. package/lib/esm/localize/drift-hook.js +37 -0
  60. package/lib/esm/localize/drift-hook.js.map +1 -0
  61. package/lib/esm/localize/drift.d.ts +36 -0
  62. package/lib/esm/localize/drift.d.ts.map +1 -0
  63. package/lib/esm/localize/drift.js +34 -0
  64. package/lib/esm/localize/drift.js.map +1 -0
  65. package/lib/esm/localize/errors.d.ts +14 -8
  66. package/lib/esm/localize/errors.d.ts.map +1 -1
  67. package/lib/esm/localize/errors.js +14 -8
  68. package/lib/esm/localize/errors.js.map +1 -1
  69. package/lib/esm/localize/fsops.d.ts +11 -27
  70. package/lib/esm/localize/fsops.d.ts.map +1 -1
  71. package/lib/esm/localize/fsops.js +40 -115
  72. package/lib/esm/localize/fsops.js.map +1 -1
  73. package/lib/esm/localize/git-hooks.d.ts +33 -0
  74. package/lib/esm/localize/git-hooks.d.ts.map +1 -0
  75. package/lib/esm/localize/git-hooks.js +146 -0
  76. package/lib/esm/localize/git-hooks.js.map +1 -0
  77. package/lib/esm/localize/hook-module.d.ts +6 -0
  78. package/lib/esm/localize/hook-module.d.ts.map +1 -0
  79. package/lib/esm/localize/hook-module.js +37 -0
  80. package/lib/esm/localize/hook-module.js.map +1 -0
  81. package/lib/esm/localize/index.d.ts +14 -15
  82. package/lib/esm/localize/index.d.ts.map +1 -1
  83. package/lib/esm/localize/index.js +8 -9
  84. package/lib/esm/localize/index.js.map +1 -1
  85. package/lib/esm/localize/list.d.ts +6 -10
  86. package/lib/esm/localize/list.d.ts.map +1 -1
  87. package/lib/esm/localize/list.js +49 -189
  88. package/lib/esm/localize/list.js.map +1 -1
  89. package/lib/esm/localize/localize.d.ts +46 -62
  90. package/lib/esm/localize/localize.d.ts.map +1 -1
  91. package/lib/esm/localize/localize.js +411 -285
  92. package/lib/esm/localize/localize.js.map +1 -1
  93. package/lib/esm/localize/members.d.ts +13 -0
  94. package/lib/esm/localize/members.d.ts.map +1 -0
  95. package/lib/esm/localize/members.js +46 -0
  96. package/lib/esm/localize/members.js.map +1 -0
  97. package/lib/esm/localize/resolve.d.ts +1 -2
  98. package/lib/esm/localize/resolve.d.ts.map +1 -1
  99. package/lib/esm/localize/resolve.js +3 -5
  100. package/lib/esm/localize/resolve.js.map +1 -1
  101. package/lib/esm/localize/run.d.ts +1 -0
  102. package/lib/esm/localize/run.d.ts.map +1 -1
  103. package/lib/esm/localize/run.js +11 -6
  104. package/lib/esm/localize/run.js.map +1 -1
  105. package/lib/esm/localize/setup-lockfile.d.ts +13 -0
  106. package/lib/esm/localize/setup-lockfile.d.ts.map +1 -0
  107. package/lib/esm/localize/setup-lockfile.js +80 -0
  108. package/lib/esm/localize/setup-lockfile.js.map +1 -0
  109. package/lib/esm/utils/createPackageLocation.d.ts +4 -13
  110. package/lib/esm/utils/createPackageLocation.d.ts.map +1 -1
  111. package/lib/esm/utils/createPackageLocation.js +12 -21
  112. package/lib/esm/utils/createPackageLocation.js.map +1 -1
  113. package/lib/esm/utils/packageSetup.d.ts +5 -2
  114. package/lib/esm/utils/packageSetup.d.ts.map +1 -1
  115. package/lib/esm/utils/packageSetup.js +9 -3
  116. package/lib/esm/utils/packageSetup.js.map +1 -1
  117. package/lib/esm/utils/platform.d.ts +24 -0
  118. package/lib/esm/utils/platform.d.ts.map +1 -0
  119. package/lib/esm/utils/platform.js +62 -0
  120. package/lib/esm/utils/platform.js.map +1 -0
  121. package/lib/esm/utils/shapeReferences.d.ts.map +1 -1
  122. package/lib/esm/utils/shapeReferences.js +3 -2
  123. package/lib/esm/utils/shapeReferences.js.map +1 -1
  124. package/lib/esm/utils/spawn.d.ts +65 -0
  125. package/lib/esm/utils/spawn.d.ts.map +1 -0
  126. package/lib/esm/utils/spawn.js +181 -0
  127. package/lib/esm/utils/spawn.js.map +1 -0
  128. package/lib/esm/utils.d.ts.map +1 -1
  129. package/lib/esm/utils.js +19 -5
  130. package/lib/esm/utils.js.map +1 -1
  131. package/lib/esm/vite-config.d.ts +5 -12
  132. package/lib/esm/vite-config.d.ts.map +1 -1
  133. package/lib/esm/vite-config.js +41 -33
  134. package/lib/esm/vite-config.js.map +1 -1
  135. package/package.json +4 -3
  136. package/lib/esm/localize/ensure.d.ts +0 -44
  137. package/lib/esm/localize/ensure.d.ts.map +0 -1
  138. package/lib/esm/localize/ensure.js +0 -151
  139. package/lib/esm/localize/ensure.js.map +0 -1
  140. package/lib/esm/localize/manifest.d.ts +0 -63
  141. package/lib/esm/localize/manifest.d.ts.map +0 -1
  142. package/lib/esm/localize/manifest.js +0 -133
  143. package/lib/esm/localize/manifest.js.map +0 -1
  144. package/lib/esm/localize/one-copy-hook.d.ts +0 -16
  145. package/lib/esm/localize/one-copy-hook.d.ts.map +0 -1
  146. package/lib/esm/localize/one-copy-hook.js +0 -20
  147. package/lib/esm/localize/one-copy-hook.js.map +0 -1
  148. package/lib/esm/localize/provided.d.ts +0 -28
  149. package/lib/esm/localize/provided.d.ts.map +0 -1
  150. package/lib/esm/localize/provided.js +0 -89
  151. package/lib/esm/localize/provided.js.map +0 -1
  152. package/lib/esm/localize/prune.d.ts +0 -165
  153. package/lib/esm/localize/prune.d.ts.map +0 -1
  154. package/lib/esm/localize/prune.js +0 -619
  155. package/lib/esm/localize/prune.js.map +0 -1
  156. package/lib/esm/localize/relink.d.ts +0 -13
  157. package/lib/esm/localize/relink.d.ts.map +0 -1
  158. package/lib/esm/localize/relink.js +0 -179
  159. package/lib/esm/localize/relink.js.map +0 -1
  160. package/lib/esm/localized-dedupe.d.ts +0 -26
  161. package/lib/esm/localized-dedupe.d.ts.map +0 -1
  162. package/lib/esm/localized-dedupe.js +0 -139
  163. package/lib/esm/localized-dedupe.js.map +0 -1
  164. package/lib/esm/utils/relinkHooks.d.ts +0 -19
  165. package/lib/esm/utils/relinkHooks.d.ts.map +0 -1
  166. package/lib/esm/utils/relinkHooks.js +0 -74
  167. package/lib/esm/utils/relinkHooks.js.map +0 -1
@@ -1,138 +1,203 @@
1
1
  /**
2
- * `delocalize` -- reverse a localize WITHOUT deleting work.
2
+ * `delocalize` -- go back to the registry copy WITHOUT losing work.
3
3
  *
4
- * With no names, every recorded entry is delocalized, so there is one revert
5
- * verb rather than two.
4
+ * A checkout is parked: moved to `.packages-local.parked/<dir>`, outside the
5
+ * `packages-local/*` glob, so npm installs the registry copy again and
6
+ * `linked localize <name>` (or `--parked`) takes it back as it was. With no
7
+ * names, every checkout is parked.
6
8
  *
7
- * The checkout is LEFT ON DISK by default: uncommitted work lives there.
8
- * `--purge` deletes it, and refuses on uncommitted changes or unpushed commits
9
- * unless `--force`.
9
+ * Then the install that swaps the links for registry copies:
10
10
  *
11
- * The restore command is PRINTED, not run. localize never invokes an install
12
- * at the consumer root -- that is the rule the whole design rests on, and it
13
- * does not get an exception for the convenient case. The printed line carries
14
- * `--no-save`, which is load-bearing rather than tidy: a plain
15
- * `npm install <name>@<range>` rewrites the range in package.json to whatever
16
- * it resolved and rewrites the lockfile with it, so a developer following the
17
- * tool's own advice would end the cycle with the exact diff the tool exists to
18
- * prevent.
11
+ * - no checkout left, `package-lock.json` tracked by git, and it and
12
+ * `package.json` unchanged from HEAD: `npm ci`, which installs exactly the
13
+ * committed lockfile;
14
+ * - otherwise (some checkouts left -- `npm ci` refuses a lockfile that lists
15
+ * members it does not -- an untracked lockfile, or a manifest being
16
+ * changed): `npm install`, then `npm rebuild <names>` -- an install that
17
+ * replaces a link with the registry copy leaves that package's bin without
18
+ * its execute bit (measured), and a rebuild relinks it.
19
+ *
20
+ * `--purge` deletes the checkout instead of parking it. It fails closed:
21
+ * unless `--force`, it refuses (exit 6) when the checkout is not a git
22
+ * repository, when any git call fails, and on uncommitted changes, commits
23
+ * that no remote has (on any branch or a detached HEAD), or a stash.
19
24
  */
20
25
  import fs from 'node:fs';
21
26
  import path from 'node:path';
22
- import { EXIT_WARNED } from './errors.js';
23
- import { SCHEMA_VERSION, readManifest, writeManifest } from './manifest.js';
24
- import { isInside, isSymlink, snapshotManifests } from './fsops.js';
25
- import { assertManifestsUntouched, declaredRange, reportError, } from './localize.js';
27
+ import { EXIT_INSTALL_FAILED, EXIT_REFUSED, EXIT_WARNED } from './errors.js';
28
+ import { checkoutRepoRoot, existsOrLink, isInside, isSymlink, moveEntry, } from './fsops.js';
29
+ import { INSTALL_ARGS, LOCKFILE, legacyManifestHint, reportError, restoreSetAside, tail, } from './localize.js';
30
+ import { LOCAL_DIR, PARKED_DIR, localMembers, parkedMembers } from './members.js';
26
31
  export function delocalize(names, opts, deps) {
27
- var _a;
28
- const before = snapshotManifests(deps.appRoot);
29
- let manifest;
32
+ legacyManifestHint(deps);
30
33
  try {
31
- manifest = readManifest(deps.appRoot, deps);
34
+ restoreSetAside(deps);
32
35
  }
33
36
  catch (e) {
34
37
  return reportError(e, deps);
35
38
  }
36
- const dir = manifest.dir;
37
- const entries = Object.assign({}, manifest.entries);
38
- const targets = names.length
39
- ? names.map((n) => toRecordedName(n, entries))
40
- : Object.keys(entries);
39
+ const members = localMembers(deps.appRoot);
41
40
  let code = 0;
42
- for (const name of targets) {
43
- const entry = entries[name];
44
- if (!entry) {
45
- deps.warn(`[localize] ${name} is not recorded — nothing to do.`);
46
- code = Math.max(code, EXIT_WARNED);
41
+ const targets = [];
42
+ for (const name of names.length ? names : members.map((m) => m.name)) {
43
+ const member = findMember(name, members);
44
+ if (member) {
45
+ targets.push(member);
47
46
  continue;
48
47
  }
49
- code = Math.max(code, unlink(name, dir, deps));
50
- const checkout = path.join(deps.appRoot, entry.path);
48
+ const parked = findMember(name, parkedMembers(deps.appRoot));
49
+ deps.warn(parked
50
+ ? `[localize] ${parked.name} is already parked in ${parked.dir}.`
51
+ : `[localize] ${name} is not localized -- nothing in ${LOCAL_DIR}/ is named that.`);
52
+ code = Math.max(code, EXIT_WARNED);
53
+ }
54
+ const done = [];
55
+ for (const member of targets) {
51
56
  if (opts.purge) {
52
- const refusal = purgeRefusal(checkout, deps);
53
- if (refusal && !opts.force) {
54
- deps.warn(`[localize] ${entry.path}: not deleting — ${refusal}. Pass --force to delete it anyway.`);
55
- code = Math.max(code, EXIT_WARNED);
56
- }
57
- else {
58
- if (refusal)
59
- deps.warn(`[localize] --force: deleting ${entry.path} despite ${refusal}`);
60
- fs.rmSync(checkout, { recursive: true, force: true });
61
- deps.log(`[localize] removed ${entry.path}`);
62
- }
57
+ // A purge that is refused is a guard refusing: nothing is deleted.
58
+ if (purge(member, opts, deps) === 'done')
59
+ done.push(member.name);
60
+ else
61
+ code = Math.max(code, EXIT_REFUSED);
63
62
  }
64
- else if (fs.existsSync(checkout)) {
65
- deps.log(`[localize] ${entry.path} left on disk (uncommitted work lives there). --purge removes it.`);
63
+ else if (park(member, deps) === 'done')
64
+ done.push(member.name);
65
+ else
66
+ code = Math.max(code, EXIT_WARNED);
67
+ }
68
+ if (!done.length)
69
+ return code;
70
+ return Math.max(code, reinstall(done, deps));
71
+ }
72
+ function park(member, deps) {
73
+ const to = `${PARKED_DIR}/${path.basename(member.dir)}`;
74
+ if (existsOrLink(path.join(deps.appRoot, to))) {
75
+ deps.warn(`[localize] ${member.name}: ${to} already exists, so ${member.dir} was not parked. Move ` +
76
+ `one of them out of the way.`);
77
+ return 'refused';
78
+ }
79
+ moveEntry(path.join(deps.appRoot, member.dir), path.join(deps.appRoot, to));
80
+ deps.log(`[localize] parked ${member.name}: ${member.dir} -> ${to} (\`linked localize ${member.name}\` takes it back)`);
81
+ return 'done';
82
+ }
83
+ function purge(member, opts, deps) {
84
+ const entry = path.join(deps.appRoot, member.dir);
85
+ const repo = checkoutRepoRoot(entry, deps.appRoot);
86
+ const refusal = repo
87
+ ? purgeRefusal(repo, deps)
88
+ : 'it is not a git checkout (no .git), so nothing says its work is saved elsewhere';
89
+ if (refusal && !opts.force) {
90
+ deps.warn(`[localize] ${member.dir}: not deleting -- ${refusal}. Pass --force to delete it anyway.`);
91
+ return 'refused';
92
+ }
93
+ if (refusal)
94
+ deps.warn(`[localize] --force: deleting ${member.dir} although ${refusal}`);
95
+ if (isSymlink(entry)) {
96
+ // A package inside a cloned monorepo: the clone goes too, unless another
97
+ // checkout or parked checkout still links into it.
98
+ fs.unlinkSync(entry);
99
+ if (repo && !linkedFromElsewhere(repo, deps)) {
100
+ fs.rmSync(repo, { recursive: true, force: true });
101
+ deps.log(`[localize] removed ${member.dir} and ${path.relative(deps.appRoot, repo)}`);
102
+ return 'done';
66
103
  }
67
- delete entries[name];
68
- const range = (_a = entry.range) !== null && _a !== void 0 ? _a : declaredRange(name, deps);
69
- if (!range && !entry.repo) {
70
- // An adopted package that was never published: there is no registry copy
71
- // to go back to, and `@latest` would only 404.
72
- deps.log(`[localize] ${name} was never declared or published — there is no registry copy to restore.`);
104
+ }
105
+ else {
106
+ fs.rmSync(entry, { recursive: true, force: true });
107
+ }
108
+ deps.log(`[localize] removed ${member.dir}`);
109
+ return 'done';
110
+ }
111
+ /** Does any remaining checkout, here or parked, live inside `repo`? */
112
+ function linkedFromElsewhere(repo, deps) {
113
+ return [...localMembers(deps.appRoot), ...parkedMembers(deps.appRoot)].some((m) => {
114
+ try {
115
+ return isInside(fs.realpathSync(path.join(deps.appRoot, m.dir)), repo);
73
116
  }
74
- else {
75
- deps.log(`[localize] restore the registry copy with: npm install --no-save ${name}@${range !== null && range !== void 0 ? range : 'latest'}`);
117
+ catch (_a) {
118
+ return false;
76
119
  }
77
- }
78
- writeManifest(deps.appRoot, {
79
- version: SCHEMA_VERSION,
80
- dir,
81
- packages: entries,
82
120
  });
83
- return Math.max(code, assertManifestsUntouched(before, deps));
84
121
  }
85
- /** Remove the link only when it is a symlink INTO the configured dir. */
86
- function unlink(name, dir, deps) {
87
- const link = path.join(deps.appRoot, 'node_modules', name);
88
- if (!fs.existsSync(link) && !isSymlink(link))
89
- return 0;
90
- if (!isSymlink(link)) {
91
- deps.warn(`[localize] node_modules/${name} is a real directory, not a link — left alone. ` +
92
- `That is the registry copy; nothing to unlink.`);
93
- return EXIT_WARNED;
94
- }
95
- let target;
96
- try {
97
- target = fs.realpathSync(link);
122
+ /** The install after parking or purging: see the module header. */
123
+ function reinstall(names, deps) {
124
+ const why = ciRefusal(deps);
125
+ const ci = why === null;
126
+ const args = ci ? ['ci', '--no-audit', '--no-fund'] : INSTALL_ARGS;
127
+ const reason = ci ? `the committed ${LOCKFILE}, unchanged` : why;
128
+ deps.log(`[localize] npm ${args.join(' ')} (app root; ${reason})`);
129
+ const r = deps.run('npm', args, { cwd: deps.appRoot });
130
+ if (r.status !== 0) {
131
+ deps.error(`[localize] npm ${args[0]} failed at the app root; node_modules may still link the ` +
132
+ `checkout${names.length === 1 ? '' : 's'}. Fix the cause and run \`npm install\`.\n` +
133
+ tail(r.stderr || r.stdout));
134
+ return EXIT_INSTALL_FAILED;
98
135
  }
99
- catch (_a) {
100
- fs.unlinkSync(link); // dangling, and ours by elimination
101
- deps.log(`[localize] unlinked ${name} (its target was already gone)`);
136
+ if (ci)
102
137
  return 0;
103
- }
104
- if (!isInside(target, path.join(deps.appRoot, dir))) {
105
- deps.warn(`[localize] node_modules/${name} links to ${target}, outside ${dir} — left alone. ` +
106
- `delocalize only removes links it could have made.`);
138
+ const rebuild = deps.run('npm', ['rebuild', ...names], { cwd: deps.appRoot });
139
+ if (rebuild.status !== 0) {
140
+ deps.warn(`[localize] npm rebuild ${names.join(' ')} failed; a registry copy's bin may not be ` +
141
+ `executable until it runs.\n${tail(rebuild.stderr || rebuild.stdout)}`);
107
142
  return EXIT_WARNED;
108
143
  }
109
- fs.unlinkSync(link);
110
- deps.log(`[localize] unlinked ${name}`);
111
144
  return 0;
112
145
  }
113
- /** @returns {string|null} the reason a purge must be refused. */
114
- function purgeRefusal(checkout, deps) {
115
- if (!fs.existsSync(checkout))
116
- return null;
117
- const status = deps.run('git', ['status', '--porcelain'], { cwd: checkout });
118
- if (status.stdout.trim()) {
119
- const n = status.stdout.split('\n').filter((l) => l.trim()).length;
120
- return `it has ${n} uncommitted change${n === 1 ? '' : 's'}`;
121
- }
122
- const unpushed = deps.run('git', ['log', '--oneline', '@{u}..HEAD'], {
123
- cwd: checkout,
124
- });
125
- if (unpushed.status === 0 && unpushed.stdout.trim()) {
126
- const n = unpushed.stdout.split('\n').filter((l) => l.trim()).length;
127
- return `it has ${n} unpushed commit${n === 1 ? '' : 's'}`;
146
+ /**
147
+ * Why the reinstall cannot be `npm ci`, or null when it can: no checkout is
148
+ * left, git tracks the lockfile, and neither it nor `package.json` differs
149
+ * from HEAD. A git call that fails counts as a reason: `npm install` is the
150
+ * safe choice.
151
+ */
152
+ function ciRefusal(deps) {
153
+ if (localMembers(deps.appRoot).length)
154
+ return `checkouts remain in ${LOCAL_DIR}/`;
155
+ const git = (args) => deps.run('git', args, { cwd: deps.appRoot }).status === 0;
156
+ if (!git(['ls-files', '--error-unmatch', '--', LOCKFILE]))
157
+ return `${LOCKFILE} is not tracked by git`;
158
+ if (!git(['diff', '--quiet', 'HEAD', '--', 'package.json', LOCKFILE]))
159
+ return `package.json or ${LOCKFILE} differs from HEAD`;
160
+ return null;
161
+ }
162
+ /**
163
+ * @returns the reason a purge must be refused, or null. Fails closed: a git
164
+ * call that fails is a reason, since a check that cannot look must not let
165
+ * a delete through.
166
+ */
167
+ function purgeRefusal(repo, deps) {
168
+ const checks = [
169
+ {
170
+ args: ['status', '--porcelain'],
171
+ what: (n) => `${n} uncommitted change${n === 1 ? '' : 's'}`,
172
+ },
173
+ {
174
+ // HEAD (a detached one has no branch) and every local branch, against every remote.
175
+ args: ['log', '--oneline', 'HEAD', '--branches', '--not', '--remotes'],
176
+ what: (n) => `${n} commit${n === 1 ? '' : 's'} no remote has`,
177
+ },
178
+ {
179
+ args: ['stash', 'list'],
180
+ what: (n) => `${n} stash entr${n === 1 ? 'y' : 'ies'}`,
181
+ },
182
+ ];
183
+ for (const { args, what } of checks) {
184
+ const r = deps.run('git', args, { cwd: repo });
185
+ if (r.status !== 0) {
186
+ const why = (r.stderr || r.stdout).trim().split('\n')[0] || `exit ${r.status}`;
187
+ return `\`git ${args.join(' ')}\` failed (${why}), so its work cannot be checked`;
188
+ }
189
+ const n = r.stdout.split('\n').filter((l) => l.trim()).length;
190
+ if (n)
191
+ return `it has ${what(n)}`;
128
192
  }
129
193
  return null;
130
194
  }
131
- /** Accept a short name where the manifest records a scoped one. */
132
- function toRecordedName(name, entries) {
133
- var _a;
134
- if (entries[name])
135
- return name;
136
- return (_a = Object.keys(entries).find((k) => k.split('/').pop() === name)) !== null && _a !== void 0 ? _a : name;
195
+ /** By full name, or by the part after the scope when that is unambiguous. */
196
+ function findMember(name, members) {
197
+ const exact = members.find((m) => m.name === name);
198
+ if (exact)
199
+ return exact;
200
+ const short = members.filter((m) => m.name.split('/').pop() === name);
201
+ return short.length === 1 ? short[0] : undefined;
137
202
  }
138
203
  //# sourceMappingURL=delocalize.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"delocalize.js","sourceRoot":"","sources":["../../../src/localize/delocalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAC,WAAW,EAAC,MAAM,aAAa,CAAC;AACxC,OAAO,EAAC,cAAc,EAAE,YAAY,EAAE,aAAa,EAAC,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAC,QAAQ,EAAE,SAAS,EAAE,iBAAiB,EAAC,MAAM,YAAY,CAAC;AAClE,OAAO,EACL,wBAAwB,EACxB,aAAa,EACb,WAAW,GACZ,MAAM,eAAe,CAAC;AAGvB,MAAM,UAAU,UAAU,CACxB,KAAe,EACf,IAAwC,EACxC,IAAU;;IAEV,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAE/C,IAAI,QAAQ,CAAC;IACb,IAAI,CAAC;QACH,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC9C,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC;IACzB,MAAM,OAAO,qBAAO,QAAQ,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM;QAC1B,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QAC9C,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,IAAI,CAAC,IAAI,CAAC,cAAc,IAAI,mCAAmC,CAAC,CAAC;YACjE,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;YACnC,SAAS;QACX,CAAC;QAED,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;QAE/C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACrD,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAC7C,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;gBAC3B,IAAI,CAAC,IAAI,CACP,cAAc,KAAK,CAAC,IAAI,oBAAoB,OAAO,qCAAqC,CACzF,CAAC;gBACF,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;YACrC,CAAC;iBAAM,CAAC;gBACN,IAAI,OAAO;oBACT,IAAI,CAAC,IAAI,CACP,gCAAgC,KAAK,CAAC,IAAI,YAAY,OAAO,EAAE,CAChE,CAAC;gBACJ,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAC,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAC,CAAC,CAAC;gBACpD,IAAI,CAAC,GAAG,CAAC,sBAAsB,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC;aAAM,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACnC,IAAI,CAAC,GAAG,CACN,cAAc,KAAK,CAAC,IAAI,mEAAmE,CAC5F,CAAC;QACJ,CAAC;QAED,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;QAErB,MAAM,KAAK,GAAG,MAAA,KAAK,CAAC,KAAK,mCAAI,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACvD,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAC1B,yEAAyE;YACzE,+CAA+C;YAC/C,IAAI,CAAC,GAAG,CACN,cAAc,IAAI,0EAA0E,CAC7F,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,GAAG,CACN,qEAAqE,IAAI,IAAI,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,QAAQ,EAAE,CACjG,CAAC;QACJ,CAAC;IACH,CAAC;IAED,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE;QAC1B,OAAO,EAAE,cAAc;QACvB,GAAG;QACH,QAAQ,EAAE,OAAO;KAClB,CAAC,CAAC;IACH,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,wBAAwB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,yEAAyE;AACzE,SAAS,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI;IAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,CAAC,CAAC;IAC3D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAEvD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,IAAI,CAAC,IAAI,CACP,2BAA2B,IAAI,iDAAiD;YAC9E,+CAA+C,CAClD,CAAC;QACF,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAAC,WAAM,CAAC;QACP,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,oCAAoC;QACzD,IAAI,CAAC,GAAG,CAAC,uBAAuB,IAAI,gCAAgC,CAAC,CAAC;QACtE,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC;QACpD,IAAI,CAAC,IAAI,CACP,2BAA2B,IAAI,aAAa,MAAM,aAAa,GAAG,iBAAiB;YACjF,mDAAmD,CACtD,CAAC;QACF,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACpB,IAAI,CAAC,GAAG,CAAC,uBAAuB,IAAI,EAAE,CAAC,CAAC;IACxC,OAAO,CAAC,CAAC;AACX,CAAC;AAED,iEAAiE;AACjE,SAAS,YAAY,CAAC,QAAQ,EAAE,IAAI;IAClC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,EAAC,GAAG,EAAE,QAAQ,EAAC,CAAC,CAAC;IAC3E,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QACzB,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACnE,OAAO,UAAU,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IAC/D,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,YAAY,CAAC,EAAE;QACnE,GAAG,EAAE,QAAQ;KACd,CAAC,CAAC;IACH,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QACpD,MAAM,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACrE,OAAO,UAAU,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,mEAAmE;AACnE,SAAS,cAAc,CAAC,IAAI,EAAE,OAAO;;IACnC,IAAI,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/B,OAAO,MAAA,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC,mCAAI,IAAI,CAAC;AAC/E,CAAC","sourcesContent":["/**\n * `delocalize` -- reverse a localize WITHOUT deleting work.\n *\n * With no names, every recorded entry is delocalized, so there is one revert\n * verb rather than two.\n *\n * The checkout is LEFT ON DISK by default: uncommitted work lives there.\n * `--purge` deletes it, and refuses on uncommitted changes or unpushed commits\n * unless `--force`.\n *\n * The restore command is PRINTED, not run. localize never invokes an install\n * at the consumer root -- that is the rule the whole design rests on, and it\n * does not get an exception for the convenient case. The printed line carries\n * `--no-save`, which is load-bearing rather than tidy: a plain\n * `npm install <name>@<range>` rewrites the range in package.json to whatever\n * it resolved and rewrites the lockfile with it, so a developer following the\n * tool's own advice would end the cycle with the exact diff the tool exists to\n * prevent.\n */\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport {EXIT_WARNED} from './errors.js';\nimport {SCHEMA_VERSION, readManifest, writeManifest} from './manifest.js';\nimport {isInside, isSymlink, snapshotManifests} from './fsops.js';\nimport {\n assertManifestsUntouched,\n declaredRange,\n reportError,\n} from './localize.js';\nimport type {Deps} from './run.js';\n\nexport function delocalize(\n names: string[],\n opts: {purge?: boolean; force?: boolean},\n deps: Deps,\n): number {\n const before = snapshotManifests(deps.appRoot);\n\n let manifest;\n try {\n manifest = readManifest(deps.appRoot, deps);\n } catch (e) {\n return reportError(e, deps);\n }\n\n const dir = manifest.dir;\n const entries = {...manifest.entries};\n const targets = names.length\n ? names.map((n) => toRecordedName(n, entries))\n : Object.keys(entries);\n let code = 0;\n\n for (const name of targets) {\n const entry = entries[name];\n if (!entry) {\n deps.warn(`[localize] ${name} is not recorded — nothing to do.`);\n code = Math.max(code, EXIT_WARNED);\n continue;\n }\n\n code = Math.max(code, unlink(name, dir, deps));\n\n const checkout = path.join(deps.appRoot, entry.path);\n if (opts.purge) {\n const refusal = purgeRefusal(checkout, deps);\n if (refusal && !opts.force) {\n deps.warn(\n `[localize] ${entry.path}: not deleting — ${refusal}. Pass --force to delete it anyway.`,\n );\n code = Math.max(code, EXIT_WARNED);\n } else {\n if (refusal)\n deps.warn(\n `[localize] --force: deleting ${entry.path} despite ${refusal}`,\n );\n fs.rmSync(checkout, {recursive: true, force: true});\n deps.log(`[localize] removed ${entry.path}`);\n }\n } else if (fs.existsSync(checkout)) {\n deps.log(\n `[localize] ${entry.path} left on disk (uncommitted work lives there). --purge removes it.`,\n );\n }\n\n delete entries[name];\n\n const range = entry.range ?? declaredRange(name, deps);\n if (!range && !entry.repo) {\n // An adopted package that was never published: there is no registry copy\n // to go back to, and `@latest` would only 404.\n deps.log(\n `[localize] ${name} was never declared or published — there is no registry copy to restore.`,\n );\n } else {\n deps.log(\n `[localize] restore the registry copy with: npm install --no-save ${name}@${range ?? 'latest'}`,\n );\n }\n }\n\n writeManifest(deps.appRoot, {\n version: SCHEMA_VERSION,\n dir,\n packages: entries,\n });\n return Math.max(code, assertManifestsUntouched(before, deps));\n}\n\n/** Remove the link only when it is a symlink INTO the configured dir. */\nfunction unlink(name, dir, deps) {\n const link = path.join(deps.appRoot, 'node_modules', name);\n if (!fs.existsSync(link) && !isSymlink(link)) return 0;\n\n if (!isSymlink(link)) {\n deps.warn(\n `[localize] node_modules/${name} is a real directory, not a link — left alone. ` +\n `That is the registry copy; nothing to unlink.`,\n );\n return EXIT_WARNED;\n }\n\n let target;\n try {\n target = fs.realpathSync(link);\n } catch {\n fs.unlinkSync(link); // dangling, and ours by elimination\n deps.log(`[localize] unlinked ${name} (its target was already gone)`);\n return 0;\n }\n\n if (!isInside(target, path.join(deps.appRoot, dir))) {\n deps.warn(\n `[localize] node_modules/${name} links to ${target}, outside ${dir} — left alone. ` +\n `delocalize only removes links it could have made.`,\n );\n return EXIT_WARNED;\n }\n\n fs.unlinkSync(link);\n deps.log(`[localize] unlinked ${name}`);\n return 0;\n}\n\n/** @returns {string|null} the reason a purge must be refused. */\nfunction purgeRefusal(checkout, deps) {\n if (!fs.existsSync(checkout)) return null;\n const status = deps.run('git', ['status', '--porcelain'], {cwd: checkout});\n if (status.stdout.trim()) {\n const n = status.stdout.split('\\n').filter((l) => l.trim()).length;\n return `it has ${n} uncommitted change${n === 1 ? '' : 's'}`;\n }\n const unpushed = deps.run('git', ['log', '--oneline', '@{u}..HEAD'], {\n cwd: checkout,\n });\n if (unpushed.status === 0 && unpushed.stdout.trim()) {\n const n = unpushed.stdout.split('\\n').filter((l) => l.trim()).length;\n return `it has ${n} unpushed commit${n === 1 ? '' : 's'}`;\n }\n return null;\n}\n\n/** Accept a short name where the manifest records a scoped one. */\nfunction toRecordedName(name, entries) {\n if (entries[name]) return name;\n return Object.keys(entries).find((k) => k.split('/').pop() === name) ?? name;\n}\n"]}
1
+ {"version":3,"file":"delocalize.js","sourceRoot":"","sources":["../../../src/localize/delocalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAC,mBAAmB,EAAE,YAAY,EAAE,WAAW,EAAC,MAAM,aAAa,CAAC;AAC3E,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,SAAS,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,IAAI,GACL,MAAM,eAAe,CAAC;AACvB,OAAO,EAAC,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAC,MAAM,cAAc,CAAC;AAIhF,MAAM,UAAU,UAAU,CACxB,KAAe,EACf,IAAwC,EACxC,IAAU;IAEV,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,CAAC;QACH,eAAe,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;IACD,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3C,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACrE,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACzC,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACrB,SAAS;QACX,CAAC;QACD,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,CACP,MAAM;YACJ,CAAC,CAAC,cAAc,MAAM,CAAC,IAAI,yBAAyB,MAAM,CAAC,GAAG,GAAG;YACjE,CAAC,CAAC,cAAc,IAAI,mCAAmC,SAAS,kBAAkB,CACrF,CAAC;QACF,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACrC,CAAC;IAED,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,mEAAmE;YACnE,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,MAAM;gBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;;gBAC5D,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;QAC3C,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,MAAM;YAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;;YAC5D,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC1C,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAC9B,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,IAAI,CAAC,MAAc,EAAE,IAAU;IACtC,MAAM,EAAE,GAAG,GAAG,UAAU,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;IACxD,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;QAC9C,IAAI,CAAC,IAAI,CACP,cAAc,MAAM,CAAC,IAAI,KAAK,EAAE,uBAAuB,MAAM,CAAC,GAAG,wBAAwB;YACvF,6BAA6B,CAChC,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IAC5E,IAAI,CAAC,GAAG,CACN,qBAAqB,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,GAAG,OAAO,EAAE,uBAAuB,MAAM,CAAC,IAAI,mBAAmB,CAC9G,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,KAAK,CACZ,MAAc,EACd,IAAuB,EACvB,IAAU;IAEV,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAClD,MAAM,IAAI,GAAG,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,IAAI;QAClB,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC;QAC1B,CAAC,CAAC,iFAAiF,CAAC;IACtF,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CACP,cAAc,MAAM,CAAC,GAAG,qBAAqB,OAAO,qCAAqC,CAC1F,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,OAAO;QACT,IAAI,CAAC,IAAI,CAAC,gCAAgC,MAAM,CAAC,GAAG,aAAa,OAAO,EAAE,CAAC,CAAC;IAE9E,IAAI,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QACrB,yEAAyE;QACzE,mDAAmD;QACnD,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACrB,IAAI,IAAI,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;YAC7C,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,EAAC,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAC,CAAC,CAAC;YAChD,IAAI,CAAC,GAAG,CACN,sBAAsB,MAAM,CAAC,GAAG,QAAQ,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAC5E,CAAC;YACF,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;SAAM,CAAC;QACN,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,EAAC,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAC,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,sBAAsB,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IAC7C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,uEAAuE;AACvE,SAAS,mBAAmB,CAAC,IAAY,EAAE,IAAU;IACnD,OAAO,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CACzE,CAAC,CAAC,EAAE,EAAE;QACJ,IAAI,CAAC;YACH,OAAO,QAAQ,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QACzE,CAAC;QAAC,WAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC;AAED,mEAAmE;AACnE,SAAS,SAAS,CAAC,KAAe,EAAE,IAAU;IAC5C,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,EAAE,GAAG,GAAG,KAAK,IAAI,CAAC;IACxB,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;IACnE,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,iBAAiB,QAAQ,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC;IACjE,IAAI,CAAC,GAAG,CAAC,kBAAkB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe,MAAM,GAAG,CAAC,CAAC;IACnE,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAC,GAAG,EAAE,IAAI,CAAC,OAAO,EAAC,CAAC,CAAC;IACrD,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnB,IAAI,CAAC,KAAK,CACR,kBAAkB,IAAI,CAAC,CAAC,CAAC,2DAA2D;YAClF,WAAW,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,4CAA4C;YACpF,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,CAC7B,CAAC;QACF,OAAO,mBAAmB,CAAC;IAC7B,CAAC;IACD,IAAI,EAAE;QAAE,OAAO,CAAC,CAAC;IACjB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,GAAG,KAAK,CAAC,EAAE,EAAC,GAAG,EAAE,IAAI,CAAC,OAAO,EAAC,CAAC,CAAC;IAC5E,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CACP,0BAA0B,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,4CAA4C;YACnF,8BAA8B,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,CACzE,CAAC;QACF,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;GAKG;AACH,SAAS,SAAS,CAAC,IAAU;IAC3B,IAAI,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM;QACnC,OAAO,uBAAuB,SAAS,GAAG,CAAC;IAC7C,MAAM,GAAG,GAAG,CAAC,IAAc,EAAE,EAAE,CAC7B,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAC,GAAG,EAAE,IAAI,CAAC,OAAO,EAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;IAC1D,IAAI,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,iBAAiB,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;QACvD,OAAO,GAAG,QAAQ,wBAAwB,CAAC;IAC7C,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,QAAQ,CAAC,CAAC;QACnE,OAAO,mBAAmB,QAAQ,oBAAoB,CAAC;IACzD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CAAC,IAAY,EAAE,IAAU;IAC5C,MAAM,MAAM,GAAoD;QAC9D;YACE,IAAI,EAAE,CAAC,QAAQ,EAAE,aAAa,CAAC;YAC/B,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE;SAC5D;QACD;YACE,oFAAoF;YACpF,IAAI,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,CAAC;YACtE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,gBAAgB;SAC9D;QACD;YACE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC;YACvB,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;SACvD;KACF,CAAC;IACF,KAAK,MAAM,EAAC,IAAI,EAAE,IAAI,EAAC,IAAI,MAAM,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAC,GAAG,EAAE,IAAI,EAAC,CAAC,CAAC;QAC7C,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnB,MAAM,GAAG,GACP,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;YACrE,OAAO,SAAS,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,GAAG,kCAAkC,CAAC;QACpF,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QAC9D,IAAI,CAAC;YAAE,OAAO,UAAU,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACpC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,6EAA6E;AAC7E,SAAS,UAAU,CAAC,IAAY,EAAE,OAAiB;IACjD,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IACnD,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC;IACxB,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC,CAAC;IACtE,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACnD,CAAC","sourcesContent":["/**\n * `delocalize` -- go back to the registry copy WITHOUT losing work.\n *\n * A checkout is parked: moved to `.packages-local.parked/<dir>`, outside the\n * `packages-local/*` glob, so npm installs the registry copy again and\n * `linked localize <name>` (or `--parked`) takes it back as it was. With no\n * names, every checkout is parked.\n *\n * Then the install that swaps the links for registry copies:\n *\n * - no checkout left, `package-lock.json` tracked by git, and it and\n * `package.json` unchanged from HEAD: `npm ci`, which installs exactly the\n * committed lockfile;\n * - otherwise (some checkouts left -- `npm ci` refuses a lockfile that lists\n * members it does not -- an untracked lockfile, or a manifest being\n * changed): `npm install`, then `npm rebuild <names>` -- an install that\n * replaces a link with the registry copy leaves that package's bin without\n * its execute bit (measured), and a rebuild relinks it.\n *\n * `--purge` deletes the checkout instead of parking it. It fails closed:\n * unless `--force`, it refuses (exit 6) when the checkout is not a git\n * repository, when any git call fails, and on uncommitted changes, commits\n * that no remote has (on any branch or a detached HEAD), or a stash.\n */\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport {EXIT_INSTALL_FAILED, EXIT_REFUSED, EXIT_WARNED} from './errors.js';\nimport {\n checkoutRepoRoot,\n existsOrLink,\n isInside,\n isSymlink,\n moveEntry,\n} from './fsops.js';\nimport {\n INSTALL_ARGS,\n LOCKFILE,\n legacyManifestHint,\n reportError,\n restoreSetAside,\n tail,\n} from './localize.js';\nimport {LOCAL_DIR, PARKED_DIR, localMembers, parkedMembers} from './members.js';\nimport type {Member} from './members.js';\nimport type {Deps} from './run.js';\n\nexport function delocalize(\n names: string[],\n opts: {purge?: boolean; force?: boolean},\n deps: Deps,\n): number {\n legacyManifestHint(deps);\n try {\n restoreSetAside(deps);\n } catch (e) {\n return reportError(e, deps);\n }\n const members = localMembers(deps.appRoot);\n let code = 0;\n const targets: Member[] = [];\n for (const name of names.length ? names : members.map((m) => m.name)) {\n const member = findMember(name, members);\n if (member) {\n targets.push(member);\n continue;\n }\n const parked = findMember(name, parkedMembers(deps.appRoot));\n deps.warn(\n parked\n ? `[localize] ${parked.name} is already parked in ${parked.dir}.`\n : `[localize] ${name} is not localized -- nothing in ${LOCAL_DIR}/ is named that.`,\n );\n code = Math.max(code, EXIT_WARNED);\n }\n\n const done: string[] = [];\n for (const member of targets) {\n if (opts.purge) {\n // A purge that is refused is a guard refusing: nothing is deleted.\n if (purge(member, opts, deps) === 'done') done.push(member.name);\n else code = Math.max(code, EXIT_REFUSED);\n } else if (park(member, deps) === 'done') done.push(member.name);\n else code = Math.max(code, EXIT_WARNED);\n }\n if (!done.length) return code;\n return Math.max(code, reinstall(done, deps));\n}\n\nfunction park(member: Member, deps: Deps): 'done' | 'refused' {\n const to = `${PARKED_DIR}/${path.basename(member.dir)}`;\n if (existsOrLink(path.join(deps.appRoot, to))) {\n deps.warn(\n `[localize] ${member.name}: ${to} already exists, so ${member.dir} was not parked. Move ` +\n `one of them out of the way.`,\n );\n return 'refused';\n }\n moveEntry(path.join(deps.appRoot, member.dir), path.join(deps.appRoot, to));\n deps.log(\n `[localize] parked ${member.name}: ${member.dir} -> ${to} (\\`linked localize ${member.name}\\` takes it back)`,\n );\n return 'done';\n}\n\nfunction purge(\n member: Member,\n opts: {force?: boolean},\n deps: Deps,\n): 'done' | 'refused' {\n const entry = path.join(deps.appRoot, member.dir);\n const repo = checkoutRepoRoot(entry, deps.appRoot);\n const refusal = repo\n ? purgeRefusal(repo, deps)\n : 'it is not a git checkout (no .git), so nothing says its work is saved elsewhere';\n if (refusal && !opts.force) {\n deps.warn(\n `[localize] ${member.dir}: not deleting -- ${refusal}. Pass --force to delete it anyway.`,\n );\n return 'refused';\n }\n if (refusal)\n deps.warn(`[localize] --force: deleting ${member.dir} although ${refusal}`);\n\n if (isSymlink(entry)) {\n // A package inside a cloned monorepo: the clone goes too, unless another\n // checkout or parked checkout still links into it.\n fs.unlinkSync(entry);\n if (repo && !linkedFromElsewhere(repo, deps)) {\n fs.rmSync(repo, {recursive: true, force: true});\n deps.log(\n `[localize] removed ${member.dir} and ${path.relative(deps.appRoot, repo)}`,\n );\n return 'done';\n }\n } else {\n fs.rmSync(entry, {recursive: true, force: true});\n }\n deps.log(`[localize] removed ${member.dir}`);\n return 'done';\n}\n\n/** Does any remaining checkout, here or parked, live inside `repo`? */\nfunction linkedFromElsewhere(repo: string, deps: Deps): boolean {\n return [...localMembers(deps.appRoot), ...parkedMembers(deps.appRoot)].some(\n (m) => {\n try {\n return isInside(fs.realpathSync(path.join(deps.appRoot, m.dir)), repo);\n } catch {\n return false;\n }\n },\n );\n}\n\n/** The install after parking or purging: see the module header. */\nfunction reinstall(names: string[], deps: Deps): number {\n const why = ciRefusal(deps);\n const ci = why === null;\n const args = ci ? ['ci', '--no-audit', '--no-fund'] : INSTALL_ARGS;\n const reason = ci ? `the committed ${LOCKFILE}, unchanged` : why;\n deps.log(`[localize] npm ${args.join(' ')} (app root; ${reason})`);\n const r = deps.run('npm', args, {cwd: deps.appRoot});\n if (r.status !== 0) {\n deps.error(\n `[localize] npm ${args[0]} failed at the app root; node_modules may still link the ` +\n `checkout${names.length === 1 ? '' : 's'}. Fix the cause and run \\`npm install\\`.\\n` +\n tail(r.stderr || r.stdout),\n );\n return EXIT_INSTALL_FAILED;\n }\n if (ci) return 0;\n const rebuild = deps.run('npm', ['rebuild', ...names], {cwd: deps.appRoot});\n if (rebuild.status !== 0) {\n deps.warn(\n `[localize] npm rebuild ${names.join(' ')} failed; a registry copy's bin may not be ` +\n `executable until it runs.\\n${tail(rebuild.stderr || rebuild.stdout)}`,\n );\n return EXIT_WARNED;\n }\n return 0;\n}\n\n/**\n * Why the reinstall cannot be `npm ci`, or null when it can: no checkout is\n * left, git tracks the lockfile, and neither it nor `package.json` differs\n * from HEAD. A git call that fails counts as a reason: `npm install` is the\n * safe choice.\n */\nfunction ciRefusal(deps: Deps): string | null {\n if (localMembers(deps.appRoot).length)\n return `checkouts remain in ${LOCAL_DIR}/`;\n const git = (args: string[]) =>\n deps.run('git', args, {cwd: deps.appRoot}).status === 0;\n if (!git(['ls-files', '--error-unmatch', '--', LOCKFILE]))\n return `${LOCKFILE} is not tracked by git`;\n if (!git(['diff', '--quiet', 'HEAD', '--', 'package.json', LOCKFILE]))\n return `package.json or ${LOCKFILE} differs from HEAD`;\n return null;\n}\n\n/**\n * @returns the reason a purge must be refused, or null. Fails closed: a git\n * call that fails is a reason, since a check that cannot look must not let\n * a delete through.\n */\nfunction purgeRefusal(repo: string, deps: Deps): string | null {\n const checks: {args: string[]; what: (n: number) => string}[] = [\n {\n args: ['status', '--porcelain'],\n what: (n) => `${n} uncommitted change${n === 1 ? '' : 's'}`,\n },\n {\n // HEAD (a detached one has no branch) and every local branch, against every remote.\n args: ['log', '--oneline', 'HEAD', '--branches', '--not', '--remotes'],\n what: (n) => `${n} commit${n === 1 ? '' : 's'} no remote has`,\n },\n {\n args: ['stash', 'list'],\n what: (n) => `${n} stash entr${n === 1 ? 'y' : 'ies'}`,\n },\n ];\n for (const {args, what} of checks) {\n const r = deps.run('git', args, {cwd: repo});\n if (r.status !== 0) {\n const why =\n (r.stderr || r.stdout).trim().split('\\n')[0] || `exit ${r.status}`;\n return `\\`git ${args.join(' ')}\\` failed (${why}), so its work cannot be checked`;\n }\n const n = r.stdout.split('\\n').filter((l) => l.trim()).length;\n if (n) return `it has ${what(n)}`;\n }\n return null;\n}\n\n/** By full name, or by the part after the scope when that is unambiguous. */\nfunction findMember(name: string, members: Member[]): Member | undefined {\n const exact = members.find((m) => m.name === name);\n if (exact) return exact;\n const short = members.filter((m) => m.name.split('/').pop() === name);\n return short.length === 1 ? short[0] : undefined;\n}\n"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Which `linked` commands report drift (drift.ts) first: the ones that run
3
+ * the app's code. Kept apart from `cli.ts`, which builds and parses the real
4
+ * program on import, so a test can install the hook on a program of its own
5
+ * and see exactly which commands trigger it.
6
+ */
7
+ import type { Command } from 'commander';
8
+ /** `linked start`, `script`, `call` and `build-all`, and nothing else. */
9
+ export declare const DRIFT_COMMANDS: ReadonlySet<string>;
10
+ /** Print each drift line for the app at `appRoot` on stderr. Never throws. */
11
+ export declare function reportDrift(appRoot: string, warn?: (line: string) => void): void;
12
+ /**
13
+ * Before one of `DRIFT_COMMANDS`, report drift for the app the command runs
14
+ * in (its workspace root, not the member it may run from). Silent when there is none or
15
+ * nothing is installed; never fails the command.
16
+ */
17
+ export declare function installDriftHook(program: Command, report?: (appRoot: string) => void): void;
18
+ //# sourceMappingURL=drift-hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drift-hook.d.ts","sourceRoot":"","sources":["../../../src/localize/drift-hook.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AAKvC,0EAA0E;AAC1E,eAAO,MAAM,cAAc,EAAE,WAAW,CAAC,MAAM,CAK7C,CAAC;AAEH,8EAA8E;AAC9E,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAmC,GAC1D,IAAI,CAON;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,EAChB,MAAM,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAkB,GAC9C,IAAI,CASN"}
@@ -0,0 +1,37 @@
1
+ import { findAppRoot } from './app-root.js';
2
+ import { describeDrift, driftProblems } from './drift.js';
3
+ /** `linked start`, `script`, `call` and `build-all`, and nothing else. */
4
+ export const DRIFT_COMMANDS = new Set([
5
+ 'start',
6
+ 'script',
7
+ 'call',
8
+ 'build-all',
9
+ ]);
10
+ /** Print each drift line for the app at `appRoot` on stderr. Never throws. */
11
+ export function reportDrift(appRoot, warn = (line) => console.warn(line)) {
12
+ try {
13
+ for (const d of driftProblems(appRoot))
14
+ warn(`[localize] drift: ${describeDrift(d)}`);
15
+ }
16
+ catch (_a) {
17
+ /* a report must never cost the command it precedes */
18
+ }
19
+ }
20
+ /**
21
+ * Before one of `DRIFT_COMMANDS`, report drift for the app the command runs
22
+ * in (its workspace root, not the member it may run from). Silent when there is none or
23
+ * nothing is installed; never fails the command.
24
+ */
25
+ export function installDriftHook(program, report = reportDrift) {
26
+ program.hook('preAction', (_program, actionCommand) => {
27
+ if (!DRIFT_COMMANDS.has(actionCommand.name()))
28
+ return;
29
+ try {
30
+ report(findAppRoot(process.cwd()));
31
+ }
32
+ catch (_a) {
33
+ /* never fails the command */
34
+ }
35
+ });
36
+ }
37
+ //# sourceMappingURL=drift-hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drift-hook.js","sourceRoot":"","sources":["../../../src/localize/drift-hook.ts"],"names":[],"mappings":"AAQA,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAC,aAAa,EAAE,aAAa,EAAC,MAAM,YAAY,CAAC;AAExD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,cAAc,GAAwB,IAAI,GAAG,CAAC;IACzD,OAAO;IACP,QAAQ;IACR,MAAM;IACN,WAAW;CACZ,CAAC,CAAC;AAEH,8EAA8E;AAC9E,MAAM,UAAU,WAAW,CACzB,OAAe,EACf,OAA+B,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;IAE3D,IAAI,CAAC;QACH,KAAK,MAAM,CAAC,IAAI,aAAa,CAAC,OAAO,CAAC;YACpC,IAAI,CAAC,qBAAqB,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAClD,CAAC;IAAC,WAAM,CAAC;QACP,sDAAsD;IACxD,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAgB,EAChB,SAAoC,WAAW;IAE/C,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE;QACpD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC;YAAE,OAAO;QACtD,IAAI,CAAC;YACH,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrC,CAAC;QAAC,WAAM,CAAC;YACP,6BAA6B;QAC/B,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Which `linked` commands report drift (drift.ts) first: the ones that run\n * the app's code. Kept apart from `cli.ts`, which builds and parses the real\n * program on import, so a test can install the hook on a program of its own\n * and see exactly which commands trigger it.\n */\nimport type {Command} from 'commander';\n\nimport {findAppRoot} from './app-root.js';\nimport {describeDrift, driftProblems} from './drift.js';\n\n/** `linked start`, `script`, `call` and `build-all`, and nothing else. */\nexport const DRIFT_COMMANDS: ReadonlySet<string> = new Set([\n 'start',\n 'script',\n 'call',\n 'build-all',\n]);\n\n/** Print each drift line for the app at `appRoot` on stderr. Never throws. */\nexport function reportDrift(\n appRoot: string,\n warn: (line: string) => void = (line) => console.warn(line),\n): void {\n try {\n for (const d of driftProblems(appRoot))\n warn(`[localize] drift: ${describeDrift(d)}`);\n } catch {\n /* a report must never cost the command it precedes */\n }\n}\n\n/**\n * Before one of `DRIFT_COMMANDS`, report drift for the app the command runs\n * in (its workspace root, not the member it may run from). Silent when there is none or\n * nothing is installed; never fails the command.\n */\nexport function installDriftHook(\n program: Command,\n report: (appRoot: string) => void = reportDrift,\n): void {\n program.hook('preAction', (_program, actionCommand) => {\n if (!DRIFT_COMMANDS.has(actionCommand.name())) return;\n try {\n report(findAppRoot(process.cwd()));\n } catch {\n /* never fails the command */\n }\n });\n}\n"]}
@@ -0,0 +1,36 @@
1
+ export type Drift = {
2
+ kind: 'second-copy';
3
+ name: string;
4
+ version: string;
5
+ at: string;
6
+ } | {
7
+ kind: 'singleton-copy';
8
+ name: string;
9
+ version: string;
10
+ at: string;
11
+ } | {
12
+ kind: 'range-miss';
13
+ name: string;
14
+ version: string;
15
+ dir: string;
16
+ range: string;
17
+ field: string;
18
+ importer: string;
19
+ } | {
20
+ kind: 'missing-dir';
21
+ dir: string;
22
+ };
23
+ /** Every drift in the installed tree, in a stable order. No install yet means none. */
24
+ export declare function driftProblems(appRoot: string): Drift[];
25
+ /** One line, for `--list` and the drift check before `start` and friends. */
26
+ export declare function describeDrift(d: Drift): string;
27
+ /**
28
+ * Does `version` satisfy `range`? `null` means "cannot tell" -- a dist-tag,
29
+ * `file:`, `npm:`, `workspace:`, a git URL, or a version that is not one --
30
+ * and drift then reports nothing: npm links the checkout for such a range
31
+ * anyway. npm's own `semver` decides, so prereleases follow npm's rule:
32
+ * `1.3.0-beta.1` satisfies a range only when one of its comparators names a
33
+ * prerelease on the same `major.minor.patch`.
34
+ */
35
+ export declare function satisfies(version: string, range: string): boolean | null;
36
+ //# sourceMappingURL=drift.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drift.d.ts","sourceRoot":"","sources":["../../../src/localize/drift.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,KAAK,GACb;IAAC,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAC,GAChE;IAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAC,GACnE;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,GACD;IAAC,IAAI,EAAE,aAAa,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAC,CAAC;AAgBvC,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,EAAE,CAEtD;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAExE"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Drift -- what npm is silent about once checkouts are workspace members:
3
+ * a second copy of a localized package, a nested copy of a singleton, a
4
+ * declared range a checkout misses, and a checkout npm installed that is gone.
5
+ *
6
+ * The implementation is `hooks/drift.cjs`, the one the lock-at-rest hook
7
+ * requires after every install: plain CommonJS that runs without `lib/`. This
8
+ * module loads it and gives it types, so `--list`, the check before `linked
9
+ * start` and the hook report the same thing by the same rules.
10
+ */
11
+ import { requireHook } from './hook-module.js';
12
+ const hook = requireHook('drift.cjs', (mod) => typeof mod.driftProblems === 'function' &&
13
+ typeof mod.describeDrift === 'function' &&
14
+ typeof mod.satisfies === 'function');
15
+ /** Every drift in the installed tree, in a stable order. No install yet means none. */
16
+ export function driftProblems(appRoot) {
17
+ return hook.driftProblems(appRoot);
18
+ }
19
+ /** One line, for `--list` and the drift check before `start` and friends. */
20
+ export function describeDrift(d) {
21
+ return hook.describeDrift(d);
22
+ }
23
+ /**
24
+ * Does `version` satisfy `range`? `null` means "cannot tell" -- a dist-tag,
25
+ * `file:`, `npm:`, `workspace:`, a git URL, or a version that is not one --
26
+ * and drift then reports nothing: npm links the checkout for such a range
27
+ * anyway. npm's own `semver` decides, so prereleases follow npm's rule:
28
+ * `1.3.0-beta.1` satisfies a range only when one of its comparators names a
29
+ * prerelease on the same `major.minor.patch`.
30
+ */
31
+ export function satisfies(version, range) {
32
+ return hook.satisfies(version, range);
33
+ }
34
+ //# sourceMappingURL=drift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drift.js","sourceRoot":"","sources":["../../../src/localize/drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAC,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAsB7C,MAAM,IAAI,GAAG,WAAW,CACtB,WAAW,EACX,CAAC,GAAG,EAAE,EAAE,CACN,OAAO,GAAG,CAAC,aAAa,KAAK,UAAU;IACvC,OAAO,GAAG,CAAC,aAAa,KAAK,UAAU;IACvC,OAAO,GAAG,CAAC,SAAS,KAAK,UAAU,CACtC,CAAC;AAEF,uFAAuF;AACvF,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,OAAO,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,CAAQ;IACpC,OAAO,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,OAAe,EAAE,KAAa;IACtD,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AACxC,CAAC","sourcesContent":["/**\n * Drift -- what npm is silent about once checkouts are workspace members:\n * a second copy of a localized package, a nested copy of a singleton, a\n * declared range a checkout misses, and a checkout npm installed that is gone.\n *\n * The implementation is `hooks/drift.cjs`, the one the lock-at-rest hook\n * requires after every install: plain CommonJS that runs without `lib/`. This\n * module loads it and gives it types, so `--list`, the check before `linked\n * start` and the hook report the same thing by the same rules.\n */\nimport {requireHook} from './hook-module.js';\n\nexport type Drift =\n | {kind: 'second-copy'; name: string; version: string; at: string}\n | {kind: 'singleton-copy'; name: string; version: string; at: string}\n | {\n kind: 'range-miss';\n name: string;\n version: string;\n dir: string;\n range: string;\n field: string;\n importer: string;\n }\n | {kind: 'missing-dir'; dir: string};\n\ninterface DriftModule {\n driftProblems: (appRoot: string) => Drift[];\n describeDrift: (d: Drift) => string;\n satisfies: (version: string, range: string) => boolean | null;\n}\n\nconst hook = requireHook<DriftModule>(\n 'drift.cjs',\n (mod) =>\n typeof mod.driftProblems === 'function' &&\n typeof mod.describeDrift === 'function' &&\n typeof mod.satisfies === 'function',\n);\n\n/** Every drift in the installed tree, in a stable order. No install yet means none. */\nexport function driftProblems(appRoot: string): Drift[] {\n return hook.driftProblems(appRoot);\n}\n\n/** One line, for `--list` and the drift check before `start` and friends. */\nexport function describeDrift(d: Drift): string {\n return hook.describeDrift(d);\n}\n\n/**\n * Does `version` satisfy `range`? `null` means \"cannot tell\" -- a dist-tag,\n * `file:`, `npm:`, `workspace:`, a git URL, or a version that is not one --\n * and drift then reports nothing: npm links the checkout for such a range\n * anyway. npm's own `semver` decides, so prereleases follow npm's rule:\n * `1.3.0-beta.1` satisfies a range only when one of its comparators names a\n * prerelease on the same `major.minor.patch`.\n */\nexport function satisfies(version: string, range: string): boolean | null {\n return hook.satisfies(version, range);\n}\n"]}
@@ -2,20 +2,26 @@
2
2
  * Every refusal in localize throws a LocalizeError carrying an exit code, so
3
3
  * callers can branch on `.code` rather than matching on a message.
4
4
  *
5
- * The codes are a public contract: scripts depend on them.
5
+ * The codes are a public contract: scripts depend on them. 3 (unusable
6
+ * manifest file) and 8 (manifests changed) belonged to the retired record
7
+ * file and the hand-written links, and are not reused.
6
8
  */
7
- /** The manifest file is unusable and localize refuses to touch it. */
8
- export declare const EXIT_BAD_FILE = 3;
9
+ /** `--list --check` found drift. */
10
+ export declare const EXIT_DRIFT = 1;
11
+ /** The command line asks for something that cannot be done as given. */
12
+ export declare const EXIT_USAGE = 2;
9
13
  /** No git repository could be resolved for the package name. */
10
14
  export declare const EXIT_NOT_FOUND = 4;
11
- /** Something was warned about and skipped; the command otherwise worked. */
15
+ /** Something was warned about and skipped; the command otherwise worked (a failed build, a name not localized). */
12
16
  export declare const EXIT_WARNED = 5;
13
- /** A guard refused to act. `--force` overrides. */
17
+ /**
18
+ * A guard refused to act: an existing directory, a conflicting override, or a
19
+ * `--purge` of a checkout with unsaved work (uncommitted, on no remote,
20
+ * stashed) or one git cannot inspect.
21
+ */
14
22
  export declare const EXIT_REFUSED = 6;
15
- /** The install inside the checkout failed: nothing linked, nothing recorded. */
23
+ /** `npm install` (or `npm ci`) at the app root failed. */
16
24
  export declare const EXIT_INSTALL_FAILED = 7;
17
- /** package.json / package-lock.json changed -- the failure this tool exists to prevent. */
18
- export declare const EXIT_MANIFEST_DIRTY = 8;
19
25
  export declare class LocalizeError extends Error {
20
26
  code: number;
21
27
  constructor(message: string, code: number);
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/localize/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,sEAAsE;AACtE,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,gEAAgE;AAChE,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,4EAA4E;AAC5E,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,mDAAmD;AACnD,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,2FAA2F;AAC3F,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,qBAAa,aAAc,SAAQ,KAAK;IACtC,IAAI,EAAE,MAAM,CAAC;gBACD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAK1C"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/localize/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,oCAAoC;AACpC,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B,wEAAwE;AACxE,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B,gEAAgE;AAChE,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,mHAAmH;AACnH,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B;;;;GAIG;AACH,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,qBAAa,aAAc,SAAQ,KAAK;IACtC,IAAI,EAAE,MAAM,CAAC;gBACD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAK1C"}
@@ -2,20 +2,26 @@
2
2
  * Every refusal in localize throws a LocalizeError carrying an exit code, so
3
3
  * callers can branch on `.code` rather than matching on a message.
4
4
  *
5
- * The codes are a public contract: scripts depend on them.
5
+ * The codes are a public contract: scripts depend on them. 3 (unusable
6
+ * manifest file) and 8 (manifests changed) belonged to the retired record
7
+ * file and the hand-written links, and are not reused.
6
8
  */
7
- /** The manifest file is unusable and localize refuses to touch it. */
8
- export const EXIT_BAD_FILE = 3;
9
+ /** `--list --check` found drift. */
10
+ export const EXIT_DRIFT = 1;
11
+ /** The command line asks for something that cannot be done as given. */
12
+ export const EXIT_USAGE = 2;
9
13
  /** No git repository could be resolved for the package name. */
10
14
  export const EXIT_NOT_FOUND = 4;
11
- /** Something was warned about and skipped; the command otherwise worked. */
15
+ /** Something was warned about and skipped; the command otherwise worked (a failed build, a name not localized). */
12
16
  export const EXIT_WARNED = 5;
13
- /** A guard refused to act. `--force` overrides. */
17
+ /**
18
+ * A guard refused to act: an existing directory, a conflicting override, or a
19
+ * `--purge` of a checkout with unsaved work (uncommitted, on no remote,
20
+ * stashed) or one git cannot inspect.
21
+ */
14
22
  export const EXIT_REFUSED = 6;
15
- /** The install inside the checkout failed: nothing linked, nothing recorded. */
23
+ /** `npm install` (or `npm ci`) at the app root failed. */
16
24
  export const EXIT_INSTALL_FAILED = 7;
17
- /** package.json / package-lock.json changed -- the failure this tool exists to prevent. */
18
- export const EXIT_MANIFEST_DIRTY = 8;
19
25
  export class LocalizeError extends Error {
20
26
  constructor(message, code) {
21
27
  super(message);