theslopmachine 0.3.2 → 0.3.4

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/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # theslopmachine 0.3
1
+ # theslopmachine
2
2
 
3
3
  Installer package for the theslopmachine workflow owner, developer agent, required skills, templates, and local support files.
4
4
 
5
- ## Planned commands
5
+ ## Commands
6
6
 
7
- - `slopmachine setup`
8
- - `slopmachine init`
7
+ - `slopmachine setup` - configures agents, files and scripts
8
+ - `slopmachine init` - sets up a project
9
9
  - `slopmachine init -o` to bootstrap the project and immediately open OpenCode inside `repo/`
10
10
 
11
11
  See `MANUAL.md` for a short usage guide and workflow summary.
@@ -17,7 +17,3 @@ See `MANUAL.md` for a short usage guide and workflow summary.
17
17
  - `assets/slopmachine/`
18
18
  - `bin/`
19
19
  - `src/`
20
-
21
- ## Status
22
-
23
- This package workspace is being built from the current local theslopmachine setup without modifying the live installation on this machine.
@@ -6,6 +6,7 @@ import { spawn } from 'node:child_process'
6
6
 
7
7
  const targetInput = process.argv[2] || '.'
8
8
  const target = path.resolve(process.cwd(), targetInput)
9
+ const bdCommand = process.env.BD_COMMAND || 'bd'
9
10
 
10
11
  function log(message) {
11
12
  console.log(`[beads-init] ${message}`)
@@ -57,11 +58,11 @@ async function pathExists(targetPath) {
57
58
  }
58
59
 
59
60
  async function runBd(args, options = {}) {
60
- return run('bd', args, { cwd: target, ...options })
61
+ return run(bdCommand, args, { cwd: target, ...options })
61
62
  }
62
63
 
63
64
  async function runBdNoninteractive(args) {
64
- return run('bd', args, {
65
+ return run(bdCommand, args, {
65
66
  cwd: target,
66
67
  env: { ...process.env, CI: '1' },
67
68
  input: '',
@@ -69,13 +70,13 @@ async function runBdNoninteractive(args) {
69
70
  }
70
71
 
71
72
  async function supportsInitFlag(flag) {
72
- const help = await run('bd', ['init', '--help'])
73
+ const help = await run(bdCommand, ['init', '--help'])
73
74
  return `${help.stdout}${help.stderr}`.includes(flag)
74
75
  }
75
76
 
76
77
  async function chooseNoninteractiveInitModeFlag(isGitRepo) {
77
78
  if (!isGitRepo) return null
78
- const help = await run('bd', ['init', '--help'])
79
+ const help = await run(bdCommand, ['init', '--help'])
79
80
  const text = `${help.stdout}${help.stderr}`
80
81
  if (text.includes('--setup-exclude')) return '--setup-exclude'
81
82
  if (text.includes('--stealth')) return '--stealth'
@@ -289,8 +290,9 @@ async function installGitHooksNonfatal() {
289
290
  }
290
291
 
291
292
  async function main() {
292
- if (!(await commandExists('bd'))) {
293
- die("'bd' is not installed or not in PATH. Install Beads first.")
293
+ const bdAvailable = bdCommand !== 'bd' ? await pathExists(bdCommand) : await commandExists('bd')
294
+ if (!bdAvailable) {
295
+ die(`'${bdCommand}' is not available. Install Beads first.`)
294
296
  }
295
297
 
296
298
  if (!(await pathExists(target))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theslopmachine",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "SlopMachine installer and project bootstrap CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/init.js CHANGED
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
 
4
4
  import { buildPaths } from './constants.js'
5
- import { commandExists, ensureDir, log, pathExists, runCommand, warn } from './utils.js'
5
+ import { ensureDir, log, pathExists, resolveCommand, runCommand, warn } from './utils.js'
6
6
 
7
7
  const GITIGNORE_ENTRIES = [
8
8
  '.DS_Store',
@@ -21,6 +21,21 @@ const GITIGNORE_ENTRIES = [
21
21
  'antigravity-logs/',
22
22
  ]
23
23
 
24
+ function getWindowsBeadsCommandCandidates() {
25
+ if (process.platform !== 'win32') {
26
+ return []
27
+ }
28
+
29
+ return [
30
+ process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Programs', 'bd', 'bd.exe') : null,
31
+ process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'go', 'bin', 'bd.exe') : null,
32
+ ].filter(Boolean)
33
+ }
34
+
35
+ async function resolveBeadsCommand() {
36
+ return resolveCommand('bd', { additionalCandidates: getWindowsBeadsCommandCandidates() })
37
+ }
38
+
24
39
  function parseInitArgs(args) {
25
40
  let openAfterInit = false
26
41
  let targetInput = '.'
@@ -104,11 +119,14 @@ async function runBeadsBootstrap(paths, targetPath, beadsScript) {
104
119
  throw new Error('Unable to locate the current Node.js executable for Beads bootstrap.')
105
120
  }
106
121
 
122
+ const beadsCommand = await resolveBeadsCommand()
123
+
107
124
  log('Running Beads setup')
108
125
  const result = await runCommand(nodeExecutable, [beadsScript, targetPath], {
109
126
  stdio: 'inherit',
110
127
  env: {
111
128
  ...process.env,
129
+ BD_COMMAND: beadsCommand || '',
112
130
  HOME: paths.home,
113
131
  },
114
132
  })
@@ -145,18 +163,19 @@ async function maybeOpenOpencode(targetPath, openAfterInit) {
145
163
  return
146
164
  }
147
165
 
148
- if (!(await commandExists('opencode'))) {
166
+ const opencodeCommand = await resolveCommand('opencode')
167
+ if (!opencodeCommand) {
149
168
  warn('OpenCode is not available in PATH, so the project was initialized but could not be opened automatically. Launch OpenCode manually inside repo/.')
150
169
  return
151
170
  }
152
171
 
153
172
  log('Opening OpenCode in repo/')
154
- const result = await runCommand('opencode', [], {
173
+ const result = await runCommand(opencodeCommand, [], {
155
174
  stdio: 'inherit',
156
175
  cwd: path.join(targetPath, 'repo'),
157
176
  })
158
177
  if (result.code !== 0) {
159
- throw new Error('Failed to launch OpenCode')
178
+ warn(`Failed to launch OpenCode automatically (${result.stderr || `exit code ${result.code}`}). Launch it manually inside repo/.`)
160
179
  }
161
180
  }
162
181
 
package/src/install.js CHANGED
@@ -16,12 +16,15 @@ import {
16
16
  copyDirIfMissing,
17
17
  copyFileIfMissing,
18
18
  ensureDir,
19
+ getGlobalNpmPackageVersion,
19
20
  log,
20
21
  makeExecutableIfShellScript,
21
22
  pathExists,
23
+ prependToProcessPath,
22
24
  promptText,
23
25
  promptYesNo,
24
26
  readJsonIfExists,
27
+ resolveCommand,
25
28
  runCommand,
26
29
  warn,
27
30
  writeJson,
@@ -31,6 +34,107 @@ function assetsRoot() {
31
34
  return path.join(PACKAGE_ROOT, 'assets')
32
35
  }
33
36
 
37
+ function getWindowsBeadsBinDirs() {
38
+ if (process.platform !== 'win32') {
39
+ return []
40
+ }
41
+
42
+ const dirs = [
43
+ process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Programs', 'bd') : null,
44
+ process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'go', 'bin') : null,
45
+ ].filter(Boolean)
46
+
47
+ return [...new Set(dirs)]
48
+ }
49
+
50
+ function getWindowsBeadsCommandCandidates() {
51
+ return getWindowsBeadsBinDirs().map((dir) => path.join(dir, 'bd.exe'))
52
+ }
53
+
54
+ async function resolveBeadsCommand() {
55
+ return resolveCommand('bd', { additionalCandidates: getWindowsBeadsCommandCandidates() })
56
+ }
57
+
58
+ async function getBeadsVersion() {
59
+ const command = await resolveBeadsCommand()
60
+ if (!command) {
61
+ return null
62
+ }
63
+
64
+ const result = await runCommand(command, ['version'])
65
+ if (result.code !== 0) {
66
+ return null
67
+ }
68
+
69
+ return { command, version: (result.stdout || result.stderr).trim() }
70
+ }
71
+
72
+ function quotePowerShell(value) {
73
+ return `'${String(value).replaceAll(`'`, `''`)}'`
74
+ }
75
+
76
+ async function updateWindowsUserPath(entry) {
77
+ const shell = await resolveCommand('pwsh') || await resolveCommand('powershell')
78
+ if (!shell) {
79
+ warn(`Unable to persist ${entry} into the Windows user PATH automatically.`)
80
+ return false
81
+ }
82
+
83
+ const quotedEntry = quotePowerShell(entry)
84
+ const script = [
85
+ `$entry = ${quotedEntry}`,
86
+ `$existing = [Environment]::GetEnvironmentVariable('Path', 'User')`,
87
+ `$parts = @()`,
88
+ `if ($existing) { $parts = $existing -split ';' | ForEach-Object { $_.Trim() } | Where-Object { $_ } }`,
89
+ `if ($parts -notcontains $entry) {`,
90
+ ` $newValue = if ($existing -and $existing.Trim()) { "$existing;$entry" } else { $entry }`,
91
+ ` [Environment]::SetEnvironmentVariable('Path', $newValue, 'User')`,
92
+ `}`,
93
+ ].join('; ')
94
+
95
+ const result = await runCommand(shell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script])
96
+ if (result.code !== 0) {
97
+ warn(`Unable to persist ${entry} into the Windows user PATH automatically.`)
98
+ return false
99
+ }
100
+
101
+ return true
102
+ }
103
+
104
+ async function ensureWindowsBeadsPath() {
105
+ if (process.platform !== 'win32') {
106
+ return
107
+ }
108
+
109
+ for (const dir of getWindowsBeadsBinDirs()) {
110
+ if (!(await pathExists(dir))) {
111
+ continue
112
+ }
113
+
114
+ prependToProcessPath(dir)
115
+ await updateWindowsUserPath(dir)
116
+ }
117
+ }
118
+
119
+ async function installBeadsOnWindows() {
120
+ const shell = await resolveCommand('pwsh') || await resolveCommand('powershell')
121
+ if (!shell) {
122
+ return { code: 1, stdout: '', stderr: 'PowerShell is required to install Beads on Windows' }
123
+ }
124
+
125
+ log('Installing Beads with the upstream Windows PowerShell installer')
126
+ const result = await runCommand(shell, [
127
+ '-NoProfile',
128
+ '-ExecutionPolicy',
129
+ 'Bypass',
130
+ '-Command',
131
+ 'irm https://raw.githubusercontent.com/steveyegge/beads/main/install.ps1 | iex',
132
+ ], { stdio: 'inherit' })
133
+
134
+ await ensureWindowsBeadsPath()
135
+ return result
136
+ }
137
+
34
138
  async function getCommandVersion(command, args = ['--version']) {
35
139
  const exists = await commandExists(command)
36
140
  if (!exists) return null
@@ -72,10 +176,14 @@ async function detectPackageManagers() {
72
176
 
73
177
  async function tryInstallCoreDependency(name) {
74
178
  if (name === 'opencode') {
75
- log(`Installing opencode-ai@${OPCODE_VERSION} globally via npm`)
76
- return runCommand('npm', ['install', '-g', `opencode-ai@${OPCODE_VERSION}`], { stdio: 'inherit' })
179
+ log('Installing opencode-ai@latest globally via npm')
180
+ return runCommand('npm', ['install', '-g', 'opencode-ai@latest'], { stdio: 'inherit' })
77
181
  }
78
182
  if (name === 'bd') {
183
+ if (process.platform === 'win32') {
184
+ return installBeadsOnWindows()
185
+ }
186
+
79
187
  log(`Installing @beads/bd@${BEADS_VERSION} globally via npm`)
80
188
  return runCommand('npm', ['install', '-g', `@beads/bd@${BEADS_VERSION}`], { stdio: 'inherit' })
81
189
  }
@@ -134,6 +242,35 @@ async function ensureDependency({ name, checkCommand, requiredVersion, installab
134
242
  }
135
243
  }
136
244
 
245
+ if (name === 'opencode') {
246
+ const npmVersion = await getGlobalNpmPackageVersion('opencode-ai')
247
+ if (npmVersion) {
248
+ log(`opencode detected via global npm package: opencode-ai@${npmVersion}`)
249
+ if (requiredVersion && !npmVersion.includes(requiredVersion)) {
250
+ warn(`${name} version differs from tested reference ${requiredVersion}`)
251
+ }
252
+ return
253
+ }
254
+
255
+ const commandPath = await resolveCommand('opencode')
256
+ if (commandPath) {
257
+ log(`opencode command detected at ${commandPath}`)
258
+ warn(`Unable to verify ${name} version without executing the binary during setup.`)
259
+ return
260
+ }
261
+ }
262
+
263
+ if (checkCommand === 'bd') {
264
+ const beads = await getBeadsVersion()
265
+ if (beads) {
266
+ log(`${name} detected via ${beads.command}: ${beads.version}`)
267
+ if (requiredVersion && !beads.version.includes(requiredVersion)) {
268
+ warn(`${name} version differs from tested reference ${requiredVersion}`)
269
+ }
270
+ return
271
+ }
272
+ }
273
+
137
274
  const version = await getCommandVersion(checkCommand)
138
275
  if (version) {
139
276
  log(`${name} detected: ${version}`)
@@ -158,6 +295,34 @@ async function ensureDependency({ name, checkCommand, requiredVersion, installab
158
295
  const result = await tryInstallCoreDependency(checkCommand)
159
296
  if (result.code !== 0) {
160
297
  warn(`Automatic installation for ${name} failed. Please install it manually.`)
298
+ return
299
+ }
300
+
301
+ if (name === 'opencode') {
302
+ const installedVersion = await getGlobalNpmPackageVersion('opencode-ai')
303
+ if (installedVersion) {
304
+ log(`Installed opencode-ai@${installedVersion}`)
305
+ const commandPath = await resolveCommand('opencode')
306
+ if (!commandPath) {
307
+ warn('opencode-ai was installed, but the `opencode` command is not visible in this shell PATH yet. Open a new terminal before using `slopmachine init -o`.')
308
+ }
309
+ return
310
+ }
311
+
312
+ warn('opencode-ai install completed, but the package could not be verified. Open a new terminal and run `opencode --version` before using `slopmachine init -o`.')
313
+ }
314
+
315
+ if (checkCommand === 'bd') {
316
+ const beads = await getBeadsVersion()
317
+ if (beads) {
318
+ log(`Installed ${name} via ${beads.command}: ${beads.version}`)
319
+ if (process.platform === 'win32' && !(await commandExists('bd'))) {
320
+ warn('Beads was installed, but `bd` is not visible in this shell PATH yet. The installer added the common Windows Beads directories to PATH for future shells.')
321
+ }
322
+ return
323
+ }
324
+
325
+ warn('Beads installation completed, but the `bd` command could not be verified. Re-open PowerShell or Command Prompt and run `bd version`.')
161
326
  }
162
327
  }
163
328
 
package/src/utils.js CHANGED
@@ -76,6 +76,7 @@ export async function backupFile(filePath) {
76
76
 
77
77
  export async function runCommand(command, args, options = {}) {
78
78
  return new Promise((resolve, reject) => {
79
+ let settled = false
79
80
  const child = spawn(command, args, {
80
81
  stdio: options.stdio || 'pipe',
81
82
  cwd: options.cwd,
@@ -98,8 +99,24 @@ export async function runCommand(command, args, options = {}) {
98
99
  })
99
100
  }
100
101
 
101
- child.on('error', reject)
102
+ child.on('error', (error) => {
103
+ if (settled) {
104
+ return
105
+ }
106
+ settled = true
107
+
108
+ if (error && error.code === 'ENOENT') {
109
+ resolve({ code: 127, stdout: stdoutText, stderr: error.message })
110
+ return
111
+ }
112
+
113
+ reject(error)
114
+ })
102
115
  child.on('close', (code) => {
116
+ if (settled) {
117
+ return
118
+ }
119
+ settled = true
103
120
  resolve({ code: code ?? 1, stdout: stdoutText, stderr: stderrText })
104
121
  })
105
122
  })
@@ -111,6 +128,80 @@ export async function commandExists(command) {
111
128
  return result.code === 0
112
129
  }
113
130
 
131
+ export async function getGlobalNpmPackageVersion(packageName) {
132
+ const result = await runCommand('npm', ['list', '-g', packageName, '--depth=0', '--json'])
133
+ const raw = result.stdout.trim()
134
+
135
+ if (!raw) {
136
+ return null
137
+ }
138
+
139
+ try {
140
+ const data = JSON.parse(raw)
141
+ return data.dependencies?.[packageName]?.version || null
142
+ } catch {
143
+ return null
144
+ }
145
+ }
146
+
147
+ async function getGlobalNpmPrefix() {
148
+ const result = await runCommand('npm', ['prefix', '-g'])
149
+ if (result.code !== 0) {
150
+ return null
151
+ }
152
+
153
+ const prefix = result.stdout.trim()
154
+ return prefix || null
155
+ }
156
+
157
+ export async function resolveCommand(command, options = {}) {
158
+ if (await commandExists(command)) {
159
+ return command
160
+ }
161
+
162
+ const prefix = await getGlobalNpmPrefix()
163
+ const candidates = []
164
+
165
+ if (prefix) {
166
+ candidates.push(...(process.platform === 'win32'
167
+ ? [
168
+ path.join(prefix, `${command}.cmd`),
169
+ path.join(prefix, `${command}.exe`),
170
+ path.join(prefix, command),
171
+ ]
172
+ : [
173
+ path.join(prefix, 'bin', command),
174
+ path.join(prefix, command),
175
+ ]))
176
+ }
177
+
178
+ if (Array.isArray(options.additionalCandidates)) {
179
+ candidates.push(...options.additionalCandidates)
180
+ }
181
+
182
+ for (const candidate of candidates) {
183
+ if (await pathExists(candidate)) {
184
+ return candidate
185
+ }
186
+ }
187
+
188
+ return null
189
+ }
190
+
191
+ export function prependToProcessPath(entry) {
192
+ if (!entry) {
193
+ return
194
+ }
195
+
196
+ const currentPath = process.env.PATH || ''
197
+ const entries = currentPath.split(path.delimiter).filter(Boolean)
198
+ if (entries.includes(entry)) {
199
+ return
200
+ }
201
+
202
+ process.env.PATH = currentPath ? `${entry}${path.delimiter}${currentPath}` : entry
203
+ }
204
+
114
205
  export async function findBashExecutable() {
115
206
  if (await commandExists('bash')) {
116
207
  return 'bash'