theslopmachine 0.3.2 → 0.3.3

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
@@ -2,10 +2,10 @@
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theslopmachine",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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',
@@ -145,18 +145,19 @@ async function maybeOpenOpencode(targetPath, openAfterInit) {
145
145
  return
146
146
  }
147
147
 
148
- if (!(await commandExists('opencode'))) {
148
+ const opencodeCommand = await resolveCommand('opencode')
149
+ if (!opencodeCommand) {
149
150
  warn('OpenCode is not available in PATH, so the project was initialized but could not be opened automatically. Launch OpenCode manually inside repo/.')
150
151
  return
151
152
  }
152
153
 
153
154
  log('Opening OpenCode in repo/')
154
- const result = await runCommand('opencode', [], {
155
+ const result = await runCommand(opencodeCommand, [], {
155
156
  stdio: 'inherit',
156
157
  cwd: path.join(targetPath, 'repo'),
157
158
  })
158
159
  if (result.code !== 0) {
159
- throw new Error('Failed to launch OpenCode')
160
+ warn(`Failed to launch OpenCode automatically (${result.stderr || `exit code ${result.code}`}). Launch it manually inside repo/.`)
160
161
  }
161
162
  }
162
163
 
package/src/install.js CHANGED
@@ -16,12 +16,14 @@ import {
16
16
  copyDirIfMissing,
17
17
  copyFileIfMissing,
18
18
  ensureDir,
19
+ getGlobalNpmPackageVersion,
19
20
  log,
20
21
  makeExecutableIfShellScript,
21
22
  pathExists,
22
23
  promptText,
23
24
  promptYesNo,
24
25
  readJsonIfExists,
26
+ resolveCommand,
25
27
  runCommand,
26
28
  warn,
27
29
  writeJson,
@@ -72,8 +74,8 @@ async function detectPackageManagers() {
72
74
 
73
75
  async function tryInstallCoreDependency(name) {
74
76
  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' })
77
+ log('Installing opencode-ai@latest globally via npm')
78
+ return runCommand('npm', ['install', '-g', 'opencode-ai@latest'], { stdio: 'inherit' })
77
79
  }
78
80
  if (name === 'bd') {
79
81
  log(`Installing @beads/bd@${BEADS_VERSION} globally via npm`)
@@ -134,6 +136,24 @@ async function ensureDependency({ name, checkCommand, requiredVersion, installab
134
136
  }
135
137
  }
136
138
 
139
+ if (name === 'opencode') {
140
+ const npmVersion = await getGlobalNpmPackageVersion('opencode-ai')
141
+ if (npmVersion) {
142
+ log(`opencode detected via global npm package: opencode-ai@${npmVersion}`)
143
+ if (requiredVersion && !npmVersion.includes(requiredVersion)) {
144
+ warn(`${name} version differs from tested reference ${requiredVersion}`)
145
+ }
146
+ return
147
+ }
148
+
149
+ const commandPath = await resolveCommand('opencode')
150
+ if (commandPath) {
151
+ log(`opencode command detected at ${commandPath}`)
152
+ warn(`Unable to verify ${name} version without executing the binary during setup.`)
153
+ return
154
+ }
155
+ }
156
+
137
157
  const version = await getCommandVersion(checkCommand)
138
158
  if (version) {
139
159
  log(`${name} detected: ${version}`)
@@ -158,6 +178,21 @@ async function ensureDependency({ name, checkCommand, requiredVersion, installab
158
178
  const result = await tryInstallCoreDependency(checkCommand)
159
179
  if (result.code !== 0) {
160
180
  warn(`Automatic installation for ${name} failed. Please install it manually.`)
181
+ return
182
+ }
183
+
184
+ if (name === 'opencode') {
185
+ const installedVersion = await getGlobalNpmPackageVersion('opencode-ai')
186
+ if (installedVersion) {
187
+ log(`Installed opencode-ai@${installedVersion}`)
188
+ const commandPath = await resolveCommand('opencode')
189
+ if (!commandPath) {
190
+ 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`.')
191
+ }
192
+ return
193
+ }
194
+
195
+ 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`.')
161
196
  }
162
197
  }
163
198
 
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,62 @@ 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) {
158
+ if (await commandExists(command)) {
159
+ return command
160
+ }
161
+
162
+ const prefix = await getGlobalNpmPrefix()
163
+ if (!prefix) {
164
+ return null
165
+ }
166
+
167
+ const candidates = process.platform === 'win32'
168
+ ? [
169
+ path.join(prefix, `${command}.cmd`),
170
+ path.join(prefix, `${command}.exe`),
171
+ path.join(prefix, command),
172
+ ]
173
+ : [
174
+ path.join(prefix, 'bin', command),
175
+ path.join(prefix, command),
176
+ ]
177
+
178
+ for (const candidate of candidates) {
179
+ if (await pathExists(candidate)) {
180
+ return candidate
181
+ }
182
+ }
183
+
184
+ return null
185
+ }
186
+
114
187
  export async function findBashExecutable() {
115
188
  if (await commandExists('bash')) {
116
189
  return 'bash'