@openscaffold/cli 0.0.0-stage → 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/README.md +19 -2
- package/dist/index.js +225 -0
- package/dist/install.js +85 -0
- package/package.json +40 -4
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# openscaffold
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Connects Claude Code to your OpenScaffold workspace. After one login, every Claude Code session you
|
|
4
|
+
start, in any repo, loads the right customer deployment's briefing, and the OpenScaffold tools
|
|
5
|
+
(search the customer's calls, tickets and emails; read the cited plan; propose plan changes).
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx @openscaffold/cli login # opens your browser: click Approve. That's it.
|
|
9
|
+
npx @openscaffold/cli status # what's set up and the next step (--json for coding agents)
|
|
10
|
+
npx @openscaffold/cli logout # removes everything it installed and revokes the login
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`login` writes only to your user config (never into a repo):
|
|
14
|
+
|
|
15
|
+
- `~/.config/openscaffold/credentials.json` (your login, mode 0600)
|
|
16
|
+
- the `openscaffold` MCP server in Claude Code (user scope)
|
|
17
|
+
- a SessionStart hook in `~/.claude/settings.json` that prints the briefing
|
|
18
|
+
- `/os-requirements`, `/os-what-changed`, `/os-check-plan`, `/os-demo-check` in `~/.claude/skills/`
|
|
19
|
+
|
|
20
|
+
Requires Node 22+ and Claude Code. Set `OPENSCAFFOLD_URL` to point at a self-hosted OpenScaffold.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `npx @openscaffold/cli login | status | session-start | logout` (SPEC §8.4). Node 22+, standard library only.
|
|
3
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
4
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir, hostname } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { gitInfo, hasSessionHook, hookCommand, MCP_NAME, skillFile, statusReport, withoutSessionHook, withSessionHook } from "./install.js";
|
|
8
|
+
const CONFIG = join(homedir(), ".config", "openscaffold");
|
|
9
|
+
const CREDENTIALS = join(CONFIG, "credentials.json");
|
|
10
|
+
const CACHE = join(CONFIG, "cache");
|
|
11
|
+
const CLAUDE = join(homedir(), ".claude");
|
|
12
|
+
const SETTINGS = join(CLAUDE, "settings.json");
|
|
13
|
+
const SKILLS = join(CLAUDE, "skills");
|
|
14
|
+
// OPENSCAFFOLD_URL overrides (local dev: http://localhost:3000).
|
|
15
|
+
const DEFAULT_URL = "https://app.openscaffold.ai";
|
|
16
|
+
const readJson = (path, fallback) => {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const writeJson = (path, value, mode) => {
|
|
25
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
26
|
+
writeFileSync(path, JSON.stringify(value, null, 2) + "\n", mode ? { mode } : undefined);
|
|
27
|
+
if (mode)
|
|
28
|
+
chmodSync(path, mode);
|
|
29
|
+
};
|
|
30
|
+
const credentials = () => readJson(CREDENTIALS, null);
|
|
31
|
+
async function api(url, path, body, { token, timeoutMs = 15_000 } = {}) {
|
|
32
|
+
const res = await fetch(new URL(path, url), {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
35
|
+
body: JSON.stringify(body),
|
|
36
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
37
|
+
});
|
|
38
|
+
return { status: res.status, body: (await res.json().catch(() => ({}))) };
|
|
39
|
+
}
|
|
40
|
+
function openBrowser(url) {
|
|
41
|
+
if (process.env.OPENSCAFFOLD_NO_BROWSER)
|
|
42
|
+
return; // tests and headless machines: the URL is printed
|
|
43
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
44
|
+
try {
|
|
45
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
}
|
|
49
|
+
/** Runs `claude …`; returns false if Claude Code isn't on PATH or the command failed. */
|
|
50
|
+
function claude(args) {
|
|
51
|
+
try {
|
|
52
|
+
execFileSync("claude", args, { stdio: "ignore", timeout: 30_000 });
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// --- login --------------------------------------------------------------------------------------
|
|
60
|
+
async function login() {
|
|
61
|
+
const url = (process.env.OPENSCAFFOLD_URL ?? credentials()?.api_url ?? DEFAULT_URL).replace(/\/$/, "");
|
|
62
|
+
const start = await api(url, "/api/connect/start", { client_label: hostname() });
|
|
63
|
+
if (start.status !== 200)
|
|
64
|
+
throw new Error(`Couldn't reach OpenScaffold at ${url} (${start.status}).`);
|
|
65
|
+
const { code, login_url, poll } = start.body;
|
|
66
|
+
console.log(`Opening ${login_url}\nClick Approve in your browser. The page should show the code ${code}.`);
|
|
67
|
+
openBrowser(login_url);
|
|
68
|
+
let creds = null;
|
|
69
|
+
for (const deadline = Date.now() + 10 * 60_000; Date.now() < deadline;) {
|
|
70
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
71
|
+
const r = await api(url, "/api/connect/exchange", { poll }).catch(() => null);
|
|
72
|
+
if (r?.status === 200) {
|
|
73
|
+
creds = { api_url: String(r.body.api_url ?? url), token: String(r.body.token), workspace: r.body.workspace };
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
if (r && r.status !== 202)
|
|
77
|
+
throw new Error(String(r.body.error ?? "Login failed. Run `npx @openscaffold/cli login` again."));
|
|
78
|
+
}
|
|
79
|
+
if (!creds)
|
|
80
|
+
throw new Error("Timed out waiting for approval. Run `npx @openscaffold/cli login` again.");
|
|
81
|
+
writeJson(CREDENTIALS, creds, 0o600);
|
|
82
|
+
const written = [`${CREDENTIALS} (your login, readable only by you)`];
|
|
83
|
+
// 1. MCP server at user scope (replace any earlier login's).
|
|
84
|
+
const mcpUrl = `${creds.api_url}/api/mcp/mcp`;
|
|
85
|
+
const addArgs = ["mcp", "add", "--transport", "http", "--scope", "user", MCP_NAME, mcpUrl, "--header", `Authorization: Bearer ${creds.token}`];
|
|
86
|
+
claude(["mcp", "remove", "--scope", "user", MCP_NAME]);
|
|
87
|
+
if (claude(addArgs))
|
|
88
|
+
written.push(`Claude Code MCP server "${MCP_NAME}" (user scope)`);
|
|
89
|
+
else
|
|
90
|
+
console.log(`\nClaude Code wasn't found on PATH. Once it is, run:\n claude ${addArgs.map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(" ")}\n`);
|
|
91
|
+
// 2. SessionStart hook: loads the right deployment's briefing in every session.
|
|
92
|
+
writeJson(SETTINGS, withSessionHook(readJson(SETTINGS, {}), hookCommand(process.argv[1], process.execPath)));
|
|
93
|
+
written.push(`${SETTINGS} (SessionStart hook)`);
|
|
94
|
+
// 3. Commands as personal skills: /os-requirements, /os-what-changed, /os-check-plan, /os-demo-check.
|
|
95
|
+
const commands = await fetch(`${creds.api_url}/api/commands`).then((r) => r.json()).catch(() => []);
|
|
96
|
+
for (const c of commands) {
|
|
97
|
+
const dir = join(SKILLS, `os-${c.name}`);
|
|
98
|
+
mkdirSync(dir, { recursive: true });
|
|
99
|
+
writeFileSync(join(dir, "SKILL.md"), skillFile(c));
|
|
100
|
+
}
|
|
101
|
+
if (commands.length)
|
|
102
|
+
written.push(`${SKILLS}/os-* (${commands.map((c) => `/os-${c.name}`).join(", ")})`);
|
|
103
|
+
const self = hookCommand(process.argv[1], process.execPath).replace(/ session-start$/, "");
|
|
104
|
+
console.log(`\nLogged in to ${creds.workspace.name}.\nWrote:\n${written.map((w) => ` - ${w}`).join("\n")}\nNothing was written into any repo.\n\nStart Claude Code in any repo; it loads the right customer automatically.\nTo undo everything: ${self} logout`);
|
|
105
|
+
}
|
|
106
|
+
// --- session-start (the SessionStart hook) ------------------------------------------------------
|
|
107
|
+
const readStdin = async () => {
|
|
108
|
+
if (process.stdin.isTTY)
|
|
109
|
+
return "";
|
|
110
|
+
let s = "";
|
|
111
|
+
for await (const chunk of process.stdin)
|
|
112
|
+
s += chunk;
|
|
113
|
+
return s;
|
|
114
|
+
};
|
|
115
|
+
const git = (cwd, args) => {
|
|
116
|
+
try {
|
|
117
|
+
return execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"] });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
/** Prints the briefing. Never breaks a session: on any problem, a cached briefing or one line, exit 0. */
|
|
124
|
+
async function sessionStart() {
|
|
125
|
+
let hook = {};
|
|
126
|
+
try {
|
|
127
|
+
hook = JSON.parse((await readStdin()) || "{}");
|
|
128
|
+
}
|
|
129
|
+
catch { }
|
|
130
|
+
const cwd = hook.cwd ?? process.cwd();
|
|
131
|
+
const creds = credentials();
|
|
132
|
+
if (!creds)
|
|
133
|
+
return console.log("OpenScaffold: not logged in (run `npx @openscaffold/cli login`).");
|
|
134
|
+
const index = readJson(join(CACHE, "index.json"), {});
|
|
135
|
+
try {
|
|
136
|
+
const r = await api(creds.api_url, "/api/sessions/start", { claude_session_id: hook.session_id ?? `manual-${Date.now()}`, cwd, ...gitInfo(git(cwd, ["remote", "get-url", "origin"]), git(cwd, ["branch", "--show-current"])) }, { token: creds.token, timeoutMs: 3000 });
|
|
137
|
+
if (r.status === 401)
|
|
138
|
+
return console.log("OpenScaffold: login expired or revoked (run `npx @openscaffold/cli login`).");
|
|
139
|
+
if (r.status !== 200)
|
|
140
|
+
throw new Error(String(r.status));
|
|
141
|
+
const briefing = String(r.body.briefing);
|
|
142
|
+
console.log(briefing);
|
|
143
|
+
const dep = r.body.deployment?.id;
|
|
144
|
+
if (dep) {
|
|
145
|
+
mkdirSync(CACHE, { recursive: true });
|
|
146
|
+
writeFileSync(join(CACHE, `${dep}.md`), briefing);
|
|
147
|
+
writeJson(join(CACHE, "index.json"), { ...index, [cwd]: { deployment: dep, at: new Date().toISOString() } });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
const last = index[cwd];
|
|
152
|
+
const cached = last && existsSync(join(CACHE, `${last.deployment}.md`)) ? readFileSync(join(CACHE, `${last.deployment}.md`), "utf8") : null;
|
|
153
|
+
console.log(cached ? `${cached}\n\n(offline, from ${last.at})` : "OpenScaffold: offline");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// --- status -------------------------------------------------------------------------------------
|
|
157
|
+
/** Read-only: what's set up on this machine and the next step. `--json` for agents. Always exits 0. */
|
|
158
|
+
async function status() {
|
|
159
|
+
const creds = credentials();
|
|
160
|
+
let mcp = null;
|
|
161
|
+
try {
|
|
162
|
+
execFileSync("claude", ["mcp", "get", MCP_NAME], { stdio: "ignore", timeout: 15_000 });
|
|
163
|
+
mcp = true;
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
mcp = e.code === "ENOENT" ? null : false;
|
|
167
|
+
}
|
|
168
|
+
// An authenticated request, so a revoked token isn't reported as ready: the MCP endpoint answers 401 to a bad token.
|
|
169
|
+
const server = creds
|
|
170
|
+
? await fetch(new URL("/api/mcp/mcp", creds.api_url), {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { authorization: `Bearer ${creds.token}`, "content-type": "application/json", accept: "application/json, text/event-stream" },
|
|
173
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
|
|
174
|
+
signal: AbortSignal.timeout(4000),
|
|
175
|
+
}).then((r) => (r.status === 401 ? "revoked" : r.status >= 500 ? "down" : "ok"), () => "down")
|
|
176
|
+
: undefined;
|
|
177
|
+
// The commands login installs: the server's list when it answers, else whatever os-* skills are on disk.
|
|
178
|
+
const expected = creds && server === "ok"
|
|
179
|
+
? await fetch(new URL("/api/commands", creds.api_url), { signal: AbortSignal.timeout(4000) }).then((r) => r.json()).then((cs) => cs.map((c) => `os-${c.name}`), () => null)
|
|
180
|
+
: null;
|
|
181
|
+
const onDisk = existsSync(SKILLS) ? readdirSync(SKILLS).filter((d) => d.startsWith("os-") && existsSync(join(SKILLS, d, "SKILL.md"))) : [];
|
|
182
|
+
const names = expected ?? onDisk;
|
|
183
|
+
const state = {
|
|
184
|
+
workspace: creds?.workspace.name ?? null,
|
|
185
|
+
apiUrl: creds?.api_url ?? null,
|
|
186
|
+
server,
|
|
187
|
+
mcp,
|
|
188
|
+
hook: hasSessionHook(readJson(SETTINGS, {})),
|
|
189
|
+
skills: names.filter((n) => onDisk.includes(n)).length,
|
|
190
|
+
skillsExpected: names.length,
|
|
191
|
+
};
|
|
192
|
+
const report = statusReport(state);
|
|
193
|
+
console.log(process.argv.includes("--json") ? JSON.stringify({ ...state, ready: report.ready, next: report.next }, null, 2) : report.text);
|
|
194
|
+
}
|
|
195
|
+
// --- logout -------------------------------------------------------------------------------------
|
|
196
|
+
async function logout() {
|
|
197
|
+
const creds = credentials();
|
|
198
|
+
if (creds)
|
|
199
|
+
await api(creds.api_url, "/api/connect/revoke", {}, { token: creds.token, timeoutMs: 5000 }).catch(() => null);
|
|
200
|
+
claude(["mcp", "remove", "--scope", "user", MCP_NAME]);
|
|
201
|
+
if (existsSync(SETTINGS))
|
|
202
|
+
writeJson(SETTINGS, withoutSessionHook(readJson(SETTINGS, {})));
|
|
203
|
+
if (existsSync(SKILLS))
|
|
204
|
+
for (const d of readdirSync(SKILLS))
|
|
205
|
+
if (d.startsWith("os-"))
|
|
206
|
+
rmSync(join(SKILLS, d), { recursive: true, force: true });
|
|
207
|
+
rmSync(CONFIG, { recursive: true, force: true });
|
|
208
|
+
console.log("Logged out: token revoked, MCP server, hook, /os-* commands and local credentials removed.");
|
|
209
|
+
}
|
|
210
|
+
// --- main ---------------------------------------------------------------------------------------
|
|
211
|
+
const [cmd] = process.argv.slice(2);
|
|
212
|
+
const commands = { login, status, "session-start": sessionStart, logout };
|
|
213
|
+
const run = commands[cmd ?? ""];
|
|
214
|
+
if (!run) {
|
|
215
|
+
console.error("Usage: npx @openscaffold/cli <login | status | logout>\n(session-start is run by Claude Code's SessionStart hook)");
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
run().then(() => process.exit(0), (e) => {
|
|
219
|
+
if (cmd === "session-start") {
|
|
220
|
+
console.log("OpenScaffold: offline"); // never break a session
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
console.error(e instanceof Error ? e.message : e);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
});
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Pure helpers for what `openscaffold login`/`logout` write into Claude Code (SPEC §8.4).
|
|
2
|
+
// Formats checked against code.claude.com docs (hooks, mcp, skills), 2026-10-23.
|
|
3
|
+
export const MCP_NAME = "openscaffold";
|
|
4
|
+
/** Our hook command mentions openscaffold and ends in `session-start` (npx form or a local build path). */
|
|
5
|
+
const isOurCommand = (cmd) => /openscaffold/.test(cmd) && /\bsession-start"?$/.test(cmd.trim());
|
|
6
|
+
const HOOK_MATCHER = "startup|resume|clear|compact";
|
|
7
|
+
const isOurs = (h) => typeof h.command === "string" && isOurCommand(h.command);
|
|
8
|
+
/** Adds (or replaces) our SessionStart hook; leaves everything else in settings.json as it was. */
|
|
9
|
+
export function withSessionHook(settings, command) {
|
|
10
|
+
const out = structuredClone(settings);
|
|
11
|
+
const groups = (out.hooks?.SessionStart ?? [])
|
|
12
|
+
.map((g) => ({ ...g, hooks: g.hooks.filter((h) => !isOurs(h)) }))
|
|
13
|
+
.filter((g) => g.hooks.length > 0);
|
|
14
|
+
groups.push({ matcher: HOOK_MATCHER, hooks: [{ type: "command", command, timeout: 10 }] });
|
|
15
|
+
out.hooks = { ...out.hooks, SessionStart: groups };
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
/** Removes our SessionStart hook (and empty leftovers); leaves other hooks alone. */
|
|
19
|
+
export function withoutSessionHook(settings) {
|
|
20
|
+
const out = structuredClone(settings);
|
|
21
|
+
if (!out.hooks?.SessionStart)
|
|
22
|
+
return out;
|
|
23
|
+
const groups = out.hooks.SessionStart.map((g) => ({ ...g, hooks: g.hooks.filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length > 0);
|
|
24
|
+
if (groups.length)
|
|
25
|
+
out.hooks.SessionStart = groups;
|
|
26
|
+
else
|
|
27
|
+
delete out.hooks.SessionStart;
|
|
28
|
+
if (!Object.keys(out.hooks).length)
|
|
29
|
+
delete out.hooks;
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The hook command. Published installs run through npx; a local checkout (development) runs its
|
|
34
|
+
* own built file, so the hook works before the package is on npm.
|
|
35
|
+
*/
|
|
36
|
+
export function hookCommand(selfPath, execPath) {
|
|
37
|
+
if (/[\\/]_npx[\\/]|[\\/]node_modules[\\/]@openscaffold[\\/]cli[\\/]/.test(selfPath))
|
|
38
|
+
return `npx -y @openscaffold/cli session-start`;
|
|
39
|
+
return `${JSON.stringify(execPath)} ${JSON.stringify(selfPath)} session-start`;
|
|
40
|
+
}
|
|
41
|
+
/** A personal skill file for one default command: /os-<name>. */
|
|
42
|
+
export function skillFile(c) {
|
|
43
|
+
return [
|
|
44
|
+
"---",
|
|
45
|
+
`name: os-${c.name}`,
|
|
46
|
+
`description: ${c.description.replace(/\n/g, " ")} (OpenScaffold)`,
|
|
47
|
+
"---",
|
|
48
|
+
"",
|
|
49
|
+
c.prompt,
|
|
50
|
+
"",
|
|
51
|
+
"Use the deployment id from the OpenScaffold briefing in context. If there is none, call the openscaffold `list_deployments` tool and ask the FDE which deployment this is for.",
|
|
52
|
+
"",
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
/** git remote/branch from `git` output; missing pieces are fine (not a repo, detached HEAD). */
|
|
56
|
+
export function gitInfo(remoteOut, branchOut) {
|
|
57
|
+
const remote = remoteOut?.trim() || undefined;
|
|
58
|
+
const branch = branchOut?.trim() || undefined;
|
|
59
|
+
return { git_remote: remote, git_branch: branch };
|
|
60
|
+
}
|
|
61
|
+
/** Whether our SessionStart hook is in settings.json. */
|
|
62
|
+
export const hasSessionHook = (settings) => (settings.hooks?.SessionStart ?? []).some((g) => g.hooks.some(isOurs));
|
|
63
|
+
const HOSTED = "https://app.openscaffold.ai";
|
|
64
|
+
const RELOGIN = (apiUrl) => `npx @openscaffold/cli login${apiUrl && apiUrl !== HOSTED ? ` (or OPENSCAFFOLD_URL=${HOSTED} npx @openscaffold/cli login to use the hosted app)` : ""}`;
|
|
65
|
+
/**
|
|
66
|
+
* `openscaffold status`: one line per piece of the setup plus the single next step, in words a person or a
|
|
67
|
+
* coding agent can act on (the agent onboarding playbook reads it). `mcp: null` = Claude Code isn't on PATH.
|
|
68
|
+
*/
|
|
69
|
+
export function statusReport(s) {
|
|
70
|
+
const commandsOk = s.skillsExpected > 0 && s.skills >= s.skillsExpected;
|
|
71
|
+
const lines = [
|
|
72
|
+
`Signed in: ${s.workspace ? `yes, workspace "${s.workspace}" (${s.apiUrl}${s.server === "down" ? ", not answering" : s.server === "revoked" ? ", login revoked" : ""})` : "no"}`,
|
|
73
|
+
`MCP server: ${s.mcp === null ? "unknown (Claude Code not on PATH)" : s.mcp ? "added" : "missing"}`,
|
|
74
|
+
`Session hook: ${s.hook ? "installed" : "missing"}`,
|
|
75
|
+
`Commands: ${commandsOk ? `${s.skills} /os-* commands` : s.skills ? `${s.skills} of ${s.skillsExpected || "?"} /os-* commands` : "missing"}`,
|
|
76
|
+
];
|
|
77
|
+
const next = !s.workspace || s.server === "revoked" || s.mcp === false || !s.hook || !commandsOk
|
|
78
|
+
? `Run: ${RELOGIN(s.apiUrl)} (one browser click to approve).`
|
|
79
|
+
: s.server !== "ok"
|
|
80
|
+
? `${s.apiUrl} isn't answering. If it's a local dev server, start it; otherwise run: ${RELOGIN(s.apiUrl)}.`
|
|
81
|
+
: s.mcp === null
|
|
82
|
+
? "Claude Code isn't on PATH, so the MCP server can't be checked or added. Put `claude` on PATH, then run: npx @openscaffold/cli login."
|
|
83
|
+
: "Ready. Start a new Claude Code session (or restart this one) to load OpenScaffold.";
|
|
84
|
+
return { ready: next.startsWith("Ready"), next, text: `OpenScaffold setup\n${lines.map((l) => ` ${l}`).join("\n")}\n\nNext: ${next}` };
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openscaffold/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Connect Claude Code to your OpenScaffold workspace: each session loads the right customer deployment.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openscaffold": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"prepublishOnly": "tsc"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "24.19.2",
|
|
23
|
+
"typescript": "5.9.3"
|
|
24
|
+
},
|
|
25
|
+
"packageManager": "pnpm@9.14.2",
|
|
26
|
+
"keywords": [
|
|
27
|
+
"openscaffold",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"mcp",
|
|
30
|
+
"forward-deployed-engineering"
|
|
31
|
+
],
|
|
32
|
+
"license": "UNLICENSED",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/Sigma-M-A/openscaffold.git",
|
|
36
|
+
"directory": "cli"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/Sigma-M-A/openscaffold",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|