altalandi 0.6.0 → 0.6.2
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 +269 -260
- package/altalandi.mjs +330 -325
- package/auth.mjs +221 -223
- package/batch.mjs +769 -620
- package/client.d.ts +57 -0
- package/client.mjs +52 -13
- package/package.json +1 -1
- package/report.mjs +74 -15
- package/skill/SKILL.md +187 -112
- package/update.mjs +315 -273
package/update.mjs
CHANGED
|
@@ -1,273 +1,315 @@
|
|
|
1
|
-
// `altalandi update` — check for a newer CLI, ask before installing it, then
|
|
2
|
-
// offer to install or refresh the /altalandi skill for Claude Code.
|
|
3
|
-
//
|
|
4
|
-
// Nothing is ever changed without consent. Interactive terminals get a y/N
|
|
5
|
-
// prompt; non-interactive callers (agents, CI, pipes) are only told what is
|
|
6
|
-
// available, unless they pass --yes, which is itself the explicit consent.
|
|
7
|
-
//
|
|
8
|
-
// Zero dependencies, like the rest of the CLI.
|
|
9
|
-
|
|
10
|
-
import { spawn } from "node:child_process";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
export const
|
|
25
|
-
export const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
if (!
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
*
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!existsSync(
|
|
125
|
-
if (!existsSync(target.
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
1
|
+
// `altalandi update` — check for a newer CLI, ask before installing it, then
|
|
2
|
+
// offer to install or refresh the /altalandi skill for Claude Code.
|
|
3
|
+
//
|
|
4
|
+
// Nothing is ever changed without consent. Interactive terminals get a y/N
|
|
5
|
+
// prompt; non-interactive callers (agents, CI, pipes) are only told what is
|
|
6
|
+
// available, unless they pass --yes, which is itself the explicit consent.
|
|
7
|
+
//
|
|
8
|
+
// Zero dependencies, like the rest of the CLI.
|
|
9
|
+
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
13
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { createInterface } from "node:readline/promises";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
export const PACKAGE = "altalandi";
|
|
20
|
+
export const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE}/latest`;
|
|
21
|
+
|
|
22
|
+
// Exit codes. 10 is deliberately distinct from the refine command's 1/2/3 so a
|
|
23
|
+
// script can tell "an update exists" apart from "something failed".
|
|
24
|
+
export const EXIT_OK = 0;
|
|
25
|
+
export const EXIT_CHECK_FAILED = 1;
|
|
26
|
+
export const EXIT_UPDATE_AVAILABLE = 10;
|
|
27
|
+
|
|
28
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
export const BUNDLED_SKILL = path.join(HERE, "skill", "SKILL.md");
|
|
30
|
+
|
|
31
|
+
export async function currentVersion() {
|
|
32
|
+
const pkg = JSON.parse(await readFile(path.join(HERE, "package.json"), "utf8"));
|
|
33
|
+
return pkg.version;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Semver-ish compare: -1 if a<b, 0 if equal, 1 if a>b. A prerelease sorts
|
|
37
|
+
* before its release (0.2.0-beta < 0.2.0). */
|
|
38
|
+
export function compareVersions(a, b) {
|
|
39
|
+
const parse = (v) => {
|
|
40
|
+
const [core, pre = ""] = String(v).trim().replace(/^v/, "").split("-", 2);
|
|
41
|
+
return { nums: core.split(".").map((n) => Number.parseInt(n, 10) || 0), pre };
|
|
42
|
+
};
|
|
43
|
+
const x = parse(a);
|
|
44
|
+
const y = parse(b);
|
|
45
|
+
for (let i = 0; i < 3; i++) {
|
|
46
|
+
const d = (x.nums[i] || 0) - (y.nums[i] || 0);
|
|
47
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
48
|
+
}
|
|
49
|
+
if (x.pre === y.pre) return 0;
|
|
50
|
+
if (!x.pre) return 1;
|
|
51
|
+
if (!y.pre) return -1;
|
|
52
|
+
return x.pre < y.pre ? -1 : 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function fetchLatestVersion({ fetchImpl = globalThis.fetch, timeoutMs = 8000 } = {}) {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetchImpl(REGISTRY_URL, {
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
headers: { accept: "application/json" },
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) throw new Error(`npm registry answered ${res.status}`);
|
|
64
|
+
const body = JSON.parse(await res.text());
|
|
65
|
+
if (!body || typeof body.version !== "string") throw new Error("npm registry returned no version");
|
|
66
|
+
return body.version;
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const norm = (p) => {
|
|
73
|
+
let out = path.resolve(p);
|
|
74
|
+
try { out = realpathSync(out); } catch { /* keep the resolved path */ }
|
|
75
|
+
return process.platform === "win32" ? out.toLowerCase() : out;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** How is this copy of the CLI being run? That decides what "update" means.
|
|
79
|
+
*
|
|
80
|
+
* npx — lives in npm's _npx cache; there is nothing to install, but a bare
|
|
81
|
+
* `npx altalandi` can keep reusing an old cached copy
|
|
82
|
+
* global — `npm install -g altalandi`
|
|
83
|
+
* local — a project dependency
|
|
84
|
+
* source — a checkout of the repository; npm cannot update that
|
|
85
|
+
*/
|
|
86
|
+
export function detectInstall({ scriptPath, globalRoot }) {
|
|
87
|
+
const p = norm(scriptPath);
|
|
88
|
+
const sep = path.sep === "\\" ? /[\\/]/ : /\//;
|
|
89
|
+
const parts = p.split(sep);
|
|
90
|
+
if (parts.includes("_npx")) return "npx";
|
|
91
|
+
const nm = parts.lastIndexOf("node_modules");
|
|
92
|
+
if (nm === -1) return "source";
|
|
93
|
+
if (globalRoot && p.startsWith(norm(globalRoot) + (process.platform === "win32" ? "\\" : "/"))) {
|
|
94
|
+
return "global";
|
|
95
|
+
}
|
|
96
|
+
return "local";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The exact command that updates this install. Constants only — never built
|
|
100
|
+
* from user input. */
|
|
101
|
+
export function updatePlan(kind, { projectDir } = {}) {
|
|
102
|
+
switch (kind) {
|
|
103
|
+
case "global":
|
|
104
|
+
return { cmd: "npm", args: ["install", "-g", `${PACKAGE}@latest`], show: `npm install -g ${PACKAGE}@latest` };
|
|
105
|
+
case "local":
|
|
106
|
+
return { cmd: "npm", args: ["install", `${PACKAGE}@latest`], cwd: projectDir, show: `npm install ${PACKAGE}@latest` };
|
|
107
|
+
case "npx":
|
|
108
|
+
return { cmd: "npx", args: ["--yes", `${PACKAGE}@latest`, "--version"], show: `npx ${PACKAGE}@latest` };
|
|
109
|
+
default:
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Where Claude Code looks for personal skills. */
|
|
115
|
+
export function skillTarget({ env = process.env, home = homedir() } = {}) {
|
|
116
|
+
const root = env.CLAUDE_CONFIG_DIR || path.join(home, ".claude");
|
|
117
|
+
return { root, file: path.join(root, "skills", PACKAGE, "SKILL.md") };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** missing | current | outdated. `absent` means Claude Code itself is not set
|
|
121
|
+
* up, in which case the skill is not offered at all — never create ~/.claude
|
|
122
|
+
* for someone who does not use it. */
|
|
123
|
+
export async function skillStatus({ bundled = BUNDLED_SKILL, target = skillTarget() } = {}) {
|
|
124
|
+
if (!existsSync(bundled)) return "unbundled";
|
|
125
|
+
if (!existsSync(target.root)) return "absent";
|
|
126
|
+
if (!existsSync(target.file)) return "missing";
|
|
127
|
+
const [a, b] = await Promise.all([readFile(bundled, "utf8"), readFile(target.file, "utf8")]);
|
|
128
|
+
const clean = (s) => s.replace(/\r\n/g, "\n").trim();
|
|
129
|
+
return clean(a) === clean(b) ? "current" : "outdated";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Fingerprints (sha256 of the text with LF line ends, trimmed; first 16 hex)
|
|
133
|
+
// of every SKILL.md a published version shipped. A skill that matches one is
|
|
134
|
+
// ours, unedited, so it may be refreshed without asking; anything else was
|
|
135
|
+
// changed by its owner and is only reported. Add the old fingerprint here in
|
|
136
|
+
// every release that changes the skill.
|
|
137
|
+
export const SHIPPED_SKILLS = new Set([
|
|
138
|
+
"1a64ea93bfae71cc", // 0.2.0
|
|
139
|
+
"51920af235d23f02", // 0.3.0, 0.3.1
|
|
140
|
+
"f7de208557f519a4", // 0.4.0
|
|
141
|
+
"4bd64565d251641e", // 0.5.0
|
|
142
|
+
"aeb88010599539fd", // 0.6.0
|
|
143
|
+
"ebfc4ef17a614973", // 0.6.1
|
|
144
|
+
"9def55ecc6b30785", // 0.6.2
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
export function skillFingerprint(text) {
|
|
148
|
+
return createHash("sha256").update(text.replace(/\r\n/g, "\n").trim()).digest("hex").slice(0, 16);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** On every ordinary run: the skill calls `npx altalandi@latest`, so this is
|
|
152
|
+
* already the newest CLI. An installed skill that is an unedited older copy
|
|
153
|
+
* of ours is brought up to date (backed up first), so people get the new
|
|
154
|
+
* skill without doing anything; an edited one is left alone with a hint.
|
|
155
|
+
* Never throws, never creates ~/.claude. ALTALANDI_NO_SKILL_REFRESH=1 opts out. */
|
|
156
|
+
export async function refreshShippedSkill({ bundled = BUNDLED_SKILL, target = skillTarget(), env = process.env, log = () => {} } = {}) {
|
|
157
|
+
try {
|
|
158
|
+
if (env.ALTALANDI_NO_SKILL_REFRESH === "1") return "skipped";
|
|
159
|
+
const status = await skillStatus({ bundled, target });
|
|
160
|
+
if (status !== "outdated") return status;
|
|
161
|
+
if (!SHIPPED_SKILLS.has(skillFingerprint(await readFile(target.file, "utf8")))) {
|
|
162
|
+
log("Altalandi: your /altalandi skill has your own edits, so it was not updated. Run `npx altalandi@latest update` to refresh it.");
|
|
163
|
+
return "edited";
|
|
164
|
+
}
|
|
165
|
+
await writeSkill({ bundled, target });
|
|
166
|
+
log("Altalandi: the /altalandi skill was updated to the newest version (it applies from your next session).");
|
|
167
|
+
return "refreshed";
|
|
168
|
+
} catch {
|
|
169
|
+
return "failed";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Install or refresh the skill. An existing copy is always backed up first:
|
|
174
|
+
* people edit their skills, and an update must never silently eat that. */
|
|
175
|
+
export async function writeSkill({ bundled = BUNDLED_SKILL, target = skillTarget(), now = new Date() } = {}) {
|
|
176
|
+
await mkdir(path.dirname(target.file), { recursive: true });
|
|
177
|
+
let backup = null;
|
|
178
|
+
if (existsSync(target.file)) {
|
|
179
|
+
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
180
|
+
backup = `${target.file}.bak-${stamp}`;
|
|
181
|
+
await copyFile(target.file, backup);
|
|
182
|
+
}
|
|
183
|
+
await writeFile(target.file, await readFile(bundled, "utf8"), "utf8");
|
|
184
|
+
return { file: target.file, backup };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function run(plan) {
|
|
188
|
+
return new Promise((resolve) => {
|
|
189
|
+
// npm and npx are .cmd shims on Windows, which need a shell. The arguments
|
|
190
|
+
// are constants from updatePlan(), so there is nothing to inject.
|
|
191
|
+
const child = spawn(plan.cmd, plan.args, {
|
|
192
|
+
cwd: plan.cwd,
|
|
193
|
+
stdio: "inherit",
|
|
194
|
+
shell: process.platform === "win32",
|
|
195
|
+
});
|
|
196
|
+
child.on("error", () => resolve(1));
|
|
197
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function npmGlobalRoot() {
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
const child = spawn("npm", ["root", "-g"], { shell: process.platform === "win32" });
|
|
204
|
+
let out = "";
|
|
205
|
+
child.stdout?.setEncoding("utf8").on("data", (c) => { out += c; });
|
|
206
|
+
child.on("error", () => resolve(null));
|
|
207
|
+
child.on("close", () => resolve(out.trim() || null));
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A y/N question on stderr; only y/yes (or j/já) is a yes. Shared with the
|
|
212
|
+
* folder and site runs, which ask before sending anything. */
|
|
213
|
+
export async function ask(question, { input = process.stdin, output = process.stderr } = {}) {
|
|
214
|
+
const rl = createInterface({ input, output });
|
|
215
|
+
try {
|
|
216
|
+
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
217
|
+
return answer === "y" || answer === "yes" || answer === "j" || answer === "já";
|
|
218
|
+
} finally {
|
|
219
|
+
rl.close();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The whole command. Every side effect is injectable so it can be tested
|
|
225
|
+
* without touching npm, the network or the real home directory.
|
|
226
|
+
*/
|
|
227
|
+
export async function runUpdate(flags, io = {}) {
|
|
228
|
+
const log = io.log || ((m) => process.stderr.write(`${m}\n`));
|
|
229
|
+
const interactive = io.interactive ?? Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
230
|
+
const confirm = io.confirm || ask;
|
|
231
|
+
const execute = io.run || run;
|
|
232
|
+
const target = io.skillTarget || skillTarget();
|
|
233
|
+
const bundled = io.bundledSkill || BUNDLED_SKILL;
|
|
234
|
+
|
|
235
|
+
// --check never changes anything. Without a terminal and without --yes there
|
|
236
|
+
// is nobody to ask, so the same rule applies: report, don't act.
|
|
237
|
+
const mayAct = !flags.check && (flags.yes || interactive);
|
|
238
|
+
const consent = async (q) => (flags.yes ? true : confirm(q));
|
|
239
|
+
|
|
240
|
+
const installed = io.currentVersion || await currentVersion();
|
|
241
|
+
|
|
242
|
+
if (!flags.skillOnly) {
|
|
243
|
+
let latest;
|
|
244
|
+
try {
|
|
245
|
+
latest = io.latestVersion || await fetchLatestVersion({ fetchImpl: io.fetch });
|
|
246
|
+
} catch (e) {
|
|
247
|
+
log(`Could not check for updates: ${e.message}`);
|
|
248
|
+
return EXIT_CHECK_FAILED;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (compareVersions(installed, latest) < 0) {
|
|
252
|
+
const scriptPath = io.scriptPath || fileURLToPath(new URL("./altalandi.mjs", import.meta.url));
|
|
253
|
+
// `npm root -g` spawns npm, so only ask it when the answer matters: a copy
|
|
254
|
+
// inside some node_modules that is not npx's cache.
|
|
255
|
+
const inNodeModules = /[\\/]node_modules[\\/]/.test(scriptPath) && !/[\\/]_npx[\\/]/.test(scriptPath);
|
|
256
|
+
const globalRoot = io.globalRoot !== undefined ? io.globalRoot
|
|
257
|
+
: (inNodeModules ? await (io.npmGlobalRoot || npmGlobalRoot)() : null);
|
|
258
|
+
const kind = io.installKind || detectInstall({ scriptPath, globalRoot });
|
|
259
|
+
const plan = updatePlan(kind, { projectDir: io.projectDir || process.cwd() });
|
|
260
|
+
log(`A new version of ${PACKAGE} is available: ${installed} → ${latest}`);
|
|
261
|
+
|
|
262
|
+
if (!plan) {
|
|
263
|
+
log("This copy runs from a source checkout, so npm cannot update it. Pull the repository instead.");
|
|
264
|
+
return EXIT_UPDATE_AVAILABLE;
|
|
265
|
+
}
|
|
266
|
+
if (!mayAct) {
|
|
267
|
+
log(`Update with: ${plan.show}`);
|
|
268
|
+
return EXIT_UPDATE_AVAILABLE;
|
|
269
|
+
}
|
|
270
|
+
if (!(await consent(`Update ${PACKAGE} to ${latest} now?`))) {
|
|
271
|
+
log(`Skipped. Update later with: ${plan.show}`);
|
|
272
|
+
return EXIT_UPDATE_AVAILABLE;
|
|
273
|
+
}
|
|
274
|
+
log(`Running: ${plan.show}`);
|
|
275
|
+
const code = await execute(plan);
|
|
276
|
+
if (code !== 0) {
|
|
277
|
+
log(`Update failed (exit ${code}). Try running it yourself: ${plan.show}`);
|
|
278
|
+
return EXIT_CHECK_FAILED;
|
|
279
|
+
}
|
|
280
|
+
log(`Updated to ${latest}.`);
|
|
281
|
+
// The skill that ships with the NEW version is the one worth installing,
|
|
282
|
+
// and this process is still the old one. Hand over to the new CLI.
|
|
283
|
+
const handoff = kind === "npx"
|
|
284
|
+
? { cmd: "npx", args: ["--yes", `${PACKAGE}@latest`, "update", "--skill-only", ...(flags.yes ? ["--yes"] : [])] }
|
|
285
|
+
: { cmd: PACKAGE, args: ["update", "--skill-only", ...(flags.yes ? ["--yes"] : [])] };
|
|
286
|
+
await execute(handoff);
|
|
287
|
+
return EXIT_OK;
|
|
288
|
+
}
|
|
289
|
+
log(`${PACKAGE} ${installed} is up to date.`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const status = await skillStatus({ bundled, target });
|
|
293
|
+
if (status === "unbundled" || status === "absent") return EXIT_OK;
|
|
294
|
+
if (status === "current") {
|
|
295
|
+
log("The /altalandi skill for Claude Code is up to date.");
|
|
296
|
+
return EXIT_OK;
|
|
297
|
+
}
|
|
298
|
+
const what = status === "missing"
|
|
299
|
+
? `Install the /altalandi skill for Claude Code (${target.file})?`
|
|
300
|
+
: "Your /altalandi skill for Claude Code differs from this version. Replace it (your copy is backed up first)?";
|
|
301
|
+
if (!mayAct) {
|
|
302
|
+
log(status === "missing"
|
|
303
|
+
? "The /altalandi skill for Claude Code is not installed. Run `altalandi update` in a terminal to install it."
|
|
304
|
+
: "Your /altalandi skill for Claude Code is out of date. Run `altalandi update` in a terminal to refresh it.");
|
|
305
|
+
return flags.skillOnly ? EXIT_OK : EXIT_UPDATE_AVAILABLE;
|
|
306
|
+
}
|
|
307
|
+
if (!(await consent(what))) {
|
|
308
|
+
log("Skill left unchanged.");
|
|
309
|
+
return EXIT_OK;
|
|
310
|
+
}
|
|
311
|
+
const written = await writeSkill({ bundled, target, now: io.now });
|
|
312
|
+
log(`Skill ${status === "missing" ? "installed" : "updated"}: ${written.file}`);
|
|
313
|
+
if (written.backup) log(`Previous copy saved as: ${written.backup}`);
|
|
314
|
+
return EXIT_OK;
|
|
315
|
+
}
|