@gitpagedocs/cli 1.1.48 → 1.1.51

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.
@@ -11,7 +11,7 @@ export function buildRootConfig(options = {}) {
11
11
  const useOfficialLayouts = !useLocalLayoutConfig;
12
12
  const githubOwner = options.githubOwner;
13
13
  const githubRepo = options.githubRepo;
14
- const repositorySearchHome = githubOwner && githubRepo ? false : true;
14
+ const repositorySearchHome = false;
15
15
  const renderingUrl =
16
16
  githubOwner && githubRepo
17
17
  ? `https://${githubOwner}.github.io/${githubRepo}/`
@@ -44,6 +44,7 @@ export function buildRootConfig(options = {}) {
44
44
  repositorySearchHome,
45
45
  rendering: renderingUrl,
46
46
  AiChatEnabled: true,
47
+ docsAccess: { enabled: false, publicKey: "" },
47
48
  langmenu: defaultLangMenu,
48
49
  },
49
50
  VersionControl: {
@@ -114,6 +114,15 @@ export function getDefaultSiteConfig(DOCS, projectLink) {
114
114
  IconNavMenuBlockInactiveReactIconesTagSize: "25px",
115
115
  IconNavMenuBlockInactiveImgWidth: 20,
116
116
  IconNavMenuBlockInactiveImgHeight: 20,
117
+ IconDocsLockLightImg: "",
118
+ IconDocsLockDarkImg: "",
119
+ IconDocsLockReactIcones: true,
120
+ IconDocsLockReactIconesTag: "FiLock",
121
+ IconDocsLockReactIconesTagColorDark: "White",
122
+ IconDocsLockReactIconesTagColorLight: "black",
123
+ IconDocsLockReactIconesTagSize: "22px",
124
+ IconDocsLockImgWidth: 20,
125
+ IconDocsLockImgHeight: 20,
117
126
  RouteguideBrandPositionDefault: "center",
118
127
  RouteguideBrandContainerTopDefault: false,
119
128
  audioPlayerEnabled: true,
@@ -38,6 +38,67 @@ export const defaultLangMenu = {
38
38
  audioPopoverStatusPaused: "Pausado",
39
39
  audioPopoverStatusLoopOn: "Loop ativado",
40
40
  audioPopoverStatusLoopOff: "Loop desativado",
41
+ aiChatTitle: "Assistente de IA",
42
+ aiChatPlaceholder: "Pergunte sobre a documentação...",
43
+ aiChatConfigTitle: "Configurar IA",
44
+ aiChatConfigDesc: "Insira sua chave de API para ativar o chat.",
45
+ aiChatClearDataBtn: "Limpar dados locais",
46
+ aiChatSendBtn: "Enviar",
47
+ aiChatSaveStartBtn: "Salvar e iniciar conversa",
48
+ aiChatAttachAriaLabel: "Anexar arquivo de áudio ou imagem",
49
+ aiChatAttachedFilesLabel: "arquivo(s) anexado(s)",
50
+ aiChatRemoveAttachmentBtn: "Remover",
51
+ aiChatCancelResponseLabel: "Cancelar resposta",
52
+ aiChatOpenBtnAriaLabel: "Abrir chat de IA",
53
+ aiChatCloseBtnAriaLabel: "Fechar chat",
54
+ aiChatSystemPrompt: "You are the official AI documentation assistant for the '{headerName}' project. Your primary role is to answer questions strictly related to the project's documentation, configurations (like config.json), and markdown files located in the 'gitpagedocs' directory. Be highly professional and helpful.\n\nIMPORTANT: You MUST address the user and respond EXCLUSIVELY in the following language: {language}.\n\nHere is the raw content of the active page the user is currently looking at:\n[Page ID]: {pageId}\n[Hidden Current Context Content]:\n{rawContent}",
55
+ aiChatEmptyStateGreeting: "Olá! Eu sou o assistente de inteligência artificial desta documentação. Você pode me fazer perguntas sobre o conteúdo ou buscar um termo em toda a documentação. Como posso ajudar você hoje?",
56
+ aiChatDisclaimer: "A IA pode cometer erros. Sempre verifique as informações.",
57
+ aiChatPasswordCreateDesc: "Crie uma senha local para criptografar suas chaves de API.",
58
+ aiChatPasswordUnlockDesc: "Digite sua senha local para desbloquear.",
59
+ aiChatPasswordPlaceholder: "Senha local",
60
+ aiChatCreatePasswordBtn: "Criar senha",
61
+ aiChatUnlockBtn: "Desbloquear",
62
+ aiChatWrongPassword: "Senha incorreta.",
63
+ aiChatLockBtn: "Bloquear",
64
+ aiChatResetBtn: "Esqueceu a senha?",
65
+ aiChatResetPopupTitle: "Redefinir senha?",
66
+ aiChatResetPopupDesc: "Isso apaga todas as chaves de API salvas e a senha local. Você criará uma nova senha na próxima vez.",
67
+ aiChatResetConfirmBtn: "Sim, redefinir",
68
+ aiChatResetCancelBtn: "Cancelar",
69
+ aiChatProviderLabel: "Provedor:",
70
+ aiChatApiKeyLabel: "Chave de API (deixe em branco para IA local):",
71
+ aiChatProviderOpenAI: "OpenAI (GPT-4o-mini)",
72
+ aiChatProviderClaude: "Anthropic Claude (3.5 Sonnet)",
73
+ aiChatProviderGemini: "Google Gemini (1.5 Flash)",
74
+ aiChatProviderOllama: "Ollama Network (Local LLMs)",
75
+ aiChatEmptyPageContent: "Nenhuma página ativa compatível.",
76
+ aiChatNoPageId: "Sem página",
77
+ aiChatOllamaUrlLabel: "URL da API Ollama (deixe em branco para local)",
78
+ aiChatClearChatPopupTitle: "Limpar conversa?",
79
+ aiChatClearChatPopupDesc: "Isso apagará todo o histórico atual do chat. Esta ação não pode ser desfeita.",
80
+ aiChatClearChatConfirmBtn: "Sim, apagar",
81
+ aiChatClearChatCancelBtn: "Cancelar",
82
+ aiChatClearDataPopupTitle: "Apagar todos os dados?",
83
+ aiChatClearDataPopupDesc: "Isso apagará todos os chats e as chaves de API locais. O chat de IA será fechado.",
84
+ aiChatClearDataConfirmBtn: "Sim, apagar tudo",
85
+ aiChatClearDataCancelBtn: "Cancelar",
86
+ aiChatUserLabel: "Você",
87
+ aiChatApiError: "Ocorreu um erro de conexão com a API.",
88
+ aiChatError401: "Chave de acesso inválida ou provedor não autorizado.",
89
+ aiChatError429: "Limite de requisições excedido. Tente novamente mais tarde.",
90
+ aiChatError500: "Ocorreu um erro interno no servidor do modelo de IA.",
91
+ aiChatErrorGeneric: "Ocorreu um erro inesperado ao se comunicar com a IA.",
92
+ docsAccessGateTitle: "Documentação protegida",
93
+ docsAccessGateDescription: "Digite a senha ou a chave privada para acessar a documentação.",
94
+ docsAccessInputPlaceholder: "Senha ou chave privada",
95
+ docsAccessUnlockBtn: "Desbloquear",
96
+ docsAccessWrongCredential: "Senha ou chave incorreta.",
97
+ docsAccessLockTooltip: "Bloquear documentação",
98
+ docsAccessBlockPopupTitle: "Bloquear a documentação?",
99
+ docsAccessBlockPopupDesc: "Você precisará da senha novamente na próxima visita.",
100
+ docsAccessBlockConfirmBtn: "Sim, bloquear",
101
+ docsAccessBlockCancelBtn: "Cancelar",
41
102
  },
42
103
  en: {
43
104
  pt: "Portuguese",
@@ -78,6 +139,67 @@ export const defaultLangMenu = {
78
139
  audioPopoverStatusPaused: "Paused",
79
140
  audioPopoverStatusLoopOn: "Loop on",
80
141
  audioPopoverStatusLoopOff: "Loop off",
142
+ aiChatTitle: "AI Assistant",
143
+ aiChatPlaceholder: "Ask about the documentation...",
144
+ aiChatConfigTitle: "Configure AI",
145
+ aiChatConfigDesc: "Enter your API key to enable the chat.",
146
+ aiChatClearDataBtn: "Clear local data",
147
+ aiChatSendBtn: "Send",
148
+ aiChatSaveStartBtn: "Save & Start Chatting",
149
+ aiChatAttachAriaLabel: "Attach audio or image file",
150
+ aiChatAttachedFilesLabel: "file(s) attached",
151
+ aiChatRemoveAttachmentBtn: "Remove",
152
+ aiChatCancelResponseLabel: "Cancel response",
153
+ aiChatOpenBtnAriaLabel: "Open AI Chat",
154
+ aiChatCloseBtnAriaLabel: "Close chat",
155
+ aiChatSystemPrompt: "You are the official AI documentation assistant for the '{headerName}' project. Your primary role is to answer questions strictly related to the project's documentation, configurations (like config.json), and markdown files located in the 'gitpagedocs' directory. Be highly professional and helpful.\n\nIMPORTANT: You MUST address the user and respond EXCLUSIVELY in the following language: {language}.\n\nHere is the raw content of the active page the user is currently looking at:\n[Page ID]: {pageId}\n[Hidden Current Context Content]:\n{rawContent}",
156
+ aiChatEmptyStateGreeting: "Hello! I am the artificial intelligence assistant of this documentation. You can ask me questions about the content or search for a term across the documentation. How can I help you today?",
157
+ aiChatDisclaimer: "AI can make mistakes. Always verify the information.",
158
+ aiChatPasswordCreateDesc: "Create a local password to encrypt your API keys.",
159
+ aiChatPasswordUnlockDesc: "Enter your local password to unlock.",
160
+ aiChatPasswordPlaceholder: "Local password",
161
+ aiChatCreatePasswordBtn: "Create password",
162
+ aiChatUnlockBtn: "Unlock",
163
+ aiChatWrongPassword: "Incorrect password.",
164
+ aiChatLockBtn: "Lock",
165
+ aiChatResetBtn: "Forgot password?",
166
+ aiChatResetPopupTitle: "Reset password?",
167
+ aiChatResetPopupDesc: "This erases all saved API keys and the local password. You'll create a new password next time.",
168
+ aiChatResetConfirmBtn: "Yes, reset",
169
+ aiChatResetCancelBtn: "Cancel",
170
+ aiChatProviderLabel: "Provider:",
171
+ aiChatApiKeyLabel: "API Key (leave blank for Local AI):",
172
+ aiChatProviderOpenAI: "OpenAI (GPT-4o-mini)",
173
+ aiChatProviderClaude: "Anthropic Claude (3.5 Sonnet)",
174
+ aiChatProviderGemini: "Google Gemini (1.5 Flash)",
175
+ aiChatProviderOllama: "Ollama Network (Local LLMs)",
176
+ aiChatEmptyPageContent: "No compatible active page.",
177
+ aiChatNoPageId: "No page",
178
+ aiChatOllamaUrlLabel: "Ollama API URL (leave blank for local)",
179
+ aiChatClearChatPopupTitle: "Clear conversation?",
180
+ aiChatClearChatPopupDesc: "This will delete all current chat history. This action cannot be undone.",
181
+ aiChatClearChatConfirmBtn: "Yes, delete",
182
+ aiChatClearChatCancelBtn: "Cancel",
183
+ aiChatClearDataPopupTitle: "Erase all data?",
184
+ aiChatClearDataPopupDesc: "This will delete all chats and local API keys. The AI chat will be closed.",
185
+ aiChatClearDataConfirmBtn: "Yes, erase everything",
186
+ aiChatClearDataCancelBtn: "Cancel",
187
+ aiChatUserLabel: "You",
188
+ aiChatApiError: "An API connection error occurred.",
189
+ aiChatError401: "Invalid access key or unauthorized provider.",
190
+ aiChatError429: "Rate limit exceeded. Please try again later.",
191
+ aiChatError500: "An internal server error occurred on the AI model.",
192
+ aiChatErrorGeneric: "An unexpected error occurred while communicating with the AI.",
193
+ docsAccessGateTitle: "Protected documentation",
194
+ docsAccessGateDescription: "Enter the password or private key to view the documentation.",
195
+ docsAccessInputPlaceholder: "Password or private key",
196
+ docsAccessUnlockBtn: "Unlock",
197
+ docsAccessWrongCredential: "Incorrect password or key.",
198
+ docsAccessLockTooltip: "Lock documentation",
199
+ docsAccessBlockPopupTitle: "Block the documentation?",
200
+ docsAccessBlockPopupDesc: "You'll need the password again on your next visit.",
201
+ docsAccessBlockConfirmBtn: "Yes, block",
202
+ docsAccessBlockCancelBtn: "Cancel",
81
203
  },
82
204
  es: {
83
205
  pt: "Portugues",
@@ -118,5 +240,66 @@ export const defaultLangMenu = {
118
240
  audioPauseLabel: "Pausar música de fondo",
119
241
  audioPlaylistTitle: "Elegir pista",
120
242
  audioPlaylistDescription: "Seleccione una pista para reproducir de la playlist.",
243
+ aiChatTitle: "Asistente de IA",
244
+ aiChatPlaceholder: "Pregunta sobre la documentación...",
245
+ aiChatConfigTitle: "Configurar IA",
246
+ aiChatConfigDesc: "Introduce tu clave de API para activar el chat.",
247
+ aiChatClearDataBtn: "Borrar datos locales",
248
+ aiChatSendBtn: "Enviar",
249
+ aiChatSaveStartBtn: "Guardar y empezar a chatear",
250
+ aiChatAttachAriaLabel: "Adjuntar archivo de audio o imagen",
251
+ aiChatAttachedFilesLabel: "archivo(s) adjunto(s)",
252
+ aiChatRemoveAttachmentBtn: "Quitar",
253
+ aiChatCancelResponseLabel: "Cancelar respuesta",
254
+ aiChatOpenBtnAriaLabel: "Abrir chat de IA",
255
+ aiChatCloseBtnAriaLabel: "Cerrar chat",
256
+ aiChatSystemPrompt: "You are the official AI documentation assistant for the '{headerName}' project. Your primary role is to answer questions strictly related to the project's documentation, configurations (like config.json), and markdown files located in the 'gitpagedocs' directory. Be highly professional and helpful.\n\nIMPORTANT: You MUST address the user and respond EXCLUSIVELY in the following language: {language}.\n\nHere is the raw content of the active page the user is currently looking at:\n[Page ID]: {pageId}\n[Hidden Current Context Content]:\n{rawContent}",
257
+ aiChatEmptyStateGreeting: "¡Hola! Soy el asistente de inteligencia artificial de esta documentación. Puedes hacerme preguntas sobre el contenido o buscar un término en toda la documentación. ¿Cómo puedo ayudarte hoy?",
258
+ aiChatDisclaimer: "La IA puede cometer errores. Verifica siempre la información.",
259
+ aiChatPasswordCreateDesc: "Crea una contraseña local para cifrar tus claves de API.",
260
+ aiChatPasswordUnlockDesc: "Introduce tu contraseña local para desbloquear.",
261
+ aiChatPasswordPlaceholder: "Contraseña local",
262
+ aiChatCreatePasswordBtn: "Crear contraseña",
263
+ aiChatUnlockBtn: "Desbloquear",
264
+ aiChatWrongPassword: "Contraseña incorrecta.",
265
+ aiChatLockBtn: "Bloquear",
266
+ aiChatResetBtn: "¿Olvidaste la contraseña?",
267
+ aiChatResetPopupTitle: "¿Restablecer contraseña?",
268
+ aiChatResetPopupDesc: "Esto borra todas las claves de API guardadas y la contraseña local. Crearás una nueva contraseña la próxima vez.",
269
+ aiChatResetConfirmBtn: "Sí, restablecer",
270
+ aiChatResetCancelBtn: "Cancelar",
271
+ aiChatProviderLabel: "Proveedor:",
272
+ aiChatApiKeyLabel: "Clave de API (déjalo en blanco para IA local):",
273
+ aiChatProviderOpenAI: "OpenAI (GPT-4o-mini)",
274
+ aiChatProviderClaude: "Anthropic Claude (3.5 Sonnet)",
275
+ aiChatProviderGemini: "Google Gemini (1.5 Flash)",
276
+ aiChatProviderOllama: "Ollama Network (Local LLMs)",
277
+ aiChatEmptyPageContent: "Ninguna página activa compatible.",
278
+ aiChatNoPageId: "Sin página",
279
+ aiChatOllamaUrlLabel: "URL de la API de Ollama (déjalo en blanco para local)",
280
+ aiChatClearChatPopupTitle: "¿Borrar conversación?",
281
+ aiChatClearChatPopupDesc: "Esto eliminará todo el historial de chat actual. Esta acción no se puede deshacer.",
282
+ aiChatClearChatConfirmBtn: "Sí, borrar",
283
+ aiChatClearChatCancelBtn: "Cancelar",
284
+ aiChatClearDataPopupTitle: "¿Borrar todos los datos?",
285
+ aiChatClearDataPopupDesc: "Esto eliminará todos los chats y las claves de API locales. El chat de IA se cerrará.",
286
+ aiChatClearDataConfirmBtn: "Sí, borrar todo",
287
+ aiChatClearDataCancelBtn: "Cancelar",
288
+ aiChatUserLabel: "Tú",
289
+ aiChatApiError: "Se produjo un error de conexión con la API.",
290
+ aiChatError401: "Clave de acceso no válida o proveedor no autorizado.",
291
+ aiChatError429: "Límite de solicitudes superado. Inténtalo de nuevo más tarde.",
292
+ aiChatError500: "Se produjo un error interno del servidor en el modelo de IA.",
293
+ aiChatErrorGeneric: "Se produjo un error inesperado al comunicarse con la IA.",
294
+ docsAccessGateTitle: "Documentación protegida",
295
+ docsAccessGateDescription: "Introduce la contraseña o la clave privada para ver la documentación.",
296
+ docsAccessInputPlaceholder: "Contraseña o clave privada",
297
+ docsAccessUnlockBtn: "Desbloquear",
298
+ docsAccessWrongCredential: "Contraseña o clave incorrecta.",
299
+ docsAccessLockTooltip: "Bloquear documentación",
300
+ docsAccessBlockPopupTitle: "¿Bloquear la documentación?",
301
+ docsAccessBlockPopupDesc: "Tendrás que introducir la contraseña de nuevo en la próxima visita.",
302
+ docsAccessBlockConfirmBtn: "Sí, bloquear",
303
+ docsAccessBlockCancelBtn: "Cancelar",
121
304
  },
122
305
  };
@@ -0,0 +1,47 @@
1
+ import fs from "node:fs/promises";
2
+ import { defaultConfigLoader } from "@gitpagedocs/tools";
3
+
4
+ export interface DocsAccessConfig {
5
+ enabled: boolean;
6
+ publicKey: string;
7
+ }
8
+
9
+ /**
10
+ * Reads and patches the generated gitpagedocs/config.json IN PLACE, preserving
11
+ * every existing key (raw-JSON round-trip — it never reserializes through a typed
12
+ * model that could drop unknown fields). Used by interactive commands that need
13
+ * to persist a single value (e.g. the documentation password public key).
14
+ */
15
+ export class GitPageDocsConfigRepository {
16
+ constructor(private readonly cwd: string = process.cwd()) {}
17
+
18
+ /** Absolute path to the JSON config, or throws a friendly, actionable error. */
19
+ async resolvePath(): Promise<string> {
20
+ const resolved = await defaultConfigLoader.resolveConfigPath(this.cwd);
21
+ if (!resolved) {
22
+ throw new Error("gitpagedocs config.json not found. Run `gitpagedocs` first to generate it.");
23
+ }
24
+ if (!resolved.endsWith(".json")) {
25
+ throw new Error(`Expected a JSON config to patch, found "${resolved}". Use a gitpagedocs/config.json.`);
26
+ }
27
+ return resolved;
28
+ }
29
+
30
+ /**
31
+ * Set `site.docsAccess = { enabled, publicKey }`, leaving all other config keys
32
+ * untouched. Returns the path that was written.
33
+ */
34
+ async patchDocsAccess(publicKey: string, enabled = true): Promise<string> {
35
+ const configPath = await this.resolvePath();
36
+ const raw = await fs.readFile(configPath, "utf-8");
37
+ const config = JSON.parse(raw) as Record<string, unknown>;
38
+ const site =
39
+ config.site && typeof config.site === "object"
40
+ ? (config.site as Record<string, unknown>)
41
+ : {};
42
+ site.docsAccess = { enabled, publicKey } satisfies DocsAccessConfig;
43
+ config.site = site;
44
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
45
+ return configPath;
46
+ }
47
+ }
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "@gitpagedocs/cli",
3
- "version": "1.1.48",
4
- "description": "CLI that scaffolds and maintains gitpagedocs documentation, generates docs with AI, configures GitHub Pages, and runs the MCP server.",
5
- "bin": {
6
- "gitpagedocs": "index.mjs"
7
- },
8
- "main": "index.mjs",
9
- "files": [
10
- "index.mjs",
11
- "start.mjs",
12
- "ai",
13
- "application",
14
- "builders",
15
- "content",
16
- "contracts",
17
- "data",
18
- "domain",
19
- "home",
20
- "infrastructure",
21
- "presentation",
22
- "runtime",
23
- "ui",
24
- "prebuilt",
25
- "README.md"
26
- ],
27
- "engines": {
28
- "node": ">=20"
29
- },
30
- "dependencies": {
31
- "@clack/prompts": "^1.5.1",
32
- "@gitpagedocs/mcp": "^1.1.48",
33
- "@gitpagedocs/tools": "^1.1.48",
34
- "tsx": "^4.21.0"
35
- },
36
- "repository": {
37
- "type": "git",
38
- "url": "git+https://github.com/Vidigal-code/git-page-docs.git",
39
- "directory": "cli"
40
- },
41
- "keywords": [
42
- "gitpagedocs",
43
- "documentation",
44
- "markdown",
45
- "spa",
46
- "cli",
47
- "github-pages",
48
- "create-gitpagedocs"
49
- ],
50
- "author": "Vidigal-code",
51
- "license": "ISC",
52
- "bugs": {
53
- "url": "https://github.com/Vidigal-code/git-page-docs/issues"
54
- },
55
- "homepage": "https://vidigal-code.github.io/git-page-docs/",
56
- "publishConfig": {
57
- "registry": "https://registry.npmjs.org/",
58
- "access": "public"
59
- }
60
- }
1
+ {
2
+ "name": "@gitpagedocs/cli",
3
+ "version": "1.1.51",
4
+ "description": "CLI that scaffolds and maintains gitpagedocs documentation, generates docs with AI, configures GitHub Pages, and runs the MCP server.",
5
+ "bin": {
6
+ "gitpagedocs": "index.mjs"
7
+ },
8
+ "main": "index.mjs",
9
+ "files": [
10
+ "index.mjs",
11
+ "start.mjs",
12
+ "ai",
13
+ "application",
14
+ "builders",
15
+ "content",
16
+ "contracts",
17
+ "data",
18
+ "domain",
19
+ "home",
20
+ "infrastructure",
21
+ "presentation",
22
+ "runtime",
23
+ "ui",
24
+ "prebuilt",
25
+ "README.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "dependencies": {
31
+ "@clack/prompts": "^1.5.1",
32
+ "@gitpagedocs/mcp": "^1.1.51",
33
+ "@gitpagedocs/tools": "^1.1.51",
34
+ "tsx": "^4.21.0"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/Vidigal-code/git-page-docs.git",
39
+ "directory": "cli"
40
+ },
41
+ "keywords": [
42
+ "gitpagedocs",
43
+ "documentation",
44
+ "markdown",
45
+ "spa",
46
+ "cli",
47
+ "github-pages",
48
+ "create-gitpagedocs"
49
+ ],
50
+ "author": "Vidigal-code",
51
+ "license": "ISC",
52
+ "bugs": {
53
+ "url": "https://github.com/Vidigal-code/git-page-docs/issues"
54
+ },
55
+ "homepage": "https://vidigal-code.github.io/git-page-docs/",
56
+ "publishConfig": {
57
+ "registry": "https://registry.npmjs.org/",
58
+ "access": "public"
59
+ }
60
+ }
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import type { CommandContext } from "./run-command";
4
4
  import { askConfirm } from "../ui/clack";
5
+ import { askOwnerRepo, interactivePromptsAvailable } from "../ui/prompts";
5
6
  // @ts-expect-error .mjs runtime module
6
7
  import { detectRepoFromGit, getCurrentGitBranch, tryConfigurePagesToGitHubActions } from "../../runtime/git-ops.mjs";
7
8
 
@@ -21,11 +22,14 @@ function readFlag(args: string[], name: string): string {
21
22
  * overwrite). Does NOT generate docs or push (that is `pages deploy`).
22
23
  */
23
24
  export async function runPagesActions(ctx: CommandContext): Promise<void> {
24
- const repo = detectRepoFromGit(ctx.cwd) as { owner: string; repo: string } | null;
25
+ let repo = detectRepoFromGit(ctx.cwd) as { owner: string; repo: string } | null;
25
26
  if (!repo) {
26
- // eslint-disable-next-line no-console
27
- console.log("\n Could not detect a GitHub repo from the git 'origin' remote.\n Add a remote or use `--push --owner <o> --repo <r>`.\n");
28
- return;
27
+ if (!interactivePromptsAvailable()) {
28
+ // eslint-disable-next-line no-console
29
+ console.log("\n Could not detect a GitHub repo from the git 'origin' remote.\n Add a remote or use `--push --owner <o> --repo <r>`.\n");
30
+ return;
31
+ }
32
+ repo = await askOwnerRepo(null);
29
33
  }
30
34
  const branch = getCurrentGitBranch(ctx.cwd) as string;
31
35
  // eslint-disable-next-line no-console
@@ -59,9 +63,14 @@ export async function runPagesDeploy(ctx: CommandContext): Promise<void> {
59
63
  }
60
64
  }
61
65
  if (!owner || !repo) {
62
- // eslint-disable-next-line no-console
63
- console.log("\n Could not determine owner/repo. Pass `--owner <o> --repo <r>` or add a git 'origin' remote.\n");
64
- return;
66
+ if (!interactivePromptsAvailable()) {
67
+ // eslint-disable-next-line no-console
68
+ console.log("\n Could not determine owner/repo. Pass `--owner <o> --repo <r>` or add a git 'origin' remote.\n");
69
+ return;
70
+ }
71
+ const answered = await askOwnerRepo(null, { owner, repo });
72
+ owner = answered.owner;
73
+ repo = answered.repo;
65
74
  }
66
75
 
67
76
  // eslint-disable-next-line no-console
@@ -76,7 +85,8 @@ export async function runPagesDeploy(ctx: CommandContext): Promise<void> {
76
85
  return;
77
86
  }
78
87
 
79
- const bin = path.join(ctx.pkgRoot, "cli", "index.mjs");
88
+ // pkgRoot is the cli package root (cli/), where the bin lives as index.mjs.
89
+ const bin = path.join(ctx.pkgRoot, "index.mjs");
80
90
  const pushArgs = ["--push", "--owner", owner, "--repo", repo, ...(docsPath ? ["--path", docsPath] : [])];
81
91
  const result = spawnSync(process.execPath, [bin, ...pushArgs], { stdio: "inherit", cwd: ctx.cwd, env: process.env });
82
92
 
@@ -0,0 +1,62 @@
1
+ import { NodeCryptoService, deriveDocAccessKeys } from "@gitpagedocs/tools";
2
+ import type { CommandContext } from "./run-command";
3
+ import { askPassword, note, outro } from "../ui/clack";
4
+ import { interactivePromptsAvailable } from "../ui/prompts";
5
+ import { GitPageDocsConfigRepository } from "../../infrastructure/gitpagedocs-config-file";
6
+
7
+ const MIN_PASSWORD_LENGTH = 4;
8
+
9
+ /**
10
+ * `gitpagedocs password` — interactively set a documentation access password.
11
+ *
12
+ * Type + confirm a password, derive the double-hash key pair, write the PUBLIC
13
+ * key into gitpagedocs/config.json (which makes the frontend gate the docs), and
14
+ * print the PRIVATE key for the user to copy. The password itself is never stored.
15
+ */
16
+ export async function runPassword(ctx: CommandContext): Promise<void> {
17
+ if (!interactivePromptsAvailable()) {
18
+ // eslint-disable-next-line no-console
19
+ console.log("\n `gitpagedocs password` is interactive — run it in a terminal (TTY).\n");
20
+ return;
21
+ }
22
+
23
+ const password = await askPassword({
24
+ message: "Documentation password:",
25
+ validate: (v) =>
26
+ v && v.trim().length >= MIN_PASSWORD_LENGTH
27
+ ? undefined
28
+ : `Use at least ${MIN_PASSWORD_LENGTH} characters.`,
29
+ });
30
+ const confirmation = await askPassword({
31
+ message: "Confirm password:",
32
+ validate: (v) => (v ? undefined : "Required."),
33
+ });
34
+
35
+ if (password !== confirmation) {
36
+ note("Passwords do not match. Nothing was changed.", "Aborted");
37
+ return;
38
+ }
39
+
40
+ const { privateKey, publicKey } = await deriveDocAccessKeys(password, new NodeCryptoService());
41
+
42
+ let configPath: string;
43
+ try {
44
+ configPath = await new GitPageDocsConfigRepository(ctx.cwd).patchDocsAccess(publicKey);
45
+ } catch (error) {
46
+ note(error instanceof Error ? error.message : String(error), "Could not save");
47
+ return;
48
+ }
49
+
50
+ note(
51
+ `Public key saved to ${configPath}\nThe documentation is now password-protected.`,
52
+ "Saved",
53
+ );
54
+ // eslint-disable-next-line no-console
55
+ console.log(
56
+ "\n PRIVATE KEY (copy & keep it safe — it also unlocks the docs):\n\n" +
57
+ ` ${privateKey}\n\n` +
58
+ " Readers can unlock with the password OR this private key.\n" +
59
+ " To remove protection, set site.docsAccess.enabled to false in config.json.\n",
60
+ );
61
+ outro("Done.");
62
+ }
@@ -21,6 +21,7 @@ import { runConfig } from "./config-info";
21
21
  import { runMcp } from "./mcp";
22
22
  import { runDocs } from "./docs";
23
23
  import { runPagesActions, runPagesDeploy } from "./pages";
24
+ import { runPassword } from "./password";
24
25
 
25
26
  const REGISTRY: Record<string, CommandHandler> = {
26
27
  version: runVersion,
@@ -34,6 +35,7 @@ const REGISTRY: Record<string, CommandHandler> = {
34
35
  config: runConfig,
35
36
  mcp: runMcp,
36
37
  docs: runDocs,
38
+ password: runPassword,
37
39
  };
38
40
 
39
41
  /** Returns true if a new command handled the invocation (skip legacy flow). */
@@ -1,7 +1,16 @@
1
1
  import type { CliOptions } from "../../domain/models/cli-options";
2
2
  import { parseCliOptions } from "./parser";
3
- import { shouldRunInteractive, promptConfigOnlyOptions, promptHomeOptions } from "../ui/prompts";
3
+ import {
4
+ shouldRunInteractive,
5
+ promptConfigOnlyOptions,
6
+ promptHomeOptions,
7
+ promptDeployOptions,
8
+ ensureGitRepoInteractive,
9
+ interactivePromptsAvailable,
10
+ } from "../ui/prompts";
4
11
  import { DEFAULTS } from "./schema";
12
+ // @ts-expect-error .mjs runtime module is type-less in this package.
13
+ import { detectRepoFromGit } from "../../runtime/git-ops.mjs";
5
14
 
6
15
  export async function resolveOptions(argv: string[], env: NodeJS.ProcessEnv): Promise<CliOptions> {
7
16
  const parsed = parseCliOptions(argv, env);
@@ -11,6 +20,20 @@ export async function resolveOptions(argv: string[], env: NodeJS.ProcessEnv): Pr
11
20
  parsed.basePath = parsed.basePath ?? parsed.docsPath ?? DEFAULTS.home.basePath;
12
21
  }
13
22
 
23
+ // `deploy` / `--push` need owner + repo AND a git repo. If anything is
24
+ // missing, prompt interactively (owner/repo pre-filled from the git origin
25
+ // remote; offer to `git init`) instead of throwing. In CI/non-TTY we fall
26
+ // through so the explicit guards in the push flow still fire clearly.
27
+ if (parsed.shouldPush && interactivePromptsAvailable()) {
28
+ let resolved = parsed;
29
+ if (!parsed.githubOwner || !parsed.githubRepo) {
30
+ const detected = detectRepoFromGit(process.cwd()) as { owner: string; repo: string } | null;
31
+ resolved = await promptDeployOptions(parsed, detected);
32
+ }
33
+ await ensureGitRepoInteractive(process.cwd());
34
+ return resolved;
35
+ }
36
+
14
37
  const runHomeInteractive = parsed.isInteractive || (shouldRunInteractive(argv) && parsed.mode === "home");
15
38
  if (runHomeInteractive && parsed.mode === "home") {
16
39
  return promptHomeOptions(parsed);
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import {
7
7
  text,
8
+ password,
8
9
  confirm,
9
10
  select,
10
11
  multiselect,
@@ -40,6 +41,22 @@ export async function askText(options: AskTextOptions): Promise<string> {
40
41
  return exitIfCancelled(result);
41
42
  }
42
43
 
44
+ export interface AskPasswordOptions {
45
+ message: string;
46
+ validate?: (value: string) => string | undefined;
47
+ }
48
+
49
+ /** Masked password prompt (input hidden as the user types). */
50
+ export async function askPassword(options: AskPasswordOptions): Promise<string> {
51
+ const result = await password({
52
+ message: options.message,
53
+ validate: options.validate
54
+ ? (value) => options.validate?.(value ?? "")
55
+ : undefined,
56
+ });
57
+ return exitIfCancelled(result);
58
+ }
59
+
43
60
  export async function askConfirm(message: string, initialValue = false): Promise<boolean> {
44
61
  const result = await confirm({ message, initialValue });
45
62
  return exitIfCancelled(result);
@@ -1,6 +1,9 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { execSync } from "node:child_process";
1
4
  import type { CliOptions } from "../../domain/models/cli-options";
2
5
  import { DEFAULTS } from "../options/schema";
3
- import { askText, askConfirm } from "./clack";
6
+ import { askText, askConfirm, note } from "./clack";
4
7
 
5
8
  function isCiOrNonTty(): boolean {
6
9
  if (process.env.CI === "true") return true;
@@ -9,6 +12,11 @@ function isCiOrNonTty(): boolean {
9
12
  return false;
10
13
  }
11
14
 
15
+ /** True when clack prompts can run (interactive TTY, not CI). */
16
+ export function interactivePromptsAvailable(): boolean {
17
+ return !isCiOrNonTty();
18
+ }
19
+
12
20
  export function shouldRunInteractive(argv: string[]): boolean {
13
21
  if (isCiOrNonTty()) return false;
14
22
  const args = argv.slice(2);
@@ -41,6 +49,72 @@ export async function promptHomeOptions(parsed: CliOptions): Promise<CliOptions>
41
49
  };
42
50
  }
43
51
 
52
+ /**
53
+ * Deploy/--push needs a GitHub owner + repo. When they are missing, prompt for
54
+ * them (pre-filled from the git `origin` remote when available) instead of
55
+ * crashing with "`--push` requires owner and repo".
56
+ */
57
+ export async function askOwnerRepo(
58
+ detected: { owner: string; repo: string } | null,
59
+ defaults?: { owner?: string; repo?: string },
60
+ ): Promise<{ owner: string; repo: string }> {
61
+ const owner = await askText({
62
+ message: "GitHub owner (user or organization):",
63
+ defaultValue: defaults?.owner || detected?.owner || "",
64
+ validate: (v) => (v && v.trim() ? undefined : "Owner is required."),
65
+ });
66
+ const repo = await askText({
67
+ message: "GitHub repository name:",
68
+ defaultValue: defaults?.repo || detected?.repo || "",
69
+ validate: (v) => (v && v.trim() ? undefined : "Repository is required."),
70
+ });
71
+ return { owner: owner.trim(), repo: repo.trim() };
72
+ }
73
+
74
+ export async function promptDeployOptions(
75
+ parsed: CliOptions,
76
+ detected: { owner: string; repo: string } | null,
77
+ ): Promise<CliOptions> {
78
+ note(
79
+ "Deploy publishes gitpagedocs to GitHub Pages and needs the target\nrepository (owner/repo). Press Enter to accept a detected value.",
80
+ "GitHub Pages deploy",
81
+ );
82
+ const { owner, repo } = await askOwnerRepo(detected, {
83
+ owner: parsed.githubOwner,
84
+ repo: parsed.githubRepo,
85
+ });
86
+
87
+ return {
88
+ ...parsed,
89
+ githubOwner: owner,
90
+ githubRepo: repo,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Deploy/--push needs a git repository in the current folder. When there isn't
96
+ * one, offer to run `git init` instead of crashing with "not a git repository".
97
+ * In CI/non-TTY this is a no-op so the downstream guard still reports clearly.
98
+ */
99
+ export async function ensureGitRepoInteractive(root: string): Promise<void> {
100
+ if (existsSync(path.join(root, ".git"))) return;
101
+ if (!interactivePromptsAvailable()) return;
102
+ const init = await askConfirm(
103
+ "This folder is not a git repository yet. Initialize one here (git init)?",
104
+ true,
105
+ );
106
+ if (!init) {
107
+ note("Deploy needs a git repository. Run `git init`, then retry.", "Heads up");
108
+ return;
109
+ }
110
+ try {
111
+ execSync("git init", { cwd: root, stdio: "ignore" });
112
+ note("Initialized an empty git repository.", "git");
113
+ } catch {
114
+ note("Could not run `git init`. Initialize git manually, then retry.", "git");
115
+ }
116
+ }
117
+
44
118
  export async function promptConfigOnlyOptions(parsed: CliOptions): Promise<CliOptions> {
45
119
  const useLocalLayoutConfig = await askConfirm(
46
120
  "Generate local layout templates?",