@use-aistack/cli 0.4.0 → 0.5.0
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 +12 -0
- package/dist/index.js +360 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,18 @@ npx @use-aistack/cli sync
|
|
|
20
20
|
|
|
21
21
|
Requires a one-time `login` first.
|
|
22
22
|
|
|
23
|
+
### `npx @use-aistack/cli sync --auto on` / `off`
|
|
24
|
+
|
|
25
|
+
Optional: keep your stack fresh without manual syncs. `on` writes a `SessionStart` hook (`async: true`) into `~/.claude/settings.json`. The hook runs a silent sync at most once per day when a Claude Code session starts. `off` removes the hook and revokes the standing opt-in.
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npx @use-aistack/cli sync --auto on # enable, default every 24h
|
|
29
|
+
npx @use-aistack/cli sync --auto on --every 12 # custom frequency in hours
|
|
30
|
+
npx @use-aistack/cli sync --auto off # revoke
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The silent run (`sync --auto`) never prompts and publishes only under this opt-in. Each run appends one line to `~/.config/aistack/sync.log` (capped at 200 lines). The next interactive `sync` reports the last result. After 3 failures in a row, one visible message appears in Claude Code and names the fix. No email, no dialogs.
|
|
34
|
+
|
|
23
35
|
### `npx @use-aistack/cli login`
|
|
24
36
|
|
|
25
37
|
Link this machine to your AI Stack account via browser.
|
package/dist/index.js
CHANGED
|
@@ -182,39 +182,67 @@ function classify(files) {
|
|
|
182
182
|
// src/config.ts
|
|
183
183
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
184
184
|
import { homedir } from "os";
|
|
185
|
-
import { join } from "path";
|
|
185
|
+
import { dirname as dirname2, join } from "path";
|
|
186
186
|
var CONFIG_DIR = join(homedir(), ".config", "aistack");
|
|
187
187
|
var CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
var DEFAULT_SERVER_URL = "https://aistack.to";
|
|
189
|
+
function readCredentials(file) {
|
|
190
|
+
const empty = { data: { servers: {} }, legacy: false };
|
|
191
|
+
if (!existsSync(file)) return empty;
|
|
190
192
|
try {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
)
|
|
194
|
-
|
|
193
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
194
|
+
if (!raw || typeof raw !== "object") return empty;
|
|
195
|
+
if (raw.servers && typeof raw.servers === "object") {
|
|
196
|
+
return {
|
|
197
|
+
data: { servers: raw.servers },
|
|
198
|
+
legacy: false
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (typeof raw.token === "string" && raw.token) {
|
|
202
|
+
return {
|
|
203
|
+
data: {
|
|
204
|
+
servers: {
|
|
205
|
+
[DEFAULT_SERVER_URL]: { token: raw.token, userId: raw.userId }
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
legacy: true
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return { data: { servers: {} }, legacy: true };
|
|
195
212
|
} catch {
|
|
196
|
-
return
|
|
213
|
+
return empty;
|
|
197
214
|
}
|
|
198
215
|
}
|
|
199
|
-
function
|
|
200
|
-
mkdirSync(
|
|
201
|
-
writeFileSync(
|
|
216
|
+
function writeCredentials(file, data) {
|
|
217
|
+
mkdirSync(dirname2(file), { recursive: true });
|
|
218
|
+
writeFileSync(file, JSON.stringify(data, null, 2));
|
|
219
|
+
}
|
|
220
|
+
function getToken(serverUrl = BASE_URL, file = CREDENTIALS_FILE) {
|
|
221
|
+
const { data, legacy } = readCredentials(file);
|
|
222
|
+
if (legacy) writeCredentials(file, data);
|
|
223
|
+
return data.servers[serverUrl]?.token ?? null;
|
|
224
|
+
}
|
|
225
|
+
function saveToken(token, userId, serverUrl = BASE_URL, file = CREDENTIALS_FILE) {
|
|
226
|
+
const { data } = readCredentials(file);
|
|
227
|
+
data.servers[serverUrl] = { token, userId };
|
|
228
|
+
writeCredentials(file, data);
|
|
202
229
|
}
|
|
203
230
|
var SETTINGS_FILE = join(CONFIG_DIR, "settings.json");
|
|
204
|
-
|
|
205
|
-
|
|
231
|
+
var DEFAULT_FREQUENCY_HOURS = 24;
|
|
232
|
+
function getSettings(file = SETTINGS_FILE) {
|
|
233
|
+
if (!existsSync(file)) return {};
|
|
206
234
|
try {
|
|
207
|
-
const raw = JSON.parse(readFileSync(
|
|
235
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
208
236
|
return raw && typeof raw === "object" ? raw : {};
|
|
209
237
|
} catch {
|
|
210
238
|
return {};
|
|
211
239
|
}
|
|
212
240
|
}
|
|
213
|
-
function saveSettings(patch) {
|
|
214
|
-
mkdirSync(
|
|
241
|
+
function saveSettings(patch, file = SETTINGS_FILE) {
|
|
242
|
+
mkdirSync(dirname2(file), { recursive: true });
|
|
215
243
|
writeFileSync(
|
|
216
|
-
|
|
217
|
-
JSON.stringify({ ...getSettings(), ...patch }, null, 2)
|
|
244
|
+
file,
|
|
245
|
+
JSON.stringify({ ...getSettings(file), ...patch }, null, 2)
|
|
218
246
|
);
|
|
219
247
|
}
|
|
220
248
|
var PROJECTS_FILE = join(CONFIG_DIR, "projects.json");
|
|
@@ -650,7 +678,7 @@ function resolvePluginLinks(installed, marketplaces, manifests) {
|
|
|
650
678
|
const marketplace = key.slice(at + 1);
|
|
651
679
|
const mpRepoUrl = marketplaceRepoUrl(marketplaces[marketplace]);
|
|
652
680
|
const entry = manifests[marketplace]?.plugins?.find(
|
|
653
|
-
(
|
|
681
|
+
(p8) => p8.name === pluginName
|
|
654
682
|
);
|
|
655
683
|
if (!entry) continue;
|
|
656
684
|
const resolved = resolveSource(entry, mpRepoUrl);
|
|
@@ -1289,7 +1317,7 @@ function diffResources(current, existing) {
|
|
|
1289
1317
|
import { spawnSync } from "child_process";
|
|
1290
1318
|
import { cpSync, existsSync as existsSync6 } from "fs";
|
|
1291
1319
|
import { homedir as homedir6 } from "os";
|
|
1292
|
-
import { dirname as
|
|
1320
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
1293
1321
|
import { fileURLToPath } from "url";
|
|
1294
1322
|
import * as p3 from "@clack/prompts";
|
|
1295
1323
|
var MANUAL_MCP_ADD = "claude mcp add --scope user aistack -- npx -y @use-aistack/cli mcp";
|
|
@@ -1319,12 +1347,12 @@ function runClaude(args) {
|
|
|
1319
1347
|
function claudeOnPath(run = runClaude) {
|
|
1320
1348
|
return !run(["--version"]).notFound;
|
|
1321
1349
|
}
|
|
1322
|
-
function findSkillSource(fromDir =
|
|
1350
|
+
function findSkillSource(fromDir = dirname3(fileURLToPath(import.meta.url))) {
|
|
1323
1351
|
let dir = fromDir;
|
|
1324
1352
|
for (let i = 0; i < 4; i++) {
|
|
1325
1353
|
const candidate = join6(dir, "skills", "aistack-sync");
|
|
1326
1354
|
if (existsSync6(join6(candidate, "SKILL.md"))) return candidate;
|
|
1327
|
-
const parent =
|
|
1355
|
+
const parent = dirname3(dir);
|
|
1328
1356
|
if (parent === dir) break;
|
|
1329
1357
|
dir = parent;
|
|
1330
1358
|
}
|
|
@@ -1431,7 +1459,7 @@ async function offerConnectUpsell() {
|
|
|
1431
1459
|
|
|
1432
1460
|
// src/commands/create.ts
|
|
1433
1461
|
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
|
|
1434
|
-
import { dirname as
|
|
1462
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
1435
1463
|
import * as p4 from "@clack/prompts";
|
|
1436
1464
|
async function createCommand() {
|
|
1437
1465
|
intro2("create");
|
|
@@ -1521,7 +1549,7 @@ async function createCommand() {
|
|
|
1521
1549
|
}
|
|
1522
1550
|
for (const f of toWrite) {
|
|
1523
1551
|
const fullPath = join7(cwd, f.path);
|
|
1524
|
-
const dir =
|
|
1552
|
+
const dir = dirname4(fullPath);
|
|
1525
1553
|
mkdirSync2(dir, { recursive: true });
|
|
1526
1554
|
writeFileSync2(fullPath, f.content);
|
|
1527
1555
|
}
|
|
@@ -1602,8 +1630,166 @@ async function loginCommand() {
|
|
|
1602
1630
|
}
|
|
1603
1631
|
|
|
1604
1632
|
// src/commands/sync.ts
|
|
1633
|
+
import * as p7 from "@clack/prompts";
|
|
1634
|
+
|
|
1635
|
+
// src/autosync/optin.ts
|
|
1605
1636
|
import * as p6 from "@clack/prompts";
|
|
1606
1637
|
|
|
1638
|
+
// src/autosync/hook.ts
|
|
1639
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
1640
|
+
import { homedir as homedir7 } from "os";
|
|
1641
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
1642
|
+
var CLAUDE_SETTINGS_FILE = join8(homedir7(), ".claude", "settings.json");
|
|
1643
|
+
var AUTO_SYNC_HOOK_COMMAND = "npx -y @use-aistack/cli@latest sync --auto || npx -y --prefer-offline @use-aistack/cli sync --auto";
|
|
1644
|
+
function isOurs(entry) {
|
|
1645
|
+
return typeof entry.command === "string" && entry.command.includes("@use-aistack/cli") && entry.command.includes("sync --auto");
|
|
1646
|
+
}
|
|
1647
|
+
function readClaudeSettings(file) {
|
|
1648
|
+
if (!existsSync8(file)) return { settings: {} };
|
|
1649
|
+
try {
|
|
1650
|
+
const raw = JSON.parse(readFileSync7(file, "utf-8"));
|
|
1651
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
1652
|
+
return { settings: raw };
|
|
1653
|
+
}
|
|
1654
|
+
return { error: `${file} does not hold a JSON object` };
|
|
1655
|
+
} catch {
|
|
1656
|
+
return { error: `${file} is not valid JSON \u2014 fix it, then retry` };
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
function installAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
|
|
1660
|
+
const read = readClaudeSettings(file);
|
|
1661
|
+
if ("error" in read) return { ok: false, message: read.error };
|
|
1662
|
+
const settings = read.settings;
|
|
1663
|
+
const hooks = settings.hooks ?? {};
|
|
1664
|
+
const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
|
|
1665
|
+
const kept = sessionStart.map((m) => ({
|
|
1666
|
+
...m,
|
|
1667
|
+
hooks: (m.hooks ?? []).filter((h) => !isOurs(h))
|
|
1668
|
+
})).filter((m) => (m.hooks?.length ?? 0) > 0);
|
|
1669
|
+
kept.push({
|
|
1670
|
+
hooks: [{ type: "command", command: AUTO_SYNC_HOOK_COMMAND, async: true }]
|
|
1671
|
+
});
|
|
1672
|
+
settings.hooks = { ...hooks, SessionStart: kept };
|
|
1673
|
+
mkdirSync3(dirname5(file), { recursive: true });
|
|
1674
|
+
writeFileSync3(file, `${JSON.stringify(settings, null, 2)}
|
|
1675
|
+
`);
|
|
1676
|
+
return { ok: true, message: `SessionStart hook written to ${file}` };
|
|
1677
|
+
}
|
|
1678
|
+
function removeAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
|
|
1679
|
+
if (!existsSync8(file)) return { ok: true, message: "no hook to remove" };
|
|
1680
|
+
const read = readClaudeSettings(file);
|
|
1681
|
+
if ("error" in read) return { ok: false, message: read.error };
|
|
1682
|
+
const settings = read.settings;
|
|
1683
|
+
const sessionStart = settings.hooks?.SessionStart;
|
|
1684
|
+
if (!Array.isArray(sessionStart)) {
|
|
1685
|
+
return { ok: true, message: "no hook to remove" };
|
|
1686
|
+
}
|
|
1687
|
+
const kept = sessionStart.map((m) => ({
|
|
1688
|
+
...m,
|
|
1689
|
+
hooks: (m.hooks ?? []).filter((h) => !isOurs(h))
|
|
1690
|
+
})).filter((m) => (m.hooks?.length ?? 0) > 0);
|
|
1691
|
+
const hooks = { ...settings.hooks };
|
|
1692
|
+
if (kept.length > 0) {
|
|
1693
|
+
hooks.SessionStart = kept;
|
|
1694
|
+
} else {
|
|
1695
|
+
delete hooks.SessionStart;
|
|
1696
|
+
}
|
|
1697
|
+
if (Object.keys(hooks).length > 0) {
|
|
1698
|
+
settings.hooks = hooks;
|
|
1699
|
+
} else {
|
|
1700
|
+
delete settings.hooks;
|
|
1701
|
+
}
|
|
1702
|
+
writeFileSync3(file, `${JSON.stringify(settings, null, 2)}
|
|
1703
|
+
`);
|
|
1704
|
+
return { ok: true, message: `hook removed from ${file}` };
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/autosync/optin.ts
|
|
1708
|
+
function enableAutoSync(frequencyHours = DEFAULT_FREQUENCY_HOURS, deps = {}) {
|
|
1709
|
+
const install = deps.installHook ?? installAutoSyncHook;
|
|
1710
|
+
const result = install();
|
|
1711
|
+
if (!result.ok) return result;
|
|
1712
|
+
saveSettings(
|
|
1713
|
+
{
|
|
1714
|
+
autoSyncAnswered: true,
|
|
1715
|
+
autoSync: { enabled: true, frequencyHours }
|
|
1716
|
+
},
|
|
1717
|
+
deps.settingsFile
|
|
1718
|
+
);
|
|
1719
|
+
return {
|
|
1720
|
+
ok: true,
|
|
1721
|
+
message: `Auto-sync is on \u2014 about every ${frequencyHours}h when a Claude Code session starts. Turn it off any time: npx @use-aistack/cli sync --auto off`
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
function disableAutoSync(deps = {}) {
|
|
1725
|
+
const remove = deps.removeHook ?? removeAutoSyncHook;
|
|
1726
|
+
const settings = getSettings(deps.settingsFile);
|
|
1727
|
+
saveSettings(
|
|
1728
|
+
{
|
|
1729
|
+
autoSyncAnswered: true,
|
|
1730
|
+
autoSync: {
|
|
1731
|
+
enabled: false,
|
|
1732
|
+
frequencyHours: settings.autoSync?.frequencyHours ?? DEFAULT_FREQUENCY_HOURS
|
|
1733
|
+
}
|
|
1734
|
+
},
|
|
1735
|
+
deps.settingsFile
|
|
1736
|
+
);
|
|
1737
|
+
const result = remove();
|
|
1738
|
+
if (!result.ok) {
|
|
1739
|
+
return {
|
|
1740
|
+
ok: false,
|
|
1741
|
+
message: `Auto-sync is off (nothing will publish), but the hook could not be removed: ${result.message}`
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
return { ok: true, message: "Auto-sync is off. The hook was removed." };
|
|
1745
|
+
}
|
|
1746
|
+
async function offerAutoSyncOptIn() {
|
|
1747
|
+
if (getSettings().autoSyncAnswered === true) return false;
|
|
1748
|
+
const answer = await p6.select({
|
|
1749
|
+
message: "Keep this stack fresh automatically?",
|
|
1750
|
+
options: [
|
|
1751
|
+
{
|
|
1752
|
+
value: "later",
|
|
1753
|
+
label: "Not now",
|
|
1754
|
+
hint: "this question will not come back"
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
value: "enable",
|
|
1758
|
+
label: "Enable",
|
|
1759
|
+
hint: "a silent daily sync when a Claude Code session starts"
|
|
1760
|
+
}
|
|
1761
|
+
],
|
|
1762
|
+
initialValue: "later"
|
|
1763
|
+
});
|
|
1764
|
+
if (p6.isCancel(answer)) return true;
|
|
1765
|
+
if (answer === "enable") {
|
|
1766
|
+
const result = enableAutoSync();
|
|
1767
|
+
if (result.ok) {
|
|
1768
|
+
p6.log.success(result.message);
|
|
1769
|
+
} else {
|
|
1770
|
+
p6.log.error(result.message);
|
|
1771
|
+
}
|
|
1772
|
+
return true;
|
|
1773
|
+
}
|
|
1774
|
+
saveSettings({ autoSyncAnswered: true });
|
|
1775
|
+
p6.log.message(
|
|
1776
|
+
`If you change your mind: ${limeBold("npx @use-aistack/cli sync --auto on")} ${dim(
|
|
1777
|
+
"(and --auto off to revoke)"
|
|
1778
|
+
)}`
|
|
1779
|
+
);
|
|
1780
|
+
return true;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// src/autosync/run.ts
|
|
1784
|
+
import {
|
|
1785
|
+
appendFileSync,
|
|
1786
|
+
mkdirSync as mkdirSync4,
|
|
1787
|
+
readFileSync as readFileSync8,
|
|
1788
|
+
writeFileSync as writeFileSync4
|
|
1789
|
+
} from "fs";
|
|
1790
|
+
import { homedir as homedir9 } from "os";
|
|
1791
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
1792
|
+
|
|
1607
1793
|
// src/sync/stage.ts
|
|
1608
1794
|
import { createHash } from "crypto";
|
|
1609
1795
|
|
|
@@ -1643,9 +1829,9 @@ function priceAt(modelKey, atMs) {
|
|
|
1643
1829
|
if (atMs === null) return null;
|
|
1644
1830
|
const periods = PRICES[modelKey];
|
|
1645
1831
|
if (!periods) return null;
|
|
1646
|
-
for (const
|
|
1647
|
-
if ((
|
|
1648
|
-
return
|
|
1832
|
+
for (const p8 of periods) {
|
|
1833
|
+
if ((p8.from === null || atMs >= p8.from) && (p8.to === null || atMs < p8.to)) {
|
|
1834
|
+
return p8;
|
|
1649
1835
|
}
|
|
1650
1836
|
}
|
|
1651
1837
|
return null;
|
|
@@ -1654,10 +1840,10 @@ function isPricedModel(modelKey) {
|
|
|
1654
1840
|
return PRICES[modelKey] !== void 0;
|
|
1655
1841
|
}
|
|
1656
1842
|
function apiEquivalentCost(modelKey, t, atMs) {
|
|
1657
|
-
const
|
|
1658
|
-
if (!
|
|
1843
|
+
const p8 = priceAt(modelKey, atMs);
|
|
1844
|
+
if (!p8) return null;
|
|
1659
1845
|
const M = 1e6;
|
|
1660
|
-
return (t.input *
|
|
1846
|
+
return (t.input * p8.input + t.output * p8.output + (t.cacheWrite5m + t.cacheWriteUnsplit) * p8.input * CACHE_WRITE_5M_MULTIPLIER + t.cacheWrite1h * p8.input * CACHE_WRITE_1H_MULTIPLIER + t.cacheRead * p8.input * CACHE_READ_MULTIPLIER) / M;
|
|
1661
1847
|
}
|
|
1662
1848
|
|
|
1663
1849
|
// src/transcripts/analyzer.ts
|
|
@@ -2024,7 +2210,7 @@ function newestVersion(versions) {
|
|
|
2024
2210
|
let best = null;
|
|
2025
2211
|
let bestParts = [];
|
|
2026
2212
|
for (const v of versions) {
|
|
2027
|
-
const parts = v.split(".").map((
|
|
2213
|
+
const parts = v.split(".").map((p8) => Number.parseInt(p8, 10));
|
|
2028
2214
|
if (parts.some((n) => !Number.isFinite(n))) continue;
|
|
2029
2215
|
if (best === null || compareParts(parts, bestParts) > 0) {
|
|
2030
2216
|
best = v;
|
|
@@ -2385,7 +2571,7 @@ function filterAtoms(atoms, sets) {
|
|
|
2385
2571
|
// src/transcripts/scan.ts
|
|
2386
2572
|
import { createReadStream } from "fs";
|
|
2387
2573
|
import { readdir, realpath, stat } from "fs/promises";
|
|
2388
|
-
import { homedir as
|
|
2574
|
+
import { homedir as homedir8 } from "os";
|
|
2389
2575
|
import path from "path";
|
|
2390
2576
|
import readline from "readline";
|
|
2391
2577
|
function transcriptRoots() {
|
|
@@ -2393,8 +2579,8 @@ function transcriptRoots() {
|
|
|
2393
2579
|
if (env) {
|
|
2394
2580
|
return env.split(",").map((s) => s.trim()).filter(Boolean).map((s) => path.join(s, "projects"));
|
|
2395
2581
|
}
|
|
2396
|
-
const roots = [path.join(
|
|
2397
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? path.join(
|
|
2582
|
+
const roots = [path.join(homedir8(), ".claude", "projects")];
|
|
2583
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path.join(homedir8(), ".config");
|
|
2398
2584
|
roots.push(path.join(xdg, "claude", "projects"));
|
|
2399
2585
|
return roots;
|
|
2400
2586
|
}
|
|
@@ -2468,9 +2654,9 @@ async function scan(agg, opts = {}) {
|
|
|
2468
2654
|
}
|
|
2469
2655
|
return stats;
|
|
2470
2656
|
}
|
|
2471
|
-
async function exists(
|
|
2657
|
+
async function exists(p8) {
|
|
2472
2658
|
try {
|
|
2473
|
-
await stat(
|
|
2659
|
+
await stat(p8);
|
|
2474
2660
|
return true;
|
|
2475
2661
|
} catch {
|
|
2476
2662
|
return false;
|
|
@@ -2889,15 +3075,132 @@ async function stageSync(deps) {
|
|
|
2889
3075
|
};
|
|
2890
3076
|
}
|
|
2891
3077
|
|
|
3078
|
+
// src/autosync/run.ts
|
|
3079
|
+
var SYNC_LOG_FILE = join9(homedir9(), ".config", "aistack", "sync.log");
|
|
3080
|
+
var SYNC_LOG_MAX_LINES = 200;
|
|
3081
|
+
var FIX_COMMAND = "npx @use-aistack/cli sync";
|
|
3082
|
+
function appendLogLine(file, line) {
|
|
3083
|
+
mkdirSync4(dirname6(file), { recursive: true });
|
|
3084
|
+
appendFileSync(file, `${line}
|
|
3085
|
+
`);
|
|
3086
|
+
const lines2 = readFileSync8(file, "utf-8").split("\n").filter(Boolean);
|
|
3087
|
+
if (lines2.length > SYNC_LOG_MAX_LINES) {
|
|
3088
|
+
writeFileSync4(file, `${lines2.slice(-SYNC_LOG_MAX_LINES).join("\n")}
|
|
3089
|
+
`);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
async function runAutoSync(deps) {
|
|
3093
|
+
const now = (deps.now ?? Date.now)();
|
|
3094
|
+
const settingsFile = deps.settingsFile;
|
|
3095
|
+
const logFile = deps.logFile ?? SYNC_LOG_FILE;
|
|
3096
|
+
const emit = deps.emit ?? ((line) => process.stdout.write(`${line}
|
|
3097
|
+
`));
|
|
3098
|
+
const stamp = new Date(now).toISOString();
|
|
3099
|
+
const settings = getSettings(settingsFile);
|
|
3100
|
+
const config = settings.autoSync;
|
|
3101
|
+
if (config?.enabled !== true) {
|
|
3102
|
+
appendLogLine(logFile, `${stamp} skipped \u2014 auto-sync is not enabled`);
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
const frequencyHours = config.frequencyHours || DEFAULT_FREQUENCY_HOURS;
|
|
3106
|
+
const state = settings.autoSyncState ?? {};
|
|
3107
|
+
const lastRunAt = state.lastRunAt ?? 0;
|
|
3108
|
+
if (now - lastRunAt < frequencyHours * 36e5) return;
|
|
3109
|
+
const stage = deps.stageImpl ?? stageSync;
|
|
3110
|
+
const publish = deps.publishImpl ?? syncPublish;
|
|
3111
|
+
let failure2 = null;
|
|
3112
|
+
let url;
|
|
3113
|
+
try {
|
|
3114
|
+
const staged = await stage({ baseUrl: deps.baseUrl, now: () => now });
|
|
3115
|
+
if (staged.blockedReason !== null) {
|
|
3116
|
+
failure2 = staged.blockedReason;
|
|
3117
|
+
} else {
|
|
3118
|
+
const res = await publish(staged.token, staged.bodyJson);
|
|
3119
|
+
url = res.url;
|
|
3120
|
+
}
|
|
3121
|
+
} catch (e) {
|
|
3122
|
+
failure2 = e instanceof Error ? e.message : String(e);
|
|
3123
|
+
}
|
|
3124
|
+
if (failure2 === null) {
|
|
3125
|
+
saveSettings(
|
|
3126
|
+
{
|
|
3127
|
+
autoSyncState: {
|
|
3128
|
+
lastRunAt: now,
|
|
3129
|
+
lastSuccessAt: now,
|
|
3130
|
+
lastResult: `ok \u2014 published at ${stamp}`,
|
|
3131
|
+
consecutiveFailures: 0,
|
|
3132
|
+
failureWarned: false
|
|
3133
|
+
}
|
|
3134
|
+
},
|
|
3135
|
+
settingsFile
|
|
3136
|
+
);
|
|
3137
|
+
appendLogLine(logFile, `${stamp} ok \u2014 published${url ? ` ${url}` : ""}`);
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
const consecutiveFailures = (state.consecutiveFailures ?? 0) + 1;
|
|
3141
|
+
const shouldWarn = consecutiveFailures >= 3 && state.failureWarned !== true;
|
|
3142
|
+
saveSettings(
|
|
3143
|
+
{
|
|
3144
|
+
autoSyncState: {
|
|
3145
|
+
...state,
|
|
3146
|
+
lastRunAt: now,
|
|
3147
|
+
lastResult: `failed at ${stamp} \u2014 ${failure2}`,
|
|
3148
|
+
consecutiveFailures,
|
|
3149
|
+
failureWarned: state.failureWarned === true || shouldWarn
|
|
3150
|
+
}
|
|
3151
|
+
},
|
|
3152
|
+
settingsFile
|
|
3153
|
+
);
|
|
3154
|
+
appendLogLine(
|
|
3155
|
+
logFile,
|
|
3156
|
+
`${stamp} fail (${consecutiveFailures} in a row) \u2014 ${failure2}`
|
|
3157
|
+
);
|
|
3158
|
+
if (shouldWarn) {
|
|
3159
|
+
emit(
|
|
3160
|
+
JSON.stringify({
|
|
3161
|
+
systemMessage: `aistack auto-sync failed ${consecutiveFailures} times in a row (${failure2}). Run \`${FIX_COMMAND}\` in a terminal to fix it, or \`${FIX_COMMAND} --auto off\` to stop these runs.`
|
|
3162
|
+
})
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
|
|
2892
3167
|
// src/commands/sync.ts
|
|
2893
|
-
async function syncCommand() {
|
|
3168
|
+
async function syncCommand(options = {}) {
|
|
3169
|
+
if (options.auto === true) {
|
|
3170
|
+
await runAutoSync({ baseUrl: BASE_URL });
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
if (options.auto === "on" || options.auto === "off") {
|
|
3174
|
+
intro2("sync");
|
|
3175
|
+
const result = options.auto === "on" ? enableAutoSync(
|
|
3176
|
+
options.every ? Number.parseInt(options.every, 10) || DEFAULT_FREQUENCY_HOURS : DEFAULT_FREQUENCY_HOURS
|
|
3177
|
+
) : disableAutoSync();
|
|
3178
|
+
if (result.ok) {
|
|
3179
|
+
p7.log.success(result.message);
|
|
3180
|
+
outro2("done");
|
|
3181
|
+
} else {
|
|
3182
|
+
outroError(result.message);
|
|
3183
|
+
process.exitCode = 1;
|
|
3184
|
+
}
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
if (options.auto !== void 0) {
|
|
3188
|
+
intro2("sync");
|
|
3189
|
+
outroError(`unknown --auto value "${options.auto}" \u2014 use on or off`);
|
|
3190
|
+
process.exitCode = 1;
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
2894
3193
|
intro2("sync");
|
|
3194
|
+
const lastAuto = getSettings().autoSyncState?.lastResult;
|
|
3195
|
+
if (lastAuto !== void 0) {
|
|
3196
|
+
p7.log.message(dim(`auto-sync: ${lastAuto}`));
|
|
3197
|
+
}
|
|
2895
3198
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2896
3199
|
outroError("sync needs an interactive terminal \u2014 nothing was sent");
|
|
2897
3200
|
process.exitCode = 1;
|
|
2898
3201
|
return;
|
|
2899
3202
|
}
|
|
2900
|
-
const s =
|
|
3203
|
+
const s = p7.spinner();
|
|
2901
3204
|
s.start("Scanning local Claude Code transcripts");
|
|
2902
3205
|
let staged;
|
|
2903
3206
|
try {
|
|
@@ -2909,13 +3212,13 @@ async function syncCommand() {
|
|
|
2909
3212
|
return;
|
|
2910
3213
|
}
|
|
2911
3214
|
s.stop("Scan complete");
|
|
2912
|
-
|
|
3215
|
+
p7.log.message(staged.summary.split("\n").join("\n"));
|
|
2913
3216
|
if (staged.blockedReason !== null) {
|
|
2914
3217
|
outroError(staged.blockedReason);
|
|
2915
3218
|
process.exitCode = 1;
|
|
2916
3219
|
return;
|
|
2917
3220
|
}
|
|
2918
|
-
const decision = await
|
|
3221
|
+
const decision = await p7.select({
|
|
2919
3222
|
message: staged.dialog.split("\n").join(dim(" \xB7 ")),
|
|
2920
3223
|
options: [
|
|
2921
3224
|
{ value: "cancel", label: "Cancel", hint: "nothing leaves this machine" },
|
|
@@ -2923,7 +3226,7 @@ async function syncCommand() {
|
|
|
2923
3226
|
],
|
|
2924
3227
|
initialValue: "cancel"
|
|
2925
3228
|
});
|
|
2926
|
-
if (
|
|
3229
|
+
if (p7.isCancel(decision) || decision !== "publish") {
|
|
2927
3230
|
outroCancel("nothing was sent");
|
|
2928
3231
|
return;
|
|
2929
3232
|
}
|
|
@@ -2944,8 +3247,9 @@ async function syncCommand() {
|
|
|
2944
3247
|
`${res.keptPrivate.stored} kept-private names went up for your review at ${res.url}/changes`
|
|
2945
3248
|
);
|
|
2946
3249
|
}
|
|
2947
|
-
|
|
2948
|
-
await
|
|
3250
|
+
p7.log.message(lines2.join("\n"));
|
|
3251
|
+
const asked = await offerAutoSyncOptIn();
|
|
3252
|
+
if (!asked) await offerConnectUpsell();
|
|
2949
3253
|
outro2("done");
|
|
2950
3254
|
} catch (e) {
|
|
2951
3255
|
s.stop("Publish failed");
|
|
@@ -2996,7 +3300,7 @@ function createSyncServer(deps, send) {
|
|
|
2996
3300
|
const now = deps.now ?? Date.now;
|
|
2997
3301
|
const stage = deps.stageImpl ?? stageSync;
|
|
2998
3302
|
const publish = deps.publishImpl ?? syncPublish;
|
|
2999
|
-
const
|
|
3303
|
+
const log7 = deps.log ?? (() => {
|
|
3000
3304
|
});
|
|
3001
3305
|
const elicitTimeoutMs = deps.elicitTimeoutMs ?? ELICIT_TIMEOUT_MS;
|
|
3002
3306
|
let clientSupportsElicitation = false;
|
|
@@ -3076,7 +3380,7 @@ function createSyncServer(deps, send) {
|
|
|
3076
3380
|
);
|
|
3077
3381
|
}
|
|
3078
3382
|
const approvedStage = staged;
|
|
3079
|
-
|
|
3383
|
+
log7(`elicitation raised for stage ${approvedStage.id}`);
|
|
3080
3384
|
request2(
|
|
3081
3385
|
"elicitation/create",
|
|
3082
3386
|
{
|
|
@@ -3099,7 +3403,7 @@ function createSyncServer(deps, send) {
|
|
|
3099
3403
|
const approved = result?.action === "accept" && result?.content?.decision === "publish";
|
|
3100
3404
|
if (!approved) {
|
|
3101
3405
|
const outcome = reply === null ? "timed out" : result?.action ?? "error";
|
|
3102
|
-
|
|
3406
|
+
log7(`elicitation resolved without consent: ${outcome}`);
|
|
3103
3407
|
return ok(
|
|
3104
3408
|
id,
|
|
3105
3409
|
textResult(
|
|
@@ -3107,7 +3411,7 @@ function createSyncServer(deps, send) {
|
|
|
3107
3411
|
)
|
|
3108
3412
|
);
|
|
3109
3413
|
}
|
|
3110
|
-
|
|
3414
|
+
log7(`consent received, sending stage ${approvedStage.id}`);
|
|
3111
3415
|
publish(approvedStage.token, approvedStage.bodyJson).then(
|
|
3112
3416
|
(res) => {
|
|
3113
3417
|
if (staged?.id === approvedStage.id) staged = null;
|
|
@@ -3150,7 +3454,7 @@ function createSyncServer(deps, send) {
|
|
|
3150
3454
|
case "initialize": {
|
|
3151
3455
|
const capabilities = params?.capabilities ?? {};
|
|
3152
3456
|
clientSupportsElicitation = "elicitation" in capabilities;
|
|
3153
|
-
|
|
3457
|
+
log7(
|
|
3154
3458
|
`initialize: elicitation ${clientSupportsElicitation ? "declared" : "ABSENT"}`
|
|
3155
3459
|
);
|
|
3156
3460
|
return ok(id, {
|
|
@@ -3205,7 +3509,7 @@ function runStdioSyncServer(deps) {
|
|
|
3205
3509
|
|
|
3206
3510
|
// src/index.ts
|
|
3207
3511
|
var program = new Command();
|
|
3208
|
-
program.name("aistack").description("Measure and share your AI stack from your terminal").version("0.
|
|
3512
|
+
program.name("aistack").description("Measure and share your AI stack from your terminal").version("0.5.0");
|
|
3209
3513
|
program.command("login").description("Authenticate with AI Stack").action(loginCommand);
|
|
3210
3514
|
program.command("collect").description("Scan and upload AI config files from your project").option("--no-global", "Exclude global config files (~/.claude, etc.)").action((options) => collectCommand({ global: options.global ?? true }));
|
|
3211
3515
|
program.command("create").description("Download and write your stack's AI config files").action(createCommand);
|
|
@@ -3218,7 +3522,13 @@ program.command("mcp").description(
|
|
|
3218
3522
|
`)
|
|
3219
3523
|
});
|
|
3220
3524
|
});
|
|
3221
|
-
program.command("sync").description("Scan, preview, and publish measured usage (rolling 30 days)").
|
|
3525
|
+
program.command("sync").description("Scan, preview, and publish measured usage (rolling 30 days)").option(
|
|
3526
|
+
"--auto [state]",
|
|
3527
|
+
"silent background sync; 'on' enables the SessionStart hook, 'off' revokes it"
|
|
3528
|
+
).option(
|
|
3529
|
+
"--every <hours>",
|
|
3530
|
+
"with --auto on: hours between auto-syncs (default 24)"
|
|
3531
|
+
).action((options) => syncCommand(options));
|
|
3222
3532
|
program.command("connect").description("Install the in-session sync surface (MCP server + Skill)").argument("<harness>", 'the harness to connect ("claude")').action(connectCommand);
|
|
3223
3533
|
program.parse();
|
|
3224
3534
|
//# sourceMappingURL=index.js.map
|