Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4e72fbf89 | ||
|
|
1d0fbea069 | ||
|
|
54b2c16a76 | ||
|
|
8f316b7a84 | ||
|
|
3aac7e0e13 | ||
|
|
5d7f1442c3 | ||
|
|
7cabf1c821 | ||
|
|
8cf0112717 | ||
|
|
8c36343570 | ||
|
|
bb4bd2d412 | ||
|
|
e3098f2225 | ||
|
|
e79718a5fb
|
||
|
|
fbac1003f0 | ||
|
|
075445250f |
@@ -0,0 +1,73 @@
|
||||
# Octopus
|
||||
|
||||
The AI coding agent built for the terminal.
|
||||
|
||||
## Install
|
||||
|
||||
**Unix (Linux/macOS):**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://eightarms.net/Octopus/octopus-release/raw/branch/main/install | bash
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
irm https://eightarms.net/Octopus/octopus-release/raw/branch/main/install.ps1 | iex
|
||||
```
|
||||
|
||||
### Manual install
|
||||
|
||||
If the one-liner fails with a connection error, the direct connection to `eightarms.net` is likely being filtered on your network. Download the archive for your platform from the [releases page](https://eightarms.net/Octopus/octopus-release/releases/latest) and install it manually instead.
|
||||
|
||||
**Unix (Linux/macOS):**
|
||||
|
||||
```bash
|
||||
# after downloading octopus-<platform>.tar.gz from the releases page
|
||||
tar -xzf octopus-linux-x64.tar.gz
|
||||
sudo mv octopus /usr/local/bin/
|
||||
octopus --version
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
After downloading `octopus-windows-x64.zip` from the releases page:
|
||||
|
||||
```powershell
|
||||
$zip = "$env:USERPROFILE\Downloads\octopus-windows-x64.zip"
|
||||
$dest = "$env:USERPROFILE\.octopus\bin"
|
||||
$tmp = Join-Path $env:TEMP "octopus_manual"
|
||||
New-Item -ItemType Directory -Force -Path $dest,$tmp | Out-Null
|
||||
Expand-Archive -Path $zip -DestinationPath $tmp -Force
|
||||
Copy-Item "$tmp\octopus.exe" "$dest\octopus.exe" -Force
|
||||
Remove-Item -Recurse -Force $tmp
|
||||
$p = [Environment]::GetEnvironmentVariable('Path','User')
|
||||
if ($p -notlike "*$dest*") { [Environment]::SetEnvironmentVariable('Path', ($p.TrimEnd(';') + ";$dest"), 'User') }
|
||||
& "$dest\octopus.exe" --version
|
||||
```
|
||||
|
||||
Open a **new** terminal and run `octopus`.
|
||||
|
||||
## Desktop (beta)
|
||||
|
||||
Prefer a GUI? The Octopus desktop app ships a Windows beta build alongside the CLI:
|
||||
|
||||
- **Windows:** download [`octopus-desktop-win-x64.exe`](https://eightarms.net/Octopus/octopus-release/releases/download/desktop-beta/octopus-desktop-win-x64.exe) from the [`desktop-beta` channel](https://eightarms.net/Octopus/octopus-release/releases/tag/desktop-beta) and run the installer. Installed builds auto-update themselves from the same channel.
|
||||
- This is a beta channel — expect rough edges. macOS builds are on the way but not published yet.
|
||||
|
||||
## Documentation
|
||||
|
||||
Visit the [documentation](https://eightarms.net/Octopus/octopus/wiki) for usage guides and API reference.
|
||||
|
||||
## Workflow
|
||||
|
||||
Curious how Octopus organizes agent work? Read about the **ticket-driven workflow system** (in Chinese): [docs/workflow.zh.md](docs/workflow.zh.md)
|
||||
|
||||
## Development
|
||||
|
||||
The source code and issue tracker are available at the [private development repository](https://eightarms.net/Octopus/octopus).
|
||||
|
||||
## License
|
||||
|
||||
MIT License — see the source repository for details.
|
||||
@@ -0,0 +1,162 @@
|
||||
# Octopus 工单驱动的工作流体系
|
||||
|
||||
> Octopus 不只是一个会写代码的终端 AI 代理——它是一套**以工单为契约、以门禁为骨架**的工程化工作流体系。每一个功能、每一个修复、每一次重构,都作为一张类型化工单进入管线,沿着确定的路由前进,穿过强制的评审与验证门禁,最终带着完整的可审计记录落进主干。
|
||||
|
||||
## 为什么需要工单驱动
|
||||
|
||||
AI 代理写代码的能力已经足够强,真正的工程难题在于**治理**。在 Octopus 中,工单(issue)不是旁路的需求登记簿,而是流程的驱动轴,它同时解决四件事:
|
||||
|
||||
| 难题 | 工单驱动给出的答案 |
|
||||
| --- | --- |
|
||||
| **边界** | 一张工单天然划定"做什么 / 不做什么",代理的每次行动都有明确授权范围 |
|
||||
| **验收** | 工单携带验收标准(acceptance criteria),完成与否可被第三方验证,而不是"看起来对了" |
|
||||
| **审计** | 从分解、评审、实现到验证,每一步都产出持久化工件,事后可以回答"这个变更为什么发生、经过谁之手" |
|
||||
| **并行** | 多个人类与多个代理会话基于工单分工协作,互不踩踏(配合 git worktree 会话隔离) |
|
||||
|
||||
支撑这一切的铁律是**声明纪律(claim discipline)**:任何状态断言——"测试通过"、"缺陷已修复"、"工作完成"——都必须有**当轮产出的新鲜证据**(命令、输出、退出码)支撑。置信不是证据,上一次运行的结果不是证据,"改动看起来对"更不是证据。
|
||||
|
||||
## 类型驱动路由:标签即流程
|
||||
|
||||
每张工单的 `Kind/*` 标签决定它的整条流程路线——进入哪个技能、跳过哪些阶段、哪些门禁必须保留。这套路由由一张**路由表**(`workflow-routing.yaml`)声明式定义,主会话在任何动作之前先经 **Step 0 路由解析**(`route_resolver`)查表,而不是临场自由发挥。
|
||||
|
||||
当前路由表速览:
|
||||
|
||||
| 工单类型 | 入口技能 | 保留的强制门禁 | 语义要点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Kind/Epic` | `analyze-dag` | review-dag, verify | 需求聚合 → 分解为任务 DAG(见下节) |
|
||||
| `Kind/Feature` | `analyze-dag` | review-dag, review-code, verify | 新功能同样走 DAG 管线;退化情形(工单自带实现)保留 review-code |
|
||||
| `Kind/Task` | 继承父级 | review-code, verify | DAG 父级下的子工单,走任务路由 implement → review-code → verify |
|
||||
| `Kind/Bug` | `implement`(bugfix 模式) | review-code, verify | 缺陷报告本身就是规格说明——无需需求/设计阶段,但修复必须被证明 |
|
||||
| `Kind/Security` | `implement`(bugfix 模式) | review-code, verify | 紧急缺陷处理;若改动认证/权限契约,属设计级触发 → 升格 `Kind/Feature` |
|
||||
| `Kind/Refactor` | `implement`(refactor 模式) | review-code, verify | 行为保持不变的重构;大型跨文件重构在迭代内分批 |
|
||||
| `Kind/Testing` | `implement` | review-code, verify | 仅测试变更;verify 强制(测试必须跑绿) |
|
||||
| `Kind/Process` | `review-artifact`(过程审计) | audit-process | 改的是 `.octopus/` 工作流系统本身——先审计再落地 |
|
||||
| `Kind/Documentation` | 直接编辑 | (可选轻量评审) | 文档不进完整管线,除非它编码了一份契约 |
|
||||
| `Kind/MVP` | 交互模式(无管线) | 无 | 早期概念:工单体即文档,维护决策日志与债务登记 |
|
||||
| 无标签 | 自然语言匹配 | 按匹配结果 | 未标注的工单行为与路由启用前完全一致 |
|
||||
|
||||
三条结构性原则:
|
||||
|
||||
1. **路由是纯增量的(additive)**:路由只能跳过某类工单*不需要*的阶段,永远不能削弱强制门禁。路由表的 `skip` 与技能自身的强制门取**交集**——两边都同意可跳,才算可跳。
|
||||
2. **标签错了不悄悄改**:如果 `Kind/Bug` 挂在了一个功能形态的工单上,代理不会擅自改标签,而是按标签走路由、同时显式上报不匹配——标签的正确性归创建者所有。
|
||||
3. **大 Bug 升格规则**:根因是设计决策、或改动共享契约/公共 API/数据迁移的缺陷,已经超出了 Bug 类型的容量——强制升格 `Kind/Feature` 走小型 DAG(预期 1–3 节点),绝不把设计级变更硬塞进 bugfix 通道。
|
||||
|
||||
## DAG 工单管线:分解即计划
|
||||
|
||||
`Kind/Epic` 与 `Kind/Feature` 进入 **DAG 工单管线**——工作流体系最核心的特征。传统做法里"路线图 → 需求 → 设计 → 迭代计划"是四份会互相漂移的文档;Octopus 把它们合并成**一份任务 DAG 工件**:
|
||||
|
||||
```
|
||||
analyze-dag 分解 ──▶ 任务 DAG 工件 ──▶ review-dag 单门 ──PASS──▶ 建 Kind/Task 子工单
|
||||
(节点=验收标准 (TOPO/REQMAP/ │
|
||||
边=跨节点契约 RELEASE 三维评审) ▼
|
||||
拓扑=执行计划) 每张子工单:
|
||||
implement → review-code → verify
|
||||
```
|
||||
|
||||
**分解(analyze-dag)**。Analyst 把 Epic 分解为一张任务 DAG:每个 `task` 节点就是一条可验收的工作项,每条边是节点之间的**契约**(跨会话接口承诺),拓扑顺序就是执行计划。分解产出即冻结副本,后续所有会话以它为共同基线。
|
||||
|
||||
**单门评审(review-dag)**。三个维度并行评审:**TOPO**(拓扑合理吗)、**REQMAP**(验收标准覆盖需求吗)、**RELEASE**(切分可发布吗)。这一道门替代了旧体系的"设计空间评审 + 迭代计划评审"两道门,且**结构性永不裁剪**(never_trim)。
|
||||
|
||||
**深度即档位(size_derivation)**。评审深度不是拍脑袋定的,由 DAG 的实测属性派生:
|
||||
|
||||
| 维度 | D1 | D2 | D3 | D4 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 节点数 | ≤3 | 4–8 | 9–15 | >15 |
|
||||
| 跨会话边数 | 0 | — | ≤3 | ≥4 |
|
||||
| 契约变更面 | 无 | — | 仅新增 | 破坏性 |
|
||||
|
||||
取三维最坏值定 D1–D4,D1 由 1 名评审员 2 轮收敛,D4 是 5 名评审员(TOPO:2 REQMAP:2 RELEASE:1 分维)最多 4 轮。**分解即定档**——不再需要人工挂规模标签。
|
||||
|
||||
**运行期增长信号**。实现期现实与分解漂移时,管线不装死:节点分裂、出现评审范围外的新跨会话边、已冻结契约被破坏——任何一条触发都强制**重新派生深度 + 重跑 review-dag**。工单类型也不是一成不变的:现实超出标签容量时,"按书面执行、把疑点摆上桌面"、可见地重分类。
|
||||
|
||||
**里程碑汇聚(verify_milestone)**。跨会话扇入 ≥2 的汇聚点自动焊入里程碑;全部入边任务完成且绿灯后触发集成验证(DoD 矩阵由 DAG 切片自动生成)。里程碑本身不建工单——它的完成定义由 verify 承担。
|
||||
|
||||
## 强制门禁:review-code 与 verify
|
||||
|
||||
管线里的每段生产工作后面都紧跟一道评审门禁,**没有例外路径**:
|
||||
|
||||
- **review-code**——代码评审门。按风险档位并行派出最多 10 名独立评审员(Explorer 子代理),每人一个维度对照评审清单扫描,Worker 编排者汇总发现、开发者修订,迭代至收敛。DAG 任务模式下评审对照冻结的 DAG 节点规格与跨会话契约做范围比对——节点实现蔓延出分解边界本身就是发现。
|
||||
- **verify**——验证门。运行迭代的 DoD(完成定义)矩阵、集成测试、NFR 校验与回归检查。**迭代在所有 DoD 项通过之前不算完成**——无论代码看起来多好。
|
||||
- **评审轮次预算**。默认 2 轮、高风险 3 轮封顶——超预算未收敛即 STOP,剩余发现转入技术债务登记册收口,不允许无限马拉松式评审。
|
||||
|
||||
## 人机决策门:自动批准的边界
|
||||
|
||||
代理需要不停地做流程决策("评审通过了,进入下一阶段吗?")。Octopus 把决策门分成性质完全不同的两类:
|
||||
|
||||
**执行类门禁可以白名单放行**。`auto_approve.stages` 声明哪些阶段的问题可自动通过(如 `review-code`、`verify`、`audit-process`)。即便自动放行,还有一条**活人降级链**:有真人在线看着会话(L1)→ 问题落到 Gitea 工单评论等回复(L2)→ 才轮到推荐项自动默认(L3)。
|
||||
|
||||
**业务决策永远必须人答**。以下类别的问题**无条件强制人工确认**(`destructive` 铁律,任何白名单都不覆盖):
|
||||
|
||||
1. 合并 PR 到主干 / 打 tag / 发版 / 上生产
|
||||
2. 生产数据库写入(含迁移、回填)
|
||||
3. 计费与套餐变更
|
||||
4. 组织 / 租户结构变更
|
||||
5. 删除持久资源
|
||||
6. 超出会话生命周期的持久配置写入
|
||||
|
||||
一条阶段白名单授予的是"执行门可以不间断前进",**永远不是**"业务决策可以自问自答"。
|
||||
|
||||
## 工单生命周期治理
|
||||
|
||||
- **认领与 WIP 上限**:开放的无主工作票超过上限(默认 40 张)时,新工作票必须携带排期证据,否则降为登记册行——用机制而非纪律防止工单堆积。
|
||||
- **冲刺降档(sprint-mode)**:冲刺期间独立工单创建冻结、登记照常(严重度零丢失),开关回落后登记行按正常链路升格。失败证据类工单不受降档影响——失败必须立即可见。
|
||||
- **MVP 早期概念模式**:一张工单、一个线程、零管线开销;工单体即文档(每决策一行:选了什么/为什么/何时失效)。出现第二个消费者、外部契约或决策冲突即"毕业"——升格标签,反向进入 DAG 管线的回填模式。
|
||||
- **生产者预检(preflight)**:复盘阶段统计每条路线首轮评审 FAIL/WARN 的高频根因,把 Top 根因作为检查清单**注入**该路线的实现派发(如"动手前每个验收标准必须映射到可证伪的测试断言")——在人犯错之前提醒人,而不是在评审里再抓一遍。清单项的进入与退出都有数据阈值,提示词永不自动变异。
|
||||
|
||||
## 两层工件体系:过程即档案
|
||||
|
||||
每个工作流运行(run)的所有产物落入**两层工件体系**,索引守卫保证可恢复、可审计:
|
||||
|
||||
| 层 | 位置 | 受众 | 内容 |
|
||||
| --- | --- | --- | --- |
|
||||
| Tier 1 | `.octopus/runs/{slug}/` | 机器 | 运行索引、DAG 冻结副本、每轮评审发现 JSON、DoD 矩阵 |
|
||||
| Tier 2 | Gitea wiki `{slug}/` 空间 | 人 | 评审综合报告、计划页、叙述性文档 |
|
||||
|
||||
配套的**压缩纪律**保证长管线跑得完:上下文压缩只发生在干净的阶段边界(工件已持久化之后),且由容量触发器驱动;压缩后代理不依赖对话记忆,而是**重读持久化工件**恢复现场。任何压缩摘要开头都有身份卡,先核对身份再继续干活——防止拿着上一个会话的残影做这个会话的决定。
|
||||
|
||||
## 多智能体协作模型
|
||||
|
||||
Octopus 的主会话是**协调者而非工人**:
|
||||
|
||||
- **Explorer 子代理**——只读探索:找文件、搜模式、回答"X 怎么工作",结果即时返回;
|
||||
- **Worker 子代理**——多步执行:写代码、跑测试、修失败,**先持久化产出再结束**;
|
||||
- **并行派发**——独立子任务一次性并行发出,避免主会话上下文被文件内容与工具输出淹没。
|
||||
|
||||
会话级隔离由 **git worktree 纪律**保障:并发分支工作必须各占独立 worktree(主 checkout 单占用),分支劫持事故有审计日志可归因;子代理共享同一工作流 worktree——隔离发生在工作流之间,而不是协同的子代理之间。
|
||||
|
||||
## 合并队列:落地的最后一道秩序
|
||||
|
||||
代码评审收敛后,PR 打上 `ready-to-merge` 标签进入**服务端串行合并队列**:
|
||||
|
||||
- 队头 PR 由协调机器人校验"标签 + 双绿(CI 检查 + 合并门)+ 风险双检(有 `Risk/Low` 且无 `Risk/High`)"后 approve 并合并;
|
||||
- 非队头 PR 自动做 keep-mergeable 保活(绝不手写合并 main 的保活提交);
|
||||
- 机械类低风险 PR(依赖升级、锁文件、bot 作者)走 **fast-lane** 快速通道自动补标签;
|
||||
- 真冲突的 PR 经 merge-tree 预探后出队留痕,不动分支。
|
||||
|
||||
## 一张工单的完整旅程
|
||||
|
||||
以一个新功能为例,把所有机制串起来:
|
||||
|
||||
1. **创建**:功能工单打上 `Kind/Feature` 标签,写清动机与验收期望;
|
||||
2. **路由**:主会话 Step 0 查路由表 → DAG 管线,先过过程评估门再动手;
|
||||
3. **分解**:`analyze-dag` 产出任务 DAG——节点是验收标准、边是契约、拓扑是计划;实测属性派生出 D1–D4 档位;
|
||||
4. **单门**:`review-dag` 三维并行评审(拓扑/需求映射/可发布性),迭代至收敛 PASS;
|
||||
5. **拆票**:聚合代理为每个任务节点建一张 `Kind/Task` 子工单,父工单聚合状态总览;
|
||||
6. **实现**:每张子工单独立走 `implement`(含生产者预检)——在独立 worktree 分支上;
|
||||
7. **代码评审**:`review-code` 多评审员对照节点规格与冻结契约并行扫描,轮次预算内收敛;
|
||||
8. **验证**:`verify` 跑 DoD 矩阵与集成测试;跨会话汇聚点触发里程碑验证;
|
||||
9. **落地**:PR 打 `ready-to-merge` 进合并队列,双绿 + 风险双检通过后自动合并进主干;
|
||||
10. **归档**:全部工件已在两层体系留档,工单关闭——过程即档案。
|
||||
|
||||
## 小结
|
||||
|
||||
工单驱动不是"给 AI 加了个 TODO 列表",而是一套完整的工程治理契约:
|
||||
|
||||
- **类型即路由**——标签决定流程,流程不再临场发挥;
|
||||
- **分解即计划**——一份 DAG 工件替代四份会漂移的文档;
|
||||
- **门禁即质量**——评审与验证不可绕过、不可裁剪、有轮次预算;
|
||||
- **证据即真相**——没有新鲜证据的状态断言一律无效;
|
||||
- **工件即档案**——过程可恢复、决策可追溯;
|
||||
- **人在环上**——执行门可自动,业务决策永远人工。
|
||||
|
||||
这套体系本身也是工单驱动的:对 `.octopus/` 工作流系统的任何改进都走 `Kind/Process` 过程审计路由——系统吃自己的狗粮。
|
||||
@@ -0,0 +1,461 @@
|
||||
#!/usr/bin/env bash
|
||||
# Public release repository:
|
||||
# https://eightarms.net/Octopus/octopus-release
|
||||
set -euo pipefail
|
||||
APP=octopus
|
||||
|
||||
MUTED='\033[0;2m'
|
||||
RED='\033[0;31m'
|
||||
ORANGE='\033[38;5;214m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Octopus Installer
|
||||
|
||||
Usage: install.sh [options]
|
||||
|
||||
Options:
|
||||
-h, --help Display this help message
|
||||
-v, --version <version> Install a specific version (e.g., 1.0.180)
|
||||
-b, --binary <path> Install from a local binary instead of downloading
|
||||
--no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
|
||||
|
||||
Examples:
|
||||
curl -fsSL https://eightarms.net/install | bash
|
||||
curl -fsSL https://eightarms.net/install | bash -s -- --version 1.0.180
|
||||
./install --binary /path/to/octopus
|
||||
EOF
|
||||
}
|
||||
|
||||
requested_version=${VERSION:-}
|
||||
no_modify_path=false
|
||||
binary_path=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
-v|--version)
|
||||
if [[ -n "${2:-}" ]]; then
|
||||
requested_version="$2"
|
||||
shift 2
|
||||
else
|
||||
echo -e "${RED}Error: --version requires a version argument${NC}"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
-b|--binary)
|
||||
if [[ -n "${2:-}" ]]; then
|
||||
binary_path="$2"
|
||||
shift 2
|
||||
else
|
||||
echo -e "${RED}Error: --binary requires a path argument${NC}"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
--no-modify-path)
|
||||
no_modify_path=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo -e "${ORANGE}Warning: Unknown option '$1'${NC}" >&2
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
INSTALL_DIR=$HOME/.octopus/bin
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# If --binary is provided, skip all download/detection logic
|
||||
if [ -n "$binary_path" ]; then
|
||||
if [ ! -f "$binary_path" ]; then
|
||||
echo -e "${RED}Error: Binary not found at ${binary_path}${NC}"
|
||||
exit 1
|
||||
fi
|
||||
specific_version="local"
|
||||
else
|
||||
raw_os=$(uname -s)
|
||||
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
|
||||
case "$raw_os" in
|
||||
Darwin*) os="darwin" ;;
|
||||
Linux*) os="linux" ;;
|
||||
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
|
||||
esac
|
||||
|
||||
arch=$(uname -m)
|
||||
if [[ "$arch" == "aarch64" ]]; then
|
||||
arch="arm64"
|
||||
fi
|
||||
if [[ "$arch" == "x86_64" ]]; then
|
||||
arch="x64"
|
||||
fi
|
||||
|
||||
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
|
||||
rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
|
||||
if [ "$rosetta_flag" = "1" ]; then
|
||||
arch="arm64"
|
||||
fi
|
||||
fi
|
||||
|
||||
combo="$os-$arch"
|
||||
case "$combo" in
|
||||
linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
archive_ext=".zip"
|
||||
if [ "$os" = "linux" ]; then
|
||||
archive_ext=".tar.gz"
|
||||
fi
|
||||
|
||||
is_musl=false
|
||||
if [ "$os" = "linux" ]; then
|
||||
if [ -f /etc/alpine-release ]; then
|
||||
is_musl=true
|
||||
fi
|
||||
|
||||
if command -v ldd >/dev/null 2>&1; then
|
||||
if ldd --version 2>&1 | grep -qi musl; then
|
||||
is_musl=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
needs_baseline=false
|
||||
if [ "$arch" = "x64" ]; then
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! grep -qwi avx2 /proc/cpuinfo 2>/dev/null; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$os" = "darwin" ]; then
|
||||
avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
|
||||
if [ "$avx2" != "1" ]; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$os" = "windows" ]; then
|
||||
ps="(Add-Type -MemberDefinition \"[DllImport(\"\"kernel32.dll\"\")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);\" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)"
|
||||
out=""
|
||||
if command -v powershell.exe >/dev/null 2>&1; then
|
||||
out=$(powershell.exe -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true)
|
||||
elif command -v pwsh >/dev/null 2>&1; then
|
||||
out=$(pwsh -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true)
|
||||
fi
|
||||
out=$(echo "$out" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
|
||||
if [ "$out" != "true" ] && [ "$out" != "1" ]; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
target="$os-$arch"
|
||||
if [ "$needs_baseline" = "true" ]; then
|
||||
target="$target-baseline"
|
||||
fi
|
||||
if [ "$is_musl" = "true" ]; then
|
||||
target="$target-musl"
|
||||
fi
|
||||
|
||||
filename="$APP-$target$archive_ext"
|
||||
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! command -v tar >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if ! command -v unzip >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$requested_version" ]; then
|
||||
specific_version=$(curl -s "${GITEA_API_URL:-https://eightarms.net/api/v1}/repos/Octopus/octopus-release/releases?limit=1" | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
|
||||
if [[ $? -ne 0 || -z "$specific_version" ]]; then
|
||||
echo -e "${RED}Failed to fetch version information${NC}"
|
||||
exit 1
|
||||
fi
|
||||
url="${OCTOPUS_DOWNLOAD_BASE:-https://eightarms.net}/Octopus/octopus-release/releases/download/v${specific_version}/$filename"
|
||||
else
|
||||
# Strip leading 'v' if present
|
||||
requested_version="${requested_version#v}"
|
||||
url="${OCTOPUS_DOWNLOAD_BASE:-https://eightarms.net}/Octopus/octopus-release/releases/download/v${requested_version}/$filename"
|
||||
specific_version=$requested_version
|
||||
|
||||
# Verify the release exists before downloading
|
||||
http_status=$(curl -sI -o /dev/null -w "%{http_code}" "${GITEA_URL:-https://eightarms.net}/Octopus/octopus-release/releases/tag/v${requested_version}")
|
||||
if [ "$http_status" = "404" ]; then
|
||||
echo -e "${RED}Error: Release v${requested_version} not found${NC}"
|
||||
echo -e "${MUTED}Available releases: ${GITEA_URL:-https://eightarms.net}/Octopus/octopus-release/releases${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
print_message() {
|
||||
local level=$1
|
||||
local message=$2
|
||||
local color=""
|
||||
|
||||
case $level in
|
||||
info) color="${NC}" ;;
|
||||
warning) color="${NC}" ;;
|
||||
error) color="${RED}" ;;
|
||||
esac
|
||||
|
||||
echo -e "${color}${message}${NC}"
|
||||
}
|
||||
|
||||
check_version() {
|
||||
if command -v octopus >/dev/null 2>&1; then
|
||||
octopus_path=$(which octopus)
|
||||
|
||||
## Check the installed version
|
||||
installed_version=$(octopus --version 2>/dev/null || echo "")
|
||||
|
||||
if [[ "$installed_version" != "$specific_version" ]]; then
|
||||
print_message info "${MUTED}Installed version: ${NC}$installed_version."
|
||||
else
|
||||
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
unbuffered_sed() {
|
||||
if echo | sed -u -e "" >/dev/null 2>&1; then
|
||||
sed -nu "$@"
|
||||
elif echo | sed -l -e "" >/dev/null 2>&1; then
|
||||
sed -nl "$@"
|
||||
else
|
||||
local pad="$(printf "\n%512s" "")"
|
||||
sed -ne "s/$/\\${pad}/" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
print_progress() {
|
||||
local bytes="$1"
|
||||
local length="$2"
|
||||
[ "$length" -gt 0 ] || return 0
|
||||
|
||||
local width=50
|
||||
local percent=$(( bytes * 100 / length ))
|
||||
[ "$percent" -gt 100 ] && percent=100
|
||||
local on=$(( percent * width / 100 ))
|
||||
local off=$(( width - on ))
|
||||
|
||||
local filled=$(printf "%*s" "$on" "")
|
||||
filled=${filled// /■}
|
||||
local empty=$(printf "%*s" "$off" "")
|
||||
empty=${empty// /・}
|
||||
|
||||
printf "\r${ORANGE}%s%s %3d%%${NC}" "$filled" "$empty" "$percent" >&4
|
||||
}
|
||||
|
||||
download_with_progress() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
|
||||
if [ -t 2 ]; then
|
||||
exec 4>&2
|
||||
else
|
||||
exec 4>/dev/null
|
||||
fi
|
||||
|
||||
local tmp_dir=${TMPDIR:-/tmp}
|
||||
local basename="${tmp_dir}/octopus_install_$$"
|
||||
local tracefile="${basename}.trace"
|
||||
|
||||
rm -f "$tracefile"
|
||||
mkfifo "$tracefile"
|
||||
|
||||
# Hide cursor
|
||||
printf "\033[?25l" >&4
|
||||
|
||||
trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN
|
||||
|
||||
(
|
||||
curl --trace-ascii "$tracefile" -s -L -o "$output" "$url"
|
||||
) &
|
||||
local curl_pid=$!
|
||||
|
||||
unbuffered_sed \
|
||||
-e 'y/ACDEGHLNORTV/acdeghlnortv/' \
|
||||
-e '/^0000: content-length:/p' \
|
||||
-e '/^<= recv data/p' \
|
||||
"$tracefile" | \
|
||||
{
|
||||
local length=0
|
||||
local bytes=0
|
||||
|
||||
while IFS=" " read -r -a line; do
|
||||
[ "${#line[@]}" -lt 2 ] && continue
|
||||
local tag="${line[0]} ${line[1]}"
|
||||
|
||||
if [ "$tag" = "0000: content-length:" ]; then
|
||||
length="${line[2]}"
|
||||
length=$(echo "$length" | tr -d '\r')
|
||||
bytes=0
|
||||
elif [ "$tag" = "<= recv" ]; then
|
||||
local size="${line[3]}"
|
||||
bytes=$(( bytes + size ))
|
||||
if [ "$length" -gt 0 ]; then
|
||||
print_progress "$bytes" "$length"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
wait $curl_pid
|
||||
local ret=$?
|
||||
echo "" >&4
|
||||
return $ret
|
||||
}
|
||||
|
||||
download_and_install() {
|
||||
print_message info "\n${MUTED}Installing ${NC}octopus ${MUTED}version: ${NC}$specific_version"
|
||||
local tmp_dir="${TMPDIR:-/tmp}/octopus_install_$$"
|
||||
mkdir -p "$tmp_dir"
|
||||
|
||||
if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then
|
||||
# Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails
|
||||
curl -# -L -o "$tmp_dir/$filename" "$url"
|
||||
fi
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
|
||||
else
|
||||
unzip -q "$tmp_dir/$filename" -d "$tmp_dir"
|
||||
fi
|
||||
|
||||
mv "$tmp_dir/octopus" "$INSTALL_DIR"
|
||||
chmod 755 "${INSTALL_DIR}/octopus"
|
||||
rm -rf "$tmp_dir"
|
||||
}
|
||||
|
||||
install_from_binary() {
|
||||
print_message info "\n${MUTED}Installing ${NC}octopus ${MUTED}from: ${NC}$binary_path"
|
||||
cp "$binary_path" "${INSTALL_DIR}/octopus"
|
||||
chmod 755 "${INSTALL_DIR}/octopus"
|
||||
}
|
||||
|
||||
if [ -n "$binary_path" ]; then
|
||||
install_from_binary
|
||||
else
|
||||
check_version
|
||||
download_and_install
|
||||
fi
|
||||
|
||||
|
||||
add_to_path() {
|
||||
local config_file=$1
|
||||
local command=$2
|
||||
|
||||
if grep -Fxq "$command" "$config_file"; then
|
||||
print_message info "Command already exists in $config_file, skipping write."
|
||||
elif [[ -w $config_file ]]; then
|
||||
echo -e "\n# octopus" >> "$config_file"
|
||||
echo "$command" >> "$config_file"
|
||||
print_message info "${MUTED}Successfully added ${NC}octopus ${MUTED}to \$PATH in ${NC}$config_file"
|
||||
else
|
||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||
print_message info " $command"
|
||||
fi
|
||||
}
|
||||
|
||||
XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
|
||||
|
||||
current_shell=$(basename "$SHELL")
|
||||
case $current_shell in
|
||||
fish)
|
||||
config_files="$HOME/.config/fish/config.fish"
|
||||
;;
|
||||
zsh)
|
||||
config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
|
||||
;;
|
||||
bash)
|
||||
config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
|
||||
;;
|
||||
ash)
|
||||
config_files="$HOME/.ashrc $HOME/.profile /etc/profile"
|
||||
;;
|
||||
sh)
|
||||
config_files="$HOME/.ashrc $HOME/.profile /etc/profile"
|
||||
;;
|
||||
*)
|
||||
# Default case if none of the above matches
|
||||
config_files="$HOME/.bashrc $HOME/.bash_profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$no_modify_path" != "true" ]]; then
|
||||
config_file=""
|
||||
for file in $config_files; do
|
||||
if [[ -f $file ]]; then
|
||||
config_file=$file
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z $config_file ]]; then
|
||||
print_message warning "No config file found for $current_shell. You may need to manually add to PATH:"
|
||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||
elif [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
||||
case $current_shell in
|
||||
fish)
|
||||
add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
|
||||
;;
|
||||
zsh)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
bash)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
ash)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
sh)
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
*)
|
||||
export PATH=$INSTALL_DIR:$PATH
|
||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
|
||||
echo "$INSTALL_DIR" >> $GITHUB_PATH
|
||||
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
echo -e ""
|
||||
echo -e "\033[38;5;214m█▀▀█ █▀▀▀ ▀▀█▀▀ █▀▀█ █▀▀█ █ █ █▀▀▀${NC}"
|
||||
echo -e "\033[38;5;172m█ █ █ █ █ █ █▀▀▀ █ █ ▀▀▀█${NC}"
|
||||
echo -e "\033[38;5;130m▀▀▀▀ ▀▀▀▀ ▀ ▀▀▀▀ ▀ ▀▀▀▀ ▀▀▀▀${NC}"
|
||||
echo -e ""
|
||||
echo -e ""
|
||||
echo -e "${MUTED}Octopus includes free models, to start:${NC}"
|
||||
echo -e ""
|
||||
echo -e "cd <project> ${MUTED}# Open directory${NC}"
|
||||
echo -e "octopus ${MUTED}# Run command${NC}"
|
||||
echo -e ""
|
||||
echo -e "${MUTED}For more information visit ${NC}https://eightarms.net/docs"
|
||||
echo -e ""
|
||||
echo -e ""
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
#Requires -Version 5.1
|
||||
# Public release repository: https://eightarms.net/Octopus/octopus-release
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Octopus installer for Windows.
|
||||
|
||||
.DESCRIPTION
|
||||
Downloads and installs the Octopus CLI from Gitea Releases.
|
||||
|
||||
.PARAMETER Version
|
||||
Install a specific version (e.g. 1.0.180). Defaults to latest.
|
||||
|
||||
.PARAMETER NoModifyPath
|
||||
Do not add the install directory to the user PATH.
|
||||
|
||||
.EXAMPLE
|
||||
irm https://eightarms.net/Octopus/octopus-release/raw/branch/dev/install.ps1 | iex
|
||||
|
||||
.EXAMPLE
|
||||
irm https://eightarms.net/Octopus/octopus-release/raw/branch/dev/install.ps1 | iex; Install-Octopus -Version 0.1.3
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Version = "",
|
||||
[switch]$NoModifyPath
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$APP = "octopus"
|
||||
$REPO = "Octopus/octopus-release"
|
||||
$baseUrl = if ($env:OCTOPUS_DOWNLOAD_BASE) { $env:OCTOPUS_DOWNLOAD_BASE } else { "https://eightarms.net" }
|
||||
$INSTALL_DIR = Join-Path $env:USERPROFILE ".octopus\bin"
|
||||
|
||||
function Write-Color([string]$Text, [string]$Color = "White") {
|
||||
Write-Host $Text -ForegroundColor $Color
|
||||
}
|
||||
|
||||
function Get-Arch {
|
||||
$arch = $env:PROCESSOR_ARCHITECTURE
|
||||
if ($arch -eq "AMD64") { return "x64" }
|
||||
if ($arch -eq "ARM64") { return "arm64" }
|
||||
throw "Unsupported architecture: $arch"
|
||||
}
|
||||
|
||||
function Test-Avx2Support {
|
||||
try {
|
||||
Add-Type -MemberDefinition @"
|
||||
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
||||
public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);
|
||||
"@ -Name "Kernel32" -Namespace "Win32" -ErrorAction SilentlyContinue | Out-Null
|
||||
# PF_AVX2_INSTRUCTIONS_AVAILABLE = 40
|
||||
return [Win32.Kernel32]::IsProcessorFeaturePresent(40)
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-LatestVersion {
|
||||
# Resolve the latest version by following the /releases/latest redirect to
|
||||
# /releases/tag/vX.Y.Z. This avoids the API, which is
|
||||
# rate-limited to 60 requests/hour per IP.
|
||||
$latestUrl = "$baseUrl/$REPO/releases/latest"
|
||||
$location = $null
|
||||
$request = [System.Net.HttpWebRequest]::Create($latestUrl)
|
||||
$request.Method = "HEAD"
|
||||
$request.AllowAutoRedirect = $false
|
||||
$request.UserAgent = "octopus-installer"
|
||||
try {
|
||||
$response = $request.GetResponse()
|
||||
$location = $response.Headers["Location"]
|
||||
$response.Close()
|
||||
} catch [System.Net.WebException] {
|
||||
$resp = $_.Exception.Response
|
||||
if ($null -ne $resp) {
|
||||
$location = $resp.Headers["Location"]
|
||||
$resp.Close()
|
||||
}
|
||||
}
|
||||
if (-not $location) { return "" }
|
||||
if ($location -match '/tag/v?([^/]+)/?$') {
|
||||
return $Matches[1] -replace '^v', ''
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function Install-Octopus {
|
||||
$arch = Get-Arch
|
||||
|
||||
# Resolve version
|
||||
if ($Version -ne "") {
|
||||
$specificVersion = $Version -replace '^v', ''
|
||||
# Verify release exists
|
||||
$releaseUrl = "$baseUrl/$REPO/releases/tag/v$specificVersion"
|
||||
try {
|
||||
$null = Invoke-WebRequest -Uri $releaseUrl -Method Head -UseBasicParsing -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Color "Error: Release v$specificVersion not found." "Red"
|
||||
Write-Color "Available releases: $baseUrl/$REPO/releases" "DarkGray"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Color "Fetching latest version..." "DarkGray"
|
||||
$specificVersion = Get-LatestVersion
|
||||
if (-not $specificVersion) {
|
||||
Write-Color "Error: Failed to fetch latest version." "Red"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Check if already installed at this version
|
||||
$existing = Join-Path $INSTALL_DIR "$APP.exe"
|
||||
if (Test-Path $existing) {
|
||||
try {
|
||||
$installedVersion = & $existing --version 2>$null
|
||||
if ($installedVersion -eq $specificVersion) {
|
||||
Write-Color "Version $specificVersion already installed." "DarkGray"
|
||||
exit 0
|
||||
} else {
|
||||
Write-Color "Installed version: $installedVersion" "DarkGray"
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
# Build filename
|
||||
$target = "$APP-windows-$arch"
|
||||
if ($arch -eq "x64" -and -not (Test-Avx2Support)) {
|
||||
$target = "$APP-windows-$arch-baseline"
|
||||
}
|
||||
$filename = "$target.zip"
|
||||
$downloadUrl = "$baseUrl/$REPO/releases/download/v$specificVersion/$filename"
|
||||
|
||||
Write-Color ""
|
||||
Write-Color "Installing octopus version: $specificVersion" "DarkGray"
|
||||
Write-Color "Downloading $filename ..." "DarkGray"
|
||||
|
||||
# Download
|
||||
$tmpDir = Join-Path $env:TEMP "octopus_install_$PID"
|
||||
New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null
|
||||
$zipPath = Join-Path $tmpDir $filename
|
||||
|
||||
try {
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $zipPath -UseBasicParsing
|
||||
} catch {
|
||||
Write-Color "Error: Failed to download $downloadUrl" "Red"
|
||||
Write-Color $_.Exception.Message "Red"
|
||||
Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Extract
|
||||
Expand-Archive -Path $zipPath -DestinationPath $tmpDir -Force
|
||||
|
||||
# Install
|
||||
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
|
||||
$exeSrc = Join-Path $tmpDir "$APP.exe"
|
||||
if (-not (Test-Path $exeSrc)) {
|
||||
Write-Color "Error: $APP.exe not found in archive." "Red"
|
||||
Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue
|
||||
exit 1
|
||||
}
|
||||
Copy-Item -Path $exeSrc -Destination (Join-Path $INSTALL_DIR "$APP.exe") -Force
|
||||
Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue
|
||||
|
||||
# Add to PATH
|
||||
if (-not $NoModifyPath) {
|
||||
$userPath = [Environment]::GetEnvironmentVariable("PATH", "User")
|
||||
if ($userPath -notlike "*$INSTALL_DIR*") {
|
||||
[Environment]::SetEnvironmentVariable("PATH", "$INSTALL_DIR;$userPath", "User")
|
||||
Write-Color "Added $INSTALL_DIR to user PATH." "DarkGray"
|
||||
Write-Color "Restart your terminal for PATH changes to take effect." "Yellow"
|
||||
}
|
||||
}
|
||||
|
||||
# CI Actions support (GitHub/Gitea)
|
||||
if ($env:GITHUB_ACTIONS -eq "true" -and $env:GITHUB_PATH) {
|
||||
Add-Content -Path $env:GITHUB_PATH -Value $INSTALL_DIR
|
||||
Write-Color "Added $INSTALL_DIR to GITHUB_PATH." "DarkGray"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " ___ _" -ForegroundColor DarkYellow
|
||||
Write-Host " / _ \ ___| |_ ___ _ __ _ _ ___" -ForegroundColor DarkYellow
|
||||
Write-Host "| | | |/ __| __/ _ \| '_ \| | | / __|" -ForegroundColor Yellow
|
||||
Write-Host "| |_| | (__| || (_) | |_) | |_| \__ \" -ForegroundColor Yellow
|
||||
Write-Host " \___/ \___|\__\___/| .__/ \__,_|___/" -ForegroundColor White
|
||||
Write-Host " |_|" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "Octopus includes free models, to start:" -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
Write-Host " cd <project> " -NoNewline; Write-Host "# Open directory" -ForegroundColor DarkGray
|
||||
Write-Host " octopus " -NoNewline; Write-Host "# Run command" -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
Write-Host "For more information visit " -NoNewline -ForegroundColor DarkGray
|
||||
Write-Host "https://eightarms.net/docs"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
Install-Octopus
|
||||
@@ -0,0 +1,75 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Regression tests for install.ps1.
|
||||
|
||||
.BUG
|
||||
`irm <url> | iex` closed the terminal window. Cause: the script used
|
||||
`exit`, which under Invoke-Expression terminates the entire PowerShell
|
||||
host process (there is no script scope to exit). Fix: use `return`
|
||||
(success) / `throw` (errors) so the host survives.
|
||||
|
||||
.NOTES
|
||||
Host-process termination cannot be observed from inside the dying process,
|
||||
so these tests combine a source-verification guard with a behavioral test
|
||||
run in a child process (`exit` cannot be caught by try/catch and kills the
|
||||
child; `throw` is catchable).
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$installScript = Join-Path $repoRoot "install.ps1"
|
||||
$failures = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
function Assert-True([bool]$Cond, [string]$Message) {
|
||||
if (-not $Cond) { $failures.Add($Message) | Out-Null }
|
||||
}
|
||||
|
||||
# --- Test 1 (source verification): no host-terminating `exit` ---
|
||||
$content = Get-Content -Path $installScript -Raw
|
||||
$exitCount = ([regex]::Matches($content, '(?m)^\s*exit\b')).Count
|
||||
Assert-True ($exitCount -eq 0) "install.ps1 must not use bare 'exit' (found $exitCount); 'irm | iex' would kill the host. Use return/throw."
|
||||
|
||||
# --- Test 2: parses cleanly ---
|
||||
$parseOk = $true
|
||||
try { [void][scriptblock]::Create($content) } catch { $parseOk = $false }
|
||||
Assert-True $parseOk "install.ps1 must parse without syntax errors."
|
||||
|
||||
# --- Test 3 (behavioral): iex must NOT kill the host ---
|
||||
# Force the version lookup to fail fast against an unreachable address so the
|
||||
# script reaches its early-exit/throw path offline. `exit` terminates the child
|
||||
# (RESULT never emitted); `throw` is caught and RESULT is emitted.
|
||||
$inner = @'
|
||||
$ErrorActionPreference = "Stop"
|
||||
$env:OCTOPUS_DOWNLOAD_BASE = "http://127.0.0.1:1"
|
||||
$r = "nothing"
|
||||
try {
|
||||
Invoke-Expression (Get-Content -Raw "__INSTALL__")
|
||||
$r = "completed"
|
||||
} catch {
|
||||
$r = "caught-throw"
|
||||
}
|
||||
Write-Output ("RESULT=$r")
|
||||
'@.Replace("__INSTALL__", $installScript)
|
||||
|
||||
$tmp = Join-Path $env:TEMP ("octopus_iex_test_" + $PID + ".ps1")
|
||||
Set-Content -Path $tmp -Value $inner -Encoding UTF8
|
||||
try {
|
||||
$childOut = (& powershell -NoProfile -File $tmp 2>&1 | Out-String)
|
||||
} finally {
|
||||
Remove-Item $tmp -ErrorAction SilentlyContinue
|
||||
}
|
||||
Assert-True ($childOut -match 'RESULT=') "iex must not terminate the host (try/catch must observe a RESULT). Child output:`n$childOut"
|
||||
|
||||
# --- Report ---
|
||||
if ($failures.Count -gt 0) {
|
||||
Write-Host ("FAIL: " + $failures.Count + " test(s)") -ForegroundColor Red
|
||||
foreach ($f in $failures) { Write-Host (" - " + $f) -ForegroundColor Red }
|
||||
exit 1
|
||||
}
|
||||
Write-Host "PASS: all regression tests passed." -ForegroundColor Green
|
||||
exit 0
|
||||
Reference in New Issue
Block a user