@doxbrix/doxloop 0.1.2 → 0.1.4

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +96 -41
  3. package/assets/doxbrix-preview.css +8 -0
  4. package/dist/agents.d.ts +4 -0
  5. package/dist/agents.d.ts.map +1 -1
  6. package/dist/agents.js +9 -0
  7. package/dist/agents.js.map +1 -1
  8. package/dist/args.d.ts.map +1 -1
  9. package/dist/args.js +2 -1
  10. package/dist/args.js.map +1 -1
  11. package/dist/auth.js +47 -4
  12. package/dist/auth.js.map +1 -1
  13. package/dist/author.d.ts +7 -5
  14. package/dist/author.d.ts.map +1 -1
  15. package/dist/author.js +49 -21
  16. package/dist/author.js.map +1 -1
  17. package/dist/cli.js +346 -72
  18. package/dist/cli.js.map +1 -1
  19. package/dist/doxbrix-markdown.js +39 -1
  20. package/dist/doxbrix-markdown.js.map +1 -1
  21. package/dist/interactive.d.ts +17 -0
  22. package/dist/interactive.d.ts.map +1 -0
  23. package/dist/interactive.js +345 -0
  24. package/dist/interactive.js.map +1 -0
  25. package/dist/preview.d.ts +1 -0
  26. package/dist/preview.d.ts.map +1 -1
  27. package/dist/preview.js +238 -1
  28. package/dist/preview.js.map +1 -1
  29. package/dist/project.d.ts +8 -0
  30. package/dist/project.d.ts.map +1 -1
  31. package/dist/project.js +227 -15
  32. package/dist/project.js.map +1 -1
  33. package/dist/prompts.d.ts +41 -0
  34. package/dist/prompts.d.ts.map +1 -0
  35. package/dist/prompts.js +290 -0
  36. package/dist/prompts.js.map +1 -0
  37. package/dist/settings.d.ts +6 -0
  38. package/dist/settings.d.ts.map +1 -0
  39. package/dist/settings.js +519 -0
  40. package/dist/settings.js.map +1 -0
  41. package/dist/sync.d.ts.map +1 -1
  42. package/dist/sync.js +46 -0
  43. package/dist/sync.js.map +1 -1
  44. package/dist/types.d.ts +18 -1
  45. package/dist/types.d.ts.map +1 -1
  46. package/docs/ci-and-automation.md +10 -0
  47. package/docs/doxbrix-http-api.md +2 -1
  48. package/docs/project-format.md +13 -1
  49. package/docs/security-model.md +4 -2
  50. package/package.json +1 -1
  51. package/skills/doxloop-authoring/references/project-format.md +20 -0
  52. package/dist/public-deploy-confirmation.d.ts +0 -5
  53. package/dist/public-deploy-confirmation.d.ts.map +0 -1
  54. package/dist/public-deploy-confirmation.js +0 -22
  55. package/dist/public-deploy-confirmation.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,18 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import { resolve } from 'node:path';
2
+ import { mkdir, readFile } from 'node:fs/promises';
3
+ import { join, relative, resolve } from 'node:path';
3
4
  import { assertAllowedFlags, booleanFlag, flag, flags, numberFlag, parseArgs, } from './args.js';
4
- import { chooseAgent, installSkill, parseAgent, skillStatus } from './agents.js';
5
- import { login, logout, whoami } from './auth.js';
5
+ import { installSkill, parseAgent, skillStatus } from './agents.js';
6
+ import { loadUserConfig, login, logout, whoami } from './auth.js';
6
7
  import { parseReasoning, resolveScreenshotIntent, runAuthor, } from './author.js';
7
8
  import { capture } from './capture.js';
8
9
  import { deploy } from './deploy.js';
9
10
  import { formatDoctorReport, runDoctor } from './doctor.js';
10
11
  import { DoxloopError, UsageError } from './errors.js';
11
12
  import { addGenerator, diagnoseGenerator, formatGeneratorInfo, formatGeneratorList, removeGenerator, } from './generator-manager.js';
12
- import { GENERATOR_CATALOG, parseGenerator } from './generators.js';
13
- import { addDesignReferences, findProjectRoot, loadProject, parseDesignReference, parseSource, resolveSeparateProjectLayout, scaffoldProject, validateProjectSourceBoundaries, } from './project.js';
13
+ import { GENERATOR_CATALOG, generatorCatalogEntry, parseGenerator, resolveGeneratorPackage, } from './generators.js';
14
+ import { formatInitPlan, promptForRequest, replayInitCommand, runCreateRescueWizard, runInitWizard, selectAgentInteractive, } from './interactive.js';
15
+ import { addDesignReferences, assertNewProjectDirectory, findProjectRoot, isSpecUrl, loadProject, parseDesignReference, parseSource, parseSpec, resolveSeparateProjectLayout, saveDefaultAgent, scaffoldProject, validateProjectSourceBoundaries, } from './project.js';
16
+ import { isInteractive, promptConfirm } from './prompts.js';
14
17
  import { startPreview } from './preview.js';
15
- import { confirmPublicDeployment } from './public-deploy-confirmation.js';
18
+ import { effectiveDeployment, formatProjectSettings, runSettingsWizard, } from './settings.js';
19
+ import { collectSourceChanges, formatSourceChanges } from './sync.js';
16
20
  import { formatValidation, validateProject } from './validation.js';
17
21
  import { VERSION } from './version.js';
18
22
  async function main() {
@@ -48,7 +52,9 @@ async function main() {
48
52
  return report.ready ? 0 : 1;
49
53
  }
50
54
  case 'create':
51
- if (flag(args, 'source') !== undefined || flag(args, 'output') !== undefined) {
55
+ if (flag(args, 'source') !== undefined ||
56
+ flag(args, 'output') !== undefined ||
57
+ flags(args, 'spec').length > 0) {
52
58
  return createProjectCommand(args, cwd);
53
59
  }
54
60
  return authorCommand(args, cwd, 'create');
@@ -73,6 +79,15 @@ async function main() {
73
79
  }
74
80
  case 'status':
75
81
  return statusCommand(cwd, outputFormat(flag(args, 'format')));
82
+ case 'settings': {
83
+ const root = await findProjectRoot(cwd);
84
+ if (!isInteractive(args)) {
85
+ process.stdout.write(`${formatProjectSettings(root, await loadProject(root))}\n`);
86
+ return 0;
87
+ }
88
+ await runSettingsWizard(root, cwd, promptIo());
89
+ return 0;
90
+ }
76
91
  case 'preview': {
77
92
  const root = await findProjectRoot(cwd);
78
93
  await startPreview({
@@ -102,21 +117,54 @@ async function main() {
102
117
  return 0;
103
118
  case 'deploy': {
104
119
  const root = await findProjectRoot(cwd);
105
- const name = flag(args, 'name');
106
- const slug = flag(args, 'slug');
107
- const apiOverride = flag(args, 'api-url');
120
+ const project = await loadProject(root);
121
+ const userConfig = await loadUserConfig();
122
+ const savedDeployment = effectiveDeployment(project, userConfig.apiUrl);
123
+ const name = flag(args, 'name') ?? savedDeployment.name;
124
+ const slug = flag(args, 'slug') ?? savedDeployment.slug;
125
+ const apiOverride = flag(args, 'api-url') ?? savedDeployment.apiUrl;
108
126
  const dryRun = booleanFlag(args, 'dry-run');
109
- const publicSite = booleanFlag(args, 'public');
110
- if (publicSite && !dryRun && !(await confirmPublicDeployment())) {
111
- throw new DoxloopError('Public deployment canceled. No data was uploaded.');
127
+ const publicSite = flag(args, 'public') !== undefined
128
+ ? booleanFlag(args, 'public')
129
+ : savedDeployment.visibility === 'public';
130
+ if (isInteractive(args) && !dryRun) {
131
+ const validation = await validateProject(root);
132
+ if (validation.errors > 0) {
133
+ throw new DoxloopError(`Deployment stopped because documentation has ${validation.errors} validation error${validation.errors === 1 ? '' : 's'}. Run \`doxloop test\`.`);
134
+ }
135
+ process.stdout.write(`\nDeployment summary\n\n Project: ${name}\n Slug: ${slug}\n Destination: ${apiOverride}\n Visibility: ${publicSite ? 'PUBLIC' : 'Private'}\n Pages: ${validation.pages.length}\n Warnings: ${validation.warnings}\n Product files: 0\n\n`);
136
+ if (publicSite) {
137
+ process.stdout.write('Anyone on the internet will be able to access this documentation.\n\n');
138
+ }
139
+ const proceed = await promptConfirm({
140
+ message: publicSite ? 'Deploy publicly?' : 'Deploy now?',
141
+ initial: !publicSite,
142
+ });
143
+ if (!proceed) {
144
+ process.stdout.write('Deployment canceled. No data was uploaded.\n');
145
+ return 0;
146
+ }
147
+ const token = userConfig.token ?? process.env.DOXLOOP_TOKEN ?? process.env.DOXBRIX_TOKEN;
148
+ if (!token) {
149
+ process.stdout.write('You are not signed in to Doxbrix.\n');
150
+ const signIn = await promptConfirm({
151
+ message: 'Sign in now?',
152
+ initial: true,
153
+ });
154
+ if (!signIn) {
155
+ process.stdout.write('Deployment canceled. No data was uploaded.\n');
156
+ return 0;
157
+ }
158
+ await login({ apiUrl: apiOverride });
159
+ }
112
160
  }
113
161
  await deploy({
114
162
  root,
115
- ...(name ? { name } : {}),
116
- ...(slug ? { slug } : {}),
163
+ name,
164
+ slug,
117
165
  dryRun,
118
166
  public: publicSite,
119
- ...(apiOverride ? { apiUrl: apiOverride } : {}),
167
+ apiUrl: apiOverride,
120
168
  });
121
169
  return 0;
122
170
  }
@@ -125,100 +173,248 @@ async function main() {
125
173
  }
126
174
  }
127
175
  async function initCommand(args, cwd) {
128
- const directory = args.positionals[0];
129
- if (!directory)
130
- throw new UsageError('Usage: doxloop init <directory> [--source name=path]');
176
+ const providedDirectory = args.positionals[0];
177
+ let sources = [
178
+ ...flags(args, 'source').map(parseSource),
179
+ ...flags(args, 'spec').map(parseSpec),
180
+ ];
181
+ let title = flag(args, 'title');
182
+ let generator = parseGenerator(flag(args, 'generator'));
183
+ let fromWizard = false;
184
+ let directory = providedDirectory;
185
+ if (!providedDirectory) {
186
+ if (!isInteractive(args)) {
187
+ throw new UsageError('Guided setup requires an interactive terminal. For automation, use `doxloop init <directory>` with optional --source or --spec values.');
188
+ }
189
+ const plan = await runInitWizard(cwd);
190
+ directory = plan.directory;
191
+ if (sources.length === 0)
192
+ sources = plan.sources;
193
+ title = title ?? plan.title;
194
+ generator = generator ?? plan.generator;
195
+ fromWizard = true;
196
+ }
131
197
  if (args.positionals.length > 1) {
132
198
  throw new UsageError('The init command accepts one destination directory.');
133
199
  }
134
- const sources = flags(args, 'source').map(parseSource);
135
- const designReferences = flags(args, 'reference').map(parseDesignReference);
136
- const title = flag(args, 'title');
137
- const generator = parseGenerator(flag(args, 'generator'));
138
- const projectRoot = resolve(cwd, directory);
139
- await validateProjectSourceBoundaries(projectRoot, sources);
140
- const root = await scaffoldProject({
141
- directory: projectRoot,
200
+ if (!directory)
201
+ throw new UsageError('A documentation project directory is required.');
202
+ const plan = {
203
+ directory,
142
204
  ...(title ? { title } : {}),
143
205
  sources,
206
+ generator: generator ?? 'doxbrix',
207
+ };
208
+ if (fromWizard && !(await confirmInitPlan(cwd, plan))) {
209
+ process.stdout.write('Setup canceled. No project was created.\n');
210
+ return 0;
211
+ }
212
+ const root = await initializeProject(args, cwd, plan, flags(args, 'reference').map(parseDesignReference));
213
+ if (fromWizard) {
214
+ process.stdout.write(`\nRerun this setup non-interactively:\n ${replayInitCommand(plan)}\n`);
215
+ }
216
+ process.stdout.write(`\nNext:\n cd ${directory}\n doxloop create\n doxloop preview --open\n doxloop test\n`);
217
+ return 0;
218
+ }
219
+ async function confirmInitPlan(cwd, plan) {
220
+ process.stdout.write(`\n${formatInitPlan(cwd, plan)}\n\n`);
221
+ return promptConfirm({ message: 'Create this project?', initial: true });
222
+ }
223
+ async function initializeProject(args, cwd, plan, designReferences = []) {
224
+ const projectRoot = resolve(cwd, plan.directory);
225
+ await validateProjectSourceBoundaries(projectRoot, plan.sources);
226
+ if (plan.generator !== 'doxbrix') {
227
+ await ensureGeneratorAvailable(args, projectRoot, plan.generator);
228
+ }
229
+ const root = await scaffoldProject({
230
+ directory: projectRoot,
231
+ ...(plan.title ? { title: plan.title } : {}),
232
+ sources: plan.sources,
144
233
  designReferences,
145
- ...(generator ? { generator } : {}),
234
+ generator: plan.generator,
146
235
  });
147
236
  const installs = await installSkill({ root });
148
237
  process.stdout.write(`Created Doxloop project at ${root}\n`);
149
238
  for (const install of installs) {
150
239
  process.stdout.write(`${install.action}: ${install.path}\n`);
151
240
  }
152
- process.stdout.write(`\nNext:\n cd ${directory}\n doxloop create\n doxloop preview --open\n doxloop test\n`);
153
- return 0;
241
+ return root;
242
+ }
243
+ async function ensureGeneratorAvailable(args, projectRoot, generator) {
244
+ const entry = generatorCatalogEntry(generator);
245
+ if (!entry?.packageName)
246
+ return;
247
+ if (resolveGeneratorPackage(projectRoot, entry.packageName))
248
+ return;
249
+ if (isInteractive(args)) {
250
+ const install = await promptConfirm({
251
+ message: `${entry.displayName} support needs ${entry.packageName}. Install it into the new project now?`,
252
+ initial: true,
253
+ });
254
+ if (!install) {
255
+ throw new DoxloopError(`${entry.displayName} support is not installed. Install it with \`doxloop generator add ${generator}\` inside the documentation project.`, 2);
256
+ }
257
+ }
258
+ await mkdir(projectRoot, { recursive: true });
259
+ await addGenerator(projectRoot, generator);
154
260
  }
155
261
  async function createProjectCommand(args, cwd) {
156
262
  const source = flag(args, 'source');
157
263
  const output = flag(args, 'output');
158
- if (!source || !output) {
159
- throw new UsageError('Creating a new documentation project requires both `--source <product-directory>` and `--output <documentation-directory>`.');
264
+ const specs = flags(args, 'spec').map(parseSpec);
265
+ if (!output || (!source && specs.length === 0)) {
266
+ throw new UsageError('Creating a new documentation project requires `--output <documentation-directory>` plus `--source <product-directory>`, `--spec <openapi-file-or-url>`, or both.');
160
267
  }
161
268
  if (flags(args, 'source').length > 1 || flags(args, 'output').length > 1) {
162
269
  throw new UsageError('The first-run create command accepts one source and one output directory.');
163
270
  }
164
- const layout = await resolveSeparateProjectLayout({ cwd, source, output });
165
- const requestedAgent = parseAgent(flag(args, 'agent'));
271
+ const layout = source
272
+ ? await resolveSeparateProjectLayout({ cwd, source, output })
273
+ : undefined;
274
+ const projectRoot = layout?.projectRoot ?? resolve(cwd, output);
275
+ const sources = [
276
+ ...(layout ? [layout.sourceBinding] : []),
277
+ ...specs.map((spec) => projectRelativeSpec(spec, cwd, projectRoot)),
278
+ ];
166
279
  const print = booleanFlag(args, 'print');
167
- const selectedAgent = print ? requestedAgent : (await chooseAgent(requestedAgent)).name;
168
280
  const designReferences = flags(args, 'reference').map(parseDesignReference);
169
- const model = flag(args, 'model');
170
- const reasoning = parseReasoning(flag(args, 'reasoning'));
171
- const screenshots = screenshotIntent(args);
172
- if (reasoning && selectedAgent && selectedAgent !== 'codex') {
173
- throw new DoxloopError(`--reasoning is only supported with Codex. Configure the reasoning behavior of ${selectedAgent} in its own settings.`, 2);
174
- }
175
- process.stdout.write(`Welcome to Doxloop\n\nProduct source:\n ${layout.sourceRoot}\n Read-only — product files will not be changed or deployed.\n\nDocumentation project:\n ${layout.projectRoot}\n Only this project can be previewed or deployed.\n\n`);
281
+ if (!layout)
282
+ await assertNewProjectDirectory(projectRoot);
283
+ await validateProjectSourceBoundaries(projectRoot, sources);
284
+ const sourceText = layout
285
+ ? `Product source:\n ${layout.sourceRoot}\n Read-only — product files will not be changed or deployed.\n\n`
286
+ : specs.length > 0
287
+ ? `API specification${specs.length === 1 ? '' : 's'}:\n${specs.map((spec) => ` ${spec.path}`).join('\n')}\n Read-only API evidence.\n\n`
288
+ : '';
289
+ process.stdout.write(`Welcome to Doxloop\n\n${sourceText}Documentation project:\n ${projectRoot}\n Only this project can be previewed or deployed.\n\n`);
176
290
  const root = await scaffoldProject({
177
- directory: layout.projectRoot,
178
- sources: [layout.sourceBinding],
291
+ directory: projectRoot,
292
+ sources,
179
293
  designReferences,
180
294
  });
181
295
  if (print) {
296
+ const requestedAgent = parseAgent(flag(args, 'agent'));
182
297
  await installSkill({
183
298
  root,
184
- ...(selectedAgent ? { agent: selectedAgent } : {}),
299
+ ...(requestedAgent ? { agent: requestedAgent } : {}),
185
300
  });
186
301
  }
187
- const result = await runAuthor({
188
- root,
189
- mode: 'create',
190
- ...(selectedAgent ? { agent: selectedAgent } : {}),
191
- ...(model ? { model } : {}),
192
- ...(reasoning ? { reasoning } : {}),
193
- screenshots,
194
- print,
195
- ...(args.positionals.length > 0
196
- ? { request: args.positionals.join(' ') }
197
- : {}),
198
- });
199
- if (result === 0 && !print) {
200
- const validation = await validateProject(root);
201
- process.stdout.write(`\nDocumentation created successfully.\n\nPages created: ${validation.pages.length}\nDocumentation project: ${root}\nProduct source files included in deployment: 0\n\nPreview the documentation:\n cd ${root}\n doxloop preview --open\n\nWhen the product changes:\n cd ${root}\n doxloop update\n`);
202
- }
302
+ const result = await authorCommand(args, root, 'create');
203
303
  return result;
204
304
  }
205
305
  async function authorCommand(args, cwd, mode) {
206
- const root = await findProjectRoot(cwd);
306
+ const interactive = isInteractive(args);
307
+ const print = booleanFlag(args, 'print');
308
+ let root;
309
+ try {
310
+ root = await findProjectRoot(cwd);
311
+ }
312
+ catch (error) {
313
+ if (mode === 'create' && interactive && !print) {
314
+ const plan = await runCreateRescueWizard(cwd, promptIo());
315
+ if (plan) {
316
+ if (!(await confirmInitPlan(cwd, plan))) {
317
+ process.stdout.write('Setup canceled. No project was created.\n');
318
+ return 0;
319
+ }
320
+ root = await initializeProject(args, cwd, plan);
321
+ process.stdout.write('\nSetup complete. Tell Doxloop what documentation to create.\n\n');
322
+ }
323
+ else {
324
+ throw error;
325
+ }
326
+ }
327
+ else {
328
+ throw error;
329
+ }
330
+ }
331
+ const project = await loadProject(root);
207
332
  if (mode !== 'review') {
208
- const project = await loadProject(root);
209
333
  await validateProjectSourceBoundaries(root, project.sources);
210
334
  }
211
- const selectedAgent = parseAgent(flag(args, 'agent'));
212
- const request = args.positionals.join(' ') || undefined;
335
+ let selectedAgent = parseAgent(flag(args, 'agent')) ?? project.defaultAgent;
336
+ let request = args.positionals.join(' ') || undefined;
337
+ let changeSummary;
338
+ let requestedInteractively = false;
339
+ let offerToRememberAgent = false;
340
+ if (mode === 'update') {
341
+ if (interactive && !print && !(await hasCompletedAuthoringRun(root))) {
342
+ process.stdout.write('This project has not completed its first documentation run yet.\n\nNext:\n doxloop create\n');
343
+ return 0;
344
+ }
345
+ if (project.sources.length === 0) {
346
+ if (interactive && !print) {
347
+ process.stdout.write('No product source or API specification is configured.\nUse `doxloop settings` to add evidence, or describe a documentation-only change below.\n\n');
348
+ if (!request) {
349
+ request = await promptForRequest('update', promptIo(), false);
350
+ requestedInteractively = true;
351
+ if (!request) {
352
+ process.stdout.write('No documentation change requested.\n');
353
+ return 0;
354
+ }
355
+ }
356
+ }
357
+ }
358
+ else {
359
+ const changes = await collectSourceChanges(root, project.sources);
360
+ changeSummary = formatSourceChanges(changes);
361
+ if (interactive && !print) {
362
+ process.stdout.write(`${formatSourceChangeOverview(changes)}\n\n`);
363
+ if (!hasPendingSourceChanges(changes) && !request) {
364
+ const proceed = await promptConfirm({
365
+ message: 'Documentation is synchronized. Make a documentation-only change?',
366
+ initial: false,
367
+ });
368
+ if (!proceed) {
369
+ process.stdout.write('Documentation is synchronized with the recorded source baseline.\n');
370
+ return 0;
371
+ }
372
+ }
373
+ }
374
+ }
375
+ }
376
+ if (interactive &&
377
+ !print &&
378
+ !request &&
379
+ !requestedInteractively &&
380
+ (mode === 'create' || mode === 'update')) {
381
+ request = await promptForRequest(mode, promptIo(), project.sources.length > 0);
382
+ }
383
+ if (interactive && !print && !selectedAgent) {
384
+ selectedAgent = await selectAgentInteractive(promptIo());
385
+ if (selectedAgent && mode !== 'review' && !project.defaultAgent) {
386
+ offerToRememberAgent = true;
387
+ }
388
+ }
389
+ if (interactive && !print && mode === 'create') {
390
+ process.stdout.write(`\nAuthoring summary\n\n Project: ${project.title}\n Evidence: ${project.sources.length === 0 ? 'None yet' : `${project.sources.length} configured source${project.sources.length === 1 ? '' : 's'}`}\n Generator: ${project.generator}\n Agent: ${selectedAgent ?? 'Automatically detected'}\n Request: ${request ?? 'Let the agent propose a documentation plan'}\n\n`);
391
+ const proceed = await promptConfirm({
392
+ message: 'Start creating documentation?',
393
+ initial: true,
394
+ });
395
+ if (!proceed) {
396
+ process.stdout.write('Authoring canceled. Project settings were preserved.\n');
397
+ return 0;
398
+ }
399
+ }
400
+ if (offerToRememberAgent && selectedAgent) {
401
+ const remember = await promptConfirm({
402
+ message: `Remember ${selectedAgent} as this project's default agent?`,
403
+ initial: true,
404
+ });
405
+ if (remember)
406
+ await saveDefaultAgent(root, selectedAgent);
407
+ }
213
408
  const designReferences = flags(args, 'reference').map(parseDesignReference);
214
409
  if (mode !== 'review')
215
410
  await addDesignReferences(root, designReferences);
216
411
  const model = flag(args, 'model');
217
412
  const reasoning = parseReasoning(flag(args, 'reasoning'));
218
413
  const screenshots = screenshotIntent(args);
219
- return runAuthor({
414
+ const result = await runAuthor({
220
415
  root,
221
416
  mode,
417
+ ...(changeSummary !== undefined ? { changeSummary } : {}),
222
418
  ...(selectedAgent ? { agent: selectedAgent } : {}),
223
419
  ...(model ? { model } : {}),
224
420
  ...(reasoning ? { reasoning } : {}),
@@ -237,6 +433,11 @@ async function authorCommand(args, cwd, mode) {
237
433
  }
238
434
  : {}),
239
435
  });
436
+ if (result === 0 && !print && mode === 'create') {
437
+ const validation = await validateProject(root);
438
+ process.stdout.write(`\nDocumentation created successfully.\n\nPages created: ${validation.pages.length}\nDocumentation project: ${root}\nProduct source files included in deployment: 0\n\nNext:\n cd ${root}\n doxloop preview --open\n doxloop test\n\nWhen the product changes:\n doxloop update\n`);
439
+ }
440
+ return result;
240
441
  }
241
442
  async function agentCommand(args, cwd) {
242
443
  const action = args.positionals[0];
@@ -294,6 +495,7 @@ async function generatorCommand(args, cwd) {
294
495
  async function statusCommand(cwd, format) {
295
496
  const root = await findProjectRoot(cwd);
296
497
  const project = await loadProject(root);
498
+ const deployment = effectiveDeployment(project);
297
499
  const result = await validateProject(root);
298
500
  if (format === 'json') {
299
501
  process.stdout.write(`${JSON.stringify({
@@ -304,12 +506,13 @@ async function statusCommand(cwd, format) {
304
506
  sources: project.sources,
305
507
  designReferences: project.designReferences,
306
508
  ...(project.application ? { application: project.application } : {}),
509
+ deployment,
307
510
  errors: result.errors,
308
511
  warnings: result.warnings,
309
512
  }, null, 2)}\n`);
310
513
  }
311
514
  else {
312
- process.stdout.write(`${project.title}\nRoot: ${root}\nGenerator: ${project.generator}\nPages: ${result.pages.length}\nSources: ${project.sources.length}\nDesign references: ${project.designReferences.length}\nApplication screenshots: ${project.application ? `${project.application.screenshots?.policy ?? 'requested'} (${project.application.baseUrl})` : 'not configured'}\nErrors: ${result.errors}\nWarnings: ${result.warnings}\n`);
515
+ process.stdout.write(`${project.title}\nRoot: ${root}\nGenerator: ${project.generator}\nPages: ${result.pages.length}\nSources: ${project.sources.length}\nDesign references: ${project.designReferences.length}\nApplication screenshots: ${project.application ? `${project.application.screenshots?.policy ?? 'requested'} (${project.application.baseUrl})` : 'not configured'}\nDeployment: ${deployment.slug} (${deployment.visibility}) → ${deployment.apiUrl}\nErrors: ${result.errors}\nWarnings: ${result.warnings}\n`);
313
516
  for (const source of project.sources) {
314
517
  process.stdout.write(`source ${source.name}: ${source.path}\n`);
315
518
  }
@@ -318,16 +521,17 @@ async function statusCommand(cwd, format) {
318
521
  }
319
522
  function validateCommandArguments(args) {
320
523
  const allowed = {
321
- init: ['title', 'source', 'reference', 'generator'],
524
+ init: ['title', 'source', 'spec', 'reference', 'generator'],
322
525
  agent: ['agent'],
323
526
  generator: [],
324
527
  doctor: ['source', 'output', 'agent'],
325
- create: ['agent', 'model', 'reasoning', 'reference', 'print', 'screenshots', 'no-screenshots', 'source', 'output'],
528
+ create: ['agent', 'model', 'reasoning', 'reference', 'print', 'screenshots', 'no-screenshots', 'source', 'spec', 'output'],
326
529
  update: ['agent', 'model', 'reasoning', 'reference', 'print', 'screenshots', 'no-screenshots'],
327
530
  review: ['agent', 'model', 'reasoning', 'reference', 'print'],
328
531
  capture: [],
329
532
  test: ['format'],
330
533
  status: ['format'],
534
+ settings: [],
331
535
  preview: ['host', 'port', 'open'],
332
536
  login: ['api-url', 'token'],
333
537
  logout: [],
@@ -347,6 +551,49 @@ function validateCommandArguments(args) {
347
551
  throw new UsageError(`The ${args.command} command does not accept arguments.`);
348
552
  }
349
553
  }
554
+ function promptIo() {
555
+ return { input: process.stdin, output: process.stdout };
556
+ }
557
+ function hasPendingSourceChanges(changes) {
558
+ return changes.some((change) => change.kind !== 'unchanged' && change.kind !== 'spec-unchanged');
559
+ }
560
+ function formatSourceChangeOverview(changes) {
561
+ const pending = changes.filter((change) => change.kind !== 'unchanged' && change.kind !== 'spec-unchanged');
562
+ const committedFiles = changes.reduce((total, change) => total + (change.kind === 'changed' ? change.changedFiles.length : 0), 0);
563
+ const workingTreeFiles = changes.reduce((total, change) => total +
564
+ ('uncommittedFiles' in change ? change.uncommittedFiles.length : 0), 0);
565
+ const status = pending.length === 0
566
+ ? 'Synchronized'
567
+ : `${pending.length} source${pending.length === 1 ? '' : 's'} need inspection`;
568
+ return `Source check\n\n Sources checked: ${changes.length}\n Status: ${status}\n Committed files: ${committedFiles}\n Working-tree files: ${workingTreeFiles}`;
569
+ }
570
+ async function hasCompletedAuthoringRun(root) {
571
+ try {
572
+ const receipt = JSON.parse(await readFile(join(root, '.doxloop', 'last-run.json'), 'utf8'));
573
+ return ((receipt.mode === 'create' || receipt.mode === 'update') &&
574
+ typeof receipt.completedAt === 'string');
575
+ }
576
+ catch {
577
+ try {
578
+ const syncState = JSON.parse(await readFile(join(root, '.doxloop', 'sync-state.json'), 'utf8'));
579
+ return (syncState.schemaVersion === 1 &&
580
+ syncState.sources !== null &&
581
+ typeof syncState.sources === 'object' &&
582
+ !Array.isArray(syncState.sources));
583
+ }
584
+ catch {
585
+ return false;
586
+ }
587
+ }
588
+ }
589
+ function projectRelativeSpec(spec, cwd, projectRoot) {
590
+ if (isSpecUrl(spec.path))
591
+ return spec;
592
+ return {
593
+ ...spec,
594
+ path: relative(projectRoot, resolve(cwd, spec.path)).split('\\').join('/'),
595
+ };
596
+ }
350
597
  function screenshotIntent(args) {
351
598
  if (args.command === 'review')
352
599
  return 'disabled';
@@ -361,16 +608,24 @@ function outputFormat(value) {
361
608
  }
362
609
  function help(command) {
363
610
  if (command === 'init') {
364
- return `Usage: doxloop init <directory> [options]
611
+ return `Usage: doxloop init [directory] [options]
365
612
 
366
613
  Create a local documentation project and install the authoring and format skills.
614
+ Run without arguments in a terminal to answer a short set of setup questions.
367
615
 
368
616
  Options:
369
617
  --title <title> Documentation site title
370
618
  --source <name=path> Add a local product source; may be repeated
619
+ --spec <name=file|url> Add an OpenAPI specification as API evidence; may be repeated
371
620
  --reference <url> Add a documentation design reference; may be repeated
372
621
  --generator <name> Generator: ${GENERATOR_CATALOG.map((entry) => entry.id).join(', ')}
622
+ --yes Never prompt; fail instead of asking
373
623
  --cwd <directory> Resolve paths from this directory
624
+
625
+ Examples:
626
+ doxloop init
627
+ doxloop init my-docs --source product=../my-app
628
+ doxloop init api-docs --spec https://example.com/openapi.json
374
629
  `;
375
630
  }
376
631
  if (command === 'agent') {
@@ -421,10 +676,10 @@ Options:
421
676
  --no-screenshots Do not capture application screenshots
422
677
  `;
423
678
  const createUsage = command === 'create'
424
- ? `\nCreate a separate documentation project from an existing product:\n doxloop create --source <product-directory> --output <documentation-directory> [request]\n`
679
+ ? `\nRun inside a Doxloop project to answer a short set of authoring questions.\nWhen run inside a detected product repository, Doxloop offers the complete setup\nwizard first. No flags are required for interactive use.\n\nOptional automation form:\n doxloop create --source <product-directory> --output <documentation-directory> [request]\n doxloop create --spec <openapi-file-or-url> --output <documentation-directory> [request]\n`
425
680
  : '';
426
681
  const createOptions = command === 'create'
427
- ? ` --source <directory> Read-only product source for a new documentation project\n --output <directory> New, separate documentation project directory\n`
682
+ ? ` --source <directory> Read-only product source for a new documentation project\n --spec <name=file|url> OpenAPI specification used as read-only API evidence\n --output <directory> New, separate documentation project directory\n`
428
683
  : '';
429
684
  return `Usage: doxloop ${command} [request] [options]
430
685
  ${createUsage}
@@ -473,6 +728,17 @@ ${command === 'test' ? 'Validate documentation structure and content.' : 'Summar
473
728
  Options:
474
729
  --format <text|json> Output format (default: text)
475
730
  --cwd <directory> Run from this project directory
731
+ `;
732
+ }
733
+ if (command === 'settings') {
734
+ return `Usage: doxloop settings
735
+
736
+ View or interactively change the current project's evidence, identity, default
737
+ agent, documentation preferences, design references, screenshots, and
738
+ deployment settings. When output is not a terminal, prints the saved settings.
739
+
740
+ Options:
741
+ --cwd <directory> Run from this project directory
476
742
  `;
477
743
  }
478
744
  if (command === 'login') {
@@ -511,6 +777,7 @@ Options:
511
777
  --name <name> Hosted project name
512
778
  --slug <slug> Hosted project slug
513
779
  --api-url <url> Override the Doxbrix API base URL
780
+ --yes Use saved settings without prompting
514
781
  --cwd <directory> Run from this project directory
515
782
  `;
516
783
  }
@@ -533,6 +800,7 @@ Author:
533
800
  Verify:
534
801
  doctor Check runtime, source, agent, generator, skills, and documentation
535
802
  status Summarize the documentation project
803
+ settings View or change project settings
536
804
  test Validate pages, navigation, links, and code fences
537
805
  preview Run a beautiful local preview
538
806
 
@@ -544,9 +812,15 @@ Publish:
544
812
 
545
813
  Global options:
546
814
  --cwd <directory> Run as if started in this directory
815
+ --yes Never prompt; accept safe defaults or fail instead of asking
547
816
  -h, --help Show help
548
817
  -v, --version Show version
549
818
 
819
+ Get started:
820
+ doxloop init Answer a few questions interactively
821
+ doxloop create Create docs from saved project settings
822
+ doxloop settings View or change project settings
823
+
550
824
  Run \`doxloop <command> --help\` for command details.
551
825
  `;
552
826
  }