@kb-labs/devkit 2.104.0 → 2.105.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.
File without changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kb-labs/devkit",
3
3
  "description": "Shared developer toolkit for KB Labs projects: TS/ESLint/Prettier/Vitest/Tsup presets and reusable GitHub Actions.",
4
- "version": "2.104.0",
4
+ "version": "2.105.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -107,6 +107,26 @@
107
107
  ".github/workflow-templates/**",
108
108
  ".github/actions/**"
109
109
  ],
110
+ "scripts": {
111
+ "build": "echo \"@kb-labs/devkit: nothing to build\"",
112
+ "clean": "rimraf dist",
113
+ "dev": "tsup --config tsup.config.ts --watch",
114
+ "fixtures": "node scripts/fixtures.js –help",
115
+ "fixtures:bootstrap": "node scripts/fixtures.js –action=bootstrap",
116
+ "fixtures:build": "node scripts/fixtures.js –action=build",
117
+ "fixtures:check": "node scripts/fixtures.js –action=check",
118
+ "fixtures:ci": "node scripts/fixtures.js –action=check –since=${GITHUB_BASE_REF:-origin/main} –concurrency=3 || node scripts/fixtures.js –action=check",
119
+ "fixtures:clean": "node scripts/fixtures.js –action=clean",
120
+ "fixtures:lint": "node scripts/fixtures.js –action=lint",
121
+ "fixtures:test": "node scripts/fixtures.js –action=test",
122
+ "lint": "echo \"@kb-labs/devkit: lint skipped (plain JS/MJS, not TS project)\" && exit 0",
123
+ "lint:fix": "eslint . --fix --ignore-pattern 'dist/**'",
124
+ "paths": "node ./bin/devkit-paths.mjs",
125
+ "postinstall": "kb-devkit-tsup-external --generate || true",
126
+ "test": "vitest run --passWithNoTests",
127
+ "test:watch": "vitest --passWithNoTests",
128
+ "type-check": "echo \"@kb-labs/devkit: type-check skipped (plain JS/MJS, not TS project)\" && exit 0"
129
+ },
110
130
  "dependencies": {
111
131
  "eslint-import-resolver-typescript": "^3.10.1",
112
132
  "eslint-plugin-import": "^2.32.0",
@@ -117,6 +137,7 @@
117
137
  "yaml": "^2.8.0"
118
138
  },
119
139
  "devDependencies": {
140
+ "@kb-labs/devkit": "workspace:*",
120
141
  "@types/node": "^24.3.3",
121
142
  "@vitest/coverage-istanbul": "^4.1.4",
122
143
  "@vitest/coverage-v8": "^3.2.4",
@@ -130,13 +151,13 @@
130
151
  "tsup": "^8.5.0",
131
152
  "typescript": "^5.9.2",
132
153
  "typescript-eslint": "^8.44.0",
133
- "vitest": "^3.2.6",
134
- "@kb-labs/devkit": "2.104.0"
154
+ "vitest": "^3.2.6"
135
155
  },
136
156
  "engines": {
137
157
  "node": ">=20.0.0",
138
158
  "pnpm": ">=9.11.0"
139
159
  },
160
+ "packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b",
140
161
  "peerDependencies": {
141
162
  "@playwright/test": "*",
142
163
  "eslint": "^9.35.0",
@@ -170,25 +191,5 @@
170
191
  },
171
192
  "publishConfig": {
172
193
  "access": "public"
173
- },
174
- "scripts": {
175
- "build": "echo \"@kb-labs/devkit: nothing to build\"",
176
- "clean": "rimraf dist",
177
- "dev": "tsup --config tsup.config.ts --watch",
178
- "fixtures": "node scripts/fixtures.js –help",
179
- "fixtures:bootstrap": "node scripts/fixtures.js –action=bootstrap",
180
- "fixtures:build": "node scripts/fixtures.js –action=build",
181
- "fixtures:check": "node scripts/fixtures.js –action=check",
182
- "fixtures:ci": "node scripts/fixtures.js –action=check –since=${GITHUB_BASE_REF:-origin/main} –concurrency=3 || node scripts/fixtures.js –action=check",
183
- "fixtures:clean": "node scripts/fixtures.js –action=clean",
184
- "fixtures:lint": "node scripts/fixtures.js –action=lint",
185
- "fixtures:test": "node scripts/fixtures.js –action=test",
186
- "lint": "echo \"@kb-labs/devkit: lint skipped (plain JS/MJS, not TS project)\" && exit 0",
187
- "lint:fix": "eslint . --fix --ignore-pattern 'dist/**'",
188
- "paths": "node ./bin/devkit-paths.mjs",
189
- "postinstall": "kb-devkit-tsup-external --generate || true",
190
- "test": "vitest run --passWithNoTests",
191
- "test:watch": "vitest --passWithNoTests",
192
- "type-check": "echo \"@kb-labs/devkit: type-check skipped (plain JS/MJS, not TS project)\" && exit 0"
193
194
  }
194
- }
195
+ }
package/tsup/node.js CHANGED
@@ -2,6 +2,10 @@ import { defineConfig } from 'tsup'
2
2
  import { readTsupExternalSync } from './external-sync.mjs'
3
3
  import { readFileSync, writeFileSync, existsSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
+ import { execFile } from 'node:child_process'
6
+ import { promisify } from 'node:util'
7
+
8
+ const execFileAsync = promisify(execFile)
5
9
 
6
10
  /**
7
11
  * Derive tsup entry points from package.json.
@@ -55,10 +59,10 @@ function resolveManifestDistPath() {
55
59
  }
56
60
 
57
61
  /**
58
- * tsup `onSuccess`: emit dist/manifest.json by reading the compiled
59
- * dist/manifest.js as TEXT and evaluating the exported object.
62
+ * Fast path: read the compiled dist/manifest.js as TEXT and evaluate just the
63
+ * `manifest` object literal via Function().
60
64
  *
61
- * Why not dynamic `import()`?
65
+ * Why not dynamic `import()` here?
62
66
  * In multi-entry tsup builds `onSuccess` fires while Node's module cache may
63
67
  * still hold a stale (empty) version of dist/manifest.js from an earlier
64
68
  * watch-mode iteration, or the file descriptors may not be fully flushed.
@@ -66,32 +70,88 @@ function resolveManifestDistPath() {
66
70
  * it returned an empty module in CI, producing a 0-byte manifest.json that
67
71
  * kb-create rejected with "unexpected end of JSON input".
68
72
  *
69
- * Reading the COMPILED JS as text is safe: tsup has already written the file
70
- * before invoking `onSuccess`, and we're parsing plain JavaScript (no type
71
- * annotations) with a small Function() eval — controlled, no user input.
73
+ * This only works when `manifest = {...}` is a self-contained literal with no
74
+ * references to other module-scope variables. Service manifests tend to be
75
+ * written that way. Plugin manifests generally are NOT — they build
76
+ * `permissions` via `combinePermissions().build()` as a prior statement and
77
+ * reference the result by identifier inside the `manifest` literal, which
78
+ * this isolated eval can't resolve. For those, see `emitManifestJsonViaSubprocess`.
79
+ */
80
+ function emitManifestJsonFast(distPath) {
81
+ const js = readFileSync(distPath, 'utf8')
82
+ // tsup ESM output pattern (from inspecting actual built files):
83
+ // var manifest = { schema: "kb.service/1", id: "...", ... };
84
+ // var manifest_default = manifest;
85
+ // export { manifest_default as default, manifest };
86
+ // Capture everything between `var manifest =` and the next `var manifest_default`.
87
+ const match = js.match(/var\s+manifest\s*=\s*(\{[\s\S]*?\n\});\s*\nvar\s+manifest_default/)
88
+ if (!match) return null
89
+ // eslint-disable-next-line no-new-func
90
+ const obj = new Function(`"use strict"; return (${match[1]})`)()
91
+ if (!obj || typeof obj !== 'object') return null
92
+ return obj
93
+ }
94
+
95
+ /**
96
+ * Fallback path: spawn a fresh `node` process to `import()` the compiled
97
+ * module and dump the resolved default export as JSON on stdout.
98
+ *
99
+ * Running in a separate process (rather than `import()` in-process) sidesteps
100
+ * the same-process module-cache staleness problem the fast path avoids by
101
+ * using text parsing — this process only ever imports the file once, fresh
102
+ * off disk. This is the same technique `internal/scan/scanner.js` in
103
+ * kb-create already uses at install time; here it runs once at build time
104
+ * instead. For plugin manifests (which build `permissions` via
105
+ * `combinePermissions().build()` as a separate statement, not an inline
106
+ * literal) this is the path that actually resolves the real object — the
107
+ * fast text-eval path can't see that prior statement's result. In practice
108
+ * this fallback fires for nearly every plugin manifest, not rarely.
109
+ */
110
+ async function emitManifestJsonViaSubprocess(distPath) {
111
+ const url = new URL(`file://${distPath}`).href
112
+ const script = `import(${JSON.stringify(url)}).then(m => process.stdout.write(JSON.stringify(m.default ?? m.manifest)))`
113
+ const { stdout } = await execFileAsync(process.execPath, ['-e', script], {
114
+ cwd: process.cwd(),
115
+ timeout: 30_000,
116
+ })
117
+ const obj = JSON.parse(stdout)
118
+ if (!obj || typeof obj !== 'object') return null
119
+ return obj
120
+ }
121
+
122
+ /**
123
+ * tsup `onSuccess`: emit dist/manifest.json from the compiled manifest
124
+ * module, for both service (`kb.service/*`) and plugin (`kb.plugin/*`)
125
+ * schemas, so kb-create (a Go binary) can read install-relevant fields
126
+ * without executing JS.
72
127
  */
73
- function emitManifestJson() {
128
+ async function emitManifestJson() {
74
129
  const distPath = resolveManifestDistPath()
75
130
  if (!distPath || !existsSync(distPath)) return
131
+ let obj = null
132
+ let fastPathError = null
133
+ try {
134
+ obj = emitManifestJsonFast(distPath)
135
+ } catch (err) {
136
+ fastPathError = err
137
+ }
138
+ if (!obj || typeof obj.schema !== 'string' || !/^kb\.(service|plugin)\//.test(obj.schema)) {
139
+ try {
140
+ obj = await emitManifestJsonViaSubprocess(distPath)
141
+ } catch (err) {
142
+ console.warn(`[kb-devkit] failed to emit manifest JSON for ${distPath}:`, fastPathError ?? err)
143
+ return
144
+ }
145
+ }
146
+ if (!obj || typeof obj.schema !== 'string' || !/^kb\.(service|plugin)\//.test(obj.schema)) {
147
+ console.warn(`[kb-devkit] manifest at ${distPath} has no recognizable kb.service/kb.plugin schema — skipping JSON emission`)
148
+ return
149
+ }
76
150
  try {
77
- const js = readFileSync(distPath, 'utf8')
78
- // tsup ESM output pattern (from inspecting actual built files):
79
- // var manifest = { schema: "kb.service/1", id: "...", ... };
80
- // var manifest_default = manifest;
81
- // export { manifest_default as default, manifest };
82
- // Capture everything between `var manifest =` and the next `var manifest_default`.
83
- const match = js.match(/var\s+manifest\s*=\s*(\{[\s\S]*?\n\});\s*\nvar\s+manifest_default/)
84
- if (!match) return
85
- // eslint-disable-next-line no-new-func
86
- const obj = new Function(`"use strict"; return (${match[1]})`)()
87
- if (!obj || typeof obj !== 'object') return
88
- // Scope to SERVICE manifests only — plugin/adapter manifests are loaded as
89
- // JS by the runtime and don't need a sibling .json.
90
- if (typeof obj.schema !== 'string' || !obj.schema.startsWith('kb.service/')) return
91
151
  const jsonPath = distPath.replace(/\.js$/, '.json')
92
152
  writeFileSync(jsonPath, JSON.stringify(obj, null, 2) + '\n')
93
- } catch {
94
- // Never fail the build over manifest emission.
153
+ } catch (err) {
154
+ console.warn(`[kb-devkit] failed to write manifest JSON for ${distPath}:`, err)
95
155
  }
96
156
  }
97
157
 
@@ -142,7 +202,8 @@ export default defineConfig({
142
202
  skipNodeModulesBundle: true,
143
203
  shims: false,
144
204
  // Emit dist/manifest.json from the built manifest module so Go installers
145
- // (kb-create) can register the service. No-op for packages without a manifest.
205
+ // (kb-create) can read service/plugin install metadata without executing
206
+ // JS. No-op for packages without a manifest.
146
207
  onSuccess: emitManifestJson,
147
208
  ignoreWatch: [
148
209
  '**/node_modules/**',