prjct-cli 2.57.0 → 2.59.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.59.0] - 2026-06-22
6
+
7
+ ### Fixed
8
+ - **Windows and Linux compatibility foundations.** Package-manager installs now use a portable Node launcher on Windows, the daemon uses named pipes on Windows and Unix sockets on macOS/Linux, Git auto-sync hooks no longer depend on Unix-only shell utilities, and CI smokes Ubuntu, macOS, and Windows.
9
+
10
+ ## [2.58.0] - 2026-06-22
11
+
12
+ ### Added
13
+ - stale sync slash copy
14
+
5
15
  ## [2.57.0] - 2026-06-22
6
16
 
7
17
  ### Added
package/README.md CHANGED
@@ -69,6 +69,24 @@ check, logs to `~/.prjct-cli/state/auto-update.log`).
69
69
 
70
70
  Full install + upgrade paths: [INSTALL_PROMPT.md](./INSTALL_PROMPT.md).
71
71
 
72
+ ### Platform support
73
+
74
+ The package-manager install path is the portable path for **macOS, Linux, and Windows**:
75
+
76
+ ```bash
77
+ npm install -g prjct-cli@latest
78
+ # or pnpm / bun / yarn global install
79
+ ```
80
+
81
+ The daemon uses Unix sockets on macOS/Linux and a Windows named pipe on Windows.
82
+ Git auto-sync hooks keep the shell wrapper POSIX-small and run rate limiting /
83
+ background spawn through Node, so they work in Git for Windows without relying on
84
+ Unix-only utilities like `md5sum`, `stat -f`, or `date +%s`.
85
+
86
+ The standalone `curl | bash` installer remains **macOS/Linux only**. Windows
87
+ users should use the package-manager install path. CI runs focused platform
88
+ compatibility smoke tests on Ubuntu, macOS, and Windows.
89
+
72
90
  ### Zero native dependencies
73
91
 
74
92
  prjct-cli uses SQLite for local project memory through the runtime's **built-in**
@@ -449,7 +467,8 @@ Bring your own MCP — prjct-cli doesn't duplicate trackers.
449
467
 
450
468
  ```
451
469
  prjct-cli/
452
- bin/prjct Thin JS shim (daemon-first)
470
+ bin/prjct.cjs Portable package-manager launcher
471
+ dist/bin/prjct.mjs Thin JS shim (daemon-first)
453
472
  core/
454
473
  cli/ CLI command handlers + dispatcher
455
474
  hooks/ 7 passive Claude Code hook subcommands
package/bin/prjct.cjs ADDED
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Portable package-manager entry point for prjct.
5
+ *
6
+ * npm/pnpm/yarn/bun generate Windows shims correctly for node shebangs, but
7
+ * not for the historical bin/prjct POSIX shell launcher. Keep this file tiny
8
+ * and dependency-free so global installs work on macOS, Linux, and Windows.
9
+ */
10
+
11
+ const childProcess = require('node:child_process')
12
+ const fs = require('node:fs')
13
+ const os = require('node:os')
14
+ const path = require('node:path')
15
+
16
+ const SCRIPT_PATH = fs.realpathSync(__filename)
17
+ const SCRIPT_DIR = path.dirname(SCRIPT_PATH)
18
+ const ROOT_DIR = path.resolve(SCRIPT_DIR, '..')
19
+ const HOME = os.homedir()
20
+
21
+ function pathEntries() {
22
+ return (process.env.PATH || '').split(path.delimiter).filter(Boolean)
23
+ }
24
+
25
+ function executableCandidates(command) {
26
+ if (process.platform !== 'win32') return [command]
27
+ const pathext = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
28
+ .split(';')
29
+ .filter(Boolean)
30
+ .map((ext) => ext.toLowerCase())
31
+ const ext = path.extname(command).toLowerCase()
32
+ if (ext && pathext.includes(ext)) return [command]
33
+ return pathext.map((candidateExt) => `${command}${candidateExt.toLowerCase()}`)
34
+ }
35
+
36
+ function findCommand(command) {
37
+ for (const dir of pathEntries()) {
38
+ for (const candidate of executableCandidates(command)) {
39
+ const fullPath = path.join(dir, candidate)
40
+ try {
41
+ fs.accessSync(fullPath, fs.constants.X_OK)
42
+ return fullPath
43
+ } catch {
44
+ /* keep searching */
45
+ }
46
+ }
47
+ }
48
+ return null
49
+ }
50
+
51
+ function nodeVersionOk() {
52
+ const match = process.versions.node.match(/^(\d+)\.(\d+)\./)
53
+ const major = Number.parseInt(match?.[1] || '0', 10)
54
+ const minor = Number.parseInt(match?.[2] || '0', 10)
55
+ return major > 22 || (major === 22 && minor >= 5)
56
+ }
57
+
58
+ function withSqliteFlag(env = process.env) {
59
+ const existing = env.NODE_OPTIONS || ''
60
+ const flag = '--experimental-sqlite'
61
+ return {
62
+ ...env,
63
+ NODE_OPTIONS: existing.includes(flag) ? existing : `${flag}${existing ? ` ${existing}` : ''}`,
64
+ }
65
+ }
66
+
67
+ function spawnAndExit(command, args, options = {}) {
68
+ const result = childProcess.spawnSync(command, args, {
69
+ stdio: 'inherit',
70
+ windowsHide: false,
71
+ ...options,
72
+ })
73
+
74
+ if (result.error) {
75
+ console.error(`Error: ${result.error.message}`)
76
+ process.exit(1)
77
+ }
78
+ if (result.signal) {
79
+ process.kill(process.pid, result.signal)
80
+ }
81
+ process.exit(result.status ?? 1)
82
+ }
83
+
84
+ function removeIfExists(filePath) {
85
+ try {
86
+ if (fs.existsSync(filePath)) fs.rmSync(filePath, { force: true })
87
+ } catch {
88
+ /* best effort */
89
+ }
90
+ }
91
+
92
+ function copyIfNewer(src, dest) {
93
+ try {
94
+ if (!fs.existsSync(src)) return
95
+ const srcStat = fs.statSync(src)
96
+ const destStat = fs.existsSync(dest) ? fs.statSync(dest) : null
97
+ if (destStat && destStat.mtimeMs >= srcStat.mtimeMs) return
98
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
99
+ fs.copyFileSync(src, dest)
100
+ } catch {
101
+ /* best effort */
102
+ }
103
+ }
104
+
105
+ function copyDirContents(srcDir, destDir) {
106
+ try {
107
+ if (!fs.existsSync(srcDir)) return
108
+ fs.mkdirSync(destDir, { recursive: true })
109
+ for (const entry of fs.readdirSync(srcDir)) {
110
+ fs.copyFileSync(path.join(srcDir, entry), path.join(destDir, entry))
111
+ }
112
+ } catch {
113
+ /* best effort */
114
+ }
115
+ }
116
+
117
+ function ensureSetup() {
118
+ removeIfExists(path.join(HOME, '.claude', 'commands', 'p.md'))
119
+ removeIfExists(path.join(HOME, '.gemini', 'commands', 'p.toml'))
120
+
121
+ const statuslineSrc = path.join(ROOT_DIR, 'assets', 'statusline', 'statusline.sh')
122
+ const statuslineDir = path.join(HOME, '.prjct-cli', 'statusline')
123
+ const statuslineDest = path.join(statuslineDir, 'statusline.sh')
124
+ const claudeStatusline = path.join(HOME, '.claude', 'prjct-statusline.sh')
125
+
126
+ try {
127
+ if (fs.existsSync(statuslineSrc)) {
128
+ const needsCopy =
129
+ !fs.existsSync(statuslineDest) ||
130
+ fs.statSync(statuslineSrc).mtimeMs > fs.statSync(statuslineDest).mtimeMs
131
+ if (needsCopy) {
132
+ fs.mkdirSync(statuslineDir, { recursive: true })
133
+ fs.copyFileSync(statuslineSrc, statuslineDest)
134
+ fs.chmodSync(statuslineDest, 0o755)
135
+
136
+ const pkg = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf-8'))
137
+ if (pkg.version) {
138
+ const patched = fs
139
+ .readFileSync(statuslineDest, 'utf-8')
140
+ .replace(/CLI_VERSION="[^"]*"/, `CLI_VERSION="${pkg.version}"`)
141
+ fs.writeFileSync(statuslineDest, patched)
142
+ }
143
+
144
+ for (const subdir of ['lib', 'components', 'themes']) {
145
+ copyDirContents(
146
+ path.join(ROOT_DIR, 'assets', 'statusline', subdir),
147
+ path.join(statuslineDir, subdir)
148
+ )
149
+ }
150
+
151
+ fs.mkdirSync(path.dirname(claudeStatusline), { recursive: true })
152
+ removeIfExists(claudeStatusline)
153
+ try {
154
+ fs.symlinkSync(statuslineDest, claudeStatusline)
155
+ } catch {
156
+ fs.copyFileSync(statuslineDest, claudeStatusline)
157
+ }
158
+ fs.chmodSync(claudeStatusline, 0o755)
159
+ }
160
+ }
161
+ } catch {
162
+ /* best effort */
163
+ }
164
+
165
+ copyIfNewer(
166
+ path.join(ROOT_DIR, 'templates', 'skills', 'prjct', 'SKILL.md'),
167
+ path.join(HOME, '.claude', 'skills', 'prjct', 'SKILL.md')
168
+ )
169
+
170
+ if (fs.existsSync(path.join(HOME, '.codex')) || findCommand('codex')) {
171
+ copyIfNewer(
172
+ path.join(ROOT_DIR, 'templates', 'codex', 'SKILL.md'),
173
+ path.join(HOME, '.codex', 'skills', 'prjct', 'SKILL.md')
174
+ )
175
+ }
176
+ }
177
+
178
+ function runMcpServer(args) {
179
+ const mcpServer = path.join(ROOT_DIR, 'dist', 'mcp', 'server.mjs')
180
+ if (!fs.existsSync(mcpServer)) {
181
+ console.error("Error: MCP server entry point not found. Run 'npm run build' first.")
182
+ process.exit(1)
183
+ }
184
+ if (!nodeVersionOk()) {
185
+ console.error(
186
+ `Error: prjct MCP needs Node >=22.5 (for node:sqlite) - found Node ${process.versions.node}.`
187
+ )
188
+ process.exit(1)
189
+ }
190
+ spawnAndExit(process.execPath, [mcpServer, ...args.slice(1)], { env: withSqliteFlag() })
191
+ }
192
+
193
+ function runWithNode(args) {
194
+ const distBin = path.join(ROOT_DIR, 'dist', 'bin', 'prjct.mjs')
195
+
196
+ if (!fs.existsSync(distBin)) {
197
+ if (
198
+ fs.existsSync(path.join(ROOT_DIR, 'scripts', 'build.js')) &&
199
+ fs.existsSync(path.join(ROOT_DIR, 'core'))
200
+ ) {
201
+ console.error('Building for Node.js (first run)...')
202
+ const build = childProcess.spawnSync(
203
+ process.execPath,
204
+ [path.join(ROOT_DIR, 'scripts', 'build.js')],
205
+ {
206
+ cwd: ROOT_DIR,
207
+ stdio: 'inherit',
208
+ }
209
+ )
210
+ if (build.status !== 0) {
211
+ console.error("Error: Build failed. Run 'npm run build' manually.")
212
+ process.exit(build.status ?? 1)
213
+ }
214
+ } else {
215
+ console.error("Error: Compiled output not found. Run 'npm run build' first.")
216
+ process.exit(1)
217
+ }
218
+ }
219
+
220
+ if (!nodeVersionOk()) {
221
+ console.error(
222
+ `Error: prjct needs Node >=22.5 (for node:sqlite) - found Node ${process.versions.node}.`
223
+ )
224
+ console.error(' Upgrade Node (https://nodejs.org) or install Bun (https://bun.sh).')
225
+ process.exit(1)
226
+ }
227
+
228
+ spawnAndExit(process.execPath, [distBin, ...args], { env: withSqliteFlag() })
229
+ }
230
+
231
+ function runWithBun(args) {
232
+ if (process.platform === 'win32') return false
233
+ const distBin = path.join(ROOT_DIR, 'dist', 'bin', 'prjct.mjs')
234
+ const bun = findCommand('bun')
235
+ if (!bun || !fs.existsSync(distBin)) return false
236
+ spawnAndExit(bun, [distBin, ...args])
237
+ return true
238
+ }
239
+
240
+ function main() {
241
+ const args = process.argv.slice(2)
242
+ if (args[0] === 'mcp-server') runMcpServer(args)
243
+
244
+ ensureSetup()
245
+
246
+ if (runWithBun(args)) return
247
+ runWithNode(args)
248
+ }
249
+
250
+ main()