singularity-forge/scripts/ensure-workspace-builds.cjs
Iouri Goussev de600c1db0 refactor(gsd): extract duplicated status guards and validation helpers (#2767)
* fix: rebuild stale workspace packages after git pull

ensure-workspace-builds.cjs only triggered a build when dist/index.js
was missing entirely. After `git pull` updates package sources, the old
dist/ stayed in place causing TypeScript type errors (bash_transform,
authMode, malformedArguments missing from compiled .d.ts files).

Now compares newest .ts mtime under src/ against dist/index.js mtime
and rebuilds any package whose sources are newer than its dist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(rtk): trust explicit binaryPath without existsSync check; add options object to shared rewriteCommandWithRtk

resolveRtkBinaryPath was calling existsSync on options.binaryPath, making
it impossible to inject a non-existent test binary — tests expected the
options-object API to bypass filesystem checks.

Also brings src/resources/extensions/shared/rtk.ts rewriteCommandWithRtk
in line with the same options-object signature already in src/rtk.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(gsd): extract duplicated status guards and validation helpers

isClosedStatus(), isNonEmptyString(), and validateStringArray() were
each copy-pasted across 5-10 tool handler files with no shared module.
Extract them into status-guards.ts and validation.ts, replace all 26
inline status checks and 8 duplicated validation functions with imports.

Standardizes "inside a closed" -> "in a closed" in two reopen error
messages as a side effect of the normalization pass.

Closes #2727

* refactor(gsd): migrate state.ts isStatusDone to isClosedStatus; fix blank lines and import order

- state.ts had a private isStatusDone() identical to isClosedStatus() —
  replace with import from status-guards.ts
- Remove double blank lines left behind in plan-{milestone,slice,task}.ts
  and replan-slice.ts after local function extraction
- Fix import ordering in reassess-roadmap.ts (node built-ins first,
  status-guards/validation before gsd-db block)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:14:43 -06:00

86 lines
2.8 KiB
JavaScript

#!/usr/bin/env node
/**
* ensure-workspace-builds.cjs
*
* Checks whether workspace packages have been compiled (dist/ exists with
* index.js) and that the build is not stale (no src/ file newer than dist/).
* If any are missing or stale, runs the build for those packages.
*
* Designed for the postinstall hook so that `npm install` in a fresh clone
* produces a working runtime without a manual `npm run build` step. Also
* catches the common case where `git pull` updates package sources but the
* old dist/ remains, causing TypeScript type errors.
*
* Skipped in CI (where the full build pipeline handles this) and when
* installing as an end-user dependency (no packages/ directory).
*/
const { existsSync, statSync, readdirSync } = require('fs')
const { resolve, join } = require('path')
const { execSync } = require('child_process')
const root = resolve(__dirname, '..')
const packagesDir = join(root, 'packages')
// Skip if packages/ doesn't exist (published tarball / end-user install)
if (!existsSync(packagesDir)) process.exit(0)
// Skip in CI — the pipeline runs `npm run build` explicitly
if (process.env.CI === 'true' || process.env.CI === '1') process.exit(0)
// Workspace packages that need dist/index.js at runtime.
// Order matters: dependencies must build before dependents.
const WORKSPACE_PACKAGES = [
'native',
'pi-tui',
'pi-ai',
'pi-agent-core',
'pi-coding-agent',
]
/**
* Returns the most recent mtime (ms) of any .ts file under dir, recursively.
* Returns 0 if no .ts files found.
*/
function newestSrcMtime(dir) {
if (!existsSync(dir)) return 0
let newest = 0
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.name === 'node_modules') continue
const full = join(dir, entry.name)
if (entry.isDirectory()) {
newest = Math.max(newest, newestSrcMtime(full))
} else if (entry.isFile() && entry.name.endsWith('.ts')) {
newest = Math.max(newest, statSync(full).mtimeMs)
}
}
return newest
}
const stale = []
for (const pkg of WORKSPACE_PACKAGES) {
const distIndex = join(packagesDir, pkg, 'dist', 'index.js')
if (!existsSync(distIndex)) {
stale.push(pkg)
continue
}
const distMtime = statSync(distIndex).mtimeMs
const srcMtime = newestSrcMtime(join(packagesDir, pkg, 'src'))
if (srcMtime > distMtime) {
stale.push(pkg)
}
}
if (stale.length === 0) process.exit(0)
process.stderr.write(` Building ${stale.length} workspace package(s) with stale or missing dist/: ${stale.join(', ')}\n`)
for (const pkg of stale) {
const pkgDir = join(packagesDir, pkg)
try {
execSync('npm run build', { cwd: pkgDir, stdio: 'pipe' })
process.stderr.write(`${pkg}\n`)
} catch (err) {
process.stderr.write(`${pkg} build failed: ${err.message}\n`)
// Non-fatal — the user can run `npm run build` manually
}
}