Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nightly-alongside-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

A nightly `init` installs beside the release and no longer offers to move `@taskless/cli` pins in scripts, CI, or hooks onto `@taskless/cli-nightly`. It also keeps the release's recorded version when the manifest predates `install.launchers`, so `.taskless/bin/cli.mjs` still has a version to run. A release `init` now retires the nightly: it removes `.taskless/bin/cli-nightly.mjs` and its lockfile, drops the nightly's recorded version and permission rules, and names any `@taskless/cli-nightly` pin left in scripts, CI, or hooks. Any `package.json` dependency on the CLI is now raised for removal in favour of the project launcher, and `pinnedCli` entries carry a `reason`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Why

Moving a production project from the nightly back to mainline showed the two channels are not symmetric in the way they should be. Installing a nightly should put it beside the release without switching what scripts, CI, and hooks run; installing the release should clean the nightly up. Neither held. A nightly `init` offered to move the project's `@taskless/cli` script, CI, and hook pins onto `@taskless/cli-nightly`, and on a project whose manifest predates `install.launchers` it overwrote the only record of the release's version, leaving `.taskless/bin/cli.mjs` with nothing to run. A release `init` left the nightly's launcher, lockfile, recorded version, and permission rules in place, and said nothing about a nightly pin newer than the release, since staleness alone never names a newer version. Separately, the CLI runs through the project launcher, yet any `package.json` dependency on it was judged only by version, so a current dependency, which drifts behind `.taskless/` on the next upgrade, went unmentioned.

## What Changes

- **A nightly installs beside the release.** It never offers to move a `@taskless/cli` pin in a script, CI file, or hook. When that nightly migrated an existing `.taskless/`, a release pin is still named, marked "leave as is", with a warning that a release predating the new schema refuses the project. A nightly install carries the release's version into `install.launchers` even when only `install.cliVersion` recorded it.
- **A release install retires the nightly.** It removes `.taskless/bin/cli-nightly.mjs` and `cli-nightly.lock.json`, drops the nightly's `install.launchers` entry (so its cached build is pruned), removes the nightly's rules from harness permission files, and raises every `@taskless/cli-nightly` pin in a script, CI file, or hook, whatever its version. `init --json` gains `retired`, the launcher files removed. The user-level global trust entry is not touched: other projects may still use the nightly.
- **A dependency on the CLI is raised whatever its version.** Any `@taskless/cli` or `@taskless/cli-nightly` entry in `dependencies`, `devDependencies`, or `optionalDependencies` is named for removal in favour of the project launcher. Version judgement now applies to script, CI, and hook lines only.
- **`pinnedCli` entries carry a `reason`**: `dependency`, `stale`, `nightly`, or `unreleased`. `findStalePins` becomes `findCliPins` and takes the run's migration. The notice is headed `TASKLESS CLI PINS`, gives each entry its action, and adds one paragraph per reason present.
- **Recipes**: `init` (v15), `update` (v20), and `info` (v4) explain the reasons and the release's retirement of the nightly.

## Capabilities

### Modified Capabilities

- `cli-init`: the two pin requirements are replaced by "Init raises Taskless CLI pins"; "Each permission file holds exactly one Taskless rule" is replaced by "Each permission file holds one Taskless rule set per installed package"; the launcher and lockfile requirements gain the release's retirement of the nightly.
- `cli-project-launcher`: "Each package has its own launcher" lets a release remove the nightly's launcher.
- `cli`: "Info reports stale CLI pins" is restated for the new entry shape.

## Delivery Shape

**Single PR.** Detection, its notice, the JSON shape, the retirement, and the recipes that read them only make sense together.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## MODIFIED Requirements

### Requirement: Each package has its own launcher

The release package SHALL install its launcher at `.taskless/bin/cli.mjs`, and the nightly package at `.taskless/bin/cli-nightly.mjs`. A build SHALL write and rewrite only its own package's launcher. A nightly build SHALL NEVER read, edit, or remove the release's launcher, so a nightly installs beside the release. A release build SHALL NOT read or edit the nightly's launcher, and SHALL remove it when it installs, because installing the release is the move back to mainline. A build whose invocation names neither package, such as a `self` build, SHALL write no launcher and remove none.

Each launcher SHALL be an ES module (`.mjs`) whose first line is `#!/usr/bin/env node`, and SHALL be written with its executable bits set where the filesystem supports them. Its content SHALL be identical for every version of its package, so an upgrade never changes its bytes unless the launcher itself changed.

#### Scenario: The release installs cli.mjs

- **WHEN** `init` runs from the release package
- **THEN** `.taskless/bin/cli.mjs` SHALL exist, begin with `#!/usr/bin/env node`, and be executable
- **AND** `.taskless/bin/cli-nightly.mjs` SHALL NOT be created

#### Scenario: A nightly leaves the release's launcher alone

- **WHEN** `.taskless/bin/cli.mjs` exists and `init` runs from a nightly
- **THEN** `.taskless/bin/cli.mjs` SHALL be byte-identical afterwards
- **AND** `.taskless/bin/cli-nightly.mjs` SHALL exist

#### Scenario: The release removes the nightly's launcher

- **WHEN** `.taskless/bin/cli-nightly.mjs` exists and `init` runs from the release
- **THEN** `.taskless/bin/cli-nightly.mjs` SHALL NOT exist afterwards
- **AND** `.taskless/bin/cli.mjs` SHALL exist

#### Scenario: An upgrade does not rewrite an unchanged launcher

- **WHEN** `init` runs at `0.13.1` over a project installed at `0.13.0`, and the launcher template did not change between them
- **THEN** the launcher's bytes SHALL be unchanged
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## MODIFIED Requirements

### Requirement: Info reports stale CLI pins

`taskless info` SHALL report the same entries `taskless init` reports under "Init raises Taskless CLI pins", judged against the running version, read from the working directory, its declared workspace packages, and its CI and hook files by the same rules. `info` runs no migration, so it SHALL never report reason `unreleased`. Under `--json` the payload SHALL carry `pinnedCli` in the shape `init --json` uses, `{ manifest, location, name, spec, installed, reason }`, present on every successful run and empty when nothing is raised. Without `--json`, when at least one entry is reported, the output SHALL list each entry with its manifest, location, spec, installed version when known, and what to do with it.

`info` is read-only: it SHALL NOT modify any `package.json`, CI file, or hook, and an unreadable file SHALL NOT make it fail. `--anonymous` SHALL NOT suppress the entries, since they are local state.

The `update` recipe SHALL read the entries from `info --json`, the read-only command it already runs, rather than directing an agent to re-run `init`.

#### Scenario: info --json carries a workspace package's stale pin

- **WHEN** `taskless info --json` runs in a project whose root `package.json` declares `workspaces: ["packages/*"]` and `packages/app/package.json` has `devDependencies["@taskless/cli"]`
- **THEN** `pinnedCli` SHALL contain `{ manifest: "packages/app/package.json", location: "devDependencies", name: "@taskless/cli", spec, installed: null, reason: "dependency" }`

#### Scenario: info --json reports no pins as an empty list

- **WHEN** `taskless info --json` runs where nothing is raised
- **THEN** `pinnedCli` SHALL be an empty array

#### Scenario: Plain info lists the pins

- **WHEN** `taskless info` runs without `--json` and an entry is raised
- **THEN** stdout SHALL name the entry's manifest, location, and spec, and what to do with it
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## 1. Pins

- [x] 1.1 In `install/pinned-cli.ts`, raise every dependency as `dependency`; judge script, CI, and hook lines by channel (`nightly`, `unreleased`, or `stale`). Rename `findStalePins` to `findCliPins` and pass the run's migration from `init` and the wizard.
- [x] 1.2 Rewrite `getPinnedCliNotice` with a per-entry action and one paragraph per reason; add `reason` to the `pinnedCli` schema and relabel plain `info`.

## 2. Launchers and permissions

- [x] 2.1 In `applyInstallPlan`, carry both packages' versions from the previous record, including one only `cliVersion` implied; on a release, drop the nightly's entry and remove its launcher and lockfile, reported as `retired`.
- [x] 2.2 In `syncPermissions`, have a release invocation remove the nightly's rules.

## 3. Recipes and tests

- [x] 3.1 `init`, `update`, `info`: describe `reason` and `retired`.
- [x] 3.2 Tests for pins, retirement (release and nightly projects), and permissions.
- [x] 3.3 `pnpm typecheck`, `pnpm lint`, and the CLI test suite pass.
Loading
Loading