getadvantage 0.1.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getadvantage",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "The getAdvantage CLI — a local, dependency-free pre-deploy gate + portable project brain for AI-built apps. Plain-language GO / NO-GO, a repo-resident PROJECT-BRIEF.md any model reads first, and a session handoff so you can switch models or tools without re-explaining your project.",
5
5
  "type": "module",
6
6
  "bin": {
package/switch.mjs ADDED
@@ -0,0 +1,60 @@
1
+ // Ship-Safe — SWITCH WITHOUT LOSS (`ship-safe switch`).
2
+ //
3
+ // The portability move. Your brain lives in the REPO (PROJECT-BRIEF.md +
4
+ // HANDOFF.md), not in any one tool or model — so switching from Claude to Cursor
5
+ // to Qwen (or just starting a fresh session) should cost nothing. `switch` makes
6
+ // that a single command:
7
+ // 1. saves your place + refreshes the brain (handoff),
8
+ // 2. wires every AI-tool entry file so the new tool auto-loads it (init),
9
+ // 3. prints the exact paste-ready prompt to start the new session.
10
+ //
11
+ // Optionally name the tool/model you're switching TO for a tailored tip:
12
+ // ship-safe switch cursor
13
+ //
14
+ // Node built-ins only. ESM. (Delegates to handoff + init.)
15
+
16
+ import { c } from "./util.mjs";
17
+ import { runHandoff } from "./handoff.mjs";
18
+ import { runInit } from "./init.mjs";
19
+
20
+ // A friendly per-target line. Unknown targets get the generic guidance.
21
+ const TOOL_TIPS = {
22
+ claude: "Claude Code reads CLAUDE.md at startup — it'll pick up your brain automatically.",
23
+ "claude-code": "Claude Code reads CLAUDE.md at startup — it'll pick up your brain automatically.",
24
+ cursor: "Cursor reads .cursorrules at startup — it'll pick up your brain automatically.",
25
+ windsurf: "Windsurf reads .windsurfrules at startup — it'll pick up your brain automatically.",
26
+ cline: "Cline reads .clinerules at startup — it'll pick up your brain automatically.",
27
+ copilot: "GitHub Copilot reads .github/copilot-instructions.md — it'll pick up your brain.",
28
+ codex: "Codex (and AGENTS.md-aware tools) read AGENTS.md — it'll pick up your brain.",
29
+ qwen: "Qwen / any model: paste the prompt below to load your brain into the new chat.",
30
+ chatgpt: "ChatGPT / any model: paste the prompt below to load your brain into the new chat.",
31
+ gemini: "Gemini / any model: paste the prompt below to load your brain into the new chat.",
32
+ };
33
+
34
+ export function runSwitch(o) {
35
+ const cwd = o.cwd;
36
+ const target = (o.target || "").toLowerCase();
37
+
38
+ console.log(c.bold("\n Switching without loss — your brain lives in the repo, not the tool.\n"));
39
+
40
+ // 1. Save your place + refresh the brain.
41
+ console.log(c.cyan(" 1. Saving your place"));
42
+ const ho = runHandoff({ cwd, quiet: true });
43
+ if (ho !== 0) {
44
+ console.log(c.yellow(" (Couldn't refresh the handoff — see the note above; continuing.)"));
45
+ }
46
+
47
+ // 2. Wire every AI-tool entry file so whatever you switch to auto-loads it.
48
+ console.log(c.cyan("\n 2. Wiring your tools"));
49
+ runInit({ cwd });
50
+
51
+ // 3. The exact prompt to start the new session/tool/model.
52
+ console.log(c.cyan("\n 3. Start the new session"));
53
+ if (target && TOOL_TIPS[target]) {
54
+ console.log(` ${c.gray(TOOL_TIPS[target])}`);
55
+ }
56
+ console.log(" Open your new tool or model and paste this:");
57
+ console.log(c.bold("\n Read PROJECT-BRIEF.md and HANDOFF.md, then continue where we left off.\n"));
58
+ console.log(c.gray(" Same project, new model — no re-explaining. Need help choosing one? `ship-safe models`."));
59
+ return 0;
60
+ }
package/util.mjs CHANGED
@@ -1,142 +1,142 @@
1
- // Ship-Safe — shared helpers (ANSI color, git wrappers, formatting).
2
- // Node built-ins only. No npm deps.
3
-
4
- import { execFileSync } from "node:child_process";
5
- import { readdirSync, readFileSync, statSync } from "node:fs";
6
- import path from "node:path";
7
-
8
- // --- ANSI color, degrading gracefully ---------------------------------------
9
- // Honour NO_COLOR (https://no-color.org/) and a non-TTY stdout (piped/redirected),
10
- // so the output stays clean when captured to a file.
11
- const COLOR_ON =
12
- !process.env.NO_COLOR &&
13
- process.env.TERM !== "dumb" &&
14
- (process.stdout.isTTY ?? false);
15
-
16
- function wrap(code) {
17
- return (s) => (COLOR_ON ? `[${code}m${s}` : String(s));
18
- }
19
-
20
- export const c = {
21
- green: wrap("32"),
22
- yellow: wrap("33"),
23
- red: wrap("31"),
24
- cyan: wrap("36"),
25
- gray: wrap("90"),
26
- bold: wrap("1"),
27
- dim: wrap("2"),
28
- };
29
-
30
- // Status glyphs. ✓ pass · ⚠ warn · ✗ fail (block).
31
- export const GLYPH = {
32
- pass: c.green("✓"),
33
- warn: c.yellow("⚠"),
34
- fail: c.red("✗"),
35
- };
36
-
37
- /** A single check result. status ∈ pass|warn|fail. fail ⇒ NO-GO. */
38
- export function result(status, label, detail, extra = []) {
39
- return { status, label, detail, extra };
40
- }
41
-
42
- /** Print one check line + any indented extra lines. */
43
- export function printResult(r) {
44
- console.log(` ${GLYPH[r.status]} ${c.bold(r.label)} — ${r.detail}`);
45
- for (const line of r.extra) console.log(` ${c.gray(line)}`);
46
- }
47
-
48
- // --- git helpers (synchronous; the CLI is short-lived) ----------------------
49
-
50
- /** Run a git command, returning trimmed stdout. Throws on non-zero exit.
51
- * NOTE: trims — do NOT use for `status --porcelain`, whose leading status
52
- * columns are space-significant (use gitRaw for that). */
53
- export function git(args, opts = {}) {
54
- return gitRaw(args, opts).trim();
55
- }
56
-
57
- /** Like git() but returns stdout UNtrimmed — required for porcelain parsing
58
- * where a leading space in the XY status field is meaningful. */
59
- export function gitRaw(args, opts = {}) {
60
- return execFileSync("git", args, {
61
- encoding: "utf8",
62
- cwd: opts.cwd ?? process.cwd(),
63
- // git can emit a lot on a large diff; give it room.
64
- maxBuffer: 64 * 1024 * 1024,
65
- });
66
- }
67
-
68
- /** Run git but never throw — returns "" on any failure (best-effort probes). */
69
- export function gitSafe(args, opts = {}) {
70
- try {
71
- return git(args, opts);
72
- } catch {
73
- return "";
74
- }
75
- }
76
-
77
- /** Repo root (absolute). */
78
- export function repoRoot(cwd = process.cwd()) {
79
- return git(["rev-parse", "--show-toplevel"], { cwd });
80
- }
81
-
82
- /** Mask a matched secret to a recognisable fingerprint — NEVER echo the full
83
- * value. Mirrors app/lib/safety.ts `fingerprint()`. */
84
- export function fingerprint(match) {
85
- const head = match.slice(0, 6);
86
- const tail = match.length > 14 ? match.slice(-4) : "";
87
- return `${head}…${tail} (${match.length} chars)`;
88
- }
89
-
90
- /** Section header. */
91
- export function section(title) {
92
- console.log("\n" + c.bold(c.cyan(title)));
93
- }
94
-
95
- // --- filesystem walk (read-only) --------------------------------------------
96
-
97
- /** Directories we never descend into when walking the tree for source scans. */
98
- const WALK_SKIP_DIR = new Set([
99
- ".git", "node_modules", ".next", ".vercel", ".data", "dist", "build", "coverage", ".turbo",
100
- ]);
101
-
102
- /**
103
- * Recursively collect file paths under `dir`, skipping vendored/generated dirs.
104
- * Returns ABSOLUTE paths. Never throws on an unreadable entry (best-effort).
105
- * @param {string} dir absolute directory to walk
106
- * @param {(rel: string) => boolean} [keep] optional filter on the path's basename+ext
107
- */
108
- export function walkFiles(dir, keep) {
109
- const out = [];
110
- let entries;
111
- try {
112
- entries = readdirSync(dir, { withFileTypes: true });
113
- } catch {
114
- return out;
115
- }
116
- for (const ent of entries) {
117
- const abs = path.join(dir, ent.name);
118
- if (ent.isDirectory()) {
119
- if (WALK_SKIP_DIR.has(ent.name)) continue;
120
- out.push(...walkFiles(abs, keep));
121
- } else if (ent.isFile()) {
122
- if (!keep || keep(abs)) out.push(abs);
123
- }
124
- }
125
- return out;
126
- }
127
-
128
- /** Read a UTF-8 text file, returning "" on any error (best-effort probe). */
129
- export function readText(abs) {
130
- try {
131
- const st = statSync(abs);
132
- if (!st.isFile() || st.size > 4_000_000) return "";
133
- return readFileSync(abs, "utf8");
134
- } catch {
135
- return "";
136
- }
137
- }
138
-
139
- /** Forward-slash a path and make it relative to the repo root for display. */
140
- export function relPath(abs, cwd) {
141
- return path.relative(cwd, abs).split(path.sep).join("/");
142
- }
1
+ // Ship-Safe — shared helpers (ANSI color, git wrappers, formatting).
2
+ // Node built-ins only. No npm deps.
3
+
4
+ import { execFileSync } from "node:child_process";
5
+ import { readdirSync, readFileSync, statSync } from "node:fs";
6
+ import path from "node:path";
7
+
8
+ // --- ANSI color, degrading gracefully ---------------------------------------
9
+ // Honour NO_COLOR (https://no-color.org/) and a non-TTY stdout (piped/redirected),
10
+ // so the output stays clean when captured to a file.
11
+ const COLOR_ON =
12
+ !process.env.NO_COLOR &&
13
+ process.env.TERM !== "dumb" &&
14
+ (process.stdout.isTTY ?? false);
15
+
16
+ function wrap(code) {
17
+ return (s) => (COLOR_ON ? `[${code}m${s}` : String(s));
18
+ }
19
+
20
+ export const c = {
21
+ green: wrap("32"),
22
+ yellow: wrap("33"),
23
+ red: wrap("31"),
24
+ cyan: wrap("36"),
25
+ gray: wrap("90"),
26
+ bold: wrap("1"),
27
+ dim: wrap("2"),
28
+ };
29
+
30
+ // Status glyphs. ✓ pass · ⚠ warn · ✗ fail (block).
31
+ export const GLYPH = {
32
+ pass: c.green("✓"),
33
+ warn: c.yellow("⚠"),
34
+ fail: c.red("✗"),
35
+ };
36
+
37
+ /** A single check result. status ∈ pass|warn|fail. fail ⇒ NO-GO. */
38
+ export function result(status, label, detail, extra = []) {
39
+ return { status, label, detail, extra };
40
+ }
41
+
42
+ /** Print one check line + any indented extra lines. */
43
+ export function printResult(r) {
44
+ console.log(` ${GLYPH[r.status]} ${c.bold(r.label)} — ${r.detail}`);
45
+ for (const line of r.extra) console.log(` ${c.gray(line)}`);
46
+ }
47
+
48
+ // --- git helpers (synchronous; the CLI is short-lived) ----------------------
49
+
50
+ /** Run a git command, returning trimmed stdout. Throws on non-zero exit.
51
+ * NOTE: trims — do NOT use for `status --porcelain`, whose leading status
52
+ * columns are space-significant (use gitRaw for that). */
53
+ export function git(args, opts = {}) {
54
+ return gitRaw(args, opts).trim();
55
+ }
56
+
57
+ /** Like git() but returns stdout UNtrimmed — required for porcelain parsing
58
+ * where a leading space in the XY status field is meaningful. */
59
+ export function gitRaw(args, opts = {}) {
60
+ return execFileSync("git", args, {
61
+ encoding: "utf8",
62
+ cwd: opts.cwd ?? process.cwd(),
63
+ // git can emit a lot on a large diff; give it room.
64
+ maxBuffer: 64 * 1024 * 1024,
65
+ });
66
+ }
67
+
68
+ /** Run git but never throw — returns "" on any failure (best-effort probes). */
69
+ export function gitSafe(args, opts = {}) {
70
+ try {
71
+ return git(args, opts);
72
+ } catch {
73
+ return "";
74
+ }
75
+ }
76
+
77
+ /** Repo root (absolute). */
78
+ export function repoRoot(cwd = process.cwd()) {
79
+ return git(["rev-parse", "--show-toplevel"], { cwd });
80
+ }
81
+
82
+ /** Mask a matched secret to a recognisable fingerprint — NEVER echo the full
83
+ * value. Mirrors app/lib/safety.ts `fingerprint()`. */
84
+ export function fingerprint(match) {
85
+ const head = match.slice(0, 6);
86
+ const tail = match.length > 14 ? match.slice(-4) : "";
87
+ return `${head}…${tail} (${match.length} chars)`;
88
+ }
89
+
90
+ /** Section header. */
91
+ export function section(title) {
92
+ console.log("\n" + c.bold(c.cyan(title)));
93
+ }
94
+
95
+ // --- filesystem walk (read-only) --------------------------------------------
96
+
97
+ /** Directories we never descend into when walking the tree for source scans. */
98
+ const WALK_SKIP_DIR = new Set([
99
+ ".git", "node_modules", ".next", ".vercel", ".data", "dist", "build", "coverage", ".turbo",
100
+ ]);
101
+
102
+ /**
103
+ * Recursively collect file paths under `dir`, skipping vendored/generated dirs.
104
+ * Returns ABSOLUTE paths. Never throws on an unreadable entry (best-effort).
105
+ * @param {string} dir absolute directory to walk
106
+ * @param {(rel: string) => boolean} [keep] optional filter on the path's basename+ext
107
+ */
108
+ export function walkFiles(dir, keep) {
109
+ const out = [];
110
+ let entries;
111
+ try {
112
+ entries = readdirSync(dir, { withFileTypes: true });
113
+ } catch {
114
+ return out;
115
+ }
116
+ for (const ent of entries) {
117
+ const abs = path.join(dir, ent.name);
118
+ if (ent.isDirectory()) {
119
+ if (WALK_SKIP_DIR.has(ent.name)) continue;
120
+ out.push(...walkFiles(abs, keep));
121
+ } else if (ent.isFile()) {
122
+ if (!keep || keep(abs)) out.push(abs);
123
+ }
124
+ }
125
+ return out;
126
+ }
127
+
128
+ /** Read a UTF-8 text file, returning "" on any error (best-effort probe). */
129
+ export function readText(abs) {
130
+ try {
131
+ const st = statSync(abs);
132
+ if (!st.isFile() || st.size > 4_000_000) return "";
133
+ return readFileSync(abs, "utf8");
134
+ } catch {
135
+ return "";
136
+ }
137
+ }
138
+
139
+ /** Forward-slash a path and make it relative to the repo root for display. */
140
+ export function relPath(abs, cwd) {
141
+ return path.relative(cwd, abs).split(path.sep).join("/");
142
+ }