paris-immersion 0.1.0 → 0.1.1

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.
@@ -2,3 +2,16 @@ export declare function installCommand(opts: {
2
2
  dir?: string;
3
3
  immersion?: string;
4
4
  }): Promise<void>;
5
+ /**
6
+ * Locate the student's workspace, or create one if none exists. Used by
7
+ * `paris serve` so the onboarding embedded terminal always opens a *coached*
8
+ * Claude session — the student never has to know `paris install` exists. This
9
+ * is what guarantees the "magic moment": open the terminal, Claude greets you
10
+ * with today's mission. Returns the workspace dir + whether it was just created.
11
+ */
12
+ export declare function ensureWorkspace(opts?: {
13
+ immersion?: string;
14
+ }): Promise<{
15
+ dir: string;
16
+ created: boolean;
17
+ }>;
@@ -1,4 +1,5 @@
1
1
  import { promises as fs } from 'node:fs';
2
+ import { homedir } from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { apiAuthFetch } from '../lib/api.js';
@@ -22,14 +23,50 @@ export async function installCommand(opts) {
22
23
  return;
23
24
  }
24
25
  process.stdout.write(c.dim('Buscando contexto da imersão...\n'));
25
- const slugQuery = opts.immersion ? `?immersion=${encodeURIComponent(opts.immersion)}` : '';
26
- const boot = await apiAuthFetch(`/immersion-app/v1/workspace/bootstrap${slugQuery}`, {
26
+ const boot = await bootstrapWorkspace(opts.immersion);
27
+ const targetDir = path.resolve(opts.dir ?? `paris-${boot.immersion.slug}`);
28
+ await writeWorkspaceFiles(targetDir, boot, creds.api_url, creds.email);
29
+ console.log();
30
+ console.log(c.green(`✓ Workspace pronto em ${targetDir}`));
31
+ console.log();
32
+ console.log(c.bold('Próximo passo:'));
33
+ console.log(` cd ${path.relative(process.cwd(), targetDir) || '.'}`);
34
+ console.log(' claude');
35
+ console.log();
36
+ console.log(c.dim(`Imersão: ${boot.immersion.name} · fase ${boot.phase} · dia ${boot.day_number}`));
37
+ }
38
+ /**
39
+ * Locate the student's workspace, or create one if none exists. Used by
40
+ * `paris serve` so the onboarding embedded terminal always opens a *coached*
41
+ * Claude session — the student never has to know `paris install` exists. This
42
+ * is what guarantees the "magic moment": open the terminal, Claude greets you
43
+ * with today's mission. Returns the workspace dir + whether it was just created.
44
+ */
45
+ export async function ensureWorkspace(opts = {}) {
46
+ const existing = await findMarker();
47
+ if (existing)
48
+ return { dir: existing.dir, created: false };
49
+ const creds = await readCredentials();
50
+ if (!creds) {
51
+ throw new Error('Você precisa rodar `paris login` antes.');
52
+ }
53
+ const boot = await bootstrapWorkspace(opts.immersion);
54
+ // Predictable, findable location regardless of where `paris serve` was run.
55
+ const targetDir = path.join(homedir(), `paris-${boot.immersion.slug}`);
56
+ await writeWorkspaceFiles(targetDir, boot, creds.api_url, creds.email);
57
+ return { dir: targetDir, created: true };
58
+ }
59
+ async function bootstrapWorkspace(immersion) {
60
+ const slugQuery = immersion ? `?immersion=${encodeURIComponent(immersion)}` : '';
61
+ return apiAuthFetch(`/immersion-app/v1/workspace/bootstrap${slugQuery}`, {
27
62
  method: 'POST',
28
63
  });
29
- const targetDir = path.resolve(opts.dir ?? `paris-${boot.immersion.slug}`);
64
+ }
65
+ /** Write the full workspace layout (marker, hooks, vendored MCP, settings, skill, CLAUDE.md). */
66
+ async function writeWorkspaceFiles(targetDir, boot, apiUrl, email) {
30
67
  await fs.mkdir(targetDir, { recursive: true });
31
68
  // .paris/workspace.json — the marker.
32
- await writeMarker(targetDir, { ...boot.marker, api_url: creds.api_url });
69
+ await writeMarker(targetDir, { ...boot.marker, api_url: apiUrl });
33
70
  // .paris/hooks/paris-hook.mjs — installed Claude Code hook.
34
71
  const hooksDir = path.join(targetDir, '.paris', 'hooks');
35
72
  await fs.mkdir(hooksDir, { recursive: true });
@@ -40,13 +77,12 @@ export async function installCommand(opts) {
40
77
  const mcpDir = path.join(targetDir, '.paris', 'mcp');
41
78
  await fs.mkdir(mcpDir, { recursive: true });
42
79
  await fs.writeFile(path.join(mcpDir, 'index.mjs'), await readVendoredMcpBundle(), { mode: 0o755 });
43
- // .claude/settings.json — wires hooks + MCP server.
80
+ // .claude/settings.json — wires hooks + MCP server. Both settings.json (for
81
+ // interactive `claude`) and settings.local.json (the source `claude -p`
82
+ // reliably loads hooks from).
44
83
  const claudeDir = path.join(targetDir, '.claude');
45
84
  await fs.mkdir(claudeDir, { recursive: true });
46
- // Write both settings.json (so the regular interactive `claude` picks it up)
47
- // and settings.local.json (so `claude -p` does too — in -p mode, local
48
- // settings are the reliably-loaded source for hooks).
49
- const settingsPayload = renderSettingsJson({ apiUrl: creds.api_url });
85
+ const settingsPayload = renderSettingsJson({ apiUrl });
50
86
  await fs.writeFile(path.join(claudeDir, 'settings.json'), JSON.stringify(settingsPayload, null, 2));
51
87
  await fs.writeFile(path.join(claudeDir, 'settings.local.json'), JSON.stringify(settingsPayload, null, 2));
52
88
  // .claude/skills/paris-coach/SKILL.md
@@ -57,16 +93,8 @@ export async function installCommand(opts) {
57
93
  await fs.writeFile(path.join(targetDir, 'CLAUDE.md'), renderClaudeMd({
58
94
  immersionName: boot.immersion.name,
59
95
  immersionSlug: boot.immersion.slug,
60
- participantEmail: creds.email,
96
+ participantEmail: email,
61
97
  }));
62
- console.log();
63
- console.log(c.green(`✓ Workspace pronto em ${targetDir}`));
64
- console.log();
65
- console.log(c.bold('Próximo passo:'));
66
- console.log(` cd ${path.relative(process.cwd(), targetDir) || '.'}`);
67
- console.log(' claude');
68
- console.log();
69
- console.log(c.dim(`Imersão: ${boot.immersion.name} · fase ${boot.phase} · dia ${boot.day_number}`));
70
98
  }
71
99
  /**
72
100
  * Reads the bundled MCP server shipped with this package. Lives at
@@ -2,6 +2,9 @@ export interface ServeOptions {
2
2
  port: number;
3
3
  allowedOrigins: string[];
4
4
  token: string;
5
+ /** Working directory the embedded `claude` runs in. Defaults to process.cwd().
6
+ * serveCommand sets this to the provisioned workspace so the session is coached. */
7
+ cwd?: string;
5
8
  /** When true, accept connections without spawning a pty — used by tests + dry-run handshake checks. */
6
9
  dryRun?: boolean;
7
10
  }
@@ -1,6 +1,7 @@
1
1
  import { WebSocketServer } from 'ws';
2
2
  import { createRequire } from 'node:module';
3
3
  import { readCredentials } from '../lib/credentials.js';
4
+ import { ensureWorkspace } from './install.js';
4
5
  import { c } from '../lib/colors.js';
5
6
  const DEFAULT_ALLOWED_ORIGINS = [
6
7
  'https://app.paris-group.com',
@@ -62,7 +63,7 @@ export async function startServeServer(opts) {
62
63
  name: 'xterm-color',
63
64
  cols: 80,
64
65
  rows: 24,
65
- cwd: process.cwd(),
66
+ cwd: opts.cwd ?? process.cwd(),
66
67
  env: {
67
68
  ...process.env,
68
69
  TOSTUDY_MCP_ENABLED: '1',
@@ -156,9 +157,25 @@ export async function serveCommand(opts) {
156
157
  process.exit(1);
157
158
  }
158
159
  const allowedOrigins = (process.env.PARIS_SERVE_ORIGINS?.split(',').map((s) => s.trim()).filter(Boolean)) ?? DEFAULT_ALLOWED_ORIGINS;
160
+ // Guarantee the embedded `claude` opens inside a provisioned (coached)
161
+ // workspace. If the student already ran `paris install`, we reuse it; else we
162
+ // bootstrap one now so the onboarding terminal is never vanilla Claude. A
163
+ // failure here is non-fatal — we fall back to cwd so the terminal still works.
164
+ let cwd = process.cwd();
165
+ try {
166
+ const { dir, created } = await ensureWorkspace({});
167
+ cwd = dir;
168
+ console.log(created
169
+ ? ` ${c.green('✓')} workspace criado em ${c.bold(dir)}`
170
+ : ` ${c.dim('workspace:')} ${dir}`);
171
+ }
172
+ catch (err) {
173
+ const msg = err instanceof Error ? err.message : String(err);
174
+ console.error(c.yellow(` ! sem workspace coachado (${msg}). Abrindo Claude em ${cwd}.`));
175
+ }
159
176
  let handle;
160
177
  try {
161
- handle = await startServeServer({ port: portNum, allowedOrigins, token });
178
+ handle = await startServeServer({ port: portNum, allowedOrigins, token, cwd });
162
179
  }
163
180
  catch (err) {
164
181
  const msg = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paris-immersion",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Paris Immersion CLI — login, install workspace, status, task control. Para alunos das imersões da Paris Group.",
5
5
  "homepage": "https://parisgroup.ai",
6
6
  "repository": {