dsh-plugin-toolkit 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.5
4
+
5
+ Four bugs found by running `release` for real against a throwaway package, instead of only `--dry-run`:
6
+
7
+ - The index entry came out with `publishTimestamp: null`. The registry was asked for the abbreviated packument (`application/vnd.npm.install-v1+json`), which has no `time` field at all — so the tool could see the version and still have no publish date. It now asks for the full document, and waits for `time[version]` before writing the entry.
8
+ - `git rev-parse` on a tag that does not exist yet printed `fatal: ambiguous argument …` in the middle of a successful release; stderr is now dropped for that call.
9
+ - "the working tree has uncommitted changes" now names up to three paths.
10
+ - A folder without `package.json` raised a stack trace instead of returning a preflight failure.
11
+
3
12
  ## 0.3.4
4
13
 
5
14
  - CI now runs on Windows as well as Linux and macOS; two tests assumed a POSIX temp path (`/tmp/…`) and were rewritten with `path.join`.
package/lib/registry.mjs CHANGED
@@ -11,8 +11,12 @@ export const REGISTRY = 'https://registry.npmjs.org';
11
11
  export async function packument(name, options = {}) {
12
12
  const base = options.registry ?? REGISTRY;
13
13
  const request = options.fetch ?? fetch;
14
+ // The full document, not the abbreviated install one: the index entry needs
15
+ // `time[version]`, and `application/vnd.npm.install-v1+json` omits `time`
16
+ // entirely — which is why an abbreviated fetch can see a version and still
17
+ // have no publish date to write.
14
18
  const response = await request(`${base}/${name.replace('/', '%2F')}`, {
15
- headers: { accept: 'application/vnd.npm.install-v1+json' },
19
+ headers: { accept: 'application/json' },
16
20
  });
17
21
  if (response.status === 404) return null;
18
22
  if (response.ok === false) {
@@ -40,6 +44,12 @@ export function hasVersion(document, version) {
40
44
  * `onWait` is called after each miss so the CLI can report progress.
41
45
  */
42
46
  export async function waitForVersion(name, version, options = {}) {
47
+ // `requireTime` matters for the market index: `versions` can list a version
48
+ // before the registry has written its `time` entry, and an index entry with a
49
+ // null timestamp is rejected by the checker.
50
+ const ready = options.requireTime === true
51
+ ? (document) => hasVersion(document, version) === true && publishedAt(document, version) !== null
52
+ : (document) => hasVersion(document, version) === true;
43
53
  const budgetMs = options.budgetMs ?? 8 * 60 * 1000;
44
54
  const intervalMs = options.intervalMs ?? 10 * 1000;
45
55
  const now = options.now ?? Date.now;
@@ -50,7 +60,7 @@ export async function waitForVersion(name, version, options = {}) {
50
60
  for (;;) {
51
61
  attempts += 1;
52
62
  const document = await packument(name, options);
53
- if (hasVersion(document, version) === true) {
63
+ if (ready(document) === true) {
54
64
  return { document, attempts, waitedMs: now() - started };
55
65
  }
56
66
  if (now() - started >= budgetMs) {
package/lib/release.mjs CHANGED
@@ -14,7 +14,10 @@ import { addVersion, buildEntry, readIndex, writeIndex } from './index-file.mjs'
14
14
  import { publishedAt, waitForVersion } from './registry.mjs';
15
15
 
16
16
  function git(dir, args) {
17
- return execFileSync('git', args, { cwd: dir, encoding: 'utf8' }).trim();
17
+ // stderr is dropped on purpose: `rev-parse` on a tag that does not exist yet is
18
+ // an expected miss, and git prints "fatal: ambiguous argument" for it, which
19
+ // reads like a failure in the middle of a successful release.
20
+ return execFileSync('git', args, { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
18
21
  }
19
22
 
20
23
  export function tagName(version, prefix = 'plugin-v') {
@@ -34,6 +37,14 @@ export function workingTreeIsClean(dir) {
34
37
  return git(dir, ['status', '--porcelain']) === '';
35
38
  }
36
39
 
40
+ /** The paths git would carry into the tag, so the message can name them. */
41
+ export function dirtyPaths(dir, limit = 3) {
42
+ const status = git(dir, ['status', '--porcelain']);
43
+ if (status === '') return [];
44
+ const paths = status.split('\n').map((line) => line.slice(3).trim()).filter((line) => line !== '');
45
+ return paths.slice(0, limit);
46
+ }
47
+
37
48
  /**
38
49
  * Steps a human must do, and steps the tool can do. Returning this list before
39
50
  * anything runs is what makes `--dry-run` useful: it shows the whole sequence
@@ -69,7 +80,14 @@ export async function release(options) {
69
80
  onChange,
70
81
  } = options;
71
82
 
72
- const report = doctor(dir);
83
+ let report;
84
+ try {
85
+ report = doctor(dir);
86
+ } catch (error) {
87
+ // A folder without package.json is a preflight failure, not a crash: the
88
+ // command's job is to say what is wrong, and a stack trace is not an answer.
89
+ return { ok: false, stage: 'preflight', failures: [{ check: 'manifest', message: error.message }] };
90
+ }
73
91
  if (report.failures.length > 0) {
74
92
  return { ok: false, stage: 'preflight', failures: report.failures };
75
93
  }
@@ -82,10 +100,12 @@ export async function release(options) {
82
100
  return { ok: false, stage: 'tag', failures: [{ check: 'tag', message: `${tag} already exists` }] };
83
101
  }
84
102
  if (workingTreeIsClean(dir) === false) {
103
+ const dirty = dirtyPaths(dir);
104
+ const listed = dirty.length > 0 ? ` (${dirty.join(', ')})` : '';
85
105
  return {
86
106
  ok: false,
87
107
  stage: 'git',
88
- failures: [{ check: 'git', message: 'the working tree has uncommitted changes' }],
108
+ failures: [{ check: 'git', message: `the working tree has uncommitted changes${listed}` }],
89
109
  };
90
110
  }
91
111
 
@@ -93,6 +113,9 @@ export async function release(options) {
93
113
  git(dir, ['push', 'origin', tag]);
94
114
 
95
115
  const waited = await waitForVersion(name, version, {
116
+ // Wait for the publish *time* as well: `versions` can appear a moment before
117
+ // `time`, and an index entry with a null timestamp fails the market checker.
118
+ requireTime: true,
96
119
  registry,
97
120
  budgetMs,
98
121
  intervalMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-toolkit",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Preflight, release and test DeepSeek Harness plugins: checks what the host needs, waits for npm to actually serve the version, and keeps the market index honest.",
5
5
  "type": "module",
6
6
  "bin": {