@airig/cli 0.0.1 → 0.0.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/README.md +48 -14
- package/dist/index.js +191 -6
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Distribute and manage AI setups across coding agents from one project-local `.ai
|
|
|
7
7
|
Install the CLI globally to use the short `airig` command:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npm install --global airig
|
|
10
|
+
npm install --global @airig/cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
```sh
|
|
@@ -18,34 +18,68 @@ airig remove [owner/repo|.]
|
|
|
18
18
|
airig publish [tag]
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
For one-off usage without a global install, run the npm
|
|
21
|
+
For one-off usage without a global install, run the npm Package directly:
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
|
-
npx airig add <owner/repo>[@version]
|
|
25
|
-
npx airig add .
|
|
26
|
-
npx airig update <owner/repo>@<version>
|
|
27
|
-
npx airig remove [owner/repo|.]
|
|
28
|
-
npx airig publish [tag]
|
|
24
|
+
npx @airig/cli add <owner/repo>[@version]
|
|
25
|
+
npx @airig/cli add .
|
|
26
|
+
npx @airig/cli update <owner/repo>@<version>
|
|
27
|
+
npx @airig/cli remove [owner/repo|.]
|
|
28
|
+
npx @airig/cli publish [tag]
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The
|
|
31
|
+
The Package is named `@airig/cli`; the installed binary is `airig`.
|
|
32
32
|
|
|
33
33
|
## What It Does
|
|
34
34
|
|
|
35
|
-
airig installs selected AI
|
|
35
|
+
airig installs selected AI Setup artifacts from immutable GitHub releases into `.ai/`, then links them into provider-specific config paths. It supports local author dogfooding with `add .`, explicit version updates, interactive removal, and publishing `.ai/` as an `ai.zip` release asset.
|
|
36
36
|
|
|
37
|
-
Remote
|
|
37
|
+
Remote Setup Releases are pinned to exact versions in `.ai/ai.json`. `add` and `update` verify GitHub release immutability before writing remote content.
|
|
38
|
+
|
|
39
|
+
## Maintainer Releases
|
|
40
|
+
|
|
41
|
+
Use Package releases to publish the `@airig/cli` npm Package, which provides the `airig` CLI:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pnpm release
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The release script is maintainer-facing. It uses `bumpp` to choose the next Package version, update Package metadata, create the release commit, create a `v<version>` tag, and push the commit and tag. Pushed `v*` tags trigger `.github/workflows/publish-package.yml`, which installs dependencies, runs tests, builds the CLI, and publishes the Package to npm.
|
|
48
|
+
|
|
49
|
+
npm publishing uses trusted publishing with GitHub Actions OIDC. Do not add a long-lived npm token for Package releases.
|
|
50
|
+
|
|
51
|
+
Package releases are separate from Setup Releases. `airig publish [tag]` creates a GitHub immutable Setup Release containing `ai.zip` from `.ai/`; it does not publish the npm Package.
|
|
38
52
|
|
|
39
53
|
## Author Workflow
|
|
40
54
|
|
|
41
|
-
1. Create
|
|
55
|
+
1. Create AI Setup artifacts under `.ai/`.
|
|
42
56
|
2. Run `airig add .` to wire local artifacts into your repo.
|
|
43
57
|
3. Tag a release with your normal git tooling.
|
|
44
|
-
4. Run `airig publish` to upload `ai.zip` to an immutable GitHub
|
|
58
|
+
4. Run `airig publish` to upload `ai.zip` to an immutable GitHub Setup Release.
|
|
45
59
|
5. Share `airig add yourname/repo`.
|
|
46
60
|
|
|
61
|
+
For AI Setup repositories, use [`bumpp`](https://github.com/antfu-collective/bumpp)
|
|
62
|
+
to create and push release tags from a package script:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"scripts": {
|
|
67
|
+
"release": "bumpp"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
To publish Setup Releases from your AI Setup repository with GitHub Actions, copy
|
|
73
|
+
`resources/templates/publish.yml` to `.github/workflows/publish.yml` in that
|
|
74
|
+
repository. The workflow publishes when `bumpp` pushes a `v*` tag and expects an
|
|
75
|
+
`AIRIG_PUBLISH_TOKEN` repository secret. Create that secret from a fine-grained
|
|
76
|
+
GitHub PAT scoped only to the Setup Release repository with:
|
|
77
|
+
|
|
78
|
+
- `Contents`: Read and write
|
|
79
|
+
- `Administration`: Read-only
|
|
80
|
+
|
|
47
81
|
## Requirements
|
|
48
82
|
|
|
49
83
|
- Node.js `24.11.0` or newer in the Node 24 release line.
|
|
50
|
-
- GitHub immutable releases enabled for repositories that publish
|
|
51
|
-
- `GITHUB_TOKEN`
|
|
84
|
+
- GitHub immutable releases enabled for repositories that publish Setup Releases.
|
|
85
|
+
- `GITHUB_TOKEN` when running `publish`. For local use or custom GitHub Actions workflows, use a fine-grained GitHub PAT scoped to the Setup Release repository with `Contents` read/write access to create releases and `Administration` read-only access so `airig publish` can verify immutable releases are enabled before publishing.
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import { execSync } from "node:child_process";
|
|
3
|
+
import { execSync, spawn } from "node:child_process";
|
|
4
4
|
import { cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, symlink, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import path, { join } from "node:path";
|
|
6
6
|
import { parseEnv } from "node:util";
|
|
7
|
-
import
|
|
8
|
-
import { createWriteStream, existsSync } from "node:fs";
|
|
7
|
+
import { ZipArchive } from "archiver";
|
|
8
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { Octokit } from "@octokit/rest";
|
|
10
10
|
import { checkbox } from "@inquirer/prompts";
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import extractZip from "extract-zip";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
13
14
|
//#region src/lib/zip.ts
|
|
14
15
|
function create(sourceDir, outputPath) {
|
|
15
16
|
return new Promise((resolve, reject) => {
|
|
16
17
|
const output = createWriteStream(outputPath);
|
|
17
|
-
const archive =
|
|
18
|
+
const archive = new ZipArchive({ zlib: { level: 9 } });
|
|
18
19
|
const sourceBaseName = path.basename(sourceDir);
|
|
19
20
|
output.on("close", resolve);
|
|
20
21
|
archive.on("error", reject);
|
|
@@ -871,12 +872,196 @@ const updateCommand = new Command("update").description("Refresh an installed Se
|
|
|
871
872
|
}
|
|
872
873
|
});
|
|
873
874
|
//#endregion
|
|
875
|
+
//#region src/lib/update-notifier.ts
|
|
876
|
+
const PACKAGE_NAME = "@airig/cli";
|
|
877
|
+
const UPDATE_CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
|
|
878
|
+
const UPDATE_NOTIFICATION_INTERVAL_MS = 1440 * 60 * 1e3;
|
|
879
|
+
const REGISTRY_LATEST_URL = "https://registry.npmjs.org/@airig%2fcli/latest";
|
|
880
|
+
function maybeNotifyForUpdate(options = {}) {
|
|
881
|
+
const env = options.env ?? process.env;
|
|
882
|
+
const stderr = options.stderr ?? process.stderr;
|
|
883
|
+
if (shouldSkipUpdateNotifier(env, stderr)) return;
|
|
884
|
+
const packageJsonPath = options.packageJsonPath ?? findOwnPackageJsonPath();
|
|
885
|
+
if (!isGlobalInstall(path.dirname(packageJsonPath), options.cwd ?? process.cwd())) return;
|
|
886
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
887
|
+
const currentVersion = readPackageVersion(packageJsonPath);
|
|
888
|
+
const statePath = path.join(options.stateDir ?? defaultStateDir(env), "update-notifier.json");
|
|
889
|
+
let state = readUpdateState(statePath);
|
|
890
|
+
if (isUpdateCheckDue(state, now)) {
|
|
891
|
+
state = {
|
|
892
|
+
...state,
|
|
893
|
+
lastUpdateCheck: now.toUTCString()
|
|
894
|
+
};
|
|
895
|
+
writeUpdateState(statePath, state);
|
|
896
|
+
(options.spawnUpdateCheck ?? spawnUpdateCheck)(statePath);
|
|
897
|
+
}
|
|
898
|
+
if (!state.latestVersion || !isVersionGreater(state.latestVersion, currentVersion)) return;
|
|
899
|
+
if (!isNotificationDue(state, now)) return;
|
|
900
|
+
stderr.write(formatUpdateMessage({
|
|
901
|
+
currentVersion,
|
|
902
|
+
latestVersion: state.latestVersion
|
|
903
|
+
}));
|
|
904
|
+
writeUpdateState(statePath, {
|
|
905
|
+
...state,
|
|
906
|
+
lastNotifiedAt: now.toUTCString()
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
function shouldSkipUpdateNotifier(env, stderr) {
|
|
910
|
+
return Boolean(env.NO_UPDATE_NOTIFIER || env.AIRIG_NO_UPDATE_NOTIFIER || env.CI || env.NODE_ENV === "test" || !stderr.isTTY);
|
|
911
|
+
}
|
|
912
|
+
function readPackageVersion(packageJsonPath) {
|
|
913
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
914
|
+
if (!parsed.version) throw new Error(`Could not read ${PACKAGE_NAME} version from ${packageJsonPath}`);
|
|
915
|
+
return parsed.version;
|
|
916
|
+
}
|
|
917
|
+
function defaultStateDir(env) {
|
|
918
|
+
if (env.AIRIG_STATE_DIR) return env.AIRIG_STATE_DIR;
|
|
919
|
+
if (env.XDG_STATE_HOME) return path.join(env.XDG_STATE_HOME, "airig");
|
|
920
|
+
return path.join(os.homedir(), ".local", "state", "airig");
|
|
921
|
+
}
|
|
922
|
+
function readUpdateState(statePath) {
|
|
923
|
+
try {
|
|
924
|
+
return JSON.parse(readFileSync(statePath, "utf8"));
|
|
925
|
+
} catch {
|
|
926
|
+
return {};
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function writeUpdateState(statePath, state) {
|
|
930
|
+
mkdirSync(path.dirname(statePath), { recursive: true });
|
|
931
|
+
writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
932
|
+
}
|
|
933
|
+
function isUpdateCheckDue(state, now) {
|
|
934
|
+
return isIntervalDue(state.lastUpdateCheck, now, UPDATE_CHECK_INTERVAL_MS);
|
|
935
|
+
}
|
|
936
|
+
function isNotificationDue(state, now) {
|
|
937
|
+
return isIntervalDue(state.lastNotifiedAt, now, UPDATE_NOTIFICATION_INTERVAL_MS);
|
|
938
|
+
}
|
|
939
|
+
function isIntervalDue(previous, now, intervalMs) {
|
|
940
|
+
if (!previous) return true;
|
|
941
|
+
const previousTime = new Date(previous).valueOf();
|
|
942
|
+
if (Number.isNaN(previousTime)) return true;
|
|
943
|
+
return now.valueOf() - previousTime >= intervalMs;
|
|
944
|
+
}
|
|
945
|
+
function spawnUpdateCheck(statePath) {
|
|
946
|
+
spawn(process.execPath, [
|
|
947
|
+
"--input-type=module",
|
|
948
|
+
"--eval",
|
|
949
|
+
UPDATE_CHECK_SCRIPT
|
|
950
|
+
], {
|
|
951
|
+
detached: true,
|
|
952
|
+
stdio: "ignore",
|
|
953
|
+
env: {
|
|
954
|
+
...process.env,
|
|
955
|
+
AIRIG_UPDATE_STATE_PATH: statePath,
|
|
956
|
+
AIRIG_UPDATE_REGISTRY_URL: REGISTRY_LATEST_URL
|
|
957
|
+
}
|
|
958
|
+
}).unref();
|
|
959
|
+
}
|
|
960
|
+
const UPDATE_CHECK_SCRIPT = `
|
|
961
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
962
|
+
import path from 'node:path'
|
|
963
|
+
|
|
964
|
+
const statePath = process.env.AIRIG_UPDATE_STATE_PATH
|
|
965
|
+
const registryUrl = process.env.AIRIG_UPDATE_REGISTRY_URL
|
|
966
|
+
const now = new Date().toUTCString()
|
|
967
|
+
|
|
968
|
+
async function readState() {
|
|
969
|
+
try {
|
|
970
|
+
return JSON.parse(await readFile(statePath, 'utf8'))
|
|
971
|
+
} catch {
|
|
972
|
+
return {}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function writeState(state) {
|
|
977
|
+
await mkdir(path.dirname(statePath), { recursive: true })
|
|
978
|
+
await writeFile(statePath, JSON.stringify(state, null, 2) + '\\n')
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const state = await readState()
|
|
982
|
+
|
|
983
|
+
try {
|
|
984
|
+
const response = await fetch(registryUrl, { signal: AbortSignal.timeout(3000) })
|
|
985
|
+
const data = response.ok ? await response.json() : undefined
|
|
986
|
+
await writeState({ ...state, lastUpdateCheck: now, latestVersion: data?.version ?? state.latestVersion })
|
|
987
|
+
} catch {
|
|
988
|
+
await writeState({ ...state, lastUpdateCheck: now })
|
|
989
|
+
}
|
|
990
|
+
`;
|
|
991
|
+
function isVersionGreater(candidate, current) {
|
|
992
|
+
const candidateVersion = parseSemver(candidate);
|
|
993
|
+
const currentVersion = parseSemver(current);
|
|
994
|
+
if (!candidateVersion || !currentVersion) return candidate !== current;
|
|
995
|
+
for (let i = 0; i < 3; i += 1) {
|
|
996
|
+
const diff = candidateVersion.numbers[i] - currentVersion.numbers[i];
|
|
997
|
+
if (diff !== 0) return diff > 0;
|
|
998
|
+
}
|
|
999
|
+
if (candidateVersion.prerelease === currentVersion.prerelease) return false;
|
|
1000
|
+
if (!candidateVersion.prerelease) return Boolean(currentVersion.prerelease);
|
|
1001
|
+
if (!currentVersion.prerelease) return false;
|
|
1002
|
+
return candidateVersion.prerelease.localeCompare(currentVersion.prerelease, void 0, { numeric: true }) > 0;
|
|
1003
|
+
}
|
|
1004
|
+
function parseSemver(version) {
|
|
1005
|
+
const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
1006
|
+
if (!match) return void 0;
|
|
1007
|
+
return {
|
|
1008
|
+
numbers: [
|
|
1009
|
+
Number(match[1]),
|
|
1010
|
+
Number(match[2]),
|
|
1011
|
+
Number(match[3])
|
|
1012
|
+
],
|
|
1013
|
+
prerelease: match[4] ?? ""
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function isGlobalInstall(packageRoot, cwd) {
|
|
1017
|
+
if (isOneOffInstallPath(packageRoot)) return false;
|
|
1018
|
+
if (findProjectRootForLocalInstall(packageRoot, cwd)) return false;
|
|
1019
|
+
return true;
|
|
1020
|
+
}
|
|
1021
|
+
function findOwnPackageJsonPath() {
|
|
1022
|
+
let currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
1023
|
+
while (true) {
|
|
1024
|
+
const candidate = path.join(currentDir, "package.json");
|
|
1025
|
+
try {
|
|
1026
|
+
if (JSON.parse(readFileSync(candidate, "utf8")).name === PACKAGE_NAME) return candidate;
|
|
1027
|
+
} catch {}
|
|
1028
|
+
const parentDir = path.dirname(currentDir);
|
|
1029
|
+
if (parentDir === currentDir) break;
|
|
1030
|
+
currentDir = parentDir;
|
|
1031
|
+
}
|
|
1032
|
+
throw new Error(`Could not locate ${PACKAGE_NAME} package metadata`);
|
|
1033
|
+
}
|
|
1034
|
+
function isOneOffInstallPath(packageRoot) {
|
|
1035
|
+
const normalizedRoot = packageRoot.split(path.sep).join("/");
|
|
1036
|
+
return normalizedRoot.includes("/_npx/") || normalizedRoot.includes("/.npm/_npx/");
|
|
1037
|
+
}
|
|
1038
|
+
function findProjectRootForLocalInstall(packageRoot, cwd) {
|
|
1039
|
+
const segments = packageRoot.split(path.sep);
|
|
1040
|
+
for (let index = segments.length - 1; index > 0; index -= 1) {
|
|
1041
|
+
if (segments[index] !== "node_modules") continue;
|
|
1042
|
+
const projectRoot = segments.slice(0, index).join(path.sep) || path.sep;
|
|
1043
|
+
if (existsSync(path.join(projectRoot, "package.json")) && isSameOrInside(cwd, projectRoot)) return projectRoot;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function isSameOrInside(candidate, parent) {
|
|
1047
|
+
const relative = path.relative(parent, candidate);
|
|
1048
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
1049
|
+
}
|
|
1050
|
+
function formatUpdateMessage(opts) {
|
|
1051
|
+
return `\nA new airig version is available: ${opts.currentVersion} -> ${opts.latestVersion}.\nYour installed airig package is outdated. Please upgrade to the latest version.\n\n`;
|
|
1052
|
+
}
|
|
1053
|
+
//#endregion
|
|
874
1054
|
//#region src/index.ts
|
|
875
|
-
const program = new Command("airig").description("Manage project-scoped AI Setup artifacts");
|
|
1055
|
+
const program = new Command("airig").description("Manage project-scoped AI Setup artifacts").version("0.0.5");
|
|
876
1056
|
program.addCommand(publishCommand);
|
|
877
1057
|
program.addCommand(addCommand);
|
|
878
1058
|
program.addCommand(updateCommand);
|
|
879
1059
|
program.addCommand(removeCommand);
|
|
880
|
-
program.
|
|
1060
|
+
program.hook("postAction", () => {
|
|
1061
|
+
try {
|
|
1062
|
+
maybeNotifyForUpdate();
|
|
1063
|
+
} catch {}
|
|
1064
|
+
});
|
|
1065
|
+
await program.parseAsync();
|
|
881
1066
|
//#endregion
|
|
882
1067
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@airig/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Distribute and manage AI setups across providers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@inquirer/prompts": "^7.5.2",
|
|
40
40
|
"@octokit/rest": "^21.1.1",
|
|
41
|
-
"archiver": "^
|
|
41
|
+
"archiver": "^8.0.0",
|
|
42
42
|
"commander": "^14.0.0",
|
|
43
43
|
"extract-zip": "^2.0.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@types/archiver": "^
|
|
46
|
+
"@types/archiver": "^8.0.0",
|
|
47
47
|
"@types/node": "^22.15.21",
|
|
48
48
|
"bumpp": "^11.1.0",
|
|
49
49
|
"tsdown": "^0.12.6",
|