onework 0.1.0-alpha.2 → 0.1.0-beta.1

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.
@@ -69,6 +69,23 @@ describe('bootstrap cli', () => {
69
69
  })
70
70
  })
71
71
 
72
+ it('routes runtime package cache versions from flags', () => {
73
+ expect(routeBootstrapCommand('runtime', [
74
+ 'install',
75
+ 'server',
76
+ '--version=2.3.4',
77
+ '--cache-version',
78
+ 'dev-local'
79
+ ])).toEqual({
80
+ action: 'install',
81
+ cacheVersion: 'dev-local',
82
+ json: false,
83
+ kind: 'runtime-package',
84
+ target: 'server',
85
+ version: '2.3.4'
86
+ })
87
+ })
88
+
72
89
  it('routes unknown commands through the CLI package', () => {
73
90
  expect(routeBootstrapCommand('hello', [])).toEqual({
74
91
  commandName: 'oneworks',
@@ -197,6 +214,46 @@ describe('bootstrap cli', () => {
197
214
  expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"requestedVersion":"1.2.3"'))
198
215
  })
199
216
 
217
+ it('dispatches runtime package installation with a cache version', async () => {
218
+ const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
219
+ const installRuntimePackage = vi.fn(async () => ({
220
+ cacheVersion: 'dev-local',
221
+ installed: true,
222
+ installedVersion: '1.2.3',
223
+ latestInstalled: true,
224
+ latestVersion: '1.2.3',
225
+ packageName: '@oneworks/server',
226
+ requestedVersion: '1.2.3',
227
+ target: 'server' as const,
228
+ updateAvailable: false
229
+ }))
230
+ const cli = createBootstrapCli({
231
+ checkRuntimePackage: vi.fn(async () => {
232
+ throw new Error('unexpected check')
233
+ }),
234
+ installRuntimePackage,
235
+ launchDesktopApp: vi.fn(async () => {}),
236
+ launchInstalledPackage: vi.fn(async () => 0)
237
+ })
238
+
239
+ await cli.parseAsync([
240
+ 'node',
241
+ 'oneworks',
242
+ 'runtime',
243
+ 'install',
244
+ 'server',
245
+ '--version=1.2.3',
246
+ '--cache-version=dev-local',
247
+ '--json'
248
+ ])
249
+
250
+ expect(installRuntimePackage).toHaveBeenCalledWith('server', {
251
+ cacheVersion: 'dev-local',
252
+ version: '1.2.3'
253
+ })
254
+ expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('"cacheVersion":"dev-local"'))
255
+ })
256
+
200
257
  it('forwards --help after a routed command', async () => {
201
258
  const launchDesktopApp = vi.fn(async () => {})
202
259
  const launchInstalledPackage = vi.fn(async () => 0)
@@ -20,6 +20,8 @@ describe('bootstrap npm package env', () => {
20
20
  vi.stubEnv('ONEWORKS_BOOTSTRAP_DISABLE_BACKGROUND_REFRESH', '1')
21
21
  vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_CACHE_FIRST', undefined)
22
22
  vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_LOOKUP_TIMEOUT_MS', '1000')
23
+ vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_TAG', undefined)
24
+ vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_VERSION', undefined)
23
25
  vi.stubEnv('NPM_CONFIG_USERCONFIG', undefined)
24
26
  vi.stubEnv('npm_config_userconfig', undefined)
25
27
  })
@@ -40,8 +42,25 @@ describe('bootstrap npm package env', () => {
40
42
  `#!/usr/bin/env node
41
43
  const delay = Number.parseInt(process.env.ONEWORKS_TEST_NPM_VIEW_DELAY_MS || '0', 10)
42
44
  const version = process.env.ONEWORKS_TEST_NPM_VIEW_VERSION || '1.0.0'
45
+ const exactVersions = JSON.parse(process.env.ONEWORKS_TEST_NPM_VIEW_EXACT_VERSIONS || '{}')
46
+ const versions = JSON.parse(process.env.ONEWORKS_TEST_NPM_VIEW_VERSIONS || '[]')
43
47
  if (process.argv[2] === 'view') {
48
+ const spec = process.argv[3]
49
+ const field = process.argv[4]
44
50
  setTimeout(() => {
51
+ if (field === 'versions') {
52
+ process.stdout.write(JSON.stringify(versions.length > 0 ? versions : [version]) + '\\n')
53
+ return
54
+ }
55
+ if (Object.prototype.hasOwnProperty.call(exactVersions, spec)) {
56
+ process.stdout.write(JSON.stringify(exactVersions[spec]) + '\\n')
57
+ return
58
+ }
59
+ if (process.env.ONEWORKS_TEST_NPM_VIEW_FAIL_EXACT === '1' && /@\\d+\\.\\d+\\.\\d+/.test(spec)) {
60
+ process.stderr.write('not found\\n')
61
+ process.exitCode = 1
62
+ return
63
+ }
45
64
  process.stdout.write(JSON.stringify(version) + '\\n')
46
65
  }, delay)
47
66
  } else {
@@ -118,6 +137,7 @@ if (process.argv[2] === 'view') {
118
137
  })
119
138
 
120
139
  it('uses installed package cache before npm view when no metadata exists', async () => {
140
+ vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_TAG', 'latest')
121
141
  await writeCachedPackage('@scope/pkg', '4.5.6')
122
142
 
123
143
  await expect(resolvePublishedPackageVersion('@scope/pkg')).resolves.toBe('4.5.6')
@@ -139,6 +159,55 @@ if (process.argv[2] === 'view') {
139
159
  expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('timed out after 20ms'))
140
160
  })
141
161
 
162
+ it('prefers the exact bootstrap prerelease version for runtime package resolution', async () => {
163
+ await installFakeNpm()
164
+ vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_CACHE_FIRST', '0')
165
+ vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_VERSION', '0.1.0-beta.0')
166
+ vi.stubEnv('ONEWORKS_TEST_NPM_VIEW_VERSION', '0.1.0-alpha.0')
167
+ vi.stubEnv(
168
+ 'ONEWORKS_TEST_NPM_VIEW_EXACT_VERSIONS',
169
+ JSON.stringify({
170
+ '@scope/pkg@0.1.0-beta.0': '0.1.0-beta.0'
171
+ })
172
+ )
173
+
174
+ await expect(resolvePublishedPackageVersion('@scope/pkg')).resolves.toBe('0.1.0-beta.0')
175
+ })
176
+
177
+ it('falls back to the highest same-core bootstrap prerelease version', async () => {
178
+ await installFakeNpm()
179
+ vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_CACHE_FIRST', '0')
180
+ vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_VERSION', '0.1.0-beta.0')
181
+ vi.stubEnv('ONEWORKS_TEST_NPM_VIEW_FAIL_EXACT', '1')
182
+ vi.stubEnv(
183
+ 'ONEWORKS_TEST_NPM_VIEW_VERSIONS',
184
+ JSON.stringify([
185
+ '0.1.0-alpha.9',
186
+ '0.1.0-beta.1',
187
+ '0.1.0-beta.2',
188
+ '0.1.0-rc.0',
189
+ '0.1.0',
190
+ '0.1.1-beta.0'
191
+ ])
192
+ )
193
+
194
+ await expect(resolvePublishedPackageVersion('@scope/pkg')).resolves.toBe('0.1.0-beta.2')
195
+ })
196
+
197
+ it('does not use an installed alpha cache for bootstrap beta resolution', async () => {
198
+ await installFakeNpm()
199
+ await writeCachedPackage('@scope/pkg', '0.1.0-alpha.0')
200
+ vi.stubEnv('ONEWORKS_BOOTSTRAP_PACKAGE_VERSION', '0.1.0-beta.0')
201
+ vi.stubEnv(
202
+ 'ONEWORKS_TEST_NPM_VIEW_EXACT_VERSIONS',
203
+ JSON.stringify({
204
+ '@scope/pkg@0.1.0-beta.0': '0.1.0-beta.0'
205
+ })
206
+ )
207
+
208
+ await expect(resolvePublishedPackageVersion('@scope/pkg')).resolves.toBe('0.1.0-beta.0')
209
+ })
210
+
142
211
  it('waits for npm view when no cached package version exists yet', async () => {
143
212
  await installFakeNpm()
144
213
  vi.stubEnv('ONEWORKS_TEST_NPM_VIEW_VERSION', '3.0.0')
@@ -26,13 +26,13 @@ describe('bootstrap runtime package commands', () => {
26
26
  await rm(tempDir, { force: true, recursive: true })
27
27
  })
28
28
 
29
- const writeCachedPackage = async (packageName: string, version: string) => {
29
+ const writeCachedPackage = async (packageName: string, version: string, cacheVersion = version) => {
30
30
  const sanitizedName = packageName.replace(/^@/, '').replace(/[\\/]/g, '__')
31
31
  const packageDir = path.join(
32
32
  tempDir,
33
33
  '.oneworks/bootstrap/npm',
34
34
  sanitizedName,
35
- version,
35
+ cacheVersion,
36
36
  'node_modules',
37
37
  ...packageName.split('/')
38
38
  )
@@ -190,6 +190,49 @@ process.exit(1)
190
190
  ).resolves.toBeUndefined()
191
191
  })
192
192
 
193
+ it('installs a runtime package under an explicit cache version', async () => {
194
+ vi.stubEnv('ONEWORKS_TEST_NPM_VERSION', '9.9.9')
195
+
196
+ await expect(installRuntimePackage('server', {
197
+ cacheVersion: 'dev-local',
198
+ version: '2.2.0'
199
+ })).resolves.toMatchObject({
200
+ cacheVersion: 'dev-local',
201
+ installedVersion: '2.2.0',
202
+ latestInstalled: true,
203
+ latestVersion: '2.2.0',
204
+ packageName: '@oneworks/server',
205
+ requestedVersion: '2.2.0',
206
+ target: 'server',
207
+ updateAvailable: false
208
+ })
209
+ await expect(
210
+ writeFile(
211
+ path.join(
212
+ tempDir,
213
+ '.oneworks/bootstrap/npm/oneworks__server/dev-local/node_modules/@oneworks/server/probe'
214
+ ),
215
+ 'ok'
216
+ )
217
+ ).resolves.toBeUndefined()
218
+ })
219
+
220
+ it('uses the runtime package cache version env for check and install', async () => {
221
+ await writeCachedPackage('@oneworks/server', '2.2.0', 'dev-env')
222
+ vi.stubEnv('ONEWORKS_RUNTIME_PACKAGE_CACHE_VERSION', 'dev-env')
223
+ vi.stubEnv('ONEWORKS_TEST_NPM_VERSION', '2.2.0')
224
+
225
+ await expect(checkRuntimePackage('server')).resolves.toMatchObject({
226
+ cacheVersion: 'dev-env',
227
+ installedVersion: '2.2.0',
228
+ latestInstalled: true,
229
+ latestVersion: '2.2.0',
230
+ packageName: '@oneworks/server',
231
+ target: 'server',
232
+ updateAvailable: false
233
+ })
234
+ })
235
+
193
236
  it('installs the latest client runtime package target', async () => {
194
237
  vi.stubEnv('ONEWORKS_TEST_NPM_VERSION', '2.2.0')
195
238
 
@@ -208,4 +251,10 @@ process.exit(1)
208
251
  'Runtime package version must be an exact semver version'
209
252
  )
210
253
  })
254
+
255
+ it('rejects unsafe runtime package cache versions', async () => {
256
+ await expect(checkRuntimePackage('cli', { cacheVersion: '../dev', version: '1.0.0' })).rejects.toThrow(
257
+ 'Runtime package cache version contains unsupported characters'
258
+ )
259
+ })
211
260
  })
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "onework",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "One Works bootstrap launcher",
4
5
  "repository": {
5
6
  "type": "git",
6
7
  "url": "https://github.com/oneworks-ai/app.git",
7
8
  "directory": "apps/bootstrap"
8
9
  },
9
- "description": "One Works bootstrap launcher",
10
10
  "oneworks": {
11
11
  "runtimeTranspile": true
12
12
  },
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "@clack/prompts": "^0.11.0",
21
21
  "commander": "^12.1.0",
22
- "@oneworks/cli-helper": "0.1.0-alpha.0"
22
+ "@oneworks/cli-helper": "0.1.0-beta.1"
23
23
  },
24
24
  "scripts": {
25
25
  "test": "pnpm -C ../.. exec vitest run --workspace vitest.workspace.ts --project node apps/bootstrap/__tests__"
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines -- package cache resolution keeps shared cache lookup and version fallback policy together. */
1
2
  import { createHash } from 'node:crypto'
2
3
  import { existsSync } from 'node:fs'
3
4
  import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises'
@@ -9,6 +10,12 @@ import { resolveBootstrapPackageCacheDir, resolveRealHomeDir } from './paths'
9
10
  const DEFAULT_PACKAGE_TAG = 'latest'
10
11
  const DEFAULT_PACKAGE_LOOKUP_TIMEOUT_MS = 1_000
11
12
  const DEFAULT_CACHE_FIRST = true
13
+ const PACKAGE_CACHE_VERSION_PATTERN = /^[\w.+-]+$/u
14
+
15
+ export const RUNTIME_PACKAGE_CACHE_VERSION_ENV = '__ONEWORKS_RUNTIME_PACKAGE_CACHE_VERSION__'
16
+ export const PUBLIC_RUNTIME_PACKAGE_CACHE_VERSION_ENV = 'ONEWORKS_RUNTIME_PACKAGE_CACHE_VERSION'
17
+ export const DESKTOP_DEV_RUNTIME_VERSION_ENV = '__ONEWORKS_DESKTOP_DEV_RUNTIME_VERSION__'
18
+ export const PUBLIC_DESKTOP_DEV_RUNTIME_VERSION_ENV = 'ONEWORKS_DESKTOP_DEV_RUNTIME_VERSION'
12
19
 
13
20
  interface PublishedPackageVersionMetadata {
14
21
  lookupKey: string
@@ -58,8 +65,33 @@ export const shouldUseCachedPackageVersionFirst = () => {
58
65
  return !['0', 'false', 'no', 'off'].includes(rawValue)
59
66
  }
60
67
 
61
- export const resolvePackageCacheDir = (packageName: string, version: string) => (
62
- path.join(resolveBootstrapPackageCacheDir(), 'npm', sanitizePackageName(packageName), version)
68
+ export const normalizePackageCacheVersion = (value: string | undefined) => {
69
+ const normalized = value?.trim()
70
+ if (normalized == null || normalized === '') return undefined
71
+ if (!PACKAGE_CACHE_VERSION_PATTERN.test(normalized) || normalized === '.' || normalized === '..') {
72
+ throw new Error(`Runtime package cache version contains unsupported characters: ${normalized}.`)
73
+ }
74
+ return normalized
75
+ }
76
+
77
+ export const resolveRuntimePackageCacheVersion = () => (
78
+ normalizePackageCacheVersion(process.env[RUNTIME_PACKAGE_CACHE_VERSION_ENV]) ??
79
+ normalizePackageCacheVersion(process.env[PUBLIC_RUNTIME_PACKAGE_CACHE_VERSION_ENV]) ??
80
+ normalizePackageCacheVersion(process.env[DESKTOP_DEV_RUNTIME_VERSION_ENV]) ??
81
+ normalizePackageCacheVersion(process.env[PUBLIC_DESKTOP_DEV_RUNTIME_VERSION_ENV])
82
+ )
83
+
84
+ export const resolvePackageCacheDir = (
85
+ packageName: string,
86
+ version: string,
87
+ options: { cacheVersion?: string } = {}
88
+ ) => (
89
+ path.join(
90
+ resolveBootstrapPackageCacheDir(),
91
+ 'npm',
92
+ sanitizePackageName(packageName),
93
+ normalizePackageCacheVersion(options.cacheVersion) ?? version
94
+ )
63
95
  )
64
96
 
65
97
  export const resolvePackageCacheRootDir = (packageName: string) => (
@@ -109,22 +141,29 @@ const readOptionalFile = async (filePath: string | undefined) => {
109
141
  }
110
142
  }
111
143
 
112
- const resolvePackageLookupKey = async (packageName: string) => {
144
+ const resolvePackageLookupKey = async (
145
+ packageName: string,
146
+ options: { lookupScope?: string } = {}
147
+ ) => {
113
148
  const env = resolvePackageManagerEnv()
114
149
  const userConfig = env.npm_config_userconfig ?? env.NPM_CONFIG_USERCONFIG
115
150
  const userConfigContent = await readOptionalFile(userConfig)
151
+ const packageTag = options.lookupScope?.trim() || resolvePackageTag()
116
152
 
117
153
  return JSON.stringify({
118
154
  packageName,
119
- packageTag: resolvePackageTag(),
155
+ packageTag,
120
156
  registry: env.npm_config_registry ?? env.NPM_CONFIG_REGISTRY ?? '',
121
157
  userConfig: userConfig ?? '',
122
158
  userConfigContentHash: userConfigContent == null ? '' : hashValue(userConfigContent)
123
159
  })
124
160
  }
125
161
 
126
- const resolvePackageVersionMetadataPath = async (packageName: string) => {
127
- const lookupKey = await resolvePackageLookupKey(packageName)
162
+ const resolvePackageVersionMetadataPath = async (
163
+ packageName: string,
164
+ options: { lookupScope?: string } = {}
165
+ ) => {
166
+ const lookupKey = await resolvePackageLookupKey(packageName, options)
128
167
  return {
129
168
  lookupKey,
130
169
  metadataPath: path.join(
@@ -134,8 +173,12 @@ const resolvePackageVersionMetadataPath = async (packageName: string) => {
134
173
  }
135
174
  }
136
175
 
137
- export const readPublishedPackageVersionMetadata = async (packageName: string) => {
138
- const { lookupKey, metadataPath } = await resolvePackageVersionMetadataPath(packageName)
176
+ export const readPublishedPackageVersionMetadata = async (
177
+ packageName: string,
178
+ options: { lookupScope?: string } = {}
179
+ ) => {
180
+ const packageTag = options.lookupScope?.trim() || resolvePackageTag()
181
+ const { lookupKey, metadataPath } = await resolvePackageVersionMetadataPath(packageName, options)
139
182
 
140
183
  try {
141
184
  const content = await readFile(metadataPath, 'utf8')
@@ -143,7 +186,7 @@ export const readPublishedPackageVersionMetadata = async (packageName: string) =
143
186
  if (
144
187
  parsed.lookupKey === lookupKey &&
145
188
  parsed.packageName === packageName &&
146
- parsed.packageTag === resolvePackageTag() &&
189
+ parsed.packageTag === packageTag &&
147
190
  typeof parsed.version === 'string' &&
148
191
  parsed.version.trim()
149
192
  ) {
@@ -159,15 +202,20 @@ export const readPublishedPackageVersionMetadata = async (packageName: string) =
159
202
  return undefined
160
203
  }
161
204
 
162
- export const writePublishedPackageVersionMetadata = async (packageName: string, version: string) => {
163
- const { lookupKey, metadataPath } = await resolvePackageVersionMetadataPath(packageName)
205
+ export const writePublishedPackageVersionMetadata = async (
206
+ packageName: string,
207
+ version: string,
208
+ options: { lookupScope?: string } = {}
209
+ ) => {
210
+ const packageTag = options.lookupScope?.trim() || resolvePackageTag()
211
+ const { lookupKey, metadataPath } = await resolvePackageVersionMetadataPath(packageName, options)
164
212
  await ensureDirectory(path.dirname(metadataPath))
165
213
 
166
214
  const tempPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`
167
215
  const metadata: PublishedPackageVersionMetadata = {
168
216
  lookupKey,
169
217
  packageName,
170
- packageTag: resolvePackageTag(),
218
+ packageTag,
171
219
  resolvedAt: new Date().toISOString(),
172
220
  version
173
221
  }
@@ -6,10 +6,12 @@ import {
6
6
  compareVersionLike,
7
7
  ensureDirectory,
8
8
  isExistingPath,
9
+ normalizePackageCacheVersion,
9
10
  resolvePackageCacheDir,
10
11
  resolvePackageCacheRootDir,
11
12
  resolvePackageInstallDir,
12
- resolvePackageManagerEnv
13
+ resolvePackageManagerEnv,
14
+ resolveRuntimePackageCacheVersion
13
15
  } from './npm-package-cache'
14
16
  import { runBufferedCommand } from './process-utils'
15
17
  import { createBootstrapProgress } from './progress'
@@ -17,6 +19,7 @@ import { createBootstrapProgress } from './progress'
17
19
  const NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm'
18
20
 
19
21
  interface InstalledPackageInfo {
22
+ cacheVersion: string
20
23
  packageDir: string
21
24
  version: string
22
25
  }
@@ -36,7 +39,13 @@ export const readInstalledPackageVersion = async (packageDir: string) => {
36
39
  }
37
40
  }
38
41
 
39
- export const findInstalledPublishedPackageVersion = async (packageName: string) => {
42
+ export const findInstalledPublishedPackageVersion = async (
43
+ packageName: string,
44
+ options: {
45
+ preferredVersion?: string
46
+ versionFilter?: (version: string) => boolean
47
+ } = {}
48
+ ) => {
40
49
  let versions: string[]
41
50
  try {
42
51
  versions = (await readdir(resolvePackageCacheRootDir(packageName), { withFileTypes: true }))
@@ -48,6 +57,9 @@ export const findInstalledPublishedPackageVersion = async (packageName: string)
48
57
 
49
58
  const installedVersions: string[] = []
50
59
  for (const version of versions) {
60
+ if (options.versionFilter != null && !options.versionFilter(version)) {
61
+ continue
62
+ }
51
63
  const packageDir = resolvePackageInstallDir(resolvePackageCacheDir(packageName, version), packageName)
52
64
  const installedVersion = await readInstalledPackageVersion(packageDir)
53
65
  if (installedVersion === version) {
@@ -55,6 +67,10 @@ export const findInstalledPublishedPackageVersion = async (packageName: string)
55
67
  }
56
68
  }
57
69
 
70
+ if (options.preferredVersion != null && installedVersions.includes(options.preferredVersion)) {
71
+ return options.preferredVersion
72
+ }
73
+
58
74
  return installedVersions.sort(compareVersionLike).at(-1)
59
75
  }
60
76
 
@@ -63,12 +79,19 @@ const formatInstallError = (message: string, stderr: string) => {
63
79
  return detail ? `${message}\n${detail}` : message
64
80
  }
65
81
 
66
- export const installPublishedPackage = async (packageName: string, version: string): Promise<InstalledPackageInfo> => {
67
- const cacheDir = resolvePackageCacheDir(packageName, version)
82
+ export const installPublishedPackage = async (
83
+ packageName: string,
84
+ version: string,
85
+ options: { cacheVersion?: string } = {}
86
+ ): Promise<InstalledPackageInfo> => {
87
+ const cacheVersion = normalizePackageCacheVersion(options.cacheVersion) ??
88
+ resolveRuntimePackageCacheVersion() ??
89
+ version
90
+ const cacheDir = resolvePackageCacheDir(packageName, version, { cacheVersion })
68
91
  const packageDir = resolvePackageInstallDir(cacheDir, packageName)
69
92
  const installedVersion = await readInstalledPackageVersion(packageDir)
70
93
  if (installedVersion === version) {
71
- return { packageDir, version }
94
+ return { cacheVersion, packageDir, version }
72
95
  }
73
96
 
74
97
  const stagingDir = `${cacheDir}.tmp-${process.pid}-${Date.now()}`
@@ -76,7 +99,9 @@ export const installPublishedPackage = async (packageName: string, version: stri
76
99
  await ensureDirectory(stagingDir)
77
100
 
78
101
  const progress = createBootstrapProgress({
79
- label: `installing ${packageName}@${version} into bootstrap cache`
102
+ label: cacheVersion === version
103
+ ? `installing ${packageName}@${version} into bootstrap cache`
104
+ : `installing ${packageName}@${version} into bootstrap cache ${cacheVersion}`
80
105
  })
81
106
  try {
82
107
  const result = await runBufferedCommand({
@@ -100,7 +125,11 @@ export const installPublishedPackage = async (packageName: string, version: stri
100
125
  await ensureDirectory(path.dirname(cacheDir))
101
126
  await rm(cacheDir, { recursive: true, force: true })
102
127
  await rename(stagingDir, cacheDir)
103
- progress.finish(`cached ${packageName}@${version}`)
128
+ progress.finish(
129
+ cacheVersion === version
130
+ ? `cached ${packageName}@${version}`
131
+ : `cached ${packageName}@${version} as ${cacheVersion}`
132
+ )
104
133
  } catch (error) {
105
134
  progress.fail(`failed to cache ${packageName}@${version}`)
106
135
  await rm(stagingDir, { recursive: true, force: true }).catch(() => {})
@@ -108,6 +137,7 @@ export const installPublishedPackage = async (packageName: string, version: stri
108
137
  }
109
138
 
110
139
  return {
140
+ cacheVersion,
111
141
  packageDir: resolvePackageInstallDir(cacheDir, packageName),
112
142
  version
113
143
  }
@@ -8,14 +8,12 @@ import {
8
8
  readPublishedPackageVersionMetadata,
9
9
  resolvePackageLookupTimeoutMs,
10
10
  resolvePackageManagerEnv,
11
- resolvePackageTag,
12
11
  shouldUseCachedPackageVersionFirst,
13
12
  writePublishedPackageVersionMetadata
14
13
  } from './npm-package-cache'
15
14
  import { findInstalledPublishedPackageVersion } from './npm-package-install'
16
- import { runBufferedCommand } from './process-utils'
17
-
18
- const NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm'
15
+ import { resolvePublishedPackageVersionFromRegistry } from './npm-registry'
16
+ import { resolvePackageVersionRequest } from './package-version-request'
19
17
 
20
18
  export { resolvePackageManagerEnv } from './npm-package-cache'
21
19
  export { installPublishedPackage, resolvePackageBinEntrypoint } from './npm-package-install'
@@ -52,68 +50,21 @@ const spawnPackageVersionRefresh = (packageName: string) => {
52
50
  }
53
51
  }
54
52
 
55
- const resolvePublishedPackageVersionFromRegistry = async (
56
- packageName: string,
57
- options: { timeoutMs?: number } = {}
58
- ) => {
59
- const spec = `${packageName}@${resolvePackageTag()}`
60
- const result = await runBufferedCommand({
61
- command: NPM_BIN,
62
- args: ['view', spec, 'version', '--json'],
63
- env: resolvePackageManagerEnv(),
64
- timeoutMs: options.timeoutMs
65
- })
66
-
67
- if (result.timedOut === true) {
68
- return {
69
- spec,
70
- timedOut: true as const
71
- }
72
- }
73
-
74
- if (result.code !== 0) {
75
- throw new Error(`Failed to resolve published version for ${spec}:\n${result.stderr.trim()}`)
76
- }
77
-
78
- const normalizedOutput = result.stdout.trim()
79
- if (!normalizedOutput) {
80
- throw new Error(`No version was returned for ${spec}.`)
81
- }
82
-
83
- try {
84
- const parsed = JSON.parse(normalizedOutput) as unknown
85
- if (typeof parsed === 'string' && parsed.trim()) {
86
- return {
87
- spec,
88
- version: parsed.trim()
89
- }
90
- }
91
- } catch {
92
- // fall through
93
- }
94
-
95
- const unquotedOutput = normalizedOutput.replace(/^"|"$/g, '').trim()
96
- if (!unquotedOutput) {
97
- throw new Error(`Invalid published version for ${spec}: ${normalizedOutput}`)
98
- }
99
-
100
- return {
101
- spec,
102
- version: unquotedOutput
103
- }
104
- }
105
-
106
53
  export const resolvePublishedPackageVersion = async (
107
54
  packageName: string,
108
55
  options: { cacheFirst?: boolean } = {}
109
56
  ) => {
110
- const cachedMetadata = await readPublishedPackageVersionMetadata(packageName)
57
+ const request = resolvePackageVersionRequest(packageName)
58
+ const cachedMetadata = await readPublishedPackageVersionMetadata(packageName, { lookupScope: request.lookupScope })
111
59
  if (cachedMetadata != null && (options.cacheFirst ?? shouldUseCachedPackageVersionFirst())) {
112
60
  spawnPackageVersionRefresh(packageName)
113
61
  return cachedMetadata.version
114
62
  }
115
63
 
116
- const cachedInstalledVersion = await findInstalledPublishedPackageVersion(packageName)
64
+ const cachedInstalledVersion = await findInstalledPublishedPackageVersion(packageName, {
65
+ preferredVersion: request.exactVersion,
66
+ versionFilter: request.versionFilter
67
+ })
117
68
  if (cachedInstalledVersion != null && (options.cacheFirst ?? shouldUseCachedPackageVersionFirst())) {
118
69
  spawnPackageVersionRefresh(packageName)
119
70
  return cachedInstalledVersion
@@ -121,6 +72,7 @@ export const resolvePublishedPackageVersion = async (
121
72
 
122
73
  const registryResult = await resolvePublishedPackageVersionFromRegistry(
123
74
  packageName,
75
+ request,
124
76
  cachedMetadata == null
125
77
  ? {}
126
78
  : {
@@ -129,15 +81,17 @@ export const resolvePublishedPackageVersion = async (
129
81
  )
130
82
 
131
83
  if ('version' in registryResult) {
132
- await writePublishedPackageVersionMetadata(packageName, registryResult.version)
84
+ await writePublishedPackageVersionMetadata(packageName, registryResult.version, {
85
+ lookupScope: request.lookupScope
86
+ })
133
87
  return registryResult.version
134
88
  }
135
89
 
136
90
  if (cachedMetadata == null) {
137
91
  // This is not expected because uncached lookups do not use a timeout.
138
- const retryResult = await resolvePublishedPackageVersionFromRegistry(packageName)
92
+ const retryResult = await resolvePublishedPackageVersionFromRegistry(packageName, request)
139
93
  if ('version' in retryResult) {
140
- await writePublishedPackageVersionMetadata(packageName, retryResult.version)
94
+ await writePublishedPackageVersionMetadata(packageName, retryResult.version, { lookupScope: request.lookupScope })
141
95
  return retryResult.version
142
96
  }
143
97
  throw new Error(`Failed to resolve published version for ${retryResult.spec}.`)
@@ -0,0 +1,154 @@
1
+ import process from 'node:process'
2
+
3
+ import { resolvePackageManagerEnv } from './npm-package-cache'
4
+ import { comparePackageVersions } from './package-version-request'
5
+ import type { PackageVersionRequest } from './package-version-request'
6
+ import { runBufferedCommand } from './process-utils'
7
+
8
+ const NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm'
9
+
10
+ const parseVersionOutput = (spec: string, output: string) => {
11
+ const normalizedOutput = output.trim()
12
+ if (!normalizedOutput) {
13
+ throw new Error(`No version was returned for ${spec}.`)
14
+ }
15
+
16
+ try {
17
+ const parsed = JSON.parse(normalizedOutput) as unknown
18
+ if (typeof parsed === 'string' && parsed.trim()) {
19
+ return parsed.trim()
20
+ }
21
+ } catch {
22
+ // fall through
23
+ }
24
+
25
+ const unquotedOutput = normalizedOutput.replace(/^"|"$/g, '').trim()
26
+ if (!unquotedOutput) {
27
+ throw new Error(`Invalid published version for ${spec}: ${normalizedOutput}`)
28
+ }
29
+
30
+ return unquotedOutput
31
+ }
32
+
33
+ const resolvePublishedPackageVersionBySpec = async (
34
+ spec: string,
35
+ options: { allowMissing?: boolean; timeoutMs?: number } = {}
36
+ ) => {
37
+ const result = await runBufferedCommand({
38
+ command: NPM_BIN,
39
+ args: ['view', spec, 'version', '--json'],
40
+ env: resolvePackageManagerEnv(),
41
+ timeoutMs: options.timeoutMs
42
+ })
43
+
44
+ if (result.timedOut === true) {
45
+ return {
46
+ spec,
47
+ timedOut: true as const
48
+ }
49
+ }
50
+
51
+ if (result.code !== 0) {
52
+ if (options.allowMissing === true) {
53
+ return {
54
+ missing: true as const,
55
+ spec
56
+ }
57
+ }
58
+ throw new Error(`Failed to resolve published version for ${spec}:\n${result.stderr.trim()}`)
59
+ }
60
+
61
+ return {
62
+ spec,
63
+ version: parseVersionOutput(spec, result.stdout)
64
+ }
65
+ }
66
+
67
+ const parseVersionsOutput = (spec: string, output: string) => {
68
+ const normalizedOutput = output.trim()
69
+ if (!normalizedOutput) {
70
+ throw new Error(`No versions were returned for ${spec}.`)
71
+ }
72
+
73
+ let parsed: unknown
74
+ try {
75
+ parsed = JSON.parse(normalizedOutput) as unknown
76
+ } catch {
77
+ parsed = normalizedOutput.replace(/^"|"$/g, '').trim()
78
+ }
79
+
80
+ const versions = Array.isArray(parsed)
81
+ ? parsed.filter((version): version is string => typeof version === 'string' && version.trim() !== '')
82
+ : typeof parsed === 'string' && parsed.trim() !== ''
83
+ ? [parsed.trim()]
84
+ : []
85
+ if (versions.length === 0) {
86
+ throw new Error(`Invalid published versions for ${spec}: ${normalizedOutput}`)
87
+ }
88
+ return versions
89
+ }
90
+
91
+ const resolvePublishedPackageVersionsFromRegistry = async (
92
+ packageName: string,
93
+ options: { timeoutMs?: number } = {}
94
+ ) => {
95
+ const result = await runBufferedCommand({
96
+ command: NPM_BIN,
97
+ args: ['view', packageName, 'versions', '--json'],
98
+ env: resolvePackageManagerEnv(),
99
+ timeoutMs: options.timeoutMs
100
+ })
101
+
102
+ if (result.timedOut === true) {
103
+ return {
104
+ spec: packageName,
105
+ timedOut: true as const
106
+ }
107
+ }
108
+
109
+ if (result.code !== 0) {
110
+ throw new Error(`Failed to resolve published versions for ${packageName}:\n${result.stderr.trim()}`)
111
+ }
112
+
113
+ return {
114
+ spec: packageName,
115
+ versions: parseVersionsOutput(packageName, result.stdout)
116
+ }
117
+ }
118
+
119
+ export const resolvePublishedPackageVersionFromRegistry = async (
120
+ packageName: string,
121
+ request: PackageVersionRequest,
122
+ options: { timeoutMs?: number } = {}
123
+ ) => {
124
+ if (request.exactVersion == null) {
125
+ return await resolvePublishedPackageVersionBySpec(request.packageSpec, options)
126
+ }
127
+
128
+ const exactResult = await resolvePublishedPackageVersionBySpec(request.packageSpec, {
129
+ allowMissing: true,
130
+ timeoutMs: options.timeoutMs
131
+ })
132
+ if ('version' in exactResult || 'timedOut' in exactResult) {
133
+ return exactResult
134
+ }
135
+
136
+ const versionsResult = await resolvePublishedPackageVersionsFromRegistry(packageName, options)
137
+ if ('timedOut' in versionsResult) {
138
+ return versionsResult
139
+ }
140
+
141
+ const matchedVersion = versionsResult.versions
142
+ .filter(version => request.versionFilter == null || request.versionFilter(version))
143
+ .sort(comparePackageVersions)
144
+ .at(-1)
145
+ if (matchedVersion == null) {
146
+ throw new Error(
147
+ `No published version for ${packageName} matches bootstrap runtime series ${request.exactVersion}.`
148
+ )
149
+ }
150
+ return {
151
+ spec: `${packageName} versions`,
152
+ version: matchedVersion
153
+ }
154
+ }
@@ -0,0 +1,121 @@
1
+ import { createRequire } from 'node:module'
2
+ import process from 'node:process'
3
+
4
+ import { compareVersionLike, resolvePackageTag } from './npm-package-cache'
5
+
6
+ const BOOTSTRAP_PACKAGE_VERSION_ENV = 'ONEWORKS_BOOTSTRAP_PACKAGE_VERSION'
7
+
8
+ interface ParsedSemver {
9
+ major: number
10
+ minor: number
11
+ patch: number
12
+ prerelease: string[]
13
+ }
14
+
15
+ export interface PackageVersionRequest {
16
+ exactVersion?: string
17
+ lookupScope: string
18
+ packageSpec: string
19
+ tag?: string
20
+ versionFilter?: (version: string) => boolean
21
+ }
22
+
23
+ const parseSemver = (version: string): ParsedSemver | undefined => {
24
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(version.trim())
25
+ if (match == null) return undefined
26
+ return {
27
+ major: Number(match[1]),
28
+ minor: Number(match[2]),
29
+ patch: Number(match[3]),
30
+ prerelease: match[4]?.split('.') ?? []
31
+ }
32
+ }
33
+
34
+ const comparePrereleaseIdentifiers = (left: string, right: string) => {
35
+ const leftNumber = /^\d+$/u.test(left) ? Number(left) : undefined
36
+ const rightNumber = /^\d+$/u.test(right) ? Number(right) : undefined
37
+ if (leftNumber != null && rightNumber != null) return leftNumber - rightNumber
38
+ if (leftNumber != null) return -1
39
+ if (rightNumber != null) return 1
40
+ return left.localeCompare(right)
41
+ }
42
+
43
+ const compareSemver = (left: ParsedSemver, right: ParsedSemver) => {
44
+ const coreDiff = left.major - right.major || left.minor - right.minor || left.patch - right.patch
45
+ if (coreDiff !== 0) return coreDiff
46
+ if (left.prerelease.length === 0 && right.prerelease.length === 0) return 0
47
+ if (left.prerelease.length === 0) return 1
48
+ if (right.prerelease.length === 0) return -1
49
+ const maxLength = Math.max(left.prerelease.length, right.prerelease.length)
50
+ for (let index = 0; index < maxLength; index += 1) {
51
+ const leftPart = left.prerelease[index]
52
+ const rightPart = right.prerelease[index]
53
+ if (leftPart == null) return -1
54
+ if (rightPart == null) return 1
55
+ const diff = comparePrereleaseIdentifiers(leftPart, rightPart)
56
+ if (diff !== 0) return diff
57
+ }
58
+ return 0
59
+ }
60
+
61
+ export const comparePackageVersions = (left: string, right: string) => {
62
+ const leftSemver = parseSemver(left)
63
+ const rightSemver = parseSemver(right)
64
+ if (leftSemver != null && rightSemver != null) return compareSemver(leftSemver, rightSemver)
65
+ return compareVersionLike(left, right)
66
+ }
67
+
68
+ const hasSameSemverCore = (left: ParsedSemver, right: ParsedSemver) => (
69
+ left.major === right.major && left.minor === right.minor && left.patch === right.patch
70
+ )
71
+
72
+ const readBootstrapPackageVersion = () => {
73
+ const envVersion = process.env[BOOTSTRAP_PACKAGE_VERSION_ENV]?.trim()
74
+ if (envVersion) return envVersion
75
+
76
+ try {
77
+ const requireFromHere = createRequire(import.meta.url)
78
+ const packageJsonPath = requireFromHere.resolve('oneworks/package.json')
79
+ const packageJson = requireFromHere(packageJsonPath) as { version?: unknown }
80
+ return typeof packageJson.version === 'string' && packageJson.version.trim()
81
+ ? packageJson.version.trim()
82
+ : undefined
83
+ } catch {
84
+ return undefined
85
+ }
86
+ }
87
+
88
+ export const resolvePackageVersionRequest = (packageName: string): PackageVersionRequest => {
89
+ const explicitTag = process.env.ONEWORKS_BOOTSTRAP_PACKAGE_TAG?.trim()
90
+ if (explicitTag) {
91
+ return {
92
+ lookupScope: `tag:${explicitTag}`,
93
+ packageSpec: `${packageName}@${explicitTag}`,
94
+ tag: explicitTag
95
+ }
96
+ }
97
+
98
+ const bootstrapVersion = readBootstrapPackageVersion()
99
+ const parsedBootstrapVersion = bootstrapVersion == null ? undefined : parseSemver(bootstrapVersion)
100
+ const prereleaseChannel = parsedBootstrapVersion?.prerelease[0]
101
+ if (bootstrapVersion != null && parsedBootstrapVersion != null && prereleaseChannel != null) {
102
+ return {
103
+ exactVersion: bootstrapVersion,
104
+ lookupScope: `bootstrap:${bootstrapVersion}:channel:${prereleaseChannel}`,
105
+ packageSpec: `${packageName}@${bootstrapVersion}`,
106
+ versionFilter: (version) => {
107
+ const parsedVersion = parseSemver(version)
108
+ return parsedVersion != null &&
109
+ hasSameSemverCore(parsedVersion, parsedBootstrapVersion) &&
110
+ parsedVersion.prerelease[0] === prereleaseChannel
111
+ }
112
+ }
113
+ }
114
+
115
+ const tag = resolvePackageTag()
116
+ return {
117
+ lookupScope: `tag:${tag}`,
118
+ packageSpec: `${packageName}@${tag}`,
119
+ tag
120
+ }
121
+ }
package/src/program.ts CHANGED
@@ -35,6 +35,7 @@ type BootstrapTarget =
35
35
  }
36
36
  | {
37
37
  action: RuntimePackageAction
38
+ cacheVersion?: string
38
39
  json: boolean
39
40
  kind: 'runtime-package'
40
41
  target?: string
@@ -88,6 +89,7 @@ const parseRuntimePackageTarget = (args: string[]): BootstrapTarget => {
88
89
  }
89
90
 
90
91
  let version: string | undefined
92
+ let cacheVersion: string | undefined
91
93
  for (let index = 0; index < forwardedArgs.length; index += 1) {
92
94
  const arg = forwardedArgs[index]
93
95
  if (arg === '--version') {
@@ -109,6 +111,28 @@ const parseRuntimePackageTarget = (args: string[]): BootstrapTarget => {
109
111
  version = value
110
112
  forwardedArgs.splice(index, 1)
111
113
  index -= 1
114
+ continue
115
+ }
116
+
117
+ if (arg === '--cache-version') {
118
+ const value = forwardedArgs[index + 1]
119
+ if (value == null || value.trim() === '') {
120
+ throw new Error('Runtime package --cache-version requires a value.')
121
+ }
122
+ cacheVersion = value
123
+ forwardedArgs.splice(index, 2)
124
+ index -= 1
125
+ continue
126
+ }
127
+
128
+ if (arg?.startsWith('--cache-version=')) {
129
+ const value = arg.slice('--cache-version='.length)
130
+ if (value.trim() === '') {
131
+ throw new Error('Runtime package --cache-version requires a value.')
132
+ }
133
+ cacheVersion = value
134
+ forwardedArgs.splice(index, 1)
135
+ index -= 1
112
136
  }
113
137
  }
114
138
 
@@ -130,6 +154,7 @@ const parseRuntimePackageTarget = (args: string[]): BootstrapTarget => {
130
154
 
131
155
  return {
132
156
  action,
157
+ ...(cacheVersion != null ? { cacheVersion } : {}),
133
158
  json,
134
159
  kind: 'runtime-package',
135
160
  target: selectorTarget,
@@ -216,6 +241,7 @@ Examples:
216
241
  npx oneworks runtime check cli@0.1.0-alpha.0
217
242
  npx oneworks runtime install server
218
243
  npx oneworks runtime install server --version 0.1.0-alpha.0
244
+ npx oneworks runtime install server --version 0.1.0-alpha.0 --cache-version dev-local
219
245
  npx oneworks app
220
246
  npx oneworks app cache
221
247
  npx oneworks app --no-cache
@@ -240,15 +266,20 @@ Examples:
240
266
  }
241
267
 
242
268
  if (target.kind === 'runtime-package') {
269
+ const runtimeOptions: RuntimePackageOptions = {
270
+ ...(target.version == null ? {} : { version: target.version }),
271
+ ...(target.cacheVersion == null ? {} : { cacheVersion: target.cacheVersion })
272
+ }
273
+ const hasRuntimeOptions = Object.keys(runtimeOptions).length > 0
243
274
  let status: RuntimePackageStatus
244
275
  if (target.action === 'install') {
245
- status = target.version == null
276
+ status = !hasRuntimeOptions
246
277
  ? await deps.installRuntimePackage(target.target)
247
- : await deps.installRuntimePackage(target.target, { version: target.version })
278
+ : await deps.installRuntimePackage(target.target, runtimeOptions)
248
279
  } else {
249
- status = target.version == null
280
+ status = !hasRuntimeOptions
250
281
  ? await deps.checkRuntimePackage(target.target)
251
- : await deps.checkRuntimePackage(target.target, { version: target.version })
282
+ : await deps.checkRuntimePackage(target.target, runtimeOptions)
252
283
  }
253
284
  const output = target.json ? JSON.stringify(status) : formatRuntimePackageStatus(status)
254
285
  process.stdout.write(`${output}\n`)
@@ -1,5 +1,10 @@
1
1
  import { resolvePublishedPackageVersion } from './npm-package'
2
- import { resolvePackageCacheDir, resolvePackageInstallDir } from './npm-package-cache'
2
+ import {
3
+ normalizePackageCacheVersion,
4
+ resolvePackageCacheDir,
5
+ resolvePackageInstallDir,
6
+ resolveRuntimePackageCacheVersion
7
+ } from './npm-package-cache'
3
8
  import {
4
9
  findInstalledPublishedPackageVersion,
5
10
  installPublishedPackage,
@@ -10,10 +15,12 @@ export type RuntimePackageAction = 'check' | 'install'
10
15
  export type RuntimePackageTarget = 'cli' | 'client' | 'server' | 'web'
11
16
 
12
17
  export interface RuntimePackageOptions {
18
+ cacheVersion?: string
13
19
  version?: string
14
20
  }
15
21
 
16
22
  export interface RuntimePackageStatus {
23
+ cacheVersion?: string
17
24
  installed: boolean
18
25
  installedVersion?: string
19
26
  latestInstalled: boolean
@@ -56,8 +63,11 @@ export const resolveRuntimePackageTarget = (value: string | undefined): RuntimeP
56
63
  )
57
64
  }
58
65
 
59
- const readVersionInstalledAt = async (packageName: string, version: string) => {
60
- const packageDir = resolvePackageInstallDir(resolvePackageCacheDir(packageName, version), packageName)
66
+ const readVersionInstalledAt = async (packageName: string, version: string, cacheVersion = version) => {
67
+ const packageDir = resolvePackageInstallDir(
68
+ resolvePackageCacheDir(packageName, version, { cacheVersion }),
69
+ packageName
70
+ )
61
71
  return await readInstalledPackageVersion(packageDir)
62
72
  }
63
73
 
@@ -70,12 +80,19 @@ const createRuntimePackageStatus = async (
70
80
  target: RuntimePackageTarget,
71
81
  packageName: string,
72
82
  targetVersion: string,
73
- requestedVersion: string | undefined
83
+ requestedVersion: string | undefined,
84
+ requestedCacheVersion: string | undefined
74
85
  ): Promise<RuntimePackageStatus> => {
75
- const installedVersion = await findInstalledPublishedPackageVersion(packageName)
76
- const targetInstalled = await readVersionInstalledAt(packageName, targetVersion) === targetVersion
86
+ const cacheVersion = normalizePackageCacheVersion(requestedCacheVersion)
87
+ const resolvedCacheVersion = cacheVersion ?? targetVersion
88
+ const installedVersion = cacheVersion == null
89
+ ? await findInstalledPublishedPackageVersion(packageName)
90
+ : await readVersionInstalledAt(packageName, targetVersion, resolvedCacheVersion)
91
+ const targetInstalled =
92
+ await readVersionInstalledAt(packageName, targetVersion, resolvedCacheVersion) === targetVersion
77
93
 
78
94
  return {
95
+ ...(cacheVersion != null ? { cacheVersion } : {}),
79
96
  installed: installedVersion != null,
80
97
  ...(installedVersion != null ? { installedVersion } : {}),
81
98
  latestInstalled: targetInstalled,
@@ -83,7 +100,9 @@ const createRuntimePackageStatus = async (
83
100
  packageName,
84
101
  ...(requestedVersion != null ? { requestedVersion } : {}),
85
102
  target,
86
- updateAvailable: requestedVersion != null ? !targetInstalled : installedVersion !== targetVersion
103
+ updateAvailable: requestedVersion != null || cacheVersion != null
104
+ ? !targetInstalled
105
+ : installedVersion !== targetVersion
87
106
  }
88
107
  }
89
108
 
@@ -94,8 +113,9 @@ export const checkRuntimePackage = async (
94
113
  const target = resolveRuntimePackageTarget(targetValue)
95
114
  const packageName = RUNTIME_PACKAGE_NAMES[target]
96
115
  const requestedVersion = normalizeRequestedVersion(options.version)
116
+ const cacheVersion = normalizePackageCacheVersion(options.cacheVersion) ?? resolveRuntimePackageCacheVersion()
97
117
  const targetVersion = requestedVersion ?? await resolveRuntimePackageVersion(packageName, options)
98
- return await createRuntimePackageStatus(target, packageName, targetVersion, requestedVersion)
118
+ return await createRuntimePackageStatus(target, packageName, targetVersion, requestedVersion, cacheVersion)
99
119
  }
100
120
 
101
121
  export const installRuntimePackage = async (
@@ -105,19 +125,31 @@ export const installRuntimePackage = async (
105
125
  const target = resolveRuntimePackageTarget(targetValue)
106
126
  const packageName = RUNTIME_PACKAGE_NAMES[target]
107
127
  const requestedVersion = normalizeRequestedVersion(options.version)
128
+ const cacheVersion = normalizePackageCacheVersion(options.cacheVersion) ?? resolveRuntimePackageCacheVersion()
108
129
  const targetVersion = requestedVersion ?? await resolveRuntimePackageVersion(packageName, options)
109
- await installPublishedPackage(packageName, targetVersion)
110
- return await createRuntimePackageStatus(target, packageName, targetVersion, requestedVersion)
130
+ await installPublishedPackage(packageName, targetVersion, cacheVersion == null ? {} : { cacheVersion })
131
+ return await createRuntimePackageStatus(target, packageName, targetVersion, requestedVersion, cacheVersion)
111
132
  }
112
133
 
113
134
  export const formatRuntimePackageStatus = (status: RuntimePackageStatus) => {
114
135
  const current = status.installedVersion ?? 'not installed'
115
136
  if (status.requestedVersion != null) {
137
+ if (status.cacheVersion != null) {
138
+ return status.latestInstalled
139
+ ? `${status.packageName}@${status.requestedVersion} cached as ${status.cacheVersion}`
140
+ : `${status.packageName}@${status.requestedVersion} not cached as ${status.cacheVersion} (${current})`
141
+ }
116
142
  return status.latestInstalled
117
143
  ? `${status.packageName}@${status.requestedVersion} cached`
118
144
  : `${status.packageName}@${status.requestedVersion} not cached (${current})`
119
145
  }
120
146
 
147
+ if (status.cacheVersion != null) {
148
+ return status.latestInstalled
149
+ ? `${status.packageName}@${status.latestVersion} cached as ${status.cacheVersion}`
150
+ : `${status.packageName}@${status.latestVersion} not cached as ${status.cacheVersion} (${current})`
151
+ }
152
+
121
153
  const suffix = status.updateAvailable
122
154
  ? `update available: ${current} -> ${status.latestVersion}`
123
155
  : `up to date: ${status.latestVersion}`