amorphous-cli 0.1.5 → 0.1.6
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 +1 -1
- package/index.mjs +5 -0
- package/lib/agent-contract.mjs +6 -2
- package/lib/agent-contract.test.mjs +26 -3
- package/lib/update-check.mjs +188 -0
- package/lib/update-check.test.mjs +270 -0
- package/lib/version.mjs +3 -0
- package/mcp.mjs +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/index.mjs
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
import { loadDotEnv } from './lib/config.mjs'
|
|
3
3
|
import { runCommand } from './lib/commands.mjs'
|
|
4
4
|
import { ensureLocalHttpsTrust } from './lib/tls.mjs'
|
|
5
|
+
import { startUpdateCheck } from './lib/update-check.mjs'
|
|
5
6
|
|
|
6
7
|
ensureLocalHttpsTrust()
|
|
7
8
|
loadDotEnv()
|
|
8
9
|
|
|
10
|
+
const update = startUpdateCheck({ argv: process.argv.slice(2) })
|
|
11
|
+
|
|
9
12
|
try {
|
|
10
13
|
await runCommand(process.argv.slice(2))
|
|
14
|
+
await update.finish()
|
|
11
15
|
} catch (e) {
|
|
12
16
|
console.error(e instanceof Error ? e.message : e)
|
|
17
|
+
await update.finish()
|
|
13
18
|
process.exit(1)
|
|
14
19
|
}
|
package/lib/agent-contract.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { dirname, join } from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { homedir } from 'node:os'
|
|
6
|
+
import { CLI_VERSION } from './version.mjs'
|
|
6
7
|
|
|
7
|
-
export const CONTRACT_VERSION =
|
|
8
|
+
export const CONTRACT_VERSION = 2
|
|
8
9
|
|
|
9
10
|
const skillRoot = join(dirname(fileURLToPath(import.meta.url)), '../skill/amorphous')
|
|
10
11
|
|
|
@@ -75,9 +76,12 @@ export function userSkillDirs(home = homedir()) {
|
|
|
75
76
|
|
|
76
77
|
export function installUserSkill(home = homedir()) {
|
|
77
78
|
const dirs = userSkillDirs(home)
|
|
79
|
+
const stamp = `${CLI_VERSION}\n`
|
|
78
80
|
for (const dest of dirs) {
|
|
81
|
+
rmSync(dest, { recursive: true, force: true })
|
|
79
82
|
mkdirSync(dest, { recursive: true })
|
|
80
83
|
cpSync(skillRoot, dest, { recursive: true })
|
|
84
|
+
writeFileSync(join(dest, '.amorphous-version'), stamp)
|
|
81
85
|
}
|
|
82
86
|
return dirs
|
|
83
87
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import test from 'node:test'
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
syncProjectContract,
|
|
11
11
|
writeStarter,
|
|
12
12
|
} from './agent-contract.mjs'
|
|
13
|
+
import { CLI_VERSION } from './version.mjs'
|
|
13
14
|
|
|
14
15
|
test('setup copies the skill, and an edited contract is left alone', () => {
|
|
15
16
|
const home = mkdtempSync(join(tmpdir(), 'amorphous-skill-'))
|
|
@@ -17,6 +18,12 @@ test('setup copies the skill, and an edited contract is left alone', () => {
|
|
|
17
18
|
assert.equal(dirs.length, 2)
|
|
18
19
|
assert.match(readFileSync(join(dirs[0], 'SKILL.md'), 'utf8'), /build abstracts/)
|
|
19
20
|
assert.match(readFileSync(join(dirs[1], 'assets/app.js'), 'utf8'), /amorphous\.me/)
|
|
21
|
+
assert.equal(readFileSync(join(dirs[0], '.amorphous-version'), 'utf8'), `${CLI_VERSION}\n`)
|
|
22
|
+
assert.equal(readFileSync(join(dirs[1], '.amorphous-version'), 'utf8'), `${CLI_VERSION}\n`)
|
|
23
|
+
|
|
24
|
+
writeFileSync(join(dirs[1], 'stray.txt'), 'gone')
|
|
25
|
+
installUserSkill(home)
|
|
26
|
+
assert.equal(existsSync(join(dirs[1], 'stray.txt')), false)
|
|
20
27
|
|
|
21
28
|
const project = mkdtempSync(join(tmpdir(), 'amorphous-page-'))
|
|
22
29
|
assert.equal(syncProjectContract(project), 'wrote')
|
|
@@ -29,7 +36,23 @@ test('setup copies the skill, and an edited contract is left alone', () => {
|
|
|
29
36
|
assert.equal(syncProjectContract(project), 'kept')
|
|
30
37
|
assert.match(readFileSync(join(project, 'AGENTS.md'), 'utf8'), /Edited\./)
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
const older = projectContract().replace(
|
|
40
|
+
`amorphous-contract: ${CONTRACT_VERSION}`,
|
|
41
|
+
'amorphous-contract: 0',
|
|
42
|
+
)
|
|
43
|
+
writeFileSync(join(project, 'AGENTS.md'), older)
|
|
33
44
|
assert.equal(syncProjectContract(project), 'refreshed')
|
|
34
|
-
assert.match(
|
|
45
|
+
assert.match(
|
|
46
|
+
readFileSync(join(project, 'AGENTS.md'), 'utf8'),
|
|
47
|
+
new RegExp(`amorphous-contract: ${CONTRACT_VERSION}`),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
writeFileSync(
|
|
51
|
+
join(project, 'AGENTS.md'),
|
|
52
|
+
projectContract().replace(
|
|
53
|
+
`amorphous-contract: ${CONTRACT_VERSION}`,
|
|
54
|
+
`amorphous-contract: ${CONTRACT_VERSION + 1}`,
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
assert.equal(syncProjectContract(project), 'kept')
|
|
35
58
|
})
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { randomBytes } from 'node:crypto'
|
|
5
|
+
import { ALIASES } from './help.mjs'
|
|
6
|
+
import { userSkillDirs } from './agent-contract.mjs'
|
|
7
|
+
import { CLI_VERSION } from './version.mjs'
|
|
8
|
+
|
|
9
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
|
10
|
+
const NUDGE = 'Run: npx amorphous-cli@latest setup\n'
|
|
11
|
+
|
|
12
|
+
function parseVersion(v) {
|
|
13
|
+
if (typeof v !== 'string') return null
|
|
14
|
+
const m = v.match(/^(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+.*)?$/)
|
|
15
|
+
if (!m) return null
|
|
16
|
+
return {
|
|
17
|
+
major: Number(m[1]),
|
|
18
|
+
minor: Number(m[2]),
|
|
19
|
+
patch: Number(m[3]),
|
|
20
|
+
prerelease: m[4] ?? null,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function compareVersions(a, b) {
|
|
25
|
+
const va = parseVersion(a)
|
|
26
|
+
const vb = parseVersion(b)
|
|
27
|
+
if (!va || !vb) return null
|
|
28
|
+
if (va.major !== vb.major) return va.major < vb.major ? -1 : 1
|
|
29
|
+
if (va.minor !== vb.minor) return va.minor < vb.minor ? -1 : 1
|
|
30
|
+
if (va.patch !== vb.patch) return va.patch < vb.patch ? -1 : 1
|
|
31
|
+
if (va.prerelease && vb.prerelease) return 0
|
|
32
|
+
if (va.prerelease && !vb.prerelease) return -1
|
|
33
|
+
if (!va.prerelease && vb.prerelease) return 1
|
|
34
|
+
return 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveCommand(argv) {
|
|
38
|
+
const raw = argv[0]
|
|
39
|
+
if (!raw) return ''
|
|
40
|
+
return ALIASES[raw] || raw
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function shouldCheck(argv, env = process.env) {
|
|
44
|
+
if ('CI' in env) return false
|
|
45
|
+
if (env.AMORPHOUS_NO_UPDATE_CHECK === '1') return false
|
|
46
|
+
if (!argv.length) return false
|
|
47
|
+
if (argv[0] === 'help') return false
|
|
48
|
+
if (argv.includes('-h') || argv.includes('--help')) return false
|
|
49
|
+
if (resolveCommand(argv) === 'mcp') return false
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function existsSkill(path) {
|
|
54
|
+
try {
|
|
55
|
+
readFileSync(path)
|
|
56
|
+
return true
|
|
57
|
+
} catch {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function skillStale(home, version) {
|
|
63
|
+
const dirs = userSkillDirs(home)
|
|
64
|
+
for (const dir of dirs) {
|
|
65
|
+
const skillPath = join(dir, 'SKILL.md')
|
|
66
|
+
if (!existsSkill(skillPath)) continue
|
|
67
|
+
const stampPath = join(dir, '.amorphous-version')
|
|
68
|
+
let stamp
|
|
69
|
+
try {
|
|
70
|
+
stamp = readFileSync(stampPath, 'utf8').trim()
|
|
71
|
+
} catch {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
const cmp = compareVersions(stamp, version)
|
|
75
|
+
if (cmp === null) continue
|
|
76
|
+
if (cmp < 0) return true
|
|
77
|
+
}
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cachePath(home) {
|
|
82
|
+
return join(home, '.config', 'amorphous', 'update-check.json')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readCache(home) {
|
|
86
|
+
try {
|
|
87
|
+
const data = JSON.parse(readFileSync(cachePath(home), 'utf8'))
|
|
88
|
+
if (!data?.checkedAt) return null
|
|
89
|
+
return data
|
|
90
|
+
} catch {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function cacheFresh(cache, now) {
|
|
96
|
+
if (!cache?.checkedAt) return false
|
|
97
|
+
const t = Date.parse(cache.checkedAt)
|
|
98
|
+
if (Number.isNaN(t)) return false
|
|
99
|
+
return now() - t < CACHE_TTL_MS
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function writeCache(home, data) {
|
|
103
|
+
const dir = join(home, '.config', 'amorphous')
|
|
104
|
+
mkdirSync(dir, { recursive: true })
|
|
105
|
+
const dest = cachePath(home)
|
|
106
|
+
const tmp = join(dir, `.update-check.${process.pid}.${randomBytes(4).toString('hex')}.tmp`)
|
|
107
|
+
writeFileSync(tmp, JSON.stringify(data) + '\n')
|
|
108
|
+
renameSync(tmp, dest)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function defaultRegistry(env) {
|
|
112
|
+
const raw = env.npm_config_registry || 'https://registry.npmjs.org'
|
|
113
|
+
return raw.replace(/\/$/, '')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function defaultFetchLatest(env = process.env) {
|
|
117
|
+
const registry = defaultRegistry(env)
|
|
118
|
+
return fetch(`${registry}/amorphous-cli/latest`, {
|
|
119
|
+
signal: AbortSignal.timeout(1500),
|
|
120
|
+
}).then(async (res) => {
|
|
121
|
+
if (!res.ok) throw new Error('registry')
|
|
122
|
+
const body = await res.json()
|
|
123
|
+
if (typeof body?.version !== 'string') throw new Error('registry')
|
|
124
|
+
return body.version
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function needsNudge({ cmd, version, latest, home }) {
|
|
129
|
+
if (latest && compareVersions(version, latest) === -1) return true
|
|
130
|
+
if (cmd !== 'setup' && skillStale(home, version)) return true
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function startUpdateCheck({
|
|
135
|
+
argv,
|
|
136
|
+
env = process.env,
|
|
137
|
+
home = homedir(),
|
|
138
|
+
version = CLI_VERSION,
|
|
139
|
+
now = () => Date.now(),
|
|
140
|
+
fetchLatest = () => defaultFetchLatest(env),
|
|
141
|
+
stderr = process.stderr,
|
|
142
|
+
} = {}) {
|
|
143
|
+
let printed = false
|
|
144
|
+
let fetchPromise = null
|
|
145
|
+
let cache = null
|
|
146
|
+
let stale = false
|
|
147
|
+
|
|
148
|
+
if (shouldCheck(argv, env)) {
|
|
149
|
+
cache = readCache(home)
|
|
150
|
+
stale = !cacheFresh(cache, now)
|
|
151
|
+
if (stale) {
|
|
152
|
+
fetchPromise = fetchLatest().catch(() => null)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
async finish() {
|
|
158
|
+
try {
|
|
159
|
+
if (!shouldCheck(argv, env)) return
|
|
160
|
+
const cmd = resolveCommand(argv)
|
|
161
|
+
let latest = typeof cache?.latest === 'string' ? cache.latest : null
|
|
162
|
+
|
|
163
|
+
if (stale) {
|
|
164
|
+
const fetched = fetchPromise ? await fetchPromise : null
|
|
165
|
+
const next = { checkedAt: new Date(now()).toISOString() }
|
|
166
|
+
if (fetched) {
|
|
167
|
+
next.latest = fetched
|
|
168
|
+
latest = fetched
|
|
169
|
+
} else if (latest) {
|
|
170
|
+
next.latest = latest
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
writeCache(home, next)
|
|
174
|
+
} catch {
|
|
175
|
+
/* a read-only home must not change the command's exit code */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!printed && needsNudge({ cmd, version, latest, home })) {
|
|
180
|
+
printed = true
|
|
181
|
+
stderr.write(NUDGE)
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
/* a failed lookup must not change the command's exit code */
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { mkdtempSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import test from 'node:test'
|
|
8
|
+
import { userSkillDirs } from './agent-contract.mjs'
|
|
9
|
+
import { CLI_VERSION } from './version.mjs'
|
|
10
|
+
import {
|
|
11
|
+
compareVersions,
|
|
12
|
+
shouldCheck,
|
|
13
|
+
skillStale,
|
|
14
|
+
startUpdateCheck,
|
|
15
|
+
} from './update-check.mjs'
|
|
16
|
+
|
|
17
|
+
const NUDGE = 'Run: npx amorphous-cli@latest setup\n'
|
|
18
|
+
|
|
19
|
+
test('compareVersions', () => {
|
|
20
|
+
assert.equal(compareVersions('1.0.0', '1.0.0'), 0)
|
|
21
|
+
assert.equal(compareVersions('1.0.0', '1.0.1'), -1)
|
|
22
|
+
assert.equal(compareVersions('1.1.0', '1.0.9'), 1)
|
|
23
|
+
assert.equal(compareVersions('2.0.0', '1.9.9'), 1)
|
|
24
|
+
assert.equal(compareVersions('1.0.0-beta', '1.0.0'), -1)
|
|
25
|
+
assert.equal(compareVersions('1.0.0-beta', '1.0.0-rc'), 0)
|
|
26
|
+
assert.equal(compareVersions('nope', '1.0.0'), null)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
function cacheFile(home) {
|
|
30
|
+
return join(home, '.config', 'amorphous', 'update-check.json')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeCache(home, data) {
|
|
34
|
+
const dir = join(home, '.config', 'amorphous')
|
|
35
|
+
mkdirSync(dir, { recursive: true })
|
|
36
|
+
writeFileSync(cacheFile(home), JSON.stringify(data) + '\n')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('fresh cache nudges when cached latest is newer', async () => {
|
|
40
|
+
const home = mkdtempSync(join(tmpdir(), 'amorphous-uc-'))
|
|
41
|
+
const now = () => Date.parse('2026-01-01T12:00:00.000Z')
|
|
42
|
+
writeCache(home, { checkedAt: '2026-01-01T10:00:00.000Z', latest: '9.9.9' })
|
|
43
|
+
let fetchCalls = 0
|
|
44
|
+
let err = ''
|
|
45
|
+
const update = startUpdateCheck({
|
|
46
|
+
argv: ['list'],
|
|
47
|
+
env: {},
|
|
48
|
+
home,
|
|
49
|
+
version: '0.1.0',
|
|
50
|
+
now,
|
|
51
|
+
fetchLatest: async () => {
|
|
52
|
+
fetchCalls++
|
|
53
|
+
return '9.9.9'
|
|
54
|
+
},
|
|
55
|
+
stderr: { write: (s) => {
|
|
56
|
+
err += s
|
|
57
|
+
} },
|
|
58
|
+
})
|
|
59
|
+
await update.finish()
|
|
60
|
+
assert.equal(fetchCalls, 0)
|
|
61
|
+
assert.equal(err, NUDGE)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('stale cache fetches and rewrites cache', async () => {
|
|
65
|
+
const home = mkdtempSync(join(tmpdir(), 'amorphous-uc-'))
|
|
66
|
+
const t0 = Date.parse('2026-01-01T00:00:00.000Z')
|
|
67
|
+
let nowMs = t0
|
|
68
|
+
writeCache(home, { checkedAt: '2025-01-01T00:00:00.000Z', latest: '0.1.0' })
|
|
69
|
+
let fetchCalls = 0
|
|
70
|
+
const update = startUpdateCheck({
|
|
71
|
+
argv: ['setup'],
|
|
72
|
+
env: {},
|
|
73
|
+
home,
|
|
74
|
+
version: CLI_VERSION,
|
|
75
|
+
now: () => nowMs,
|
|
76
|
+
fetchLatest: async () => {
|
|
77
|
+
fetchCalls++
|
|
78
|
+
return CLI_VERSION
|
|
79
|
+
},
|
|
80
|
+
stderr: { write: () => {} },
|
|
81
|
+
})
|
|
82
|
+
nowMs = t0 + 1000
|
|
83
|
+
await update.finish()
|
|
84
|
+
assert.equal(fetchCalls, 1)
|
|
85
|
+
const cache = JSON.parse(readFileSync(cacheFile(home), 'utf8'))
|
|
86
|
+
assert.equal(cache.latest, CLI_VERSION)
|
|
87
|
+
assert.ok(cache.checkedAt)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('fetch failure keeps old latest and prints nothing', async () => {
|
|
91
|
+
const home = mkdtempSync(join(tmpdir(), 'amorphous-uc-'))
|
|
92
|
+
writeCache(home, { checkedAt: '2025-01-01T00:00:00.000Z', latest: '0.1.0' })
|
|
93
|
+
let err = ''
|
|
94
|
+
const update = startUpdateCheck({
|
|
95
|
+
argv: ['setup'],
|
|
96
|
+
env: {},
|
|
97
|
+
home,
|
|
98
|
+
version: CLI_VERSION,
|
|
99
|
+
now: () => Date.parse('2026-01-02T00:00:00.000Z'),
|
|
100
|
+
fetchLatest: async () => {
|
|
101
|
+
throw new Error('offline')
|
|
102
|
+
},
|
|
103
|
+
stderr: { write: (s) => {
|
|
104
|
+
err += s
|
|
105
|
+
} },
|
|
106
|
+
})
|
|
107
|
+
await update.finish()
|
|
108
|
+
assert.equal(err, '')
|
|
109
|
+
const cache = JSON.parse(readFileSync(cacheFile(home), 'utf8'))
|
|
110
|
+
assert.equal(cache.latest, '0.1.0')
|
|
111
|
+
assert.ok(cache.checkedAt)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('corrupt cache triggers fetch', async () => {
|
|
115
|
+
const home = mkdtempSync(join(tmpdir(), 'amorphous-uc-'))
|
|
116
|
+
const dir = join(home, '.config', 'amorphous')
|
|
117
|
+
mkdirSync(dir, { recursive: true })
|
|
118
|
+
writeFileSync(cacheFile(home), '{not json')
|
|
119
|
+
let fetchCalls = 0
|
|
120
|
+
const update = startUpdateCheck({
|
|
121
|
+
argv: ['setup'],
|
|
122
|
+
env: {},
|
|
123
|
+
home,
|
|
124
|
+
version: CLI_VERSION,
|
|
125
|
+
now: () => Date.now(),
|
|
126
|
+
fetchLatest: async () => {
|
|
127
|
+
fetchCalls++
|
|
128
|
+
return CLI_VERSION
|
|
129
|
+
},
|
|
130
|
+
stderr: { write: () => {} },
|
|
131
|
+
})
|
|
132
|
+
await update.finish()
|
|
133
|
+
assert.equal(fetchCalls, 1)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('equal or newer running version stays quiet', async () => {
|
|
137
|
+
const home = mkdtempSync(join(tmpdir(), 'amorphous-uc-'))
|
|
138
|
+
writeCache(home, { checkedAt: new Date().toISOString(), latest: CLI_VERSION })
|
|
139
|
+
let err = ''
|
|
140
|
+
const update = startUpdateCheck({
|
|
141
|
+
argv: ['list'],
|
|
142
|
+
env: {},
|
|
143
|
+
home,
|
|
144
|
+
version: CLI_VERSION,
|
|
145
|
+
now: () => Date.now(),
|
|
146
|
+
fetchLatest: async () => '0.0.1',
|
|
147
|
+
stderr: { write: (s) => {
|
|
148
|
+
err += s
|
|
149
|
+
} },
|
|
150
|
+
})
|
|
151
|
+
await update.finish()
|
|
152
|
+
assert.equal(err, '')
|
|
153
|
+
|
|
154
|
+
writeCache(home, { checkedAt: new Date().toISOString(), latest: '0.1.0' })
|
|
155
|
+
err = ''
|
|
156
|
+
const update2 = startUpdateCheck({
|
|
157
|
+
argv: ['list'],
|
|
158
|
+
env: {},
|
|
159
|
+
home,
|
|
160
|
+
version: CLI_VERSION,
|
|
161
|
+
now: () => Date.now(),
|
|
162
|
+
fetchLatest: async () => '0.1.0',
|
|
163
|
+
stderr: { write: (s) => {
|
|
164
|
+
err += s
|
|
165
|
+
} },
|
|
166
|
+
})
|
|
167
|
+
await update2.finish()
|
|
168
|
+
assert.equal(err, '')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('shouldCheck', () => {
|
|
172
|
+
const off = {}
|
|
173
|
+
assert.equal(shouldCheck([], off), false)
|
|
174
|
+
assert.equal(shouldCheck(['help'], off), false)
|
|
175
|
+
assert.equal(shouldCheck(['list', '--help'], off), false)
|
|
176
|
+
assert.equal(shouldCheck(['login', '-h'], off), false)
|
|
177
|
+
assert.equal(shouldCheck(['mcp'], off), false)
|
|
178
|
+
assert.equal(shouldCheck(['mcp', 'whatever'], off), false)
|
|
179
|
+
assert.equal(shouldCheck(['setup'], { CI: '1' }), false)
|
|
180
|
+
assert.equal(shouldCheck(['setup'], { AMORPHOUS_NO_UPDATE_CHECK: '1' }), false)
|
|
181
|
+
assert.equal(shouldCheck(['setup'], off), true)
|
|
182
|
+
assert.equal(shouldCheck(['grant'], off), true)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('skillStale', () => {
|
|
186
|
+
const home = mkdtempSync(join(tmpdir(), 'amorphous-skill-stale-'))
|
|
187
|
+
assert.equal(skillStale(home, CLI_VERSION), false)
|
|
188
|
+
|
|
189
|
+
const [d0, d1] = userSkillDirs(home)
|
|
190
|
+
mkdirSync(d0, { recursive: true })
|
|
191
|
+
writeFileSync(join(d0, 'SKILL.md'), 'skill')
|
|
192
|
+
assert.equal(skillStale(home, CLI_VERSION), true)
|
|
193
|
+
|
|
194
|
+
writeFileSync(join(d0, '.amorphous-version'), '0.1.0\n')
|
|
195
|
+
assert.equal(skillStale(home, CLI_VERSION), true)
|
|
196
|
+
|
|
197
|
+
writeFileSync(join(d0, '.amorphous-version'), `${CLI_VERSION}\n`)
|
|
198
|
+
assert.equal(skillStale(home, CLI_VERSION), false)
|
|
199
|
+
|
|
200
|
+
writeFileSync(join(d0, '.amorphous-version'), 'not-a-version\n')
|
|
201
|
+
assert.equal(skillStale(home, CLI_VERSION), false)
|
|
202
|
+
|
|
203
|
+
mkdirSync(d1, { recursive: true })
|
|
204
|
+
writeFileSync(join(d1, 'SKILL.md'), 'skill')
|
|
205
|
+
writeFileSync(join(d1, '.amorphous-version'), '9.9.9\n')
|
|
206
|
+
assert.equal(skillStale(home, CLI_VERSION), false)
|
|
207
|
+
|
|
208
|
+
writeFileSync(join(d1, '.amorphous-version'), '0.1.0\n')
|
|
209
|
+
assert.equal(skillStale(home, CLI_VERSION), true)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('setup skips skill stamp nudge when registry matches', async () => {
|
|
213
|
+
const home = mkdtempSync(join(tmpdir(), 'amorphous-uc-'))
|
|
214
|
+
const [d0] = userSkillDirs(home)
|
|
215
|
+
mkdirSync(d0, { recursive: true })
|
|
216
|
+
writeFileSync(join(d0, 'SKILL.md'), 'skill')
|
|
217
|
+
writeCache(home, { checkedAt: new Date().toISOString(), latest: CLI_VERSION })
|
|
218
|
+
let err = ''
|
|
219
|
+
const update = startUpdateCheck({
|
|
220
|
+
argv: ['setup'],
|
|
221
|
+
env: {},
|
|
222
|
+
home,
|
|
223
|
+
version: CLI_VERSION,
|
|
224
|
+
now: () => Date.now(),
|
|
225
|
+
fetchLatest: async () => CLI_VERSION,
|
|
226
|
+
stderr: { write: (s) => {
|
|
227
|
+
err += s
|
|
228
|
+
} },
|
|
229
|
+
})
|
|
230
|
+
await update.finish()
|
|
231
|
+
assert.equal(err, '')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('non-setup nudges on stale skill when registry matches', async () => {
|
|
235
|
+
const home = mkdtempSync(join(tmpdir(), 'amorphous-uc-'))
|
|
236
|
+
const [d0] = userSkillDirs(home)
|
|
237
|
+
mkdirSync(d0, { recursive: true })
|
|
238
|
+
writeFileSync(join(d0, 'SKILL.md'), 'skill')
|
|
239
|
+
writeCache(home, { checkedAt: new Date().toISOString(), latest: CLI_VERSION })
|
|
240
|
+
let err = ''
|
|
241
|
+
const update = startUpdateCheck({
|
|
242
|
+
argv: ['list'],
|
|
243
|
+
env: {},
|
|
244
|
+
home,
|
|
245
|
+
version: CLI_VERSION,
|
|
246
|
+
now: () => Date.now(),
|
|
247
|
+
fetchLatest: async () => CLI_VERSION,
|
|
248
|
+
stderr: { write: (s) => {
|
|
249
|
+
err += s
|
|
250
|
+
} },
|
|
251
|
+
})
|
|
252
|
+
await update.finish()
|
|
253
|
+
assert.equal(err, NUDGE)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('integration help skips update check file', () => {
|
|
257
|
+
const home = mkdtempSync(join(tmpdir(), 'amorphous-int-'))
|
|
258
|
+
const repoRoot = join(import.meta.dirname, '../..')
|
|
259
|
+
const env = { ...process.env, HOME: home, USERPROFILE: home }
|
|
260
|
+
delete env.AMORPHOUS_NO_UPDATE_CHECK
|
|
261
|
+
delete env.CI
|
|
262
|
+
const r = spawnSync(process.execPath, ['cli/index.mjs', '--help'], {
|
|
263
|
+
cwd: repoRoot,
|
|
264
|
+
env,
|
|
265
|
+
encoding: 'utf8',
|
|
266
|
+
})
|
|
267
|
+
assert.equal(r.status, 0)
|
|
268
|
+
assert.ok(!r.stderr.includes('Run: npx amorphous-cli@latest setup'))
|
|
269
|
+
assert.throws(() => readFileSync(cacheFile(home)), { code: 'ENOENT' })
|
|
270
|
+
})
|
package/lib/version.mjs
ADDED
package/mcp.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
9
9
|
import { McpServer } from '@modelcontextprotocol/server'
|
|
10
10
|
import { serveStdio } from '@modelcontextprotocol/server/stdio'
|
|
11
11
|
import { z } from 'zod'
|
|
12
|
+
import { CLI_VERSION } from './lib/version.mjs'
|
|
12
13
|
|
|
13
14
|
const cliPath = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs')
|
|
14
15
|
|
|
@@ -49,7 +50,7 @@ async function call(args) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
await serveStdio(() => {
|
|
52
|
-
const server = new McpServer({ name: 'amorphous', version:
|
|
53
|
+
const server = new McpServer({ name: 'amorphous', version: CLI_VERSION })
|
|
53
54
|
|
|
54
55
|
server.registerTool(
|
|
55
56
|
'list',
|