@impulselab/directory 1.1.0 → 2.7.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 +60 -120
- package/dist/index.js +1615 -0
- package/package.json +27 -19
- package/index.js +0 -1048
package/dist/index.js
ADDED
|
@@ -0,0 +1,1615 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import path7 from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/commands/add.ts
|
|
10
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
11
|
+
import { homedir as homedir3 } from "os";
|
|
12
|
+
import path3 from "path";
|
|
13
|
+
import {
|
|
14
|
+
cancel,
|
|
15
|
+
intro,
|
|
16
|
+
isCancel,
|
|
17
|
+
multiselect,
|
|
18
|
+
outro,
|
|
19
|
+
spinner
|
|
20
|
+
} from "@clack/prompts";
|
|
21
|
+
|
|
22
|
+
// ../shared/src/artifact.ts
|
|
23
|
+
var ARTIFACT_KINDS = ["skill", "command", "agent", "rule"];
|
|
24
|
+
var legacyTargetToKindMap = {
|
|
25
|
+
"claude-slash": "command",
|
|
26
|
+
"claude-sub-agent": "agent",
|
|
27
|
+
"claude-skill": "skill",
|
|
28
|
+
"cursor-command": "command",
|
|
29
|
+
"cursor-rule": "rule"
|
|
30
|
+
};
|
|
31
|
+
var LEGACY_TARGETS = Object.keys(legacyTargetToKindMap);
|
|
32
|
+
function legacyTargetToKind(target) {
|
|
33
|
+
if (target in legacyTargetToKindMap) {
|
|
34
|
+
return legacyTargetToKindMap[target];
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ../shared/src/hash.ts
|
|
40
|
+
import { createHash } from "crypto";
|
|
41
|
+
function sha256Hex(input) {
|
|
42
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ../shared/src/manifest.ts
|
|
46
|
+
import { z } from "zod";
|
|
47
|
+
|
|
48
|
+
// ../shared/src/safe-segment.ts
|
|
49
|
+
var SAFE_PATH_SEGMENT_REGEX = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/i;
|
|
50
|
+
function isSafePathSegment(value) {
|
|
51
|
+
return value.length <= 128 && SAFE_PATH_SEGMENT_REGEX.test(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ../shared/src/manifest.ts
|
|
55
|
+
var sha256HexSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
56
|
+
var safePathSegmentSchema = z.string().max(256).regex(SAFE_PATH_SEGMENT_REGEX);
|
|
57
|
+
var manifestArtifactSchema = z.object({
|
|
58
|
+
id: z.string(),
|
|
59
|
+
slug: safePathSegmentSchema,
|
|
60
|
+
ownerSlug: safePathSegmentSchema,
|
|
61
|
+
path: z.string(),
|
|
62
|
+
title: z.string(),
|
|
63
|
+
kind: z.enum(ARTIFACT_KINDS),
|
|
64
|
+
version: z.number().int().min(1),
|
|
65
|
+
contentHash: sha256HexSchema
|
|
66
|
+
});
|
|
67
|
+
var manifestStackSchema = z.object({
|
|
68
|
+
id: z.string(),
|
|
69
|
+
slug: safePathSegmentSchema,
|
|
70
|
+
title: z.string(),
|
|
71
|
+
artifacts: z.array(manifestArtifactSchema)
|
|
72
|
+
});
|
|
73
|
+
var syncManifestSchema = z.object({
|
|
74
|
+
generatedAt: z.iso.datetime(),
|
|
75
|
+
stacks: z.array(manifestStackSchema)
|
|
76
|
+
});
|
|
77
|
+
var artifactResponseSchema = manifestArtifactSchema.extend({
|
|
78
|
+
content: z.string(),
|
|
79
|
+
changelog: z.string().nullable()
|
|
80
|
+
});
|
|
81
|
+
var pushVersionRequestSchema = z.object({
|
|
82
|
+
content: z.string().min(1),
|
|
83
|
+
baseVersion: z.number().int().min(1),
|
|
84
|
+
changelog: z.string().max(500).optional()
|
|
85
|
+
});
|
|
86
|
+
var pushVersionResponseSchema = z.object({
|
|
87
|
+
id: z.string(),
|
|
88
|
+
version: z.number().int().min(1),
|
|
89
|
+
contentHash: sha256HexSchema
|
|
90
|
+
});
|
|
91
|
+
var meResponseSchema = z.object({
|
|
92
|
+
user: z.object({
|
|
93
|
+
id: z.string(),
|
|
94
|
+
slug: safePathSegmentSchema,
|
|
95
|
+
name: z.string(),
|
|
96
|
+
email: z.string()
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ../shared/src/targets.ts
|
|
101
|
+
var AGENT_NAMES = ["claude", "codex", "cursor"];
|
|
102
|
+
var TARGET_RULES = {
|
|
103
|
+
skill: [
|
|
104
|
+
{
|
|
105
|
+
agent: "claude",
|
|
106
|
+
scopes: ["global", "project"],
|
|
107
|
+
relativePath: (slug) => `.claude/skills/${slug}/SKILL.md`
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
command: [
|
|
111
|
+
{
|
|
112
|
+
agent: "claude",
|
|
113
|
+
scopes: ["global", "project"],
|
|
114
|
+
relativePath: (slug) => `.claude/commands/${slug}.md`
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
agent: "codex",
|
|
118
|
+
scopes: ["global"],
|
|
119
|
+
relativePath: (slug) => `.codex/prompts/${slug}.md`
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
agent: "cursor",
|
|
123
|
+
scopes: ["project"],
|
|
124
|
+
relativePath: (slug) => `.cursor/commands/${slug}.md`
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
agent: [
|
|
128
|
+
{
|
|
129
|
+
agent: "claude",
|
|
130
|
+
scopes: ["global", "project"],
|
|
131
|
+
relativePath: (slug) => `.claude/agents/${slug}.md`
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
rule: [
|
|
135
|
+
{
|
|
136
|
+
agent: "cursor",
|
|
137
|
+
scopes: ["project"],
|
|
138
|
+
relativePath: (slug) => `.cursor/rules/${slug}.mdc`
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
};
|
|
142
|
+
function resolveInstallPaths(kind, slug, agents2, scope) {
|
|
143
|
+
if (!isSafePathSegment(slug)) {
|
|
144
|
+
throw new Error(`Invalid artifact slug '${slug}'`);
|
|
145
|
+
}
|
|
146
|
+
return TARGET_RULES[kind].filter(
|
|
147
|
+
(rule) => rule.scopes.includes(scope) && agents2.includes(rule.agent)
|
|
148
|
+
).map((rule) => rule.relativePath(slug));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/commands/add.ts
|
|
152
|
+
import pc from "picocolors";
|
|
153
|
+
|
|
154
|
+
// src/lib/cli-invocation.ts
|
|
155
|
+
var cliInvocation = "npx @impulselab/directory";
|
|
156
|
+
function cliCommand(args) {
|
|
157
|
+
return `${cliInvocation} ${args}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/lib/config.ts
|
|
161
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
162
|
+
import { homedir } from "os";
|
|
163
|
+
import path from "path";
|
|
164
|
+
import { z as z2 } from "zod";
|
|
165
|
+
var configPath = path.join(homedir(), ".impulse", "config.json");
|
|
166
|
+
var schemeRegex = /^[a-z][a-z\d+\-.]*:\/\//i;
|
|
167
|
+
var trailingSlashesRegex = /\/+$/;
|
|
168
|
+
var trailingSlashRegex = /\/$/;
|
|
169
|
+
var httpSchemeRegex = /^https?:\/\//i;
|
|
170
|
+
var agentConfigSchema = z2.object({
|
|
171
|
+
claude: z2.boolean().default(true),
|
|
172
|
+
codex: z2.boolean().default(true),
|
|
173
|
+
cursor: z2.boolean().default(true)
|
|
174
|
+
});
|
|
175
|
+
var configSchema = z2.object({
|
|
176
|
+
baseUrl: z2.string().default(normalizeBaseUrl(process.env.IMPULSE_BASE_URL)),
|
|
177
|
+
apiKey: z2.string().optional(),
|
|
178
|
+
agents: agentConfigSchema.default({
|
|
179
|
+
claude: true,
|
|
180
|
+
codex: true,
|
|
181
|
+
cursor: true
|
|
182
|
+
})
|
|
183
|
+
});
|
|
184
|
+
function normalizeBaseUrl(input) {
|
|
185
|
+
const raw = (input || "https://impulse.directory").trim();
|
|
186
|
+
const host = raw === "::1" ? "[::1]" : raw;
|
|
187
|
+
const withScheme = schemeRegex.test(raw) ? raw : `${isLocalHost(raw) ? "http" : "https"}://${host}`;
|
|
188
|
+
try {
|
|
189
|
+
const url = new URL(withScheme);
|
|
190
|
+
url.pathname = url.pathname.replace(trailingSlashesRegex, "");
|
|
191
|
+
url.search = "";
|
|
192
|
+
url.hash = "";
|
|
193
|
+
return url.toString().replace(trailingSlashRegex, "");
|
|
194
|
+
} catch {
|
|
195
|
+
throw new Error(`Invalid base URL: ${input}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function defaultConfig() {
|
|
199
|
+
return configSchema.parse({});
|
|
200
|
+
}
|
|
201
|
+
async function loadConfig() {
|
|
202
|
+
try {
|
|
203
|
+
const raw = await readFile(configPath, "utf8");
|
|
204
|
+
const parsed = configSchema.parse(JSON.parse(raw));
|
|
205
|
+
return {
|
|
206
|
+
...parsed,
|
|
207
|
+
baseUrl: normalizeBaseUrl(process.env.IMPULSE_BASE_URL || parsed.baseUrl)
|
|
208
|
+
};
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (isNotFound(error)) {
|
|
211
|
+
return defaultConfig();
|
|
212
|
+
}
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function saveConfig(config) {
|
|
217
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
218
|
+
const temporaryPath = `${configPath}.tmp`;
|
|
219
|
+
await writeFile(temporaryPath, `${JSON.stringify(config, null, 2)}
|
|
220
|
+
`, {
|
|
221
|
+
mode: 384
|
|
222
|
+
});
|
|
223
|
+
await chmod(temporaryPath, 384);
|
|
224
|
+
await rename(temporaryPath, configPath);
|
|
225
|
+
await chmod(configPath, 384);
|
|
226
|
+
}
|
|
227
|
+
function isLocalHost(input) {
|
|
228
|
+
const withoutScheme = input.trim().replace(httpSchemeRegex, "");
|
|
229
|
+
const hostname2 = extractHostname(withoutScheme);
|
|
230
|
+
return hostname2 === "localhost" || hostname2 === "127.0.0.1" || hostname2 === "::1";
|
|
231
|
+
}
|
|
232
|
+
function extractHostname(input) {
|
|
233
|
+
if (input === "::1") {
|
|
234
|
+
return "::1";
|
|
235
|
+
}
|
|
236
|
+
if (input.startsWith("[")) {
|
|
237
|
+
const bracketIndex = input.indexOf("]");
|
|
238
|
+
return bracketIndex === -1 ? input : input.slice(1, bracketIndex);
|
|
239
|
+
}
|
|
240
|
+
return input.split(":")[0];
|
|
241
|
+
}
|
|
242
|
+
function isNotFound(error) {
|
|
243
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/lib/api.ts
|
|
247
|
+
var ApiError = class extends Error {
|
|
248
|
+
status;
|
|
249
|
+
payload;
|
|
250
|
+
constructor(status, message, payload) {
|
|
251
|
+
super(message);
|
|
252
|
+
this.name = "ApiError";
|
|
253
|
+
this.status = status;
|
|
254
|
+
this.payload = payload;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
function createApiClient(baseUrl, apiKey) {
|
|
258
|
+
assertAllowedBaseUrl(baseUrl);
|
|
259
|
+
async function request(apiPath, schema, init = {}) {
|
|
260
|
+
let response;
|
|
261
|
+
try {
|
|
262
|
+
response = await fetch(`${baseUrl}${apiPath}`, {
|
|
263
|
+
...init,
|
|
264
|
+
headers: {
|
|
265
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
266
|
+
...init.body ? { "content-type": "application/json" } : {},
|
|
267
|
+
...init.headers
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
272
|
+
throw new Error(`Could not reach ${baseUrl}: ${message}`);
|
|
273
|
+
}
|
|
274
|
+
const payload = await readPayload(response);
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
throw new ApiError(
|
|
277
|
+
response.status,
|
|
278
|
+
errorMessage(response.status, payload),
|
|
279
|
+
payload
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return schema.parse(payload);
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
artifact(id, version) {
|
|
286
|
+
if (version !== void 0 && (!Number.isInteger(version) || version < 1)) {
|
|
287
|
+
throw new Error("Artifact version must be a positive integer");
|
|
288
|
+
}
|
|
289
|
+
const query = version !== void 0 ? `?version=${version}` : "";
|
|
290
|
+
return request(
|
|
291
|
+
`/api/v1/artifacts/${encodeURIComponent(id)}${query}`,
|
|
292
|
+
artifactResponseSchema
|
|
293
|
+
);
|
|
294
|
+
},
|
|
295
|
+
manifest() {
|
|
296
|
+
return request("/api/v1/sync/manifest", syncManifestSchema);
|
|
297
|
+
},
|
|
298
|
+
me() {
|
|
299
|
+
return request("/api/v1/me", meResponseSchema);
|
|
300
|
+
},
|
|
301
|
+
pushVersion(id, body) {
|
|
302
|
+
const parsed = pushVersionRequestSchema.parse(body);
|
|
303
|
+
return request(
|
|
304
|
+
`/api/v1/artifacts/${encodeURIComponent(id)}/versions`,
|
|
305
|
+
pushVersionResponseSchema,
|
|
306
|
+
{
|
|
307
|
+
body: JSON.stringify(parsed),
|
|
308
|
+
method: "POST"
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function assertAllowedBaseUrl(baseUrl) {
|
|
315
|
+
const url = new URL(baseUrl);
|
|
316
|
+
if (url.protocol === "http:" && process.env.IMPULSE_ALLOW_HTTP !== "1" && !isLocalHost(url.host)) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
"Refusing to send credentials over HTTP to a non-local host. Use HTTPS or set IMPULSE_ALLOW_HTTP=1 to override."
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function readPayload(response) {
|
|
323
|
+
const text = await response.text();
|
|
324
|
+
if (!text) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
return JSON.parse(text);
|
|
329
|
+
} catch {
|
|
330
|
+
return text;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function errorMessage(status, payload) {
|
|
334
|
+
if (status === 401) {
|
|
335
|
+
return `Invalid or missing API key - run \`${cliCommand("login")}\``;
|
|
336
|
+
}
|
|
337
|
+
if (status === 403) {
|
|
338
|
+
return "You do not have permission to perform this action";
|
|
339
|
+
}
|
|
340
|
+
if (status === 409 && isObject(payload) && payload.error === "version_conflict") {
|
|
341
|
+
return `Remote is at v${payload.currentVersion}, run \`${cliCommand("sync")}\` first (your file is kept)`;
|
|
342
|
+
}
|
|
343
|
+
if (status === 422) {
|
|
344
|
+
return "The server rejected the request as invalid";
|
|
345
|
+
}
|
|
346
|
+
if (isObject(payload) && typeof payload.error === "string") {
|
|
347
|
+
return payload.error;
|
|
348
|
+
}
|
|
349
|
+
return `Request failed with HTTP ${status}`;
|
|
350
|
+
}
|
|
351
|
+
function isObject(value) {
|
|
352
|
+
return typeof value === "object" && value !== null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/lib/paths.ts
|
|
356
|
+
import { existsSync, realpathSync } from "fs";
|
|
357
|
+
import { homedir as homedir2 } from "os";
|
|
358
|
+
import path2 from "path";
|
|
359
|
+
function canonicalize(target) {
|
|
360
|
+
const resolved = path2.resolve(target);
|
|
361
|
+
try {
|
|
362
|
+
return existsSync(resolved) ? realpathSync.native(resolved) : resolved;
|
|
363
|
+
} catch {
|
|
364
|
+
return resolved;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function findUp(names, start = process.cwd(), options = {}) {
|
|
368
|
+
let current = path2.resolve(start);
|
|
369
|
+
const stopBefore = options.stopBefore ? canonicalize(options.stopBefore) : null;
|
|
370
|
+
while (true) {
|
|
371
|
+
if (stopBefore && path2.relative(stopBefore, canonicalize(current)) === "") {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
if (names.some((name) => existsSync(path2.join(current, name)))) {
|
|
375
|
+
return current;
|
|
376
|
+
}
|
|
377
|
+
const parent = path2.dirname(current);
|
|
378
|
+
if (parent === current) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
current = parent;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function resolveProjectRoot(start = process.cwd()) {
|
|
385
|
+
return findUp([".git", ".claude", ".cursor"], start, {
|
|
386
|
+
stopBefore: homedir2()
|
|
387
|
+
}) || path2.resolve(start);
|
|
388
|
+
}
|
|
389
|
+
function resolveAddScopeRoot() {
|
|
390
|
+
const projectRoot = findUp([".claude", ".cursor"]);
|
|
391
|
+
if (projectRoot) {
|
|
392
|
+
return { scope: "project", scopeRoot: projectRoot };
|
|
393
|
+
}
|
|
394
|
+
return { scope: "global", scopeRoot: homedir2() };
|
|
395
|
+
}
|
|
396
|
+
function resolveCursorProjectScopeRoot(start = process.cwd()) {
|
|
397
|
+
return {
|
|
398
|
+
scope: "project",
|
|
399
|
+
scopeRoot: findUp([".claude", ".cursor"], start, { stopBefore: homedir2() }) || path2.resolve(start)
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function relativeDisplay(filePath) {
|
|
403
|
+
const relative = path2.relative(process.cwd(), filePath);
|
|
404
|
+
return relative && !relative.startsWith("..") ? relative : filePath;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/commands/add.ts
|
|
408
|
+
var defaultTarget = "claude-slash";
|
|
409
|
+
var scopeOrder = ["global", "project"];
|
|
410
|
+
var agentLabels = {
|
|
411
|
+
claude: "Claude Code",
|
|
412
|
+
codex: "Codex",
|
|
413
|
+
cursor: "Cursor"
|
|
414
|
+
};
|
|
415
|
+
var accentsRegex = /[\u0300-\u036f]/g;
|
|
416
|
+
var dotDotRegex = /\.\./g;
|
|
417
|
+
var unsafeFilenameRegex = /[<>:"/\\|?*]/g;
|
|
418
|
+
var pathSeparatorRegex = /[/\\]+/g;
|
|
419
|
+
var unsupportedSlugCharsRegex = /[^a-zA-Z0-9._-]+/g;
|
|
420
|
+
var edgeDashesRegex = /^-+|-+$/g;
|
|
421
|
+
var targetAgents = {
|
|
422
|
+
"claude-skill": ["claude"],
|
|
423
|
+
"claude-slash": ["claude"],
|
|
424
|
+
"claude-sub-agent": ["claude"],
|
|
425
|
+
"cursor-command": ["cursor"],
|
|
426
|
+
"cursor-rule": ["cursor"]
|
|
427
|
+
};
|
|
428
|
+
async function addCommand(identifier, options) {
|
|
429
|
+
if (identifier === "stack") {
|
|
430
|
+
throw new Error("Missing stack id");
|
|
431
|
+
}
|
|
432
|
+
if (identifier.includes("/")) {
|
|
433
|
+
if (shouldRunInteractive(options)) {
|
|
434
|
+
await addInteractive(identifier);
|
|
435
|
+
} else {
|
|
436
|
+
await addRaw(identifier, options.target || defaultTarget);
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
throw new Error(
|
|
441
|
+
`Use \`${cliCommand("add <owner/slug>")}\` or \`${cliCommand(
|
|
442
|
+
"add stack <id>"
|
|
443
|
+
)}\``
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
function shouldRunInteractive(options) {
|
|
447
|
+
return Boolean(
|
|
448
|
+
process.stdout.isTTY && process.stdin.isTTY && !options.target && !process.env.CI
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
function isArtifactKind(value) {
|
|
452
|
+
return value !== null && ARTIFACT_KINDS.includes(value);
|
|
453
|
+
}
|
|
454
|
+
function buildInstallOptions(kind, slug) {
|
|
455
|
+
const options = [];
|
|
456
|
+
for (const agent of AGENT_NAMES) {
|
|
457
|
+
for (const scope of scopeOrder) {
|
|
458
|
+
const paths = resolveInstallPaths(kind, slug, [agent], scope);
|
|
459
|
+
const relativePath = paths[0];
|
|
460
|
+
if (!relativePath) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const scopeLabel = scope === "global" ? "global" : "this project";
|
|
464
|
+
const pathLabel = scope === "global" ? `~/${relativePath}` : relativePath;
|
|
465
|
+
options.push({
|
|
466
|
+
agent,
|
|
467
|
+
label: `${agentLabels[agent]} \u2014 ${scopeLabel} (${pathLabel})`,
|
|
468
|
+
relativePath,
|
|
469
|
+
scope,
|
|
470
|
+
value: `${agent}:${scope}`
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return options;
|
|
475
|
+
}
|
|
476
|
+
function scopeRootFor(scope) {
|
|
477
|
+
if (scope === "global") {
|
|
478
|
+
return homedir3();
|
|
479
|
+
}
|
|
480
|
+
return resolveCursorProjectScopeRoot().scopeRoot;
|
|
481
|
+
}
|
|
482
|
+
function tildeDisplay(absPath) {
|
|
483
|
+
const home = homedir3();
|
|
484
|
+
return absPath.startsWith(home) ? `~${absPath.slice(home.length)}` : absPath;
|
|
485
|
+
}
|
|
486
|
+
async function addInteractive(identifier) {
|
|
487
|
+
const config = await loadConfig();
|
|
488
|
+
assertAllowedBaseUrl(config.baseUrl);
|
|
489
|
+
const slug = sanitizeSlug(identifier.split("/").at(-1) || "artifact");
|
|
490
|
+
intro(pc.cyan(`impulse \xB7 install ${identifier}`));
|
|
491
|
+
const loading = spinner();
|
|
492
|
+
loading.start("Fetching artifact");
|
|
493
|
+
let content;
|
|
494
|
+
let kind;
|
|
495
|
+
try {
|
|
496
|
+
const response = await fetch(
|
|
497
|
+
`${config.baseUrl}/raw/${identifier.split("/").map((segment) => encodeURIComponent(segment)).join("/")}`
|
|
498
|
+
);
|
|
499
|
+
if (!response.ok) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
response.status === 404 ? `Command '${identifier}' not found` : `Request failed with HTTP ${response.status}`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
content = await response.text();
|
|
505
|
+
const kindHeader = response.headers.get("X-Prompt-Kind");
|
|
506
|
+
kind = isArtifactKind(kindHeader) ? kindHeader : legacyTargetToKind(defaultTarget) ?? "command";
|
|
507
|
+
} catch (error) {
|
|
508
|
+
loading.stop("Fetch failed");
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
loading.stop("Fetched artifact");
|
|
512
|
+
const options = buildInstallOptions(kind, slug);
|
|
513
|
+
if (options.length === 0) {
|
|
514
|
+
cancel(`No install destinations available for kind '${kind}'`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const defaultValue = options.find((option) => option.value === "claude:global")?.value ?? options[0].value;
|
|
518
|
+
const selection = await multiselect({
|
|
519
|
+
initialValues: [defaultValue],
|
|
520
|
+
message: "Where do you want to install it?",
|
|
521
|
+
options: options.map((option) => ({
|
|
522
|
+
label: option.label,
|
|
523
|
+
value: option.value
|
|
524
|
+
})),
|
|
525
|
+
required: true
|
|
526
|
+
});
|
|
527
|
+
if (isCancel(selection)) {
|
|
528
|
+
cancel("Cancelled");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const chosen = options.filter((option) => selection.includes(option.value));
|
|
532
|
+
const written = [];
|
|
533
|
+
await Promise.all(
|
|
534
|
+
chosen.map(async (option) => {
|
|
535
|
+
const absPath = path3.join(
|
|
536
|
+
scopeRootFor(option.scope),
|
|
537
|
+
option.relativePath
|
|
538
|
+
);
|
|
539
|
+
await mkdir2(path3.dirname(absPath), { recursive: true });
|
|
540
|
+
await writeFile2(absPath, content, "utf8");
|
|
541
|
+
written.push(absPath);
|
|
542
|
+
})
|
|
543
|
+
);
|
|
544
|
+
outro(
|
|
545
|
+
written.map((absPath) => `${pc.green("\u2713")} ${tildeDisplay(absPath)}`).join("\n")
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
async function addStackCommand(stackId, options) {
|
|
549
|
+
const config = await loadConfig();
|
|
550
|
+
assertAllowedBaseUrl(config.baseUrl);
|
|
551
|
+
const payload = await fetchJson(
|
|
552
|
+
`${config.baseUrl}/api/stacks/${encodeURIComponent(stackId)}/npx`
|
|
553
|
+
);
|
|
554
|
+
const prompts = payload.prompts || [];
|
|
555
|
+
console.info(
|
|
556
|
+
`${pc.green("\u2713")} Installing stack '${payload.stack?.title || stackId}' (${prompts.length})`
|
|
557
|
+
);
|
|
558
|
+
let skipped = 0;
|
|
559
|
+
const installs = await Promise.all(
|
|
560
|
+
prompts.map(async (prompt, index) => {
|
|
561
|
+
const target = options.target || prompt.target || defaultTarget;
|
|
562
|
+
const markdown = prompt.markdown;
|
|
563
|
+
if (!markdown) {
|
|
564
|
+
skipped += 1;
|
|
565
|
+
console.info(
|
|
566
|
+
`${pc.yellow("\u26A0")} skipped ${prompt.slug || prompt.id || index + 1}`
|
|
567
|
+
);
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
await installLegacy({
|
|
571
|
+
content: markdown,
|
|
572
|
+
identifier: prompt.path || prompt.slug || prompt.id || `prompt-${index + 1}`,
|
|
573
|
+
slug: sanitizeSlug(
|
|
574
|
+
prompt.slug || prompt.path || prompt.id || `prompt-${index + 1}`
|
|
575
|
+
),
|
|
576
|
+
target
|
|
577
|
+
});
|
|
578
|
+
return true;
|
|
579
|
+
})
|
|
580
|
+
);
|
|
581
|
+
const installed = installs.filter(Boolean).length;
|
|
582
|
+
console.info(
|
|
583
|
+
`${pc.green("\u2713")} Installed ${installed} prompt${installed === 1 ? "" : "s"}${skipped ? ` (skipped ${skipped})` : ""}`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
async function addRaw(identifier, target) {
|
|
587
|
+
const config = await loadConfig();
|
|
588
|
+
assertAllowedBaseUrl(config.baseUrl);
|
|
589
|
+
const response = await fetch(
|
|
590
|
+
`${config.baseUrl}/raw/${identifier.split("/").map((segment) => encodeURIComponent(segment)).join("/")}`
|
|
591
|
+
);
|
|
592
|
+
if (!response.ok) {
|
|
593
|
+
throw new Error(
|
|
594
|
+
response.status === 404 ? `Command '${identifier}' not found` : `Request failed with HTTP ${response.status}`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
await installLegacy({
|
|
598
|
+
content: await response.text(),
|
|
599
|
+
identifier,
|
|
600
|
+
slug: sanitizeSlug(identifier.split("/").at(-1) || "artifact"),
|
|
601
|
+
target
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
async function installLegacy(input) {
|
|
605
|
+
const kind = legacyTargetToKind(input.target);
|
|
606
|
+
if (!kind) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`Unknown target '${input.target}'. Available targets: ${LEGACY_TARGETS.join(
|
|
609
|
+
", "
|
|
610
|
+
)}`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const agents2 = targetAgents[input.target] || agentsForKind(kind);
|
|
614
|
+
const { scope, scopeRoot } = agents2.includes("cursor") ? resolveCursorProjectScopeRoot() : await resolveAddScopeRoot();
|
|
615
|
+
const paths = resolveInstallPaths(kind, input.slug, agents2, scope);
|
|
616
|
+
if (paths.length === 0) {
|
|
617
|
+
throw new Error(
|
|
618
|
+
`Target '${input.target}' is not available in ${scope} scope`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
await Promise.all(
|
|
622
|
+
paths.map(async (relativePath) => {
|
|
623
|
+
const absPath = path3.join(scopeRoot, relativePath);
|
|
624
|
+
await mkdir2(path3.dirname(absPath), { recursive: true });
|
|
625
|
+
await writeFile2(absPath, input.content, "utf8");
|
|
626
|
+
console.info(
|
|
627
|
+
`${pc.green("\u2713")} Installed '${input.identifier}' to ${absPath}`
|
|
628
|
+
);
|
|
629
|
+
})
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
async function fetchJson(url) {
|
|
633
|
+
const response = await fetch(url);
|
|
634
|
+
if (!response.ok) {
|
|
635
|
+
throw new Error(`Request failed with HTTP ${response.status}`);
|
|
636
|
+
}
|
|
637
|
+
return await response.json();
|
|
638
|
+
}
|
|
639
|
+
function agentsForKind(kind) {
|
|
640
|
+
if (kind === "rule") {
|
|
641
|
+
return ["cursor"];
|
|
642
|
+
}
|
|
643
|
+
return ["claude"];
|
|
644
|
+
}
|
|
645
|
+
function sanitizeSlug(input) {
|
|
646
|
+
return input.normalize("NFKD").replace(accentsRegex, "").split("").filter((char) => {
|
|
647
|
+
const code = char.charCodeAt(0);
|
|
648
|
+
return code > 31 && (code < 127 || code > 159);
|
|
649
|
+
}).join("").replace(dotDotRegex, "").replace(unsafeFilenameRegex, "").replace(pathSeparatorRegex, "-").replace(unsupportedSlugCharsRegex, "-").replace(edgeDashesRegex, "").toLowerCase() || "artifact";
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/commands/agents.ts
|
|
653
|
+
import pc2 from "picocolors";
|
|
654
|
+
var agentNames = ["claude", "codex", "cursor"];
|
|
655
|
+
async function listAgentsCommand() {
|
|
656
|
+
const config = await loadConfig();
|
|
657
|
+
for (const agent of agentNames) {
|
|
658
|
+
const state = config.agents[agent] ? pc2.green("enabled") : pc2.dim("disabled");
|
|
659
|
+
console.info(`${agent.padEnd(8)} ${state}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async function updateAgentCommand(action, agent) {
|
|
663
|
+
if (!agentNames.includes(agent)) {
|
|
664
|
+
throw new Error(`Unknown agent '${agent}'`);
|
|
665
|
+
}
|
|
666
|
+
const config = await loadConfig();
|
|
667
|
+
const agents2 = {
|
|
668
|
+
...config.agents,
|
|
669
|
+
[agent]: action === "enable"
|
|
670
|
+
};
|
|
671
|
+
await saveConfig({ ...config, agents: agents2 });
|
|
672
|
+
console.info(
|
|
673
|
+
pc2.green(`\u2713 ${agent} ${action === "enable" ? "enabled" : "disabled"}`)
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/commands/login.ts
|
|
678
|
+
import pc3 from "picocolors";
|
|
679
|
+
|
|
680
|
+
// src/lib/device-auth.ts
|
|
681
|
+
import { spawn } from "child_process";
|
|
682
|
+
import { hostname, platform } from "os";
|
|
683
|
+
import { z as z3 } from "zod";
|
|
684
|
+
var CLIENT_ID = "impulse-cli";
|
|
685
|
+
var deviceCodeSchema = z3.object({
|
|
686
|
+
device_code: z3.string(),
|
|
687
|
+
user_code: z3.string(),
|
|
688
|
+
verification_uri: z3.string(),
|
|
689
|
+
verification_uri_complete: z3.string().optional(),
|
|
690
|
+
expires_in: z3.number(),
|
|
691
|
+
interval: z3.number()
|
|
692
|
+
});
|
|
693
|
+
var tokenSuccessSchema = z3.object({
|
|
694
|
+
access_token: z3.string()
|
|
695
|
+
});
|
|
696
|
+
var tokenErrorSchema = z3.object({
|
|
697
|
+
error: z3.string(),
|
|
698
|
+
error_description: z3.string().optional()
|
|
699
|
+
});
|
|
700
|
+
var apiKeySchema = z3.object({
|
|
701
|
+
key: z3.string()
|
|
702
|
+
});
|
|
703
|
+
async function authRequest(baseUrl, path8, init) {
|
|
704
|
+
let response;
|
|
705
|
+
try {
|
|
706
|
+
response = await fetch(`${baseUrl}/api/auth${path8}`, init);
|
|
707
|
+
} catch (error) {
|
|
708
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
709
|
+
throw new Error(`Could not reach ${baseUrl}: ${message}`);
|
|
710
|
+
}
|
|
711
|
+
const text = await response.text();
|
|
712
|
+
let payload = null;
|
|
713
|
+
if (text) {
|
|
714
|
+
try {
|
|
715
|
+
payload = JSON.parse(text);
|
|
716
|
+
} catch {
|
|
717
|
+
payload = text;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return { status: response.status, payload };
|
|
721
|
+
}
|
|
722
|
+
async function requestDeviceCode(baseUrl) {
|
|
723
|
+
const { status, payload } = await authRequest(baseUrl, "/device/code", {
|
|
724
|
+
method: "POST",
|
|
725
|
+
headers: { "content-type": "application/json" },
|
|
726
|
+
body: JSON.stringify({ client_id: CLIENT_ID })
|
|
727
|
+
});
|
|
728
|
+
if (status !== 200) {
|
|
729
|
+
throw new Error(
|
|
730
|
+
errorDescription(payload) ?? "Could not start device login"
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
return deviceCodeSchema.parse(payload);
|
|
734
|
+
}
|
|
735
|
+
function sleep(ms) {
|
|
736
|
+
return new Promise((resolve) => {
|
|
737
|
+
setTimeout(resolve, ms);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
async function pollForToken(baseUrl, device) {
|
|
741
|
+
const deadline = Date.now() + device.expires_in * 1e3;
|
|
742
|
+
let intervalMs = Math.max(device.interval, 1) * 1e3;
|
|
743
|
+
while (Date.now() < deadline) {
|
|
744
|
+
await sleep(intervalMs);
|
|
745
|
+
const { status, payload } = await authRequest(baseUrl, "/device/token", {
|
|
746
|
+
method: "POST",
|
|
747
|
+
headers: { "content-type": "application/json" },
|
|
748
|
+
body: JSON.stringify({
|
|
749
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
750
|
+
device_code: device.device_code,
|
|
751
|
+
client_id: CLIENT_ID
|
|
752
|
+
})
|
|
753
|
+
});
|
|
754
|
+
if (status === 200) {
|
|
755
|
+
return tokenSuccessSchema.parse(payload).access_token;
|
|
756
|
+
}
|
|
757
|
+
const parsed = tokenErrorSchema.safeParse(payload);
|
|
758
|
+
const errorCode = parsed.success ? parsed.data.error : "invalid_request";
|
|
759
|
+
if (errorCode === "authorization_pending") {
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
if (errorCode === "slow_down") {
|
|
763
|
+
intervalMs += 5e3;
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
if (errorCode === "access_denied") {
|
|
767
|
+
throw new Error(
|
|
768
|
+
`Authorization was denied. Run \`${cliCommand("login")}\` to retry.`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
if (errorCode === "expired_token") {
|
|
772
|
+
throw new Error(
|
|
773
|
+
`The code expired. Run \`${cliCommand("login")}\` to get a new one.`
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
throw new Error(
|
|
777
|
+
errorDescription(payload) ?? `Device login failed (${errorCode})`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
throw new Error(
|
|
781
|
+
`The code expired. Run \`${cliCommand("login")}\` to get a new one.`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
async function createApiKey(baseUrl, accessToken) {
|
|
785
|
+
const { status, payload } = await authRequest(baseUrl, "/api-key/create", {
|
|
786
|
+
method: "POST",
|
|
787
|
+
headers: {
|
|
788
|
+
"content-type": "application/json",
|
|
789
|
+
authorization: `Bearer ${accessToken}`
|
|
790
|
+
},
|
|
791
|
+
body: JSON.stringify({ name: `cli-${hostname()}` })
|
|
792
|
+
});
|
|
793
|
+
if (status !== 200) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
errorDescription(payload) ?? "Could not create an API key for the CLI"
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
return apiKeySchema.parse(payload).key;
|
|
799
|
+
}
|
|
800
|
+
function openBrowser(url) {
|
|
801
|
+
let parsed;
|
|
802
|
+
try {
|
|
803
|
+
parsed = new URL(url);
|
|
804
|
+
} catch {
|
|
805
|
+
throw new Error(`Refusing to open invalid URL: ${url}`);
|
|
806
|
+
}
|
|
807
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
808
|
+
throw new Error(
|
|
809
|
+
`Refusing to open URL with unsupported protocol: ${parsed.protocol}`
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
const commands = {
|
|
813
|
+
darwin: ["open", [url]],
|
|
814
|
+
win32: ["cmd", ["/c", "start", "", `"${url}"`]]
|
|
815
|
+
};
|
|
816
|
+
const [command, args] = commands[platform()] ?? ["xdg-open", [url]];
|
|
817
|
+
try {
|
|
818
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
819
|
+
child.on("error", () => {
|
|
820
|
+
});
|
|
821
|
+
child.unref();
|
|
822
|
+
} catch {
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
function errorDescription(payload) {
|
|
826
|
+
if (payload && typeof payload === "object") {
|
|
827
|
+
const record = payload;
|
|
828
|
+
const description = record.error_description ?? record.message ?? record.error;
|
|
829
|
+
if (typeof description === "string") {
|
|
830
|
+
return description;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/commands/login.ts
|
|
837
|
+
async function loginCommand(options) {
|
|
838
|
+
const existing = await loadConfig();
|
|
839
|
+
const baseUrl = normalizeBaseUrl(options.url || existing.baseUrl);
|
|
840
|
+
assertAllowedBaseUrl(baseUrl);
|
|
841
|
+
const apiKey = options.key ?? await deviceLogin(baseUrl);
|
|
842
|
+
const config = {
|
|
843
|
+
...existing,
|
|
844
|
+
apiKey,
|
|
845
|
+
baseUrl
|
|
846
|
+
};
|
|
847
|
+
const me = await createApiClient(baseUrl, apiKey).me();
|
|
848
|
+
await saveConfig(config);
|
|
849
|
+
console.info(pc3.green(`\u2713 Logged in as ${me.user.slug}`));
|
|
850
|
+
}
|
|
851
|
+
async function deviceLogin(baseUrl) {
|
|
852
|
+
const device = await requestDeviceCode(baseUrl);
|
|
853
|
+
const openUrl = device.verification_uri_complete ?? device.verification_uri;
|
|
854
|
+
console.info("");
|
|
855
|
+
console.info("Authorize the Impulse CLI in your browser:");
|
|
856
|
+
console.info(` ${pc3.cyan(pc3.bold(device.verification_uri))}`);
|
|
857
|
+
console.info("");
|
|
858
|
+
console.info(` Your code: ${pc3.bold(device.user_code)}`);
|
|
859
|
+
console.info("");
|
|
860
|
+
openBrowser(openUrl);
|
|
861
|
+
console.info(pc3.dim("Waiting for approval\u2026"));
|
|
862
|
+
const accessToken = await pollForToken(baseUrl, device);
|
|
863
|
+
return createApiKey(baseUrl, accessToken);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/commands/logout.ts
|
|
867
|
+
import pc4 from "picocolors";
|
|
868
|
+
async function logoutCommand() {
|
|
869
|
+
const config = await loadConfig();
|
|
870
|
+
const nextConfig = { ...config };
|
|
871
|
+
nextConfig.apiKey = void 0;
|
|
872
|
+
await saveConfig(nextConfig);
|
|
873
|
+
console.info(pc4.green("\u2713 Logged out"));
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/commands/push.ts
|
|
877
|
+
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
878
|
+
import { homedir as homedir5 } from "os";
|
|
879
|
+
import path5 from "path";
|
|
880
|
+
import pc5 from "picocolors";
|
|
881
|
+
|
|
882
|
+
// src/lib/lock.ts
|
|
883
|
+
import { randomUUID } from "crypto";
|
|
884
|
+
import { mkdir as mkdir3, readFile as readFile2, rename as rename2, rm, writeFile as writeFile3 } from "fs/promises";
|
|
885
|
+
import { homedir as homedir4 } from "os";
|
|
886
|
+
import path4 from "path";
|
|
887
|
+
import { z as z4 } from "zod";
|
|
888
|
+
var lockEntrySchema = z4.object({
|
|
889
|
+
slug: z4.string(),
|
|
890
|
+
kind: z4.enum(ARTIFACT_KINDS),
|
|
891
|
+
version: z4.number().int().min(1),
|
|
892
|
+
contentHash: z4.string(),
|
|
893
|
+
files: z4.array(z4.string())
|
|
894
|
+
});
|
|
895
|
+
var lockfileSchema = z4.object({
|
|
896
|
+
artifacts: z4.record(z4.string(), lockEntrySchema).default({})
|
|
897
|
+
});
|
|
898
|
+
function lockPath(scope, scopeRoot) {
|
|
899
|
+
return scope === "global" ? path4.join(homedir4(), ".impulse", "lock.json") : path4.join(scopeRoot, ".impulse", "lock.json");
|
|
900
|
+
}
|
|
901
|
+
async function readLockfile(scope, scopeRoot) {
|
|
902
|
+
try {
|
|
903
|
+
const raw = await readFile2(lockPath(scope, scopeRoot), "utf8");
|
|
904
|
+
return lockfileSchema.parse(JSON.parse(raw));
|
|
905
|
+
} catch (error) {
|
|
906
|
+
if (isNotFound2(error)) {
|
|
907
|
+
return { artifacts: {} };
|
|
908
|
+
}
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
async function writeLockfile(scope, scopeRoot, lock) {
|
|
913
|
+
const target = lockPath(scope, scopeRoot);
|
|
914
|
+
const temporaryPath = path4.join(
|
|
915
|
+
path4.dirname(target),
|
|
916
|
+
`.${path4.basename(target)}.${process.pid}.${randomUUID()}.tmp`
|
|
917
|
+
);
|
|
918
|
+
await mkdir3(path4.dirname(target), { recursive: true });
|
|
919
|
+
try {
|
|
920
|
+
await writeFile3(temporaryPath, `${JSON.stringify(lock, null, 2)}
|
|
921
|
+
`, {
|
|
922
|
+
mode: 384
|
|
923
|
+
});
|
|
924
|
+
await rename2(temporaryPath, target);
|
|
925
|
+
} catch (error) {
|
|
926
|
+
try {
|
|
927
|
+
await rm(temporaryPath, { force: true });
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
function isNotFound2(error) {
|
|
934
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/commands/push.ts
|
|
938
|
+
async function pushCommand(filePath, options) {
|
|
939
|
+
const config = await loadConfig();
|
|
940
|
+
if (!config.apiKey) {
|
|
941
|
+
throw new Error(
|
|
942
|
+
`Invalid or missing API key - run \`${cliCommand("login")}\``
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
const absPath = path5.resolve(filePath);
|
|
946
|
+
const match = await findLockedFile(absPath);
|
|
947
|
+
if (!match) {
|
|
948
|
+
throw new Error(`No lock entry found for ${absPath}`);
|
|
949
|
+
}
|
|
950
|
+
const content = await readFile3(absPath, "utf8");
|
|
951
|
+
const api = createApiClient(config.baseUrl, config.apiKey);
|
|
952
|
+
try {
|
|
953
|
+
const response = await api.pushVersion(match.artifactId, {
|
|
954
|
+
baseVersion: match.entry.version,
|
|
955
|
+
changelog: options.message,
|
|
956
|
+
content
|
|
957
|
+
});
|
|
958
|
+
const lock = await readLockfile(match.scope, match.scopeRoot);
|
|
959
|
+
const currentEntry = lock.artifacts[match.artifactId] ?? match.entry;
|
|
960
|
+
const contentHash = response.contentHash || sha256Hex(content);
|
|
961
|
+
await updateUntouchedSiblings({
|
|
962
|
+
absPath,
|
|
963
|
+
content,
|
|
964
|
+
contentHash,
|
|
965
|
+
entry: currentEntry,
|
|
966
|
+
match
|
|
967
|
+
});
|
|
968
|
+
lock.artifacts[match.artifactId] = {
|
|
969
|
+
...currentEntry,
|
|
970
|
+
contentHash,
|
|
971
|
+
version: response.version
|
|
972
|
+
};
|
|
973
|
+
await writeLockfile(match.scope, match.scopeRoot, lock);
|
|
974
|
+
console.info(
|
|
975
|
+
pc5.green(`\u2713 Published ${match.entry.slug} v${response.version}`)
|
|
976
|
+
);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
if (error instanceof ApiError && error.status === 409) {
|
|
979
|
+
console.error(error.message);
|
|
980
|
+
process.exitCode = 1;
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
throw error;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
async function findLockedFile(absPath) {
|
|
987
|
+
const candidates = [
|
|
988
|
+
{ scope: "global", scopeRoot: homedir5() },
|
|
989
|
+
{
|
|
990
|
+
scope: "project",
|
|
991
|
+
scopeRoot: await resolveProjectRoot(path5.dirname(absPath))
|
|
992
|
+
}
|
|
993
|
+
];
|
|
994
|
+
const locks = await Promise.all(
|
|
995
|
+
candidates.map(async (candidate) => ({
|
|
996
|
+
candidate,
|
|
997
|
+
lock: await readLockfile(candidate.scope, candidate.scopeRoot)
|
|
998
|
+
}))
|
|
999
|
+
);
|
|
1000
|
+
for (const { candidate, lock } of locks) {
|
|
1001
|
+
for (const [artifactId, entry] of Object.entries(lock.artifacts)) {
|
|
1002
|
+
if (entry.files.map((file) => path5.resolve(file)).includes(absPath)) {
|
|
1003
|
+
return { artifactId, entry, ...candidate };
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
function resolveLockPath(scopeRoot, lockedFile) {
|
|
1010
|
+
const resolved = path5.isAbsolute(lockedFile) ? lockedFile : path5.resolve(scopeRoot, lockedFile);
|
|
1011
|
+
const relative = path5.relative(scopeRoot, resolved);
|
|
1012
|
+
const outside = relative !== "" && (relative.startsWith("..") || path5.isAbsolute(relative));
|
|
1013
|
+
if (outside) {
|
|
1014
|
+
console.info(
|
|
1015
|
+
`${pc5.yellow("\u26A0")} skipped lock entry outside scope root: ${relativeDisplay(
|
|
1016
|
+
resolved
|
|
1017
|
+
)}`
|
|
1018
|
+
);
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
return resolved;
|
|
1022
|
+
}
|
|
1023
|
+
async function updateUntouchedSiblings(input) {
|
|
1024
|
+
const { scopeRoot } = input.match;
|
|
1025
|
+
await Promise.all(
|
|
1026
|
+
input.entry.files.map((lockedFile) => resolveLockPath(scopeRoot, lockedFile)).filter((siblingPath) => siblingPath !== null).filter((siblingPath) => siblingPath !== input.absPath).map((siblingPath) => updateUntouchedSibling(input, siblingPath))
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
async function updateUntouchedSibling(input, siblingPath) {
|
|
1030
|
+
try {
|
|
1031
|
+
const siblingContent = await readFile3(siblingPath, "utf8");
|
|
1032
|
+
const siblingHash = sha256Hex(siblingContent);
|
|
1033
|
+
if (siblingHash === input.match.entry.contentHash) {
|
|
1034
|
+
await writeFile4(siblingPath, input.content, "utf8");
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (siblingHash !== input.contentHash) {
|
|
1038
|
+
console.info(
|
|
1039
|
+
`${pc5.yellow("\u26A0")} left modified sibling unchanged: ${relativeDisplay(
|
|
1040
|
+
siblingPath
|
|
1041
|
+
)}`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
if (!isNotFound3(error)) {
|
|
1046
|
+
throw error;
|
|
1047
|
+
}
|
|
1048
|
+
console.info(
|
|
1049
|
+
`${pc5.yellow("\u26A0")} left missing sibling unchanged: ${relativeDisplay(
|
|
1050
|
+
siblingPath
|
|
1051
|
+
)}`
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function isNotFound3(error) {
|
|
1056
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/commands/status.ts
|
|
1060
|
+
import { homedir as homedir6 } from "os";
|
|
1061
|
+
|
|
1062
|
+
// src/lib/installer.ts
|
|
1063
|
+
import { mkdir as mkdir4, readFile as readFile4, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
1064
|
+
import path6 from "path";
|
|
1065
|
+
function flattenManifest(manifest) {
|
|
1066
|
+
const artifactsById = /* @__PURE__ */ new Map();
|
|
1067
|
+
for (const stack of manifest.stacks) {
|
|
1068
|
+
for (const artifact of stack.artifacts) {
|
|
1069
|
+
if (!artifactsById.has(artifact.id)) {
|
|
1070
|
+
artifactsById.set(artifact.id, artifact);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return [...artifactsById.values()];
|
|
1075
|
+
}
|
|
1076
|
+
function planSync(input) {
|
|
1077
|
+
const artifacts = flattenManifest(input.manifest);
|
|
1078
|
+
const rawActions = artifacts.flatMap(
|
|
1079
|
+
(artifact) => planArtifact(input, artifact)
|
|
1080
|
+
);
|
|
1081
|
+
if (input.prune) {
|
|
1082
|
+
const currentTargetPaths = new Set(
|
|
1083
|
+
targetPathsForManifest(
|
|
1084
|
+
input.manifest,
|
|
1085
|
+
input.agents,
|
|
1086
|
+
input.scope,
|
|
1087
|
+
input.scopeRoot
|
|
1088
|
+
)
|
|
1089
|
+
);
|
|
1090
|
+
rawActions.push(
|
|
1091
|
+
...planPrune(
|
|
1092
|
+
input,
|
|
1093
|
+
new Set(artifacts.map((artifact) => artifact.id)),
|
|
1094
|
+
currentTargetPaths
|
|
1095
|
+
)
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
const claimedTargets = firstTargetClaims(rawActions);
|
|
1099
|
+
const actions = [];
|
|
1100
|
+
for (const action of rawActions) {
|
|
1101
|
+
const claimedBy = claimedTargets.get(action.absPath);
|
|
1102
|
+
if (claimedBy && claimedBy.id !== action.artifact.id) {
|
|
1103
|
+
actions.push({
|
|
1104
|
+
...action,
|
|
1105
|
+
conflictWith: claimedBy,
|
|
1106
|
+
type: "skip-conflict"
|
|
1107
|
+
});
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
actions.push(action);
|
|
1111
|
+
}
|
|
1112
|
+
return demoteModifiedSiblings(actions);
|
|
1113
|
+
}
|
|
1114
|
+
function demoteModifiedSiblings(actions) {
|
|
1115
|
+
const modifiedIds = new Set(
|
|
1116
|
+
actions.filter((action) => action.type === "skip-modified").map((action) => action.artifact.id)
|
|
1117
|
+
);
|
|
1118
|
+
if (modifiedIds.size === 0) {
|
|
1119
|
+
return actions;
|
|
1120
|
+
}
|
|
1121
|
+
return actions.map((action) => {
|
|
1122
|
+
if (modifiedIds.has(action.artifact.id) && (action.type === "install" || action.type === "update")) {
|
|
1123
|
+
return { ...action, type: "skip-modified" };
|
|
1124
|
+
}
|
|
1125
|
+
return action;
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
function firstTargetClaims(actions) {
|
|
1129
|
+
const claims = /* @__PURE__ */ new Map();
|
|
1130
|
+
for (const action of actions) {
|
|
1131
|
+
if (action.type === "prune" || claims.has(action.absPath)) {
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
claims.set(action.absPath, action.artifact);
|
|
1135
|
+
}
|
|
1136
|
+
return claims;
|
|
1137
|
+
}
|
|
1138
|
+
async function readFilesForPlan(paths) {
|
|
1139
|
+
const entries = await Promise.all(
|
|
1140
|
+
paths.map(async (filePath) => {
|
|
1141
|
+
try {
|
|
1142
|
+
return [filePath, await readFile4(filePath, "utf8")];
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
if (isNotFound4(error)) {
|
|
1145
|
+
return [filePath, null];
|
|
1146
|
+
}
|
|
1147
|
+
throw error;
|
|
1148
|
+
}
|
|
1149
|
+
})
|
|
1150
|
+
);
|
|
1151
|
+
return Object.fromEntries(entries);
|
|
1152
|
+
}
|
|
1153
|
+
function targetPathsForManifest(manifest, agents2, scope, scopeRoot) {
|
|
1154
|
+
return flattenManifest(manifest).flatMap(
|
|
1155
|
+
(artifact) => resolveInstallPaths(artifact.kind, artifact.slug, agents2, scope).map(
|
|
1156
|
+
(relativePath) => path6.join(scopeRoot, relativePath)
|
|
1157
|
+
)
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
async function applyActions(actions, options) {
|
|
1161
|
+
const summary = emptySummary();
|
|
1162
|
+
const contentCache = /* @__PURE__ */ new Map();
|
|
1163
|
+
await Promise.all(
|
|
1164
|
+
actions.map(async (action) => {
|
|
1165
|
+
if (action.type === "install" || action.type === "update") {
|
|
1166
|
+
assertContained(options.scopeRoot, action.absPath);
|
|
1167
|
+
const contentPromise = contentCache.get(action.artifact.id) ?? options.fetchContent(action.artifact);
|
|
1168
|
+
contentCache.set(action.artifact.id, contentPromise);
|
|
1169
|
+
await mkdir4(path6.dirname(action.absPath), { recursive: true });
|
|
1170
|
+
await writeFile5(action.absPath, await contentPromise, "utf8");
|
|
1171
|
+
}
|
|
1172
|
+
if (action.type === "prune") {
|
|
1173
|
+
assertContained(options.scopeRoot, action.absPath);
|
|
1174
|
+
await pruneIfUnmodified(action);
|
|
1175
|
+
}
|
|
1176
|
+
})
|
|
1177
|
+
);
|
|
1178
|
+
for (const action of actions) {
|
|
1179
|
+
summary[action.type] += 1;
|
|
1180
|
+
}
|
|
1181
|
+
return summary;
|
|
1182
|
+
}
|
|
1183
|
+
function emptySummary() {
|
|
1184
|
+
return {
|
|
1185
|
+
install: 0,
|
|
1186
|
+
prune: 0,
|
|
1187
|
+
"skip-conflict": 0,
|
|
1188
|
+
"skip-modified": 0,
|
|
1189
|
+
"up-to-date": 0,
|
|
1190
|
+
update: 0
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function mergeSummary(actions) {
|
|
1194
|
+
const summary = emptySummary();
|
|
1195
|
+
for (const action of actions) {
|
|
1196
|
+
summary[action.type] += 1;
|
|
1197
|
+
}
|
|
1198
|
+
return summary;
|
|
1199
|
+
}
|
|
1200
|
+
function classifyAction(input) {
|
|
1201
|
+
if (input.fileHash === null) {
|
|
1202
|
+
return "install";
|
|
1203
|
+
}
|
|
1204
|
+
if (!input.previousHash) {
|
|
1205
|
+
return input.fileHash === input.manifestHash ? "up-to-date" : "skip-modified";
|
|
1206
|
+
}
|
|
1207
|
+
if (input.fileHash !== input.previousHash) {
|
|
1208
|
+
return input.force ? "update" : "skip-modified";
|
|
1209
|
+
}
|
|
1210
|
+
return input.manifestHash === input.previousHash ? "up-to-date" : "update";
|
|
1211
|
+
}
|
|
1212
|
+
function planArtifact(input, artifact) {
|
|
1213
|
+
const previous = input.lock.artifacts[artifact.id];
|
|
1214
|
+
const targetActions = resolveInstallPaths(
|
|
1215
|
+
artifact.kind,
|
|
1216
|
+
artifact.slug,
|
|
1217
|
+
input.agents,
|
|
1218
|
+
input.scope
|
|
1219
|
+
).map((relativePath) => {
|
|
1220
|
+
const absPath = path6.join(input.scopeRoot, relativePath);
|
|
1221
|
+
const content = input.filesOnDisk[absPath] ?? null;
|
|
1222
|
+
const fileHash = content === null ? null : sha256Hex(content);
|
|
1223
|
+
return {
|
|
1224
|
+
absPath,
|
|
1225
|
+
artifact,
|
|
1226
|
+
previous,
|
|
1227
|
+
type: classifyAction({
|
|
1228
|
+
fileHash,
|
|
1229
|
+
force: input.force ?? false,
|
|
1230
|
+
manifestHash: artifact.contentHash,
|
|
1231
|
+
previousHash: previous?.contentHash
|
|
1232
|
+
})
|
|
1233
|
+
};
|
|
1234
|
+
});
|
|
1235
|
+
if (!(input.prune && previous)) {
|
|
1236
|
+
return targetActions;
|
|
1237
|
+
}
|
|
1238
|
+
const currentPaths = new Set(targetActions.map((action) => action.absPath));
|
|
1239
|
+
const stalePruneActions = previous.files.filter((absPath) => !currentPaths.has(absPath)).map((absPath) => ({
|
|
1240
|
+
absPath,
|
|
1241
|
+
artifact,
|
|
1242
|
+
previous,
|
|
1243
|
+
type: pruneActionType(input.filesOnDisk[absPath], previous)
|
|
1244
|
+
}));
|
|
1245
|
+
return [...targetActions, ...stalePruneActions];
|
|
1246
|
+
}
|
|
1247
|
+
function planPrune(input, manifestIds, currentTargetPaths) {
|
|
1248
|
+
return Object.entries(input.lock.artifacts).flatMap(([id, entry]) => {
|
|
1249
|
+
if (manifestIds.has(id)) {
|
|
1250
|
+
return [];
|
|
1251
|
+
}
|
|
1252
|
+
return entry.files.filter((absPath) => !currentTargetPaths.has(absPath)).map((absPath) => ({
|
|
1253
|
+
absPath,
|
|
1254
|
+
artifact: {
|
|
1255
|
+
contentHash: entry.contentHash,
|
|
1256
|
+
id,
|
|
1257
|
+
kind: entry.kind,
|
|
1258
|
+
path: entry.slug,
|
|
1259
|
+
slug: entry.slug,
|
|
1260
|
+
title: entry.slug,
|
|
1261
|
+
version: entry.version
|
|
1262
|
+
},
|
|
1263
|
+
previous: entry,
|
|
1264
|
+
type: pruneActionType(input.filesOnDisk[absPath], entry)
|
|
1265
|
+
}));
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
function pruneActionType(content, entry) {
|
|
1269
|
+
if (content === null || content === void 0) {
|
|
1270
|
+
return "prune";
|
|
1271
|
+
}
|
|
1272
|
+
return sha256Hex(content) === entry.contentHash ? "prune" : "skip-modified";
|
|
1273
|
+
}
|
|
1274
|
+
async function pruneIfUnmodified(action) {
|
|
1275
|
+
if (!action.previous) {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
try {
|
|
1279
|
+
const content = await readFile4(action.absPath, "utf8");
|
|
1280
|
+
if (sha256Hex(content) !== action.previous.contentHash) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
await rm2(action.absPath);
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
if (!isNotFound4(error)) {
|
|
1286
|
+
throw error;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function assertContained(scopeRoot, absPath) {
|
|
1291
|
+
const relative = path6.relative(scopeRoot, absPath);
|
|
1292
|
+
if (relative === "" || !(relative.startsWith("..") || path6.isAbsolute(relative))) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
throw new Error(
|
|
1296
|
+
`Refusing to write outside sync scope: ${absPath} is not under ${scopeRoot}`
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
function isNotFound4(error) {
|
|
1300
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// src/lib/output.ts
|
|
1304
|
+
import pc6 from "picocolors";
|
|
1305
|
+
function printError(error) {
|
|
1306
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1307
|
+
console.error(pc6.red(`\u2716 ${message}`));
|
|
1308
|
+
}
|
|
1309
|
+
function enabledAgents(agents2) {
|
|
1310
|
+
return Object.keys(agents2).filter(
|
|
1311
|
+
(agent) => agents2[agent]
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
function printPlan(actions) {
|
|
1315
|
+
for (const action of actions) {
|
|
1316
|
+
const prefix = prefixForAction(action.type);
|
|
1317
|
+
const conflict = action.conflictWith ? ` (conflicts with ${action.conflictWith.slug})` : "";
|
|
1318
|
+
console.info(
|
|
1319
|
+
`${prefix} ${action.type.padEnd(13)} ${action.artifact.slug} -> ${action.absPath}${conflict}`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
function prefixForAction(type) {
|
|
1324
|
+
switch (type) {
|
|
1325
|
+
case "install":
|
|
1326
|
+
return pc6.green("\u2713");
|
|
1327
|
+
case "update":
|
|
1328
|
+
return pc6.cyan("\u2191");
|
|
1329
|
+
case "up-to-date":
|
|
1330
|
+
return pc6.dim("\u2261");
|
|
1331
|
+
case "skip-modified":
|
|
1332
|
+
return pc6.yellow("\u26A0");
|
|
1333
|
+
case "skip-conflict":
|
|
1334
|
+
return pc6.yellow("\u26A0");
|
|
1335
|
+
case "prune":
|
|
1336
|
+
return pc6.red("\u2212");
|
|
1337
|
+
default:
|
|
1338
|
+
return "";
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// src/commands/status.ts
|
|
1343
|
+
async function statusCommand(options) {
|
|
1344
|
+
const config = await loadConfig();
|
|
1345
|
+
if (!config.apiKey) {
|
|
1346
|
+
throw new Error(
|
|
1347
|
+
`Invalid or missing API key - run \`${cliCommand("login")}\``
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
const scope = options.project ? "project" : "global";
|
|
1351
|
+
const scopeRoot = options.project ? await resolveProjectRoot() : homedir6();
|
|
1352
|
+
const agents2 = enabledAgents(config.agents);
|
|
1353
|
+
const manifest = await createApiClient(
|
|
1354
|
+
config.baseUrl,
|
|
1355
|
+
config.apiKey
|
|
1356
|
+
).manifest();
|
|
1357
|
+
const lock = await readLockfile(scope, scopeRoot);
|
|
1358
|
+
const targetPaths = targetPathsForManifest(
|
|
1359
|
+
manifest,
|
|
1360
|
+
agents2,
|
|
1361
|
+
scope,
|
|
1362
|
+
scopeRoot
|
|
1363
|
+
);
|
|
1364
|
+
const filesOnDisk = await readFilesForPlan(targetPaths);
|
|
1365
|
+
const actions = planSync({
|
|
1366
|
+
agents: agents2,
|
|
1367
|
+
filesOnDisk,
|
|
1368
|
+
lock,
|
|
1369
|
+
manifest,
|
|
1370
|
+
scope,
|
|
1371
|
+
scopeRoot
|
|
1372
|
+
});
|
|
1373
|
+
const rows = actions.map((action) => ({
|
|
1374
|
+
kind: action.artifact.kind,
|
|
1375
|
+
path: action.absPath,
|
|
1376
|
+
slug: action.artifact.slug,
|
|
1377
|
+
state: statusForAction(action),
|
|
1378
|
+
version: action.artifact.version
|
|
1379
|
+
}));
|
|
1380
|
+
if (options.json) {
|
|
1381
|
+
console.info(JSON.stringify(rows, null, 2));
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
for (const row of rows) {
|
|
1385
|
+
console.info(
|
|
1386
|
+
`${row.slug.padEnd(24)} ${row.kind.padEnd(8)} v${String(row.version).padEnd(4)} ${row.state}`
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
function statusForAction(action) {
|
|
1391
|
+
switch (action.type) {
|
|
1392
|
+
case "install":
|
|
1393
|
+
return "missing";
|
|
1394
|
+
case "skip-modified":
|
|
1395
|
+
return "modified";
|
|
1396
|
+
case "skip-conflict":
|
|
1397
|
+
return "modified";
|
|
1398
|
+
case "update":
|
|
1399
|
+
return "outdated";
|
|
1400
|
+
case "up-to-date":
|
|
1401
|
+
return "synced";
|
|
1402
|
+
case "prune":
|
|
1403
|
+
return "outdated";
|
|
1404
|
+
default:
|
|
1405
|
+
return "modified";
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// src/commands/sync.ts
|
|
1410
|
+
import { homedir as homedir7 } from "os";
|
|
1411
|
+
import pc7 from "picocolors";
|
|
1412
|
+
async function syncCommand(options) {
|
|
1413
|
+
const config = await loadConfig();
|
|
1414
|
+
if (!config.apiKey) {
|
|
1415
|
+
throw new Error(
|
|
1416
|
+
`Invalid or missing API key - run \`${cliCommand("login")}\``
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
const scope = options.project ? "project" : "global";
|
|
1420
|
+
const scopeRoot = options.project ? await resolveProjectRoot() : homedir7();
|
|
1421
|
+
const agents2 = enabledAgents(config.agents);
|
|
1422
|
+
const api = createApiClient(config.baseUrl, config.apiKey);
|
|
1423
|
+
const manifest = await api.manifest();
|
|
1424
|
+
const lock = await readLockfile(scope, scopeRoot);
|
|
1425
|
+
const targetPaths = targetPathsForManifest(
|
|
1426
|
+
manifest,
|
|
1427
|
+
agents2,
|
|
1428
|
+
scope,
|
|
1429
|
+
scopeRoot
|
|
1430
|
+
);
|
|
1431
|
+
const prunePaths = options.prune ? Object.values(lock.artifacts).flatMap((entry) => entry.files) : [];
|
|
1432
|
+
const filesOnDisk = await readFilesForPlan([...targetPaths, ...prunePaths]);
|
|
1433
|
+
const actions = planSync({
|
|
1434
|
+
agents: agents2,
|
|
1435
|
+
filesOnDisk,
|
|
1436
|
+
force: options.force,
|
|
1437
|
+
lock,
|
|
1438
|
+
manifest,
|
|
1439
|
+
prune: options.prune,
|
|
1440
|
+
scope,
|
|
1441
|
+
scopeRoot
|
|
1442
|
+
});
|
|
1443
|
+
if (options.dryRun) {
|
|
1444
|
+
const summary2 = mergeSummary(actions);
|
|
1445
|
+
if (options.json) {
|
|
1446
|
+
console.info(JSON.stringify({ actions, summary: summary2 }, null, 2));
|
|
1447
|
+
} else {
|
|
1448
|
+
printPlan(actions);
|
|
1449
|
+
printSummary(summary2);
|
|
1450
|
+
}
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
const summary = await applyActions(actions, {
|
|
1454
|
+
fetchContent: async (artifact) => (await api.artifact(artifact.id, artifact.version)).content,
|
|
1455
|
+
scopeRoot
|
|
1456
|
+
});
|
|
1457
|
+
await writeLockfile(scope, scopeRoot, nextLock(lock, actions));
|
|
1458
|
+
if (options.json) {
|
|
1459
|
+
console.info(JSON.stringify({ actions, summary }, null, 2));
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
printSummary(summary);
|
|
1463
|
+
const skipped = actions.filter((action) => action.type === "skip-modified");
|
|
1464
|
+
for (const action of skipped) {
|
|
1465
|
+
console.info(
|
|
1466
|
+
`${pc7.yellow("\u26A0")} skipped locally modified ${relativeDisplay(action.absPath)}`
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
if (skipped.length > 0) {
|
|
1470
|
+
console.info(
|
|
1471
|
+
`Run \`${cliCommand("push <path>")}\` to publish, or \`${cliCommand(
|
|
1472
|
+
"sync --force"
|
|
1473
|
+
)}\`.`
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
const conflicts = actions.filter((action) => action.type === "skip-conflict");
|
|
1477
|
+
for (const action of conflicts) {
|
|
1478
|
+
console.info(
|
|
1479
|
+
`${pc7.yellow("\u26A0")} skipped path conflict for ${action.artifact.slug} at ${relativeDisplay(action.absPath)}`
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
function nextLock(lock, actions) {
|
|
1484
|
+
const next = {
|
|
1485
|
+
artifacts: { ...lock.artifacts }
|
|
1486
|
+
};
|
|
1487
|
+
const actionsByArtifact = /* @__PURE__ */ new Map();
|
|
1488
|
+
for (const action of actions) {
|
|
1489
|
+
actionsByArtifact.set(action.artifact.id, [
|
|
1490
|
+
...actionsByArtifact.get(action.artifact.id) || [],
|
|
1491
|
+
action
|
|
1492
|
+
]);
|
|
1493
|
+
}
|
|
1494
|
+
for (const [artifactId, artifactActions] of actionsByArtifact) {
|
|
1495
|
+
if (artifactActions.some(
|
|
1496
|
+
(action) => action.type === "skip-modified" || action.type === "skip-conflict"
|
|
1497
|
+
)) {
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
const targetActions = artifactActions.filter(
|
|
1501
|
+
(action) => action.type === "install" || action.type === "update" || action.type === "up-to-date"
|
|
1502
|
+
);
|
|
1503
|
+
if (targetActions.length === 0) {
|
|
1504
|
+
delete next.artifacts[artifactId];
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
const artifact = targetActions[0].artifact;
|
|
1508
|
+
next.artifacts[artifactId] = {
|
|
1509
|
+
contentHash: artifact.contentHash,
|
|
1510
|
+
files: targetActions.map((action) => action.absPath),
|
|
1511
|
+
kind: artifact.kind,
|
|
1512
|
+
slug: artifact.slug,
|
|
1513
|
+
version: artifact.version
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
return next;
|
|
1517
|
+
}
|
|
1518
|
+
function printSummary(summary) {
|
|
1519
|
+
console.info(
|
|
1520
|
+
[
|
|
1521
|
+
`${pc7.green("\u2713")} installed ${summary.install}`,
|
|
1522
|
+
`${pc7.cyan("\u2191")} updated ${summary.update}`,
|
|
1523
|
+
`${pc7.dim("\u2261")} up-to-date ${summary["up-to-date"]}`,
|
|
1524
|
+
`${pc7.yellow("\u26A0")} skipped ${summary["skip-modified"]}`,
|
|
1525
|
+
`${pc7.yellow("\u26A0")} conflicts ${summary["skip-conflict"]}`,
|
|
1526
|
+
`${pc7.red("\u2212")} pruned ${summary.prune}`
|
|
1527
|
+
].join("\n")
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// src/commands/whoami.ts
|
|
1532
|
+
import pc8 from "picocolors";
|
|
1533
|
+
async function whoamiCommand() {
|
|
1534
|
+
const config = await loadConfig();
|
|
1535
|
+
if (!config.apiKey) {
|
|
1536
|
+
throw new Error(`Not logged in - run \`${cliCommand("login")}\``);
|
|
1537
|
+
}
|
|
1538
|
+
const me = await createApiClient(config.baseUrl, config.apiKey).me();
|
|
1539
|
+
console.info(`${pc8.green("\u2713")} ${me.user.slug} <${me.user.email}>`);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// src/index.ts
|
|
1543
|
+
var knownCommands = /* @__PURE__ */ new Set([
|
|
1544
|
+
"add",
|
|
1545
|
+
"agents",
|
|
1546
|
+
"help",
|
|
1547
|
+
"login",
|
|
1548
|
+
"logout",
|
|
1549
|
+
"push",
|
|
1550
|
+
"status",
|
|
1551
|
+
"sync",
|
|
1552
|
+
"whoami"
|
|
1553
|
+
]);
|
|
1554
|
+
var packageJson = JSON.parse(
|
|
1555
|
+
readFileSync(
|
|
1556
|
+
path7.join(path7.dirname(fileURLToPath(import.meta.url)), "../package.json"),
|
|
1557
|
+
"utf8"
|
|
1558
|
+
)
|
|
1559
|
+
);
|
|
1560
|
+
var program = new Command();
|
|
1561
|
+
program.name("impulse").description("Sync versioned skills, commands and agent configs").version(packageJson.version).exitOverride();
|
|
1562
|
+
program.command("login").option("--key <key>", "API key").option("--url <baseUrl>", "Impulse Directory base URL").action(wrap(loginCommand));
|
|
1563
|
+
program.command("logout").action(wrap(logoutCommand));
|
|
1564
|
+
program.command("whoami").action(wrap(whoamiCommand));
|
|
1565
|
+
program.command("sync").option("--project", "sync project-scoped targets").option("--prune", "remove locked files no longer in the manifest").option("--force", "overwrite locally modified files").option("--dry-run", "print planned changes without writing files").option("--json", "print machine-readable output").action(wrap(syncCommand));
|
|
1566
|
+
program.command("status").option("--project", "check project-scoped targets").option("--json", "print machine-readable output").action(wrap(statusCommand));
|
|
1567
|
+
program.command("push").argument("<path>", "file to publish").option("-m, --message <changelog>", "version changelog").action(wrap((filePath, options) => pushCommand(filePath, options)));
|
|
1568
|
+
var add = program.command("add").argument("<owner/slug>", "artifact path or 'stack'").argument("[id]", "stack id").option("-t, --target <legacy-target>", "legacy installation target").action(
|
|
1569
|
+
wrap(
|
|
1570
|
+
(identifier, id, options) => identifier === "stack" ? addStackCommand(required(id, "Missing stack id"), options) : addCommand(identifier, options)
|
|
1571
|
+
)
|
|
1572
|
+
);
|
|
1573
|
+
add.command("stack").argument("<id>", "stack id").option("-t, --target <legacy-target>", "legacy installation target").action(wrap(addStackCommand));
|
|
1574
|
+
var agents = program.command("agents").action(wrap(listAgentsCommand));
|
|
1575
|
+
agents.command("enable").argument("<agent>", "claude, codex or cursor").action(wrap((agent) => updateAgentCommand("enable", agent)));
|
|
1576
|
+
agents.command("disable").argument("<agent>", "claude, codex or cursor").action(wrap((agent) => updateAgentCommand("disable", agent)));
|
|
1577
|
+
rewriteLegacyArgv(process.argv);
|
|
1578
|
+
try {
|
|
1579
|
+
await program.parseAsync(process.argv);
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
if (isCommanderHelp(error)) {
|
|
1582
|
+
process.exitCode = 0;
|
|
1583
|
+
} else {
|
|
1584
|
+
printError(error);
|
|
1585
|
+
process.exitCode = 1;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function rewriteLegacyArgv(argv) {
|
|
1589
|
+
const first = argv[2];
|
|
1590
|
+
if (!first || first.startsWith("-") || knownCommands.has(first)) {
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (first.includes("/") || first === "stack") {
|
|
1594
|
+
argv.splice(2, 0, "add");
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
function wrap(fn) {
|
|
1598
|
+
return async (...args) => {
|
|
1599
|
+
try {
|
|
1600
|
+
await fn(...args);
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
printError(error);
|
|
1603
|
+
process.exitCode = 1;
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
function required(value, message) {
|
|
1608
|
+
if (!value) {
|
|
1609
|
+
throw new Error(message);
|
|
1610
|
+
}
|
|
1611
|
+
return value;
|
|
1612
|
+
}
|
|
1613
|
+
function isCommanderHelp(error) {
|
|
1614
|
+
return typeof error === "object" && error !== null && "code" in error && (error.code === "commander.helpDisplayed" || error.code === "commander.version");
|
|
1615
|
+
}
|