@dsh-workbench/shared 1.1.4 → 1.1.5
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/dist/api/host-write.d.ts +4 -1
- package/dist/api/workbench-client.d.ts +2 -0
- package/dist/components/ApprovalPlan.css +7 -8
- package/dist/config/constants.d.ts +4 -0
- package/dist/config/constants.js +4 -0
- package/dist/contract/approval.d.ts +4 -0
- package/dist/contract/approval.js +4 -0
- package/dist/contract/command.d.ts +33 -0
- package/dist/contract/command.js +1 -1
- package/dist/contract/needs.d.ts +17 -0
- package/dist/contract/needs.js +41 -1
- package/dist/contract/protocol/bundle-install.d.ts +2 -0
- package/dist/contract/unit.d.ts +10 -0
- package/dist/host/adapter-types.d.ts +2 -0
- package/dist/host/data-error.d.ts +2 -0
- package/dist/host/data-error.js +3 -0
- package/dist/host/http.js +1 -0
- package/dist/host/install-failure.d.ts +49 -0
- package/dist/host/install-failure.js +167 -0
- package/dist/host/installed-sources.d.ts +10 -0
- package/dist/host/installed-sources.js +64 -0
- package/dist/host/lib-install.d.ts +12 -1
- package/dist/host/lib-install.js +33 -1
- package/dist/host/paths.d.ts +7 -0
- package/dist/host/paths.js +17 -0
- package/dist/host/runtime-links.d.ts +7 -0
- package/dist/host/runtime-links.js +25 -0
- package/dist/host/tool-approval.d.ts +4 -2
- package/dist/host/tool-approval.js +4 -2
- package/dist/host.d.ts +3 -0
- package/dist/host.js +3 -0
- package/dist/i18n/en.js +40 -0
- package/dist/i18n/index.d.ts +41 -1
- package/dist/i18n/zh.d.ts +40 -0
- package/dist/i18n/zh.js +40 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/src/api/host-write.ts +5 -1
- package/src/api/workbench-client.ts +2 -0
- package/src/components/ApprovalPlan.css +7 -8
- package/src/config/constants.ts +6 -0
- package/src/contract/__tests__/needs.test.ts +53 -0
- package/src/contract/approval.ts +4 -0
- package/src/contract/command.ts +22 -0
- package/src/contract/needs.ts +56 -1
- package/src/contract/protocol/bundle-install.ts +3 -0
- package/src/contract/unit.ts +11 -0
- package/src/host/adapter-types.ts +2 -0
- package/src/host/data-error.ts +3 -1
- package/src/host/http.ts +1 -0
- package/src/host/install-failure.test.ts +121 -0
- package/src/host/install-failure.ts +198 -0
- package/src/host/installed-sources.test.ts +80 -0
- package/src/host/installed-sources.ts +73 -0
- package/src/host/lib-install.ts +38 -2
- package/src/host/paths.test.ts +26 -0
- package/src/host/paths.ts +18 -0
- package/src/host/runtime-links.test.ts +31 -0
- package/src/host/runtime-links.ts +23 -0
- package/src/host/tool-approval.ts +11 -4
- package/src/host.ts +3 -0
- package/src/i18n/en.ts +41 -0
- package/src/i18n/zh.ts +41 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
forgetPackageSource,
|
|
7
|
+
installedSourcesFile,
|
|
8
|
+
packageSourceOf,
|
|
9
|
+
readInstalledSources,
|
|
10
|
+
rememberPackageSource,
|
|
11
|
+
} from './installed-sources.js';
|
|
12
|
+
|
|
13
|
+
const dirs: string[] = [];
|
|
14
|
+
|
|
15
|
+
function profile(): string {
|
|
16
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wb-sources-'));
|
|
17
|
+
dirs.push(dir);
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
for (const dir of dirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('按包记安装来源(检查更新只认这一份)', () => {
|
|
26
|
+
it('写一条再读回来:版本与源都在,文件是临时名换过去的', () => {
|
|
27
|
+
const dir = profile();
|
|
28
|
+
rememberPackageSource(dir, '@a/mod', { version: '0.3.1', registry: 'https://r1.example/' });
|
|
29
|
+
|
|
30
|
+
expect(packageSourceOf(dir, '@a/mod')).toEqual({ version: '0.3.1', registry: 'https://r1.example/' });
|
|
31
|
+
expect(fs.existsSync(`${installedSourcesFile(dir)}.tmp`)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('★ 同一个 scope 的两个包可以各记各的源(.npmrc 做不到这件事)', () => {
|
|
35
|
+
const dir = profile();
|
|
36
|
+
rememberPackageSource(dir, '@x/a', { version: '1.0.0', registry: 'https://r1.example/' });
|
|
37
|
+
rememberPackageSource(dir, '@x/b', { version: '2.0.0', registry: 'https://r2.example/' });
|
|
38
|
+
|
|
39
|
+
expect(packageSourceOf(dir, '@x/a')?.registry).toBe('https://r1.example/');
|
|
40
|
+
expect(packageSourceOf(dir, '@x/b')?.registry).toBe('https://r2.example/');
|
|
41
|
+
expect(readInstalledSources(dir)).toHaveProperty('@x/a');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('再装一次同一个包 → 覆盖成这次记录的版本与源', () => {
|
|
45
|
+
const dir = profile();
|
|
46
|
+
rememberPackageSource(dir, '@x/a', { version: '1.0.0', registry: 'https://r1.example/' });
|
|
47
|
+
rememberPackageSource(dir, '@x/a', { version: '1.1.0', registry: 'https://r2.example/' });
|
|
48
|
+
|
|
49
|
+
expect(packageSourceOf(dir, '@x/a')).toEqual({ version: '1.1.0', registry: 'https://r2.example/' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('卸载后删掉;没有这条时也不写空文件', () => {
|
|
53
|
+
const dir = profile();
|
|
54
|
+
rememberPackageSource(dir, '@x/a', { version: '1.0.0', registry: 'https://r1.example/' });
|
|
55
|
+
forgetPackageSource(dir, '@x/a');
|
|
56
|
+
|
|
57
|
+
expect(packageSourceOf(dir, '@x/a')).toBeUndefined();
|
|
58
|
+
const before = fs.readFileSync(installedSourcesFile(dir), 'utf8');
|
|
59
|
+
forgetPackageSource(dir, '@x/never');
|
|
60
|
+
expect(fs.readFileSync(installedSourcesFile(dir), 'utf8')).toBe(before);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('文件坏了 / 没有 / profile 不认识 → 一律当没有记录,不抛', () => {
|
|
64
|
+
const dir = profile();
|
|
65
|
+
fs.writeFileSync(installedSourcesFile(dir), '{ 坏的 json', 'utf8');
|
|
66
|
+
expect(readInstalledSources(dir)).toEqual({});
|
|
67
|
+
expect(packageSourceOf(dir, '@x/a')).toBeUndefined();
|
|
68
|
+
expect(readInstalledSources(null)).toEqual({});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('空包名 / 空源 / 空版本不落盘(别把没问到的写进去当事实)', () => {
|
|
72
|
+
const dir = profile();
|
|
73
|
+
rememberPackageSource(dir, ' ', { version: '1.0.0', registry: 'https://r1.example/' });
|
|
74
|
+
rememberPackageSource(dir, '@x/a', { version: '', registry: 'https://r1.example/' });
|
|
75
|
+
rememberPackageSource(dir, '@x/b', { version: '1.0.0', registry: ' ' });
|
|
76
|
+
|
|
77
|
+
expect(readInstalledSources(dir)).toEqual({});
|
|
78
|
+
expect(fs.existsSync(installedSourcesFile(dir))).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface InstalledSource {
|
|
5
|
+
readonly version: string;
|
|
6
|
+
readonly registry: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type InstalledSourceMap = Readonly<Record<string, InstalledSource>>;
|
|
10
|
+
|
|
11
|
+
const FILE_NAME = '.dsh-workbench-sources.json';
|
|
12
|
+
const FILE_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
function asRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {
|
|
15
|
+
return typeof value === 'object' && value !== null ? (value as Readonly<Record<string, unknown>>) : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function asText(value: unknown): string {
|
|
19
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function installedSourcesFile(profileDir: string): string {
|
|
23
|
+
return path.join(profileDir, FILE_NAME);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function readInstalledSources(profileDir: string | null): InstalledSourceMap {
|
|
27
|
+
if (profileDir === null) return {};
|
|
28
|
+
let parsed: unknown;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(fs.readFileSync(installedSourcesFile(profileDir), 'utf8'));
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
const packages = asRecord(asRecord(parsed)?.packages);
|
|
35
|
+
if (packages === undefined) return {};
|
|
36
|
+
const found: Record<string, InstalledSource> = {};
|
|
37
|
+
for (const [pkg, value] of Object.entries(packages)) {
|
|
38
|
+
const record = asRecord(value);
|
|
39
|
+
if (record === undefined) continue;
|
|
40
|
+
const version = asText(record.version);
|
|
41
|
+
const registry = asText(record.registry);
|
|
42
|
+
if (pkg === '' || version === '' || registry === '') continue;
|
|
43
|
+
found[pkg] = { version, registry };
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function packageSourceOf(profileDir: string | null, pkg: string): InstalledSource | undefined {
|
|
49
|
+
return readInstalledSources(profileDir)[pkg];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeInstalledSources(profileDir: string, sources: InstalledSourceMap): void {
|
|
53
|
+
const file = installedSourcesFile(profileDir);
|
|
54
|
+
const temp = `${file}.tmp`;
|
|
55
|
+
const text = `${JSON.stringify({ version: FILE_VERSION, packages: sources }, null, 2)}\n`;
|
|
56
|
+
fs.writeFileSync(temp, text, { mode: 0o600 });
|
|
57
|
+
fs.renameSync(temp, file);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function rememberPackageSource(profileDir: string, pkg: string, source: InstalledSource): void {
|
|
61
|
+
const trimmed = pkg.trim();
|
|
62
|
+
if (trimmed === '' || source.version.trim() === '' || source.registry.trim() === '') return;
|
|
63
|
+
const all = { ...readInstalledSources(profileDir) };
|
|
64
|
+
all[trimmed] = { version: source.version.trim(), registry: source.registry.trim() };
|
|
65
|
+
writeInstalledSources(profileDir, all);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function forgetPackageSource(profileDir: string, pkg: string): void {
|
|
69
|
+
const all = { ...readInstalledSources(profileDir) };
|
|
70
|
+
if (all[pkg] === undefined) return;
|
|
71
|
+
delete all[pkg];
|
|
72
|
+
writeInstalledSources(profileDir, all);
|
|
73
|
+
}
|
package/src/host/lib-install.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { LibNeed, SupplySource } from '../contract/needs.js';
|
|
1
|
+
import type { BinNeed, LibNeed, SupplySource } from '../contract/needs.js';
|
|
2
|
+
import type { HostOs } from '../contract/protocol/platform.js';
|
|
2
3
|
import { npmRegistryOf } from './plugin-source.js';
|
|
3
4
|
import { quoteShellArg } from './shell-text.js';
|
|
4
5
|
import { libsPrefix, npmCacheDir } from './paths.js';
|
|
@@ -9,10 +10,45 @@ export function libInstallCommand(options: {
|
|
|
9
10
|
readonly need: LibNeed;
|
|
10
11
|
readonly source: NpmSource;
|
|
11
12
|
readonly version?: string | undefined;
|
|
13
|
+
readonly env?: NodeJS.ProcessEnv | undefined;
|
|
12
14
|
}): string {
|
|
13
15
|
const { need, source, version } = options;
|
|
14
|
-
const registry = npmRegistryOf(source, process.env);
|
|
16
|
+
const registry = npmRegistryOf(source, options.env ?? process.env);
|
|
15
17
|
const spec = version ?? need.version ?? 'latest';
|
|
16
18
|
const registryFlag = registry.ok ? ` --registry ${quoteShellArg(registry.value)}` : '';
|
|
17
19
|
return `npm i --prefix ${quoteShellArg(libsPrefix())} ${quoteShellArg(`${need.name}@${spec}`)}${registryFlag} --cache ${quoteShellArg(npmCacheDir())}`;
|
|
18
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 这个 bin 在这台机器上怎么装:作者写了 `install` 且平台对得上才给出那条命令。
|
|
24
|
+
* `command` 原样照抄;`npm` 装进隔离运行时的共用依赖前缀(那份包里的 bin 会自动上 PATH)。
|
|
25
|
+
* @param need 一条 bin 需要。
|
|
26
|
+
* @param os 宿主平台(由平台适配器给出)。
|
|
27
|
+
* @param env 取 registry 环境变量的地方。
|
|
28
|
+
* @returns 一条能直接粘进终端的命令;没声明或平台不对就是 undefined。
|
|
29
|
+
*/
|
|
30
|
+
export function binInstallCommand(
|
|
31
|
+
need: BinNeed,
|
|
32
|
+
os: HostOs,
|
|
33
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
34
|
+
): string | undefined {
|
|
35
|
+
const install = need.install;
|
|
36
|
+
if (install === undefined) return undefined;
|
|
37
|
+
if (install.platforms !== undefined && !install.platforms.includes(os)) return undefined;
|
|
38
|
+
if (install.kind === 'command') {
|
|
39
|
+
const line = install.line.trim();
|
|
40
|
+
return line === '' ? undefined : line;
|
|
41
|
+
}
|
|
42
|
+
const packageName = install.package.trim();
|
|
43
|
+
if (packageName === '') return undefined;
|
|
44
|
+
return libInstallCommand({
|
|
45
|
+
need: { kind: 'lib', name: packageName, purpose: need.purpose },
|
|
46
|
+
source: {
|
|
47
|
+
kind: 'npm',
|
|
48
|
+
package: packageName,
|
|
49
|
+
...(install.registryEnv === undefined ? {} : { registryEnv: install.registryEnv }),
|
|
50
|
+
...(install.registryDefault === undefined ? {} : { registryDefault: install.registryDefault }),
|
|
51
|
+
},
|
|
52
|
+
env,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { delimiter } from 'node:path';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { ensureLibsBinOnPath, libsBinDir, npmCacheDir } from './paths.js';
|
|
4
|
+
|
|
5
|
+
describe('隔离运行时的路径', () => {
|
|
6
|
+
it('共用依赖的 bin 目录就在 libs 下面', () => {
|
|
7
|
+
expect(libsBinDir().endsWith('/opencli/libs/node_modules/.bin')).toBe(true);
|
|
8
|
+
expect(npmCacheDir().endsWith('/opencli/npm-cache')).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('★ 把 bin 目录前插一次;再插不会重复(PATH 不许越跑越长)', () => {
|
|
12
|
+
const env = { PATH: `/usr/bin${delimiter}/bin` };
|
|
13
|
+
const dir = libsBinDir();
|
|
14
|
+
|
|
15
|
+
expect(ensureLibsBinOnPath(env)).toBe(true);
|
|
16
|
+
expect(env.PATH).toBe(`${dir}${delimiter}/usr/bin${delimiter}/bin`);
|
|
17
|
+
expect(ensureLibsBinOnPath(env)).toBe(false);
|
|
18
|
+
expect(env.PATH.split(delimiter).filter((one) => one === dir)).toHaveLength(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('PATH 是空的也能插', () => {
|
|
22
|
+
const env: NodeJS.ProcessEnv = {};
|
|
23
|
+
expect(ensureLibsBinOnPath(env)).toBe(true);
|
|
24
|
+
expect(env.PATH).toBe(libsBinDir());
|
|
25
|
+
});
|
|
26
|
+
});
|
package/src/host/paths.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { delimiter } from 'node:path';
|
|
1
2
|
import { workbenchRoot } from './data-root.js';
|
|
2
3
|
|
|
3
4
|
export function isolatedRoot(): string {
|
|
@@ -35,3 +36,20 @@ export function vendorPeerLink(): { readonly dir: string; readonly parent: strin
|
|
|
35
36
|
export function npmCacheDir(): string {
|
|
36
37
|
return `${isolatedRoot()}/npm-cache`;
|
|
37
38
|
}
|
|
39
|
+
|
|
40
|
+
export function libsBinDir(): string {
|
|
41
|
+
return `${libsPrefix()}/node_modules/.bin`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 把隔离运行时的共用依赖 bin 目录放到 PATH 最前面(可执行文件都从这儿找)。
|
|
46
|
+
* @param env 改哪份环境(默认当前进程)。
|
|
47
|
+
* @returns 真的插进去了就 `true`;已经在里面就 `false`。
|
|
48
|
+
*/
|
|
49
|
+
export function ensureLibsBinOnPath(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
50
|
+
const dir = libsBinDir();
|
|
51
|
+
const parts = (env.PATH ?? '').split(delimiter).filter((one) => one !== '');
|
|
52
|
+
if (parts.includes(dir)) return false;
|
|
53
|
+
env.PATH = [dir, ...parts].join(delimiter);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { ensureLibsOpencliPeer } from './runtime-links.js';
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.unstubAllEnvs();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('ensureLibsOpencliPeer', () => {
|
|
12
|
+
it('★ 运行时那份 opencli 在 → 链到 libs,libs 里的命令才 import 得到它', () => {
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), 'wb-peer-'));
|
|
14
|
+
vi.stubEnv('DSH_WORKBENCH_HOME', root);
|
|
15
|
+
mkdirSync(join(root, 'opencli/toolchain/node_modules/@jackwener/opencli'), { recursive: true });
|
|
16
|
+
|
|
17
|
+
expect(ensureLibsOpencliPeer()).toBe(true);
|
|
18
|
+
const link = join(root, 'opencli/libs/node_modules/@jackwener/opencli');
|
|
19
|
+
expect(existsSync(link)).toBe(true);
|
|
20
|
+
expect(realpathSync(link)).toContain('toolchain/node_modules/@jackwener/opencli');
|
|
21
|
+
expect(ensureLibsOpencliPeer()).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('运行时还没装 opencli → 不建空链接(等它装好再说)', () => {
|
|
25
|
+
const root = mkdtempSync(join(tmpdir(), 'wb-peer-'));
|
|
26
|
+
vi.stubEnv('DSH_WORKBENCH_HOME', root);
|
|
27
|
+
|
|
28
|
+
expect(ensureLibsOpencliPeer()).toBe(false);
|
|
29
|
+
expect(existsSync(join(root, 'opencli/libs/node_modules/@jackwener/opencli'))).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, symlinkSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { OPENCLI_NPM_PACKAGE, libsPrefix, toolchainOpencliDir } from './paths.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 让共用依赖也能 import opencli:把隔离运行时里那份 opencli 链接到 libs 的 node_modules。
|
|
7
|
+
* 跑在 libs 里的命令(比如 opencli-workflow 带的 gitops)会按包名找 @jackwener/opencli,
|
|
8
|
+
* 找不到就起不来 —— 它不在 libs 的依赖里,是运行时提供的那一份。
|
|
9
|
+
* @returns 这次真的建了链接就 `true`;已经有了、或运行时那份还没装,就是 `false`。
|
|
10
|
+
*/
|
|
11
|
+
export function ensureLibsOpencliPeer(): boolean {
|
|
12
|
+
const link = `${libsPrefix()}/node_modules/${OPENCLI_NPM_PACKAGE}`;
|
|
13
|
+
if (existsSync(link)) return false;
|
|
14
|
+
const target = toolchainOpencliDir();
|
|
15
|
+
if (!existsSync(target)) return false;
|
|
16
|
+
try {
|
|
17
|
+
mkdirSync(dirname(link), { recursive: true });
|
|
18
|
+
symlinkSync(target, link);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export { APPROVAL_NOTE_ARG } from '../contract/approval.js';
|
|
2
|
+
|
|
3
|
+
import { APPROVAL_NOTE_ARG } from '../contract/approval.js';
|
|
4
|
+
|
|
1
5
|
export type AskDecision =
|
|
2
6
|
| {
|
|
3
7
|
readonly kind: 'ask';
|
|
@@ -16,8 +20,6 @@ export interface ApprovalDetail {
|
|
|
16
20
|
|
|
17
21
|
export type ApprovalFacts = ApprovalDetail | { readonly blocked: string };
|
|
18
22
|
|
|
19
|
-
export const APPROVAL_NOTE_ARG = 'approval_note';
|
|
20
|
-
|
|
21
23
|
export const APPROVAL_NOTE_LIMIT = 40;
|
|
22
24
|
|
|
23
25
|
export const APPROVAL_DETAIL_LIMIT = 20;
|
|
@@ -77,7 +79,8 @@ export interface ApprovalCall {
|
|
|
77
79
|
|
|
78
80
|
/**
|
|
79
81
|
* 套件对一次写调用给出的判定:`read` 只是看看、`allow` 会写但本套件不问、`deny` 这一次根本跑不成
|
|
80
|
-
* (连问都不该问,`reason` 原样交给 agent)、`ask` 要用户点头(`question` 接在明细后,`fallback`
|
|
82
|
+
* (连问都不该问,`reason` 原样交给 agent)、`ask` 要用户点头(`question` 接在明细后,`fallback` 是没明细时的整句;
|
|
83
|
+
* `noteInDetail` 为真表示套件把 agent 的附注画在自己的详情座里,标题只留动作、不再追加)。
|
|
81
84
|
*/
|
|
82
85
|
export type ApprovalVerdict =
|
|
83
86
|
| { readonly kind: 'read' }
|
|
@@ -91,6 +94,7 @@ export type ApprovalVerdict =
|
|
|
91
94
|
readonly reason: string;
|
|
92
95
|
readonly question: ApprovalDetail;
|
|
93
96
|
readonly fallback: ApprovalDetail;
|
|
97
|
+
readonly noteInDetail?: boolean;
|
|
94
98
|
};
|
|
95
99
|
|
|
96
100
|
export type ApprovalPolicy = (call: ApprovalCall) => ApprovalVerdict | undefined;
|
|
@@ -168,9 +172,12 @@ export async function approvalFor(
|
|
|
168
172
|
|
|
169
173
|
if (context.skipAsk === true) return undefined;
|
|
170
174
|
|
|
175
|
+
const note =
|
|
176
|
+
context.declaredNote === true && verdict.noteInDetail !== true ? approvalNoteOf(call) : undefined;
|
|
177
|
+
|
|
171
178
|
return {
|
|
172
179
|
kind: 'ask',
|
|
173
180
|
reason: verdict.reason,
|
|
174
|
-
displayReason: compose(facts, verdict,
|
|
181
|
+
displayReason: compose(facts, verdict, note),
|
|
175
182
|
};
|
|
176
183
|
}
|
package/src/host.ts
CHANGED
|
@@ -42,6 +42,8 @@ export * from './host/data-root.js';
|
|
|
42
42
|
export * from './host/shell-text.js';
|
|
43
43
|
export * from './host/shell.js';
|
|
44
44
|
export * from './host/data-error.js';
|
|
45
|
+
export * from './host/install-failure.js';
|
|
46
|
+
export * from './host/runtime-links.js';
|
|
45
47
|
export * from './host/provision.js';
|
|
46
48
|
export * from './host/channel-plugin.js';
|
|
47
49
|
export * from './host/recipe.js';
|
|
@@ -75,3 +77,4 @@ export * from './host/version-range.js';
|
|
|
75
77
|
export * from './host/market-index.js';
|
|
76
78
|
export * from './host/market-index-claim.js';
|
|
77
79
|
export * from './host/changelog.js';
|
|
80
|
+
export * from './host/installed-sources.js';
|
package/src/i18n/en.ts
CHANGED
|
@@ -156,6 +156,41 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
156
156
|
'panels.market.confirmNo': 'Cancel',
|
|
157
157
|
'panels.market.noMarketSource': 'The index has no newer version of this module (refresh the index and retry)',
|
|
158
158
|
'panels.market.failed': 'Failed: {reason}',
|
|
159
|
+
'panels.market.askAgent': 'Ask the agent',
|
|
160
|
+
'panels.market.askAgentCopied': 'Copied',
|
|
161
|
+
'panels.market.askAgentText':
|
|
162
|
+
'This workbench module cannot be used: "{name}" (unit {unit}) — {reason}. Work out what is missing and how to add it, then confirm it works.',
|
|
163
|
+
'panels.market.failure.permission': "Didn't install: no write access",
|
|
164
|
+
'panels.market.failure.diskFull': "Didn't install: the disk is full",
|
|
165
|
+
'panels.market.failure.sandbox': "Didn't install: the system blocked it",
|
|
166
|
+
'panels.market.failure.unknown': "Didn't install: cause not recognised",
|
|
167
|
+
'panels.market.failure.factOwner': '{path} belongs to {owner}, and this account ({me}) cannot write to it. Hand it back to yourself (asks for your login password).',
|
|
168
|
+
'panels.market.failure.factMode': '{path} is mode {mode}, so this account cannot write to it.',
|
|
169
|
+
'panels.market.failure.factSpace': 'The disk holding {path} is full.',
|
|
170
|
+
'panels.market.failure.factStuck': 'This command ran in a restricted environment and cannot write to {path}.',
|
|
171
|
+
'panels.market.failure.nowhere': '(the directory was not identified)',
|
|
172
|
+
'panels.market.failure.copyFix': 'Copy the fix command',
|
|
173
|
+
'panels.market.failure.copied': 'Copied',
|
|
174
|
+
'panels.market.failure.copiedHint': 'Paste it in a terminal, run it, then click Check again.',
|
|
175
|
+
'panels.market.failure.askAgent': 'Copy for the agent',
|
|
176
|
+
'panels.market.failure.askAgentText': 'Installing this module could not write: `{command}` exited {code} at {path}. Work out whether the sandbox or file permissions blocked it, then install again somewhere writable.',
|
|
177
|
+
'panels.market.failure.askAgentHint': 'Paste it into the chat and let the agent carry on.',
|
|
178
|
+
'panels.market.failure.recheck': 'Check again',
|
|
179
|
+
'panels.market.failure.showOutput': 'Show install output',
|
|
180
|
+
'panels.market.failure.hideOutput': 'Hide install output',
|
|
181
|
+
'panels.market.failure.term.commandLine': 'command on line {line}',
|
|
182
|
+
'panels.market.failure.term.signal': 'killed by {signal}',
|
|
183
|
+
'panels.market.failure.term.exitCode': 'exit code {exitCode}',
|
|
184
|
+
'panels.market.failure.term.noExitCode': 'no exit code',
|
|
185
|
+
'panels.market.failure.term.running': 'running',
|
|
186
|
+
'panels.market.failure.term.failed': 'failed',
|
|
187
|
+
'panels.market.failure.term.done': 'done',
|
|
188
|
+
'panels.market.failure.term.copy': 'Copy',
|
|
189
|
+
'panels.market.failure.term.copied': 'Copied',
|
|
190
|
+
'panels.market.failure.term.noOutput': 'No output',
|
|
191
|
+
'panels.market.failure.term.collapse': 'Collapse',
|
|
192
|
+
'panels.market.failure.term.expandAria': 'Show the remaining {hidden} lines',
|
|
193
|
+
'panels.market.failure.term.expand': 'Show {hidden} lines',
|
|
159
194
|
'panels.market.needEnv': 'Set {env} in the terminal that started the workbench first',
|
|
160
195
|
'panels.market.needCommands': 'Missing commands: {commands}',
|
|
161
196
|
'panels.market.needBinary': 'Install the {command} command first',
|
|
@@ -195,6 +230,9 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
195
230
|
'panels.market.checkingNow': 'Checking…',
|
|
196
231
|
'panels.market.checkFailed': 'The last check failed. Check your connection and retry.',
|
|
197
232
|
'panels.market.checkedAt': 'Last checked: {time}',
|
|
233
|
+
'panels.market.latestPartial': 'Could not check {n} modules',
|
|
234
|
+
'panels.market.latestNoRegistry': 'No source configured — the latest version was not checked',
|
|
235
|
+
'panels.market.latestProbeFailed': 'Could not reach the source — try again later',
|
|
198
236
|
'panels.market.justNow': 'just now',
|
|
199
237
|
'panels.market.minutesAgo': '{n} min ago',
|
|
200
238
|
'panels.market.hoursAgo': '{n} h ago',
|
|
@@ -670,6 +708,9 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
670
708
|
'approval.longTask.what': 'Task to stop',
|
|
671
709
|
'approval.longTask.signal':
|
|
672
710
|
'stopping only sends an interrupt signal \u2014 wrapping up the run is up to the module',
|
|
711
|
+
'approval.plugin.name': 'Plugin',
|
|
712
|
+
'approval.plugin.command': 'Command',
|
|
713
|
+
'approval.plugin.note': 'Agent says',
|
|
673
714
|
'approval.publish.module': 'Module',
|
|
674
715
|
'approval.publish.target': 'Target',
|
|
675
716
|
'approval.publish.version': 'Version',
|
package/src/i18n/zh.ts
CHANGED
|
@@ -153,6 +153,41 @@ export const zh = {
|
|
|
153
153
|
'panels.market.confirmNo': '取消',
|
|
154
154
|
'panels.market.noMarketSource': '索引里没有这个模块的新版本(刷新一次索引再试)',
|
|
155
155
|
'panels.market.failed': '没成功:{reason}',
|
|
156
|
+
'panels.market.askAgent': '问 agent',
|
|
157
|
+
'panels.market.askAgentCopied': '已复制',
|
|
158
|
+
'panels.market.askAgentText':
|
|
159
|
+
'工作台里「{name}」(单元 {unit})用不了:{reason}。帮我查清缺什么、怎么补,补完确认真能用。',
|
|
160
|
+
'panels.market.failure.permission': '没装上:没有写入权限',
|
|
161
|
+
'panels.market.failure.diskFull': '没装上:磁盘满了',
|
|
162
|
+
'panels.market.failure.sandbox': '没装上:被系统拦住了',
|
|
163
|
+
'panels.market.failure.unknown': '没装上:原因没认出来',
|
|
164
|
+
'panels.market.failure.factOwner': '{path} 属于 {owner},当前账号({me})写不进去。把它还给自己就行(会要开机密码)。',
|
|
165
|
+
'panels.market.failure.factMode': '{path} 的权限是 {mode},当前账号没有写权限。',
|
|
166
|
+
'panels.market.failure.factSpace': '{path} 所在的磁盘满了。',
|
|
167
|
+
'panels.market.failure.factStuck': '这条命令跑在受限环境里,写不进 {path}。',
|
|
168
|
+
'panels.market.failure.nowhere': '(没认出是哪个目录)',
|
|
169
|
+
'panels.market.failure.copyFix': '复制修复命令',
|
|
170
|
+
'panels.market.failure.copied': '已复制',
|
|
171
|
+
'panels.market.failure.copiedHint': '粘到终端跑一次,回来点「再检测」。',
|
|
172
|
+
'panels.market.failure.askAgent': '复制给 agent',
|
|
173
|
+
'panels.market.failure.askAgentText': '装这个模块时写不进去:`{command}` 退出码 {code},卡在 {path}。帮我看看是被沙箱拦的还是权限问题,换一条能写进去的路再装。',
|
|
174
|
+
'panels.market.failure.askAgentHint': '粘到对话里,让 agent 接着弄。',
|
|
175
|
+
'panels.market.failure.recheck': '再检测',
|
|
176
|
+
'panels.market.failure.showOutput': '查看安装输出',
|
|
177
|
+
'panels.market.failure.hideOutput': '收起安装输出',
|
|
178
|
+
'panels.market.failure.term.commandLine': '第 {line} 行的命令',
|
|
179
|
+
'panels.market.failure.term.signal': '被 {signal} 终止',
|
|
180
|
+
'panels.market.failure.term.exitCode': '退出码 {exitCode}',
|
|
181
|
+
'panels.market.failure.term.noExitCode': '没有退出码',
|
|
182
|
+
'panels.market.failure.term.running': '在跑',
|
|
183
|
+
'panels.market.failure.term.failed': '没跑通',
|
|
184
|
+
'panels.market.failure.term.done': '跑完了',
|
|
185
|
+
'panels.market.failure.term.copy': '复制',
|
|
186
|
+
'panels.market.failure.term.copied': '已复制',
|
|
187
|
+
'panels.market.failure.term.noOutput': '没有输出',
|
|
188
|
+
'panels.market.failure.term.collapse': '收起',
|
|
189
|
+
'panels.market.failure.term.expandAria': '展开剩下的 {hidden} 行',
|
|
190
|
+
'panels.market.failure.term.expand': '展开 {hidden} 行',
|
|
156
191
|
'panels.market.needEnv': '先在启动工作台的终端里设 {env}',
|
|
157
192
|
'panels.market.needCommands': '缺这几条命令:{commands}',
|
|
158
193
|
'panels.market.needBinary': '先装 {command} 命令',
|
|
@@ -192,6 +227,9 @@ export const zh = {
|
|
|
192
227
|
'panels.market.checkingNow': '正在检查…',
|
|
193
228
|
'panels.market.checkFailed': '最近一次没查到,检查网络后重试',
|
|
194
229
|
'panels.market.checkedAt': '最近检查:{time}',
|
|
230
|
+
'panels.market.latestPartial': '{n} 个模块没查成',
|
|
231
|
+
'panels.market.latestNoRegistry': '没配来源,未检查新版',
|
|
232
|
+
'panels.market.latestProbeFailed': '问不到源,稍后重试',
|
|
195
233
|
'panels.market.justNow': '刚刚',
|
|
196
234
|
'panels.market.minutesAgo': '{n} 分钟前',
|
|
197
235
|
'panels.market.hoursAgo': '{n} 小时前',
|
|
@@ -665,6 +703,9 @@ export const zh = {
|
|
|
665
703
|
'approval.record.noSwitch': '录完不会替你切回来(左栏底部会亮一条状态)',
|
|
666
704
|
'approval.longTask.what': '要停的那次任务',
|
|
667
705
|
'approval.longTask.signal': '停了只是让工作台发出中断信号 —— 这次任务怎么收尾由模块自己负责',
|
|
706
|
+
'approval.plugin.name': '插件',
|
|
707
|
+
'approval.plugin.command': '命令',
|
|
708
|
+
'approval.plugin.note': 'agent 说',
|
|
668
709
|
'approval.publish.module': '模块',
|
|
669
710
|
'approval.publish.target': '目标',
|
|
670
711
|
'approval.publish.version': '版本',
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ export * from './components/board-module.js';
|
|
|
35
35
|
export * from './components/icons.js';
|
|
36
36
|
export * from './config/constants.js';
|
|
37
37
|
export * from './config/storage.js';
|
|
38
|
+
export * from './contract/approval.js';
|
|
38
39
|
export * from './contract/blocker.js';
|
|
39
40
|
export * from './contract/data.js';
|
|
40
41
|
export * from './contract/extension.js';
|