@actionway/cli-dev 0.0.0-dev.32112453761.a2fa31914131
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 +91 -0
- package/assets/dev/actionway-dev-entry.mjs +19 -0
- package/assets/dev/actionway-dev-launcher.mjs +370 -0
- package/assets/dev/install-actionway-dev.mjs +185 -0
- package/assets/skill/actionway/SKILL.md +56 -0
- package/assets/skill/actionway/agents/openai.yaml +4 -0
- package/assets/skill/actionway-dev/SKILL.md +29 -0
- package/assets/skill/actionway-dev/agents/openai.yaml +4 -0
- package/dist/index.js +13940 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @actionway/cli — 本地端壳(bin: `actionway`)
|
|
2
|
+
|
|
3
|
+
actionway CLI 的本地端壳(迁移文档 `docs/DL_TO_ACTIONWAY_CLI_MIGRATION.md` §3.2 / §3.4):
|
|
4
|
+
终端用户在自己机器上的 Coding Agent(Codex / Claude Code)通过它调用 Actionway 能力。
|
|
5
|
+
|
|
6
|
+
## 安装
|
|
7
|
+
|
|
8
|
+
要求 Node.js 20 或更新版本;CI 覆盖 Node.js 20 和 22。Windows 使用 npm 生成的 `.cmd` 入口,避免 PowerShell
|
|
9
|
+
执行策略阻止 `.ps1` shim:
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
npm.cmd install --global @actionway/cli@latest
|
|
13
|
+
actionway.cmd doctor
|
|
14
|
+
actionway.cmd init
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
macOS / Linux:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install --global @actionway/cli@latest
|
|
21
|
+
actionway doctor
|
|
22
|
+
actionway init
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`doctor` 只输出脱敏后的 Node/npm、registry、global PATH、配置目录、loopback
|
|
26
|
+
和 OAuth discovery 检查;它不会修改 npm、代理、CA、PATH 或 PowerShell policy。
|
|
27
|
+
|
|
28
|
+
### Development 通道
|
|
29
|
+
|
|
30
|
+
成功合并并部署 `dev` 后,workflow 从同一 bundle 生成独立公共包 `@actionway/cli-dev`。它只暴露 `actionway-dev`,固定连接 `https://dev.actionway.ai`,使用 `${HOME}/.actionway-dev` 和 `actionway-dev` Skill;不会安装或覆盖正式 `actionway`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install --global @actionway/cli-dev@latest
|
|
34
|
+
actionway-dev doctor
|
|
35
|
+
actionway-dev init
|
|
36
|
+
actionway-dev status
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`actionway-dev update` 只读取和更新 `@actionway/cli-dev@latest`。每个 dev npm 版本包含不可变 source SHA 与 Actions run metadata;同一 tarball 也保存在私有 GitHub prerelease,后者仅用于内部审计和精确回滚,普通安装不需要 GitHub 账号。
|
|
40
|
+
|
|
41
|
+
- **auth 唯一途径:Clerk OAuth(PKCE)**。issuer / client_id 运行时从
|
|
42
|
+
`{public origin}/api/auth/cli-config` 发现;浏览器授权走随机 `127.0.0.1` 回调端口;
|
|
43
|
+
凭据存 `~/.actionway/credentials.json`(POSIX 上为 0600 owner-only 文件;
|
|
44
|
+
Windows 上 chmod 无效,保密性依赖 `%USERPROFILE%` 的继承 ACL——不要把
|
|
45
|
+
`ACTIONWAY_CONFIG_DIR` 指到共享目录)。CLI 从不打印或存储 OAuth client secret。
|
|
46
|
+
- **命令面 = 明码写出的注册表**(无 capability profile flag;production/development 只切换包、命令、状态、Skill、更新源与 Gateway):
|
|
47
|
+
- `tools search / inspect / call` 是长尾 capability 的规范发现与执行面;
|
|
48
|
+
- 高频 typed command 继续提供参数与文件 UX,但投影到同一个 Tool Call;
|
|
49
|
+
- `account usage / wallet / transactions` 只读 Actionway Business 数据;
|
|
50
|
+
- 本壳另有 `init` / `doctor` / `update` / `login` / `logout` / `whoami`,cli-core
|
|
51
|
+
继续提供 wait/poll 与已冻结的共享 typed commands。
|
|
52
|
+
|
|
53
|
+
- **账户响应无本地换算**:`account` 响应同时保留精确整数 micros 与可直接展示的 USD 十进制字符串;钱包包含快照更新时间,交易包含状态、说明、完成时间和 signed balance delta。Usage/Transactions 支持 ISO 时间窗口、领域筛选和 opaque cursor,返回的 summary 覆盖完整筛选集合而非当前页。CLI 不自行换算资金,也不改变服务端返回的字段语义。
|
|
54
|
+
- **计费零逻辑**:报价、扣费、错误面裁剪全在 gateway;本壳只透传。旧
|
|
55
|
+
stock download 直连 CDN、Gateway 无法观察结局,因此在阶段二的
|
|
56
|
+
command-level 计费编排完成前明确 fail-closed,不把迁移缺口当免费能力。
|
|
57
|
+
- **版本三链路**(与 pi-mono dl 的 actionway profile 同构):
|
|
58
|
+
1. 输出注入 —— 每个成功 JSON 输出带 `cli_version` + `skill_source`,
|
|
59
|
+
本地缓存显示有新版时追加 `notes` 更新提示(update 命令自身输出带
|
|
60
|
+
`update_available` 时跳过,防更新循环);
|
|
61
|
+
2. HTTP header —— 请求捎带 `x-actionway-cli-version`,响应的
|
|
62
|
+
`x-actionway-cli-latest` / `x-actionway-cli-min` 喂本地缓存;
|
|
63
|
+
3. registry —— `actionway update --check` 查 npm(24h 缓存),
|
|
64
|
+
`actionway update` 全局安装新版并 re-exec 刷新物化 skill。
|
|
65
|
+
- **skill 物化**:`actionway init` 把 `assets/skill/actionway` 拷到
|
|
66
|
+
`~/.actionway/skill/actionway` 并把 CLI 版本盖进 SKILL.md frontmatter
|
|
67
|
+
`metadata.version`;
|
|
68
|
+
Agent 从该路径自取安装——CLI 从不写入任何 Agent 自己的配置目录。包内
|
|
69
|
+
Skill 不复制 capability catalog;未知或长尾需求走 Search → Inspect → Call,
|
|
70
|
+
`confirmation_required` 必须由用户确认后恢复同一个 `call_ref`。
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pnpm cli init # 浏览器授权 + 物化 skill(首次安装入口)
|
|
74
|
+
pnpm cli whoami # 用户友好登录摘要 + 本地凭据快照(不出网)
|
|
75
|
+
pnpm cli logout # 删除本机凭据;重复执行也是成功的幂等操作
|
|
76
|
+
pnpm cli update --check # 查新版(24h 缓存)
|
|
77
|
+
pnpm cli -- tools search "generate a product image"
|
|
78
|
+
pnpm cli -- tools inspect media.image.generate
|
|
79
|
+
pnpm cli -- account usage
|
|
80
|
+
pnpm cli -- account usage --from 2026-08-01 --to 2026-09-01 --service generate-image
|
|
81
|
+
pnpm cli -- account transactions --from 2026-08-01 --kind top_up
|
|
82
|
+
pnpm cli -- generate-video --print-schema
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
发布 bundle 会把 cli-core 打进带 shebang 的 `dist/index.js`;`pnpm build`
|
|
86
|
+
生成该产物,`pnpm pack` 的 tarball 可由 Node 直接执行。手动发布由统一的
|
|
87
|
+
`.github/workflows/release.yml` 负责:勾选 `actionway_cli`(可单独选择,也可与 Worker 组合)并选择版本 bump 后,
|
|
88
|
+
workflow 以 npm 已发布的最高版本为基线生成下一版本,构建当前仓库的
|
|
89
|
+
tarball,验证真实 npm install 后的 `.bin/actionway`、`--help`、`--version`
|
|
90
|
+
和 `logout`,再发布到 `latest`。`@actionway/sandbox-cli` 继续独立于本地 CLI;
|
|
91
|
+
其 iLands rollout 尚未接入 Production Release,保留单独的手动发布 workflow。
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
process.env.ACTIONWAY_CLI_CHANNEL = "development";
|
|
9
|
+
process.env.ACTIONWAY_CONFIG_DIR ||= join(homedir(), ".actionway-dev");
|
|
10
|
+
process.env.ACTIONWAY_GATEWAY_URL ||= "https://dev.actionway.ai";
|
|
11
|
+
|
|
12
|
+
const executable = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "index.js");
|
|
13
|
+
const result = spawnSync(process.execPath, [executable, ...process.argv.slice(2)], {
|
|
14
|
+
stdio: "inherit",
|
|
15
|
+
env: process.env,
|
|
16
|
+
windowsHide: true,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
process.exit(result.status || 0);
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import {
|
|
6
|
+
cpSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
mkdtempSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { homedir, tmpdir } from "node:os";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
const repository = (process.env.ACTIONWAY_DEV_REPOSITORY || "PawLogic/actionway").trim();
|
|
18
|
+
const packageName = "@actionway/cli-dev";
|
|
19
|
+
const devHome = resolve(process.env.ACTIONWAY_DEV_HOME || join(homedir(), ".actionway-dev"));
|
|
20
|
+
const metadataPath = join(devHome, "current-cli.json");
|
|
21
|
+
const updateIntervalMs = Number(process.env.ACTIONWAY_DEV_UPDATE_INTERVAL_MS || 300_000);
|
|
22
|
+
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
23
|
+
|
|
24
|
+
export function commandInvocation(command, args, platform = process.platform, env = process.env) {
|
|
25
|
+
if (platform === "win32" && /\.(?:cmd|bat)$/i.test(command)) {
|
|
26
|
+
const commandProcessor = (env.ComSpec ?? env.COMSPEC ?? "cmd.exe").trim().replace(/^"|"$/g, "") || "cmd.exe";
|
|
27
|
+
return {
|
|
28
|
+
command: commandProcessor,
|
|
29
|
+
args: ["/d", "/s", "/c", command, ...args],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return { command, args: [...args] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function run(command, args, { capture = false, env = process.env } = {}) {
|
|
36
|
+
const invocation = commandInvocation(command, args, process.platform, env);
|
|
37
|
+
const result = spawnSync(invocation.command, invocation.args, {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
env,
|
|
40
|
+
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
41
|
+
windowsHide: true,
|
|
42
|
+
});
|
|
43
|
+
if (result.error) throw result.error;
|
|
44
|
+
if (result.status !== 0) {
|
|
45
|
+
if (capture && result.stderr) process.stderr.write(result.stderr);
|
|
46
|
+
const error = new Error(`${command} exited with status ${result.status}`);
|
|
47
|
+
error.exitCode = result.status || 1;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
return capture ? result.stdout.trim() : "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readMetadata() {
|
|
54
|
+
if (!existsSync(metadataPath)) return null;
|
|
55
|
+
return JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeMetadata(metadata) {
|
|
59
|
+
mkdirSync(devHome, { recursive: true, mode: 0o700 });
|
|
60
|
+
writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, { mode: 0o600 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function validateReleaseManifest(manifest, expectedRepository = repository) {
|
|
64
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
65
|
+
throw new Error("Development release manifest must be a JSON object.");
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
manifest.schema_version !== 1 ||
|
|
69
|
+
manifest.channel !== "development" ||
|
|
70
|
+
manifest.distribution !== "github_release"
|
|
71
|
+
) {
|
|
72
|
+
throw new Error("Development release manifest has an unsupported schema, channel, or distribution.");
|
|
73
|
+
}
|
|
74
|
+
if (manifest.repository !== expectedRepository) {
|
|
75
|
+
throw new Error("Development release manifest uses an unexpected repository.");
|
|
76
|
+
}
|
|
77
|
+
if (typeof manifest.source_sha !== "string" || !/^[0-9a-f]{40}$/.test(manifest.source_sha)) {
|
|
78
|
+
throw new Error("Development release manifest has an invalid source_sha.");
|
|
79
|
+
}
|
|
80
|
+
if (manifest.release_tag !== `actionway-dev-${manifest.source_sha}`) {
|
|
81
|
+
throw new Error("Development release manifest tag does not match source_sha.");
|
|
82
|
+
}
|
|
83
|
+
if (manifest.gateway_url !== "https://dev.actionway.ai") {
|
|
84
|
+
throw new Error("Development release manifest does not target the development Gateway.");
|
|
85
|
+
}
|
|
86
|
+
if (!manifest.archive || typeof manifest.archive !== "object" || Array.isArray(manifest.archive)) {
|
|
87
|
+
throw new Error("Development release manifest is missing archive metadata.");
|
|
88
|
+
}
|
|
89
|
+
if (typeof manifest.archive.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(manifest.archive.sha256)) {
|
|
90
|
+
throw new Error("Development release manifest has an invalid archive sha256.");
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
manifest.archive.asset_name !==
|
|
94
|
+
`actionway-cli-${manifest.source_sha}-${manifest.archive.sha256}.tgz`
|
|
95
|
+
) {
|
|
96
|
+
throw new Error("Development release archive asset does not match source_sha and sha256.");
|
|
97
|
+
}
|
|
98
|
+
if (!Number.isSafeInteger(manifest.archive.size_bytes) || manifest.archive.size_bytes <= 0) {
|
|
99
|
+
throw new Error("Development release manifest has an invalid archive size.");
|
|
100
|
+
}
|
|
101
|
+
if (!Number.isSafeInteger(manifest.run_id) || manifest.run_id <= 0) {
|
|
102
|
+
throw new Error("Development release manifest has an invalid run_id.");
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
manifest.run_url !==
|
|
106
|
+
`https://github.com/${manifest.repository}/actions/runs/${manifest.run_id}`
|
|
107
|
+
) {
|
|
108
|
+
throw new Error("Development release manifest has an invalid run_url.");
|
|
109
|
+
}
|
|
110
|
+
if (manifest.artifact_name !== `actionway-cli-dev-${manifest.source_sha}`) {
|
|
111
|
+
throw new Error("Development release manifest has an invalid artifact_name.");
|
|
112
|
+
}
|
|
113
|
+
if (typeof manifest.published_at !== "string" || !Number.isFinite(Date.parse(manifest.published_at))) {
|
|
114
|
+
throw new Error("Development release manifest has an invalid published_at timestamp.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
headSha: manifest.source_sha,
|
|
119
|
+
runId: manifest.run_id,
|
|
120
|
+
runUrl: manifest.run_url,
|
|
121
|
+
artifact: manifest.artifact_name,
|
|
122
|
+
releaseTag: manifest.release_tag,
|
|
123
|
+
archiveAsset: manifest.archive.asset_name,
|
|
124
|
+
archiveSha256: manifest.archive.sha256,
|
|
125
|
+
archiveSize: manifest.archive.size_bytes,
|
|
126
|
+
archivePath: null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveLatestRelease() {
|
|
131
|
+
const localArchive = process.env.ACTIONWAY_DEV_ARCHIVE?.trim();
|
|
132
|
+
if (localArchive) {
|
|
133
|
+
return {
|
|
134
|
+
headSha: (process.env.ACTIONWAY_DEV_SOURCE_SHA || "local-development").trim(),
|
|
135
|
+
runId: null,
|
|
136
|
+
runUrl: null,
|
|
137
|
+
artifact: null,
|
|
138
|
+
releaseTag: null,
|
|
139
|
+
archiveAsset: null,
|
|
140
|
+
archiveSha256: null,
|
|
141
|
+
archiveSize: null,
|
|
142
|
+
archivePath: resolve(localArchive),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const manifest = JSON.parse(run(npmCommand, ["view", `${packageName}@latest`, "--json"], { capture: true }));
|
|
147
|
+
if (manifest?.name !== packageName || typeof manifest.version !== "string") {
|
|
148
|
+
throw new Error(`npm did not return a valid ${packageName}@latest manifest.`);
|
|
149
|
+
}
|
|
150
|
+
const metadata = manifest.actionway;
|
|
151
|
+
if (
|
|
152
|
+
metadata?.channel !== "development" ||
|
|
153
|
+
!/^[0-9a-f]{40}$/.test(metadata.source_sha || "") ||
|
|
154
|
+
!Number.isSafeInteger(metadata.run_id) ||
|
|
155
|
+
metadata.run_id <= 0 ||
|
|
156
|
+
metadata.run_url !== `https://github.com/${repository}/actions/runs/${metadata.run_id}` ||
|
|
157
|
+
metadata.gateway_url !== "https://dev.actionway.ai"
|
|
158
|
+
) {
|
|
159
|
+
throw new Error(`The published ${packageName}@${manifest.version} metadata is invalid.`);
|
|
160
|
+
}
|
|
161
|
+
if (typeof manifest.dist?.integrity !== "string" || !manifest.dist.integrity.startsWith("sha512-")) {
|
|
162
|
+
throw new Error(`The published ${packageName}@${manifest.version} integrity is missing.`);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
headSha: metadata.source_sha,
|
|
166
|
+
runId: metadata.run_id,
|
|
167
|
+
runUrl: metadata.run_url,
|
|
168
|
+
artifact: `actionway-cli-dev-${metadata.source_sha}`,
|
|
169
|
+
releaseTag: `actionway-dev-${metadata.source_sha}`,
|
|
170
|
+
archiveAsset: null,
|
|
171
|
+
archiveSha256: null,
|
|
172
|
+
archiveSize: null,
|
|
173
|
+
archivePath: null,
|
|
174
|
+
packageVersion: manifest.version,
|
|
175
|
+
npmIntegrity: manifest.dist.integrity,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function installSkill(packageRoot) {
|
|
180
|
+
const source = join(packageRoot, "assets", "skill", "actionway-dev");
|
|
181
|
+
if (!existsSync(source)) throw new Error("The development CLI artifact is missing its actionway-dev Skill.");
|
|
182
|
+
const explicitTarget = process.env.ACTIONWAY_DEV_SKILL_DIR?.trim();
|
|
183
|
+
const officialRoot = join(homedir(), ".agents", "skills");
|
|
184
|
+
const legacyRoot = join(homedir(), ".codex", "skills");
|
|
185
|
+
const skillRoot = process.env.CODEX_HOME?.trim()
|
|
186
|
+
? join(resolve(process.env.CODEX_HOME), "skills")
|
|
187
|
+
: existsSync(legacyRoot) && !existsSync(officialRoot)
|
|
188
|
+
? legacyRoot
|
|
189
|
+
: officialRoot;
|
|
190
|
+
const target = explicitTarget ? resolve(explicitTarget) : join(skillRoot, "actionway-dev");
|
|
191
|
+
mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
|
|
192
|
+
rmSync(target, { recursive: true, force: true });
|
|
193
|
+
cpSync(source, target, { recursive: true });
|
|
194
|
+
return target;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function refreshInstalledAssets(metadata) {
|
|
198
|
+
const packageRoot = join(metadata.runtime_dir, "node_modules", "@actionway", "cli-dev");
|
|
199
|
+
if (!existsSync(packageRoot)) throw new Error("The installed Actionway development CLI package is missing.");
|
|
200
|
+
const skillDir = installSkill(packageRoot);
|
|
201
|
+
const packagedLauncher = join(packageRoot, "assets", "dev", "actionway-dev-launcher.mjs");
|
|
202
|
+
const installedLauncher = join(devHome, "bin", "actionway-dev-launcher.mjs");
|
|
203
|
+
if (existsSync(packagedLauncher)) {
|
|
204
|
+
mkdirSync(dirname(installedLauncher), { recursive: true, mode: 0o700 });
|
|
205
|
+
cpSync(packagedLauncher, installedLauncher, { force: true });
|
|
206
|
+
}
|
|
207
|
+
return { ...metadata, skill_dir: skillDir };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function verifyArchiveBytes(bytes, expectedSize, expectedSha256) {
|
|
211
|
+
const archive = Buffer.from(bytes);
|
|
212
|
+
if (archive.byteLength !== expectedSize) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Development archive size mismatch: expected ${expectedSize}, received ${archive.byteLength}.`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const actualSha256 = createHash("sha256").update(archive).digest("hex");
|
|
218
|
+
if (actualSha256 !== expectedSha256) throw new Error("Development archive checksum mismatch.");
|
|
219
|
+
return archive;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function installRelease(release) {
|
|
223
|
+
const downloadDir = mkdtempSync(join(tmpdir(), "actionway-dev-cli-"));
|
|
224
|
+
try {
|
|
225
|
+
let archivePath = release.archivePath;
|
|
226
|
+
if (!archivePath) {
|
|
227
|
+
const packed = JSON.parse(
|
|
228
|
+
run(npmCommand, ["pack", `${packageName}@${release.packageVersion}`, "--pack-destination", downloadDir, "--json"], {
|
|
229
|
+
capture: true,
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
const filename = packed?.[0]?.filename;
|
|
233
|
+
if (typeof filename !== "string") throw new Error(`npm pack did not return ${packageName}@${release.packageVersion}.`);
|
|
234
|
+
archivePath = join(downloadDir, filename);
|
|
235
|
+
const actualIntegrity = `sha512-${createHash("sha512").update(readFileSync(archivePath)).digest("base64")}`;
|
|
236
|
+
if (actualIntegrity !== release.npmIntegrity) throw new Error("Development npm archive integrity mismatch.");
|
|
237
|
+
}
|
|
238
|
+
if (!existsSync(archivePath)) throw new Error(`Actionway development CLI archive not found: ${archivePath}`);
|
|
239
|
+
|
|
240
|
+
const runtimeDir = join(devHome, "runtimes", release.headSha);
|
|
241
|
+
mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
|
|
242
|
+
run(npmCommand, [
|
|
243
|
+
"install",
|
|
244
|
+
"--ignore-scripts",
|
|
245
|
+
"--no-audit",
|
|
246
|
+
"--no-fund",
|
|
247
|
+
"--prefix",
|
|
248
|
+
runtimeDir,
|
|
249
|
+
archivePath,
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
const packageRoot = join(runtimeDir, "node_modules", "@actionway", "cli-dev");
|
|
253
|
+
const executable = join(runtimeDir, "node_modules", ".bin", process.platform === "win32" ? "actionway-dev.cmd" : "actionway-dev");
|
|
254
|
+
if (!existsSync(executable)) throw new Error("The development CLI artifact did not install an actionway-dev executable.");
|
|
255
|
+
const skillDir = installSkill(packageRoot);
|
|
256
|
+
|
|
257
|
+
const packagedLauncher = join(packageRoot, "assets", "dev", "actionway-dev-launcher.mjs");
|
|
258
|
+
const installedLauncher = join(devHome, "bin", "actionway-dev-launcher.mjs");
|
|
259
|
+
if (existsSync(packagedLauncher)) {
|
|
260
|
+
mkdirSync(dirname(installedLauncher), { recursive: true, mode: 0o700 });
|
|
261
|
+
cpSync(packagedLauncher, installedLauncher, { force: true });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const now = new Date().toISOString();
|
|
265
|
+
const metadata = {
|
|
266
|
+
version: 1,
|
|
267
|
+
source_sha: release.headSha,
|
|
268
|
+
run_id: release.runId,
|
|
269
|
+
run_url: release.runUrl,
|
|
270
|
+
artifact: release.artifact,
|
|
271
|
+
release_tag: release.releaseTag,
|
|
272
|
+
archive_asset: release.archiveAsset,
|
|
273
|
+
archive_sha256: release.archiveSha256,
|
|
274
|
+
package: packageName,
|
|
275
|
+
package_version: release.packageVersion ?? null,
|
|
276
|
+
npm_integrity: release.npmIntegrity ?? null,
|
|
277
|
+
runtime_dir: runtimeDir,
|
|
278
|
+
skill_dir: skillDir,
|
|
279
|
+
gateway_url: "https://dev.actionway.ai",
|
|
280
|
+
installed_at: now,
|
|
281
|
+
last_checked_at: now,
|
|
282
|
+
};
|
|
283
|
+
writeMetadata(metadata);
|
|
284
|
+
return metadata;
|
|
285
|
+
} finally {
|
|
286
|
+
rmSync(downloadDir, { recursive: true, force: true });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function checkForUpdate({ install, skipInterval = false } = {}) {
|
|
291
|
+
const current = readMetadata();
|
|
292
|
+
if (!skipInterval && current?.last_checked_at) {
|
|
293
|
+
const age = Date.now() - Date.parse(current.last_checked_at);
|
|
294
|
+
if (Number.isFinite(age) && age >= 0 && age < updateIntervalMs) return current;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const latest = resolveLatestRelease();
|
|
298
|
+
const updateAvailable = current?.source_sha !== latest.headSha;
|
|
299
|
+
if (!install) {
|
|
300
|
+
process.stdout.write(
|
|
301
|
+
`${JSON.stringify({
|
|
302
|
+
ok: true,
|
|
303
|
+
update_available: updateAvailable,
|
|
304
|
+
installed_source_sha: current?.source_sha || null,
|
|
305
|
+
latest_source_sha: latest.headSha,
|
|
306
|
+
})}\n`,
|
|
307
|
+
);
|
|
308
|
+
return current;
|
|
309
|
+
}
|
|
310
|
+
if (!updateAvailable) {
|
|
311
|
+
const refreshed = refreshInstalledAssets(current);
|
|
312
|
+
refreshed.last_checked_at = new Date().toISOString();
|
|
313
|
+
writeMetadata(refreshed);
|
|
314
|
+
return refreshed;
|
|
315
|
+
}
|
|
316
|
+
return installRelease(latest);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function executableFor(metadata) {
|
|
320
|
+
return join(
|
|
321
|
+
metadata.runtime_dir,
|
|
322
|
+
"node_modules",
|
|
323
|
+
".bin",
|
|
324
|
+
process.platform === "win32" ? "actionway-dev.cmd" : "actionway-dev",
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function main(args = process.argv.slice(2)) {
|
|
329
|
+
if (args[0] === "status") {
|
|
330
|
+
process.stdout.write(`${JSON.stringify({ ok: true, channel: "development", ...readMetadata() }, null, 2)}\n`);
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
if (args[0] === "update") {
|
|
334
|
+
const checkOnly = args.includes("--check");
|
|
335
|
+
const metadata = checkForUpdate({ install: !checkOnly, skipInterval: true });
|
|
336
|
+
if (!checkOnly && metadata) {
|
|
337
|
+
process.stdout.write(`Actionway development CLI is current at ${metadata.source_sha}.\n`);
|
|
338
|
+
}
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let metadata = readMetadata();
|
|
343
|
+
try {
|
|
344
|
+
metadata = checkForUpdate({ install: true }) || metadata;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (!metadata) throw error;
|
|
347
|
+
process.stderr.write(`Actionway development update check failed; using ${metadata.source_sha}: ${error.message}\n`);
|
|
348
|
+
}
|
|
349
|
+
if (!metadata) throw new Error("Actionway development CLI is not installed. Run the installer first.");
|
|
350
|
+
|
|
351
|
+
const executable = executableFor(metadata);
|
|
352
|
+
if (!existsSync(executable)) throw new Error("The installed Actionway development CLI runtime is missing.");
|
|
353
|
+
const childEnv = {
|
|
354
|
+
...process.env,
|
|
355
|
+
ACTIONWAY_CONFIG_DIR: devHome,
|
|
356
|
+
ACTIONWAY_GATEWAY_URL: "https://dev.actionway.ai",
|
|
357
|
+
};
|
|
358
|
+
const invocation = commandInvocation(executable, args.length > 0 ? args : ["--help"], process.platform, childEnv);
|
|
359
|
+
const result = spawnSync(invocation.command, invocation.args, {
|
|
360
|
+
stdio: "inherit",
|
|
361
|
+
env: childEnv,
|
|
362
|
+
windowsHide: true,
|
|
363
|
+
});
|
|
364
|
+
if (result.error) throw result.error;
|
|
365
|
+
return result.status || 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (process.env.ACTIONWAY_DEV_ASSET_IMPORT_ONLY !== "1") {
|
|
369
|
+
process.exit(main());
|
|
370
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import {
|
|
5
|
+
accessSync,
|
|
6
|
+
chmodSync,
|
|
7
|
+
constants,
|
|
8
|
+
copyFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
const repository = (process.env.ACTIONWAY_DEV_REPOSITORY || "PawLogic/actionway").trim();
|
|
19
|
+
const bootstrapRef = (process.env.ACTIONWAY_DEV_BOOTSTRAP_REF || "dev").trim();
|
|
20
|
+
const devHome = resolve(process.env.ACTIONWAY_DEV_HOME || join(homedir(), ".actionway-dev"));
|
|
21
|
+
const installedLauncher = join(devHome, "bin", "actionway-dev-launcher.mjs");
|
|
22
|
+
const marker = "Actionway managed development launcher";
|
|
23
|
+
|
|
24
|
+
export function commandInvocation(command, args, platform = process.platform, env = process.env) {
|
|
25
|
+
if (platform === "win32" && /\.(?:cmd|bat)$/i.test(command)) {
|
|
26
|
+
const commandProcessor = (env.ComSpec ?? env.COMSPEC ?? "cmd.exe").trim().replace(/^"|"$/g, "") || "cmd.exe";
|
|
27
|
+
return {
|
|
28
|
+
command: commandProcessor,
|
|
29
|
+
args: ["/d", "/s", "/c", command, ...args],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return { command, args: [...args] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function run(command, args, { capture = false } = {}) {
|
|
36
|
+
const invocation = commandInvocation(command, args);
|
|
37
|
+
const result = spawnSync(invocation.command, invocation.args, {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
env: process.env,
|
|
40
|
+
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
41
|
+
windowsHide: true,
|
|
42
|
+
});
|
|
43
|
+
if (result.error) throw result.error;
|
|
44
|
+
if (result.status !== 0) {
|
|
45
|
+
if (capture && result.stderr) process.stderr.write(result.stderr);
|
|
46
|
+
throw new Error(`${command} exited with status ${result.status || 1}`);
|
|
47
|
+
}
|
|
48
|
+
return capture ? result.stdout : "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function requireNode20() {
|
|
52
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
53
|
+
if (!Number.isInteger(major) || major < 20) {
|
|
54
|
+
throw new Error(`Actionway development requires Node.js 20 or newer; found ${process.version}.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function pathEntries() {
|
|
59
|
+
return (process.env.PATH || "")
|
|
60
|
+
.split(delimiter)
|
|
61
|
+
.map((entry) => entry.trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((entry) => resolve(entry));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function samePath(left, right) {
|
|
67
|
+
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isOnPath(directory) {
|
|
71
|
+
const target = resolve(directory);
|
|
72
|
+
return pathEntries().some((entry) => samePath(entry, target));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ensureWritable(directory) {
|
|
76
|
+
try {
|
|
77
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
78
|
+
accessSync(directory, constants.W_OK);
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function existingActionwayBin() {
|
|
86
|
+
const executableNames = process.platform === "win32" ? ["actionway.cmd", "actionway.exe", "actionway"] : ["actionway"];
|
|
87
|
+
for (const directory of pathEntries()) {
|
|
88
|
+
if (executableNames.some((name) => existsSync(join(directory, name))) && ensureWritable(directory)) return directory;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function npmGlobalBin() {
|
|
94
|
+
try {
|
|
95
|
+
const prefix = run(process.platform === "win32" ? "npm.cmd" : "npm", ["prefix", "--global"], {
|
|
96
|
+
capture: true,
|
|
97
|
+
}).trim();
|
|
98
|
+
if (!prefix) return null;
|
|
99
|
+
const directory = process.platform === "win32" ? resolve(prefix) : resolve(prefix, "bin");
|
|
100
|
+
return isOnPath(directory) && ensureWritable(directory) ? directory : null;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveBinDir() {
|
|
107
|
+
if (process.env.ACTIONWAY_DEV_BIN_DIR?.trim()) {
|
|
108
|
+
const explicit = resolve(process.env.ACTIONWAY_DEV_BIN_DIR.trim());
|
|
109
|
+
if (!ensureWritable(explicit)) throw new Error(`Development CLI bin directory is not writable: ${explicit}`);
|
|
110
|
+
return explicit;
|
|
111
|
+
}
|
|
112
|
+
return existingActionwayBin() || npmGlobalBin() || join(homedir(), ".local", "bin");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function localLauncherSource() {
|
|
116
|
+
const candidates = [
|
|
117
|
+
process.env.ACTIONWAY_DEV_LAUNCHER?.trim(),
|
|
118
|
+
join(dirname(fileURLToPath(import.meta.url)), "actionway-dev-launcher.mjs"),
|
|
119
|
+
join(process.cwd(), "packages", "cli", "assets", "dev", "actionway-dev-launcher.mjs"),
|
|
120
|
+
].filter(Boolean);
|
|
121
|
+
return candidates.find((candidate) => existsSync(resolve(candidate))) || null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function fetchLauncher() {
|
|
125
|
+
const endpoint = `repos/${repository}/contents/packages/cli/assets/dev/actionway-dev-launcher.mjs?ref=${encodeURIComponent(bootstrapRef)}`;
|
|
126
|
+
const source = run(
|
|
127
|
+
"gh",
|
|
128
|
+
["api", "-H", "Accept: application/vnd.github.raw+json", endpoint],
|
|
129
|
+
{ capture: true },
|
|
130
|
+
);
|
|
131
|
+
if (!source.startsWith("#!/usr/bin/env node")) {
|
|
132
|
+
throw new Error(`GitHub did not return the Actionway development launcher from ${repository}@${bootstrapRef}.`);
|
|
133
|
+
}
|
|
134
|
+
return source;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function installLauncher() {
|
|
138
|
+
mkdirSync(dirname(installedLauncher), { recursive: true, mode: 0o700 });
|
|
139
|
+
const localSource = localLauncherSource();
|
|
140
|
+
if (localSource) copyFileSync(resolve(localSource), installedLauncher);
|
|
141
|
+
else writeFileSync(installedLauncher, fetchLauncher(), { mode: 0o600 });
|
|
142
|
+
run(process.execPath, ["--check", installedLauncher], { capture: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function installShim(binDir) {
|
|
146
|
+
if (!ensureWritable(binDir)) throw new Error(`Development CLI bin directory is not writable: ${binDir}`);
|
|
147
|
+
const shimPath = join(binDir, process.platform === "win32" ? "actionway-dev.cmd" : "actionway-dev");
|
|
148
|
+
if (existsSync(shimPath) && !readFileSync(shimPath, "utf8").includes(marker)) {
|
|
149
|
+
throw new Error(`Refusing to overwrite unmanaged executable: ${shimPath}`);
|
|
150
|
+
}
|
|
151
|
+
const shim =
|
|
152
|
+
process.platform === "win32"
|
|
153
|
+
? `@echo off\r\nrem ${marker}\r\nnode "${installedLauncher}" %*\r\n`
|
|
154
|
+
: `#!/usr/bin/env node\n// ${marker}\nimport(${JSON.stringify(pathToFileURL(installedLauncher).href)});\n`;
|
|
155
|
+
writeFileSync(shimPath, shim, { mode: 0o755 });
|
|
156
|
+
if (process.platform !== "win32") chmodSync(shimPath, 0o755);
|
|
157
|
+
return shimPath;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function main() {
|
|
161
|
+
requireNode20();
|
|
162
|
+
run(process.platform === "win32" ? "npm.cmd" : "npm", ["--version"], { capture: true });
|
|
163
|
+
if (!process.env.ACTIONWAY_DEV_ARCHIVE?.trim()) {
|
|
164
|
+
run("gh", ["auth", "status", "--hostname", "github.com"], { capture: true });
|
|
165
|
+
}
|
|
166
|
+
installLauncher();
|
|
167
|
+
const binDir = resolveBinDir();
|
|
168
|
+
const shimPath = installShim(binDir);
|
|
169
|
+
run(process.execPath, [installedLauncher, "update"]);
|
|
170
|
+
|
|
171
|
+
process.stdout.write(`Installed ${shimPath}.\n`);
|
|
172
|
+
if (!isOnPath(binDir)) {
|
|
173
|
+
process.stdout.write(`Add ${binDir} to PATH, then open a new terminal.\n`);
|
|
174
|
+
}
|
|
175
|
+
process.stdout.write("Stable command: actionway\nDevelopment command: actionway-dev\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (process.env.ACTIONWAY_DEV_ASSET_IMPORT_ONLY !== "1") {
|
|
179
|
+
try {
|
|
180
|
+
main();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
process.stderr.write(`Actionway development installation failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|