@tricknowtech/context 0.1.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/dist/cli.d.cts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ interface CommandResult {
3
+ code: number;
4
+ lines: string[];
5
+ }
6
+
7
+ declare function run(argv: string[]): CommandResult;
8
+
9
+ export { run };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ interface CommandResult {
3
+ code: number;
4
+ lines: string[];
5
+ }
6
+
7
+ declare function run(argv: string[]): CommandResult;
8
+
9
+ export { run };
package/dist/cli.js ADDED
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ALL_TIERS,
4
+ LOCAL_TIERS,
5
+ LocalStore,
6
+ collect,
7
+ configPath,
8
+ cwdKey,
9
+ defaultConfig,
10
+ ensureGitignoreEntries,
11
+ findProjectRoot,
12
+ formatBytes,
13
+ formatHits,
14
+ installSlashCommand,
15
+ isGitRepo,
16
+ loadConfig,
17
+ saveConfig,
18
+ scanFiles,
19
+ storeDir,
20
+ summarize,
21
+ userClaudeDir
22
+ } from "./chunk-DEH5MUFT.js";
23
+
24
+ // src/commands.ts
25
+ import fs from "fs";
26
+ import path from "path";
27
+ function ok(lines) {
28
+ return { code: 0, lines };
29
+ }
30
+ function fail(lines) {
31
+ return { code: 1, lines };
32
+ }
33
+ function requireProject() {
34
+ const root = findProjectRoot();
35
+ if (!root) {
36
+ return fail([
37
+ "Not inside a project.",
38
+ "Run this from a git repository, or run `ctx init` to create a store here."
39
+ ]);
40
+ }
41
+ const cfg = loadConfig(root);
42
+ if (!cfg) {
43
+ return fail([`No context store found at ${path.join(root, ".contextsync")}.`, "Run `ctx init` first."]);
44
+ }
45
+ return { root, cfg };
46
+ }
47
+ function templateContext(root) {
48
+ return { userClaude: userClaudeDir(), project: root, cwdKey: cwdKey(root) };
49
+ }
50
+ function effectiveTiers(cfg) {
51
+ const tiers = cfg.tiers.filter((t) => LOCAL_TIERS.includes(t));
52
+ const refused = cfg.tiers.filter((t) => !LOCAL_TIERS.includes(t));
53
+ return { tiers, refused };
54
+ }
55
+ function cmdInit(opts = {}) {
56
+ const root = findProjectRoot() ?? process.cwd();
57
+ const existing = loadConfig(root);
58
+ if (existing && !opts.force) {
59
+ return fail([`Already initialised at ${configPath(root)}.`, "Pass --force to overwrite the config."]);
60
+ }
61
+ const cfg = defaultConfig(root);
62
+ if (opts.artifacts) cfg.tiers = [...cfg.tiers, "artifacts"];
63
+ saveConfig(root, cfg);
64
+ const lines = [
65
+ `Initialised context store for "${cfg.name}"`,
66
+ ` store ${path.relative(root, storeDir(root))}/`,
67
+ ` tiers ${cfg.tiers.join(", ")}`
68
+ ];
69
+ const slash = installSlashCommand(root);
70
+ lines.push(` command ${slash.path}${slash.written ? "" : " (already present, left alone)"}`);
71
+ const added = ensureGitignoreEntries(root, Boolean(opts.artifacts));
72
+ if (added.length > 0) lines.push(` ignored ${added.join(", ")}`);
73
+ if (!isGitRepo(root)) {
74
+ lines.push("", "Note: this is not a git repository, so the store will not travel with the code.");
75
+ }
76
+ lines.push("", "Next: run `/context push` in Claude Code, or `ctx push` directly.");
77
+ return ok(lines);
78
+ }
79
+ function cmdPush(opts = {}) {
80
+ const found = requireProject();
81
+ if ("code" in found) return found;
82
+ const { root, cfg } = found;
83
+ const { tiers, refused } = effectiveTiers(cfg);
84
+ const { files, skippedTracked } = collect(root, cfg, tiers);
85
+ const lines = [];
86
+ if (refused.length > 0) {
87
+ lines.push(
88
+ `Skipping ${refused.join(", ")} \u2014 not supported by a local store.`,
89
+ " Transcripts run to hundreds of MB and would permanently bloat the repo.",
90
+ " Add a cloud remote to sync them.",
91
+ ""
92
+ );
93
+ }
94
+ if (files.length === 0) {
95
+ lines.push("Nothing to sync.");
96
+ if (skippedTracked.length > 0) {
97
+ lines.push(`(${skippedTracked.length} project files skipped \u2014 git already tracks them.)`);
98
+ }
99
+ return ok(lines);
100
+ }
101
+ const hits = scanFiles(files);
102
+ if (hits.length > 0 && !opts.allowSecrets) {
103
+ return fail([
104
+ ...lines,
105
+ `Refusing to push \u2014 ${hits.length} possible secret${hits.length === 1 ? "" : "s"} found:`,
106
+ "",
107
+ formatHits(hits),
108
+ "",
109
+ "These would be committed to the repository and be very hard to remove.",
110
+ "Fix the source, add an exclude pattern, or re-run with --allow-secrets if these are false positives."
111
+ ]);
112
+ }
113
+ if (hits.length > 0) {
114
+ lines.push(`Warning: pushing ${hits.length} possible secret(s) because --allow-secrets was set.`, "");
115
+ }
116
+ const totals = summarize(files);
117
+ const totalBytes = files.reduce((n, f) => n + f.size, 0);
118
+ if (opts.dryRun) {
119
+ lines.push(`Would sync ${files.length} files (${formatBytes(totalBytes)}):`);
120
+ } else {
121
+ const store = new LocalStore(root);
122
+ store.write(files, root);
123
+ lines.push(`Synced ${files.length} files (${formatBytes(totalBytes)}) to ${path.relative(root, storeDir(root))}/`);
124
+ }
125
+ for (const tier of ALL_TIERS) {
126
+ const t = totals[tier];
127
+ if (t.count > 0) lines.push(` ${tier.padEnd(11)} ${String(t.count).padStart(4)} files ${formatBytes(t.bytes)}`);
128
+ }
129
+ if (skippedTracked.length > 0) {
130
+ lines.push("", `${skippedTracked.length} project files skipped \u2014 git already tracks them.`);
131
+ }
132
+ if (!opts.dryRun) {
133
+ lines.push("", "Commit .contextsync/ to carry this context with the repo.");
134
+ }
135
+ return ok(lines);
136
+ }
137
+ function cmdPull(opts = {}) {
138
+ const found = requireProject();
139
+ if ("code" in found) return found;
140
+ const { root } = found;
141
+ const store = new LocalStore(root);
142
+ const manifest = store.readManifest();
143
+ if (!manifest) {
144
+ return fail([`No manifest in ${path.relative(root, storeDir(root))}/.`, "Run `ctx push` on the source machine first."]);
145
+ }
146
+ const ctx = templateContext(root);
147
+ const { restored, skipped } = store.restore(ctx, { force: opts.force });
148
+ const lines = [`Restored ${restored.length} of ${manifest.entries.length} files.`];
149
+ if (manifest.writtenFrom && manifest.writtenFrom !== root) {
150
+ lines.push(` Rewrote paths from ${manifest.writtenFrom} \u2192 ${root}`);
151
+ }
152
+ if (skipped.length > 0) {
153
+ lines.push(
154
+ "",
155
+ `${skipped.length} file(s) left alone because the local copy differs:`,
156
+ ...skipped.slice(0, 15).map((s) => ` ${s}`),
157
+ ...skipped.length > 15 ? [` \u2026 and ${skipped.length - 15} more`] : [],
158
+ "",
159
+ "Re-run with --force to overwrite them."
160
+ );
161
+ }
162
+ const handoff = store.readHandoff();
163
+ if (handoff) {
164
+ lines.push("", `Handoff (${handoff.updatedAt}):`, ` goal: ${handoff.goal}`, ` next: ${handoff.nextStep}`);
165
+ }
166
+ return ok(lines);
167
+ }
168
+ function cmdStatus() {
169
+ const found = requireProject();
170
+ if ("code" in found) return found;
171
+ const { root, cfg } = found;
172
+ const store = new LocalStore(root);
173
+ const manifest = store.readManifest();
174
+ const { tiers } = effectiveTiers(cfg);
175
+ const { files, skippedTracked } = collect(root, cfg, tiers);
176
+ const lines = [
177
+ `Project ${cfg.name}`,
178
+ `Store ${path.relative(root, storeDir(root))}/`,
179
+ `Tiers ${cfg.tiers.join(", ")}`,
180
+ `Remotes ${Object.keys(cfg.remotes).length > 0 ? Object.keys(cfg.remotes).join(", ") : "none (local only)"}`,
181
+ ""
182
+ ];
183
+ if (!manifest) {
184
+ lines.push(`Never pushed. ${files.length} files ready to sync.`);
185
+ return ok(lines);
186
+ }
187
+ const stored = new Map(manifest.entries.map((e) => [e.storePath, e]));
188
+ const current = new Map(files.map((f) => [f.storePath, f]));
189
+ const added = [...current.keys()].filter((k) => !stored.has(k));
190
+ const removed = [...stored.keys()].filter((k) => !current.has(k));
191
+ const changed = [...current.entries()].filter(([k, f]) => {
192
+ const e = stored.get(k);
193
+ if (!e) return false;
194
+ if (e.size !== f.size) return true;
195
+ try {
196
+ return !fs.readFileSync(path.join(storeDir(root), k)).equals(fs.readFileSync(f.sourcePath));
197
+ } catch {
198
+ return true;
199
+ }
200
+ }).map(([k]) => k);
201
+ lines.push(`Last push ${manifest.updatedAt}`);
202
+ if (added.length + removed.length + changed.length === 0) {
203
+ lines.push("", "Up to date.");
204
+ } else {
205
+ lines.push("");
206
+ for (const k of changed.slice(0, 20)) lines.push(` modified ${k}`);
207
+ for (const k of added.slice(0, 20)) lines.push(` new ${k}`);
208
+ for (const k of removed.slice(0, 20)) lines.push(` removed ${k}`);
209
+ const shown = Math.min(changed.length, 20) + Math.min(added.length, 20) + Math.min(removed.length, 20);
210
+ const total = changed.length + added.length + removed.length;
211
+ if (total > shown) lines.push(` \u2026 and ${total - shown} more`);
212
+ lines.push("", "Run `ctx push` to sync.");
213
+ }
214
+ if (skippedTracked.length > 0) {
215
+ lines.push("", `${skippedTracked.length} project files carried by git directly.`);
216
+ }
217
+ return ok(lines);
218
+ }
219
+
220
+ // src/cli.ts
221
+ var USAGE = `tricknowtech context-sync \u2014 carry a project's LLM context between machines
222
+
223
+ Usage
224
+ ctx init [--artifacts] [--force] Create the store and install /context
225
+ ctx push [--dry-run] Collect context into the store
226
+ ctx pull [--force] Restore context from the store
227
+ ctx status Show what has changed since the last push
228
+
229
+ Options
230
+ --artifacts Include derived indexes (graphify-out/, etc.)
231
+ --allow-secrets Push even if the secret scan finds hits (think first)
232
+ --dry-run Show what would be synced without writing
233
+ --force init: overwrite config \xB7 pull: overwrite differing files
234
+ -h, --help Show this help
235
+ -v, --version Show version
236
+
237
+ The store lives in .contextsync/ and is meant to be committed, so context
238
+ travels with the code. Session transcripts are excluded from local mode.`;
239
+ function parseArgs(argv) {
240
+ const flags = /* @__PURE__ */ new Set();
241
+ let command = "";
242
+ for (const arg of argv) {
243
+ if (arg.startsWith("-")) flags.add(arg.replace(/^-+/, ""));
244
+ else if (!command) command = arg;
245
+ }
246
+ return { command, flags };
247
+ }
248
+ function run(argv) {
249
+ const { command, flags } = parseArgs(argv);
250
+ if (flags.has("h") || flags.has("help")) return { code: 0, lines: [USAGE] };
251
+ if (flags.has("v") || flags.has("version")) return { code: 0, lines: ["0.1.0"] };
252
+ switch (command) {
253
+ case "init":
254
+ return cmdInit({ artifacts: flags.has("artifacts"), force: flags.has("force") });
255
+ case "push":
256
+ return cmdPush({ allowSecrets: flags.has("allow-secrets"), dryRun: flags.has("dry-run") });
257
+ case "pull":
258
+ return cmdPull({ force: flags.has("force") });
259
+ case "status":
260
+ case "":
261
+ return cmdStatus();
262
+ default:
263
+ return { code: 1, lines: [`Unknown command: ${command}`, "", USAGE] };
264
+ }
265
+ }
266
+ var result = run(process.argv.slice(2));
267
+ var out = result.lines.join("\n");
268
+ if (result.code === 0) console.log(out);
269
+ else console.error(out);
270
+ process.exit(result.code);
271
+ export {
272
+ run
273
+ };