tadforce 0.2.0 → 0.4.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.
@@ -4,14 +4,14 @@
4
4
  "name": "Jihad AZAMI HASSANI"
5
5
  },
6
6
  "metadata": {
7
- "description": "TADForce — technical architecture documents for Salesforce orgs, written by your agent from the org's own metadata."
7
+ "description": "TADForce — AI-Powered Salesforce Technical Documentation, written by your agent from the org's own metadata."
8
8
  },
9
9
  "plugins": [
10
10
  {
11
11
  "name": "tadforce",
12
12
  "source": "./",
13
- "description": "Generate, review and validate the Technical Architecture Document of any Salesforce org (/tadforce, /tadforce-review).",
14
- "version": "0.2.0",
13
+ "description": "AI-Powered Salesforce Technical Documentation: generate, review, validate and update the Technical Architecture Document of any Salesforce org (/tadforce, /tadforce-review, /tadforce-update).",
14
+ "version": "0.4.0",
15
15
  "author": { "name": "Jihad AZAMI HASSANI" },
16
16
  "license": "Apache-2.0",
17
17
  "category": "documentation"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tadforce",
3
- "version": "0.2.0",
4
- "description": "Point it at a Salesforce org, get the Technical Architecture Document a senior architect would write: evidence-based, reproducible, reviewable in the browser. All Salesforce products and industries.",
3
+ "version": "0.4.0",
4
+ "description": "AI-Powered Salesforce Technical Documentation — the Technical Architecture Document of any Salesforce org, written from the org's own metadata: evidence-based, reproducible, reviewable in the browser, kept up to date. All Salesforce products and industries.",
5
5
  "author": {
6
6
  "name": "Jihad AZAMI HASSANI",
7
7
  "url": "https://github.com/azamiji"
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <img src="assets/banner.svg" alt="TADForce — point it at a Salesforce org, get the architecture document a senior architect would write" width="100%">
4
4
 
5
- ### The architect's scribe for Salesforce orgs.
5
+ ### AI-Powered Salesforce Technical Documentation
6
6
 
7
7
  **Four commands. One org. One Technical Architecture Document — evidence-based, reproducible, reviewable in the browser.**
8
8
 
@@ -20,7 +20,7 @@
20
20
  [![Copilot](https://img.shields.io/badge/GitHub%20Copilot-skill-6E40C9)](#-quick-start)
21
21
  [![Read-only](https://img.shields.io/badge/org%20access-read--only-16A34A)](#-read-only-by-design)
22
22
 
23
- [**Live demo**](https://azamiji.github.io/TADForce/) · [Quick start](#-quick-start) · [The four commands](#-four-commands) · [Every project type](#-every-kind-of-salesforce-project) · [Français](README.fr.md)
23
+ [**Live demo**](https://azamiji.github.io/TADForce/) · [Quick start](#-quick-start) · [The four commands](#-four-commands) · [Every project type](#-every-kind-of-salesforce-project)
24
24
 
25
25
  </div>
26
26
 
@@ -47,11 +47,9 @@ From your Salesforce DX project (or an empty folder):
47
47
  npx tadforce doctor
48
48
  ```
49
49
 
50
- Then, in your AI agent (Claude Code, Codex, Cursor, Copilot…):
50
+ That one command installs everything: it downloads TADForce, installs the agent skills in your project, connects to the org, collects the metadata and asks you the few questions only a human can answer. When everything is green, it says so and **offers to hand over to your AI agent** (Claude Code, Codex, Gemini CLI or Cursor, detected on your machine), which writes, builds and validates the document — nothing else to type.
51
51
 
52
- ```text
53
- /tadforce
54
- ```
52
+ Already in your agent? Type `/tadforce`: it runs the same checks, then writes.
55
53
 
56
54
  <table>
57
55
  <tr><th width="33%">A — Claude Code plugin</th><th width="33%">B — Any agent (Agent Skills)</th><th width="33%">C — Deterministic / CI</th></tr>
@@ -140,7 +138,7 @@ The document carries **the client's logo and colours**: the logo is searched in
140
138
  | **No org access**, sources only | `--source local` documents the package directories of the project |
141
139
  | **Unlocked or managed packages** | Namespaces and installed packages are detected and documented with their evidence |
142
140
  | **Any cloud, any industry** | Sections switch on only when the product is detected, each with its writing guide (below) |
143
- | **French or English** | Document, skeletons and interface in both; Salesforce terms stay in English |
141
+ | **Any language** | English and French built in. Any other language (Spanish, German, Italian, Portuguese, Arabic, Japanese…): pick it in `doctor`, your agent translates the headings and labels once, then writes in that language. Salesforce terms stay in English |
144
142
 
145
143
  ### Products and industries
146
144
 
@@ -219,7 +217,7 @@ The TADForce CLI sends nothing anywhere: no telemetry, no server, no account. It
219
217
 
220
218
  ## 🗺️ Roadmap
221
219
 
222
- - [ ] More languages for the skeletons (es, de, pt, it)
220
+ - [ ] Built-in skeletons for more languages (es, de, pt, it) — any language already works through the agent; contributions welcome
223
221
  - [ ] Your own template: add, hide or rename chapters from `tadforce/templates/`
224
222
  - [ ] More examples: Service + Experience Cloud, an ISV package
225
223
  - [ ] Salesforce CLI plugin wrapper (`sf tad …`)
@@ -232,7 +230,17 @@ Issues and pull requests are welcome — new product modules, new extractors, ne
232
230
 
233
231
  ## 👥 Community
234
232
 
235
- TADForce was born in the **Salesforce Architect Community Group of Casablanca**. Share your generated documents (anonymised!), your module ideas and your war stories in the issues. Star the repo if TADForce saved you a week. ⭐
233
+ TADForce was born for the **Salesforce ecosystem**: architects, consultants, integrators and in-house teams who need documentation they can trust.
234
+
235
+ Your experience is what makes it better. Tell us, in the [issues](https://github.com/azamiji/TADForce/issues) and [discussions](https://github.com/azamiji/TADForce/discussions):
236
+
237
+ - **How it went** — the kind of org and project, the time it took, what worked out of the box.
238
+ - **How good the result was** — sections that read like a senior architect wrote them, and sections that did not.
239
+ - **What to improve** — a product it should detect better, a section that misses the point, a question it should have asked.
240
+
241
+ Please share your feedback, not your documents: they describe your clients' systems.
242
+
243
+ If TADForce saved you a week, **star the repo** ⭐ — it is how other Salesforce teams find it.
236
244
 
237
245
  ## 👤 Author
238
246
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tadforce",
3
- "version": "0.2.0",
4
- "description": "Point it at a Salesforce org. Get the technical architecture document a senior architect would write — evidence-based, reproducible, reviewable.",
3
+ "version": "0.4.0",
4
+ "description": "AI-Powered Salesforce Technical Documentation — the Technical Architecture Document of any Salesforce org, written from the org's own metadata: evidence-based, reproducible, reviewable in the browser, kept up to date.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "tadforce": "bin/tadforce.js"
@@ -19,7 +19,7 @@ The CLI (`npx tadforce …`) does everything deterministic: org access, retrieve
19
19
  6. **Numbers come from facts.** Write counts as `{{n:apex.classes}}`, links as `{{ref:section-id}}`. Never type a count by hand.
20
20
  7. **Client document ≠ internal review.** The TAD is factual and positive. Debt, risks and recommendations go to the Improvement Axes document.
21
21
  8. **Technical names in code font must exist** in the inventory (`tadforce validate` checks it).
22
- 9. **Write in the document language** (`tadforce.config.json › language`). Keep Salesforce terms in English (Lead, Queue, Record Type, Named Credential…).
22
+ 9. **Write in the document language** (`tadforce.config.json › language` — any language; outside English and French, translate the headings and labels once, see `references/workflow.md › Other languages`). Keep Salesforce terms in English (Lead, Queue, Record Type, Named Credential…).
23
23
  10. **Ask in batches.** Group your questions; never ask what the facts already answer.
24
24
 
25
25
  ## The run
@@ -4,23 +4,22 @@ Ask in **blocks**, each in one message, with your proposed answer for every item
4
4
 
5
5
  ## Block A — startup (before or right after authorization)
6
6
 
7
- `npx tadforce doctor --json` returns these questions with a `recommended` answer each (from the org when connected). Ask them all in one message:
7
+ `npx tadforce doctor --json` asks, in this order and one step at a time: the **project** (is this the Salesforce project to document?), the **org** (is this the org?), then these framing questions — six, each with a `recommended` answer. Ask them all in one message and let the human accept them all at once:
8
8
 
9
- 1. Document language — recommended: the org default language.
10
- 2. Client name and short code (file names) — recommended: `Organization.Name` / `<CODE>`.
11
- 3. Integrator — **optional**. Blank: the document names the client only (no empty « Integrator » line, no badge).
9
+ 1. Document language — recommended: the org default language. English and French are built in; any other language works (the headings and labels are translated once, see `workflow.md › Other languages`).
10
+ 2. Client name, as it must appear in the document — recommended: `Organization.Name`. The short code (file names) and the reference are derived from it.
11
+ 3. Integrator — **optional**. Blank: the document names the client only (no empty « Integrator » line, no badge). The Improvement Axes then carry the client's brand.
12
12
  4. Author — **optional**. Blank: no author is shown anywhere. Never put your own name or the agent's.
13
- 5. Document title — recommended: « Salesforce CRM Platform » (or the client's name for the solution).
13
+ 5. Functional specifications — **optional**: paths to PDF / DOCX / MD files (copied into `knowledge/inputs/specs/`).
14
14
  6. Scope — **optional**: name patterns of the components the project owns (`NWB_*,Loan*`), when the org is shared with other teams. Blank: the whole org.
15
- 7. Client logo — **optional**: a file or a URL. Blank: TADForce searches the org (static resources, content assets, site assets), then falls back to an initials badge. The document colours follow the org's Lightning theme, else the logo. If none is found and the client has a public website, you may *offer* to look for the logo there — fetch it only after the human says yes.
16
- 8. Functional specifications — **optional**: paths to PDF / DOCX / MD files (copied into `knowledge/inputs/specs/`).
17
- 9. Metadata — `org` (recommended: retrieve everything from the org), `local` (the sources of this project), or `both`.
18
15
 
19
- Answer with `npx tadforce doctor --json --confirm --lang fr --client "…" --short ABC --integrator none --author "…" --title "…" [--scope "…"] [--logo …] [--specs … | --no-specs] [--source org]`.
16
+ Answer with `npx tadforce doctor --json --confirm --lang fr --client "…" --integrator none --author none [--specs … | --no-specs] [--scope "…"]`. The title stays « Salesforce CRM Platform » unless the human gives one (`--title`).
17
+
18
+ Then come the metadata choice (`source`, which is also the go for the retrieve), the **branding** (logo and colours found in the org — never fetch a logo from the web without the human's go-ahead), and Blocks B to D.
20
19
 
21
20
  FR example:
22
21
 
23
- > Pour démarrer, je propose : langue **français** (langue par défaut de l'org : fr_FR) · client **« Banque Exemple »** (code `BEX`) · intégrateur : **aucun** · auteur : **aucun** · titre **« Plateforme CRM Salesforce »** · périmètre : **toute l'org** · logo : **cherché dans l'org** · spécifications fonctionnelles : **aucune** · métadonnées : **depuis l'org**. Je confirme ? Indiquez seulement ce qui change.
22
+ > Pour démarrer, je propose : langue **français** (langue par défaut de l'org : fr_FR) · client **« Banque Exemple »** · intégrateur : **aucun** · auteur : **aucun** · spécifications fonctionnelles : **aucune** · périmètre : **toute l'org**. Je valide tout ? Indiquez seulement ce qui change.
24
23
 
25
24
  ## Block B — external systems (from `facts/questions.json › systems`)
26
25
 
@@ -13,11 +13,16 @@ npx tadforce doctor --json
13
13
  | `next` says | You do |
14
14
  |---|---|
15
15
  | install the missing tools | `sf-cli` missing: you may run `npm install --global @salesforce/cli` after asking. Node or a browser missing: human action (or `npx @puppeteer/browsers install chrome-headless-shell@stable` + `TADFORCE_CHROME`) |
16
- | the human logs in | No org, or an expired session. Ask the human to run `npx tadforce doctor` in a terminal (it opens the browser) or `sf org login web --alias <a> --set-default [--instance-url https://test.salesforce.com]`. No org at all: `--source local` documents the local sources |
17
- | ask the human these questions | Block A of `questions.md`: one message, a recommendation for each. Then `npx tadforce doctor --json --confirm --lang fr --client "…" --short ABC --integrator "…"\|none --author "…"\|none --title "…" [--scope "NWB_*,Loan*"] [--logo <file\|url>] [--specs a.pdf,b.docx \| --no-specs] [--source org\|local\|both]` |
18
- | write the answers to the knowledge files | Blocks B, C and D of `questions.md` → `knowledge/glossary.yml`, `knowledge/exclusions.yml`; then `npx tadforce facts` and `npx tadforce doctor --json --facts-reviewed` |
16
+ | question `project` | Confirm with the human that this Salesforce project is the one to document → `--project yes`. Not in a project: `doctor` creates nothing; it proposes the projects found nearby → `--project <path>` (then always pass `--cwd <path>`), or `--project create` only if the human asks for a new project here |
17
+ | question `org` | Show the org (alias, user, URL) and ask whether the TAD is about it → `--org <alias>`. Another org, or none connected: the human logs in (`sf org login web --alias <a> [--instance-url https://test.salesforce.com]`, it opens the browser), then ask again and pass `--org <a>`. No org at all: `--source local` |
18
+ | framing questions | Block A of `questions.md`: one message, the recommended answer for each, « accept all » allowed → `--confirm --lang … --client "…" --integrator …|none --author …|none [--specs …|--no-specs] [--scope …]` |
19
+ | question `source` | org (retrieve everything, read-only, several minutes), local, or self (the human retrieves first). The answer **is the go**: ask before passing `--source org` — the retrieve starts right away. `self`: wait until the human says the retrieve is done, then `--source local` |
20
+ | question `branding` | Show the logo and colours found → `--branding keep`, or `--logo <file|url>` |
21
+ | systems / exclusions / features | Blocks B, C and D of `questions.md`, recommended answers shown → write `knowledge/*.yml`, `npx tadforce facts`, then `--facts-reviewed` |
19
22
  | `tadforce build` | Ready: write and build |
20
23
 
24
+ Run by a person in a terminal, a green `doctor` starts the AI agent found on the machine on `/tadforce` (`--agent claude|codex|gemini|cursor`, `--no-launch` to stop there). Run by you (`--json`, or inside an agent session), it never starts another agent: carry on with the writing.
25
+
21
26
  What `doctor` does by itself, read-only:
22
27
 
23
28
  - creates the SFDX project and the `tadforce/` workspace if missing, installs the skills for the agents found;
@@ -25,6 +30,16 @@ What `doctor` does by itself, read-only:
25
30
  - metadata, as chosen (`source`): **org** (recommended — the org is the truth) retrieves every useful type with a `*` manifest into `tadforce/.snapshot` (`force-app` is never touched; large orgs take several minutes: tell the human); **local** reads the package directories; **both** combines them. Then `facts/facts.json`, `facts/org.json` (aggregates), `facts/questions.json`, the chapter skeletons in `tadforce/src/`;
26
31
  - branding from evidence: the client logo searched in the snapshot (static resources, content assets, site assets named *logo*), the colours from the org's Lightning theme (`BrandingSet`), else from the logo. Never fetch a logo from the web without the human's go-ahead; when they agree, pass its URL with `--logo`.
27
32
 
33
+ ### Other languages
34
+
35
+ English and French are built in. Any other language (`--lang es`, `de`, `it`, `pt`, `nl`, `ar`, `ja`…) works too: the skeletons are scaffolded in English and `tadforce/i18n/<lang>.json` is created from the English labels. Once, before writing:
36
+
37
+ 1. Translate the headings of every `tadforce/src/*/*.md` file into the document language. Keep the `{#anchors}`, the `{-}` markers, the `<!-- tad:… -->` lines and the `@@…@@` directives exactly as they are.
38
+ 2. Translate every value of `tadforce/i18n/<lang>.json` (cover, tables, figures, buttons of the HTML). Keep the keys and the `{placeholders}`; set `meta.htmlLang`. Delete the `_translate` line.
39
+ 3. Write the sections in that language; Salesforce terms stay in English.
40
+
41
+ `doctor` shows a reminder until step 2 is done. To ship the language for everyone, contribute it (issue template « New language »).
42
+
28
43
  ### Optional inputs
29
44
 
30
45
  - **Functional specifications** (`--specs`): copied into `knowledge/inputs/specs/`. Read them before chapters 3 and 5; they are evidence level P2/P3 (`evidence-rules.md`). They explain the *why*; the metadata still decides the *what*.
@@ -107,7 +107,7 @@ export async function buildDocument(wsCtx, doc, opts = {}) {
107
107
  const problems = [];
108
108
  const facts = loadFacts(wsCtx.ws);
109
109
  if (!facts) throw new Error('facts/facts.json not found: run `tadforce facts` first.');
110
- const { lang, dict } = loadI18n(cfg.language);
110
+ const { lang, dict } = loadI18n(cfg.language, wsCtx.ws);
111
111
  const glossary = wsCtx.glossary() || {};
112
112
  const srcDir = wsCtx.p('src', doc);
113
113
  if (!exists(srcDir)) throw new Error(`No sources in ${srcDir}: run \`tadforce scaffold --doc ${doc}\`.`);
package/src/cli.js CHANGED
@@ -38,7 +38,7 @@ const COMMANDS = { ...MAIN, ...ADVANCED };
38
38
 
39
39
  function help(all) {
40
40
  const v = readJson(path.join(PKG_ROOT, 'package.json')).version;
41
- console.log(`${c.violet('▲')} ${c.bold('TADForce')} ${v} — technical architecture documents for Salesforce orgs. ${c.dim('No evidence, no sentence.')}\n`);
41
+ console.log(`${c.violet('▲')} ${c.bold('TADForce')} ${v} — AI-Powered Salesforce Technical Documentation. ${c.dim('No evidence, no sentence.')}\n`);
42
42
  console.log(`Usage: ${c.bold('npx tadforce <command> [options]')}\n`);
43
43
  for (const [k, [, d]] of Object.entries(MAIN)) console.log(` ${c.cyan(k.padEnd(15))} ${d}`);
44
44
  if (all) {
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { exists, writeJson, readText, copyDir, stableStringify } from '../util/fsx.js';
5
5
  import { loadWorkspace, sourceDirs } from '../util/config.js';
6
6
  import { TEMPLATES } from '../util/paths.js';
7
+ import { LANGUAGES, normalizeLang, templateLang } from '../util/i18n.js';
7
8
  import { extractFacts, readCodeAnalyzer, loadFacts } from '../facts/index.js';
8
9
  import { collectOrgFacts } from '../facts/org.js';
9
10
  import { fingerprints } from '../facts/fingerprints.js';
@@ -70,7 +71,19 @@ export function buildQuestions(f, orgFacts) {
70
71
 
71
72
  export async function scaffold(args) {
72
73
  const ctx = loadWorkspace(args.cwd || process.cwd());
73
- const lang = ctx.cfg.language === 'fr' ? 'fr' : 'en';
74
+ const lang = templateLang(ctx.cfg.language);
75
+ const target = normalizeLang(ctx.cfg.language);
76
+ // Any other language: English skeletons and labels, translated once by the agent in the workspace.
77
+ if (!LANGUAGES.includes(target)) {
78
+ const own = ctx.p('i18n', `${target}.json`);
79
+ if (!exists(own)) {
80
+ const en = JSON.parse(readText(path.join(TEMPLATES, 'i18n', 'en.json')));
81
+ en.meta.htmlLang = target;
82
+ en._translate = `Translate every value of this file into the document language (${target}); keep the keys and the {placeholders}. Then delete this line.`;
83
+ writeJson(own, en);
84
+ }
85
+ log.human(`document language "${target}": the skeletons are in English. Your agent translates the headings of src/*/*.md (keep the {#anchors}) and the labels of tadforce/i18n/${target}.json, once.`);
86
+ }
74
87
  for (const doc of docsOf(args)) {
75
88
  const src = path.join(TEMPLATES, doc, lang);
76
89
  const dst = ctx.p('src', doc);
@@ -8,11 +8,12 @@ import YAML from 'yaml';
8
8
  import { exists, readJson, readText, writeText } from '../util/fsx.js';
9
9
  import { findWorkspace, defaultConfig, DEFAULT_BRAND, loadWorkspace, sourceDirs } from '../util/config.js';
10
10
  import { CONFIG_FILE } from '../util/paths.js';
11
- import { LANGUAGES } from '../util/i18n.js';
11
+ import { LANGUAGES, normalizeLang } from '../util/i18n.js';
12
12
  import { sfVersion, sfJson, sfInteractive, soql } from '../sf/sf.js';
13
13
  import { findBrowser, closeBrowser } from '../build/browser.js';
14
14
  import { findLogoCandidates, orgThemeColors, imageColors, paletteFrom } from '../build/brand.js';
15
15
  import { log, c, setQuiet } from '../util/log.js';
16
+ import { installedAgents, insideAgent, launchAgent } from '../util/agents.js';
16
17
  import { ensureWorkspace, installSkills } from './init.js';
17
18
  import * as org from './org.js';
18
19
  import * as docs from './docs.js';
@@ -65,19 +66,25 @@ async function proposals(alias) {
65
66
  const out = {};
66
67
  try {
67
68
  const o = ((await soql('SELECT Name, IsSandbox, LanguageLocaleKey FROM Organization', alias)).records || [])[0] || {};
68
- out.client = o.Name; out.language = /^fr/i.test(o.LanguageLocaleKey || '') ? 'fr' : 'en'; out.environment = o.IsSandbox ? 'Sandbox' : 'Production';
69
+ out.client = o.Name; out.language = normalizeLang(String(o.LanguageLocaleKey || 'en').split('_')[0]); out.environment = o.IsSandbox ? 'Sandbox' : 'Production';
69
70
  } catch { /* proposals stay empty */ }
70
71
  return out;
71
72
  }
72
73
 
74
+ /** Short code for file names: initials of a multi-word name (Northwind Bank → NB), else the name cut to 8. */
75
+ export function shortCode(name) {
76
+ const words = name.normalize('NFD').replace(/[̀-ͯ]/g, '').split(/[^A-Za-z0-9]+/).filter(Boolean);
77
+ return (words.length > 1 ? words.map((w) => w[0]).join('') : words[0] || 'CLIENT').slice(0, 8).toUpperCase();
78
+ }
79
+
73
80
  const listArg = (v) => (typeof v !== 'string' ? undefined : v.split(',').map((x) => x.trim()).filter(Boolean));
74
81
 
75
82
  /** Answers given as flags (the agent's way of answering) are written to the config. */
76
83
  async function applyAnswers(ctx, args) {
77
84
  const cfg = ctx.cfg;
78
85
  const set = (cond, fn) => { if (cond) fn(); };
79
- set(args.lang, () => { cfg.language = LANGUAGES.includes(args.lang) ? args.lang : 'en'; });
80
- set(args.client, () => { cfg.client.name = String(args.client); if (!args.short && (cfg.client.short === 'CLIENT' || !cfg.client.short)) cfg.client.short = String(args.client).replace(/[^A-Za-z0-9]/g, '').slice(0, 8).toUpperCase(); });
86
+ set(args.lang, () => { cfg.language = normalizeLang(args.lang); });
87
+ set(args.client, () => { cfg.client.name = String(args.client); if (!args.short && (cfg.client.short === 'CLIENT' || !cfg.client.short)) cfg.client.short = shortCode(String(args.client)); });
81
88
  set(args.short, () => { cfg.client.short = String(args.short); });
82
89
  set(args.integrator !== undefined, () => { cfg.integrator.name = args.integrator === true || args.integrator === '' || args.integrator === 'none' ? null : String(args.integrator); });
83
90
  set(args.author !== undefined, () => { cfg.document.author = args.author === true || args.author === '' || args.author === 'none' ? null : String(args.author); });
@@ -103,6 +110,8 @@ async function applyAnswers(ctx, args) {
103
110
  if (args.specs !== undefined) cfg.setup.specsAsked = true; // --no-specs: none provided
104
111
  if (args.confirm || args.yes) cfg.setup.confirmed = true;
105
112
  if (args.factsReviewed) cfg.setup.factsReviewed = true;
113
+ if (args.branding === 'keep') cfg.setup.brandingConfirmed = true;
114
+ if (cfg.document.reference === 'TAD-CLIENT-CRM' && cfg.client.short !== 'CLIENT') cfg.document.reference = `TAD-${cfg.client.short}-CRM`;
106
115
  ctx.save(cfg);
107
116
  }
108
117
 
@@ -159,34 +168,13 @@ async function brand(ctx) {
159
168
  return notes;
160
169
  }
161
170
 
162
- /** Questions only a human can answer, each with a recommended answer. */
163
- function framingQuestions(cfg, prop, alias, hasLocal) {
164
- const q = [];
165
- const ask = (id, question, recommended, extra = {}) => q.push({ id, question, recommended, ...extra });
166
- if (!cfg.setup.confirmed) {
167
- ask('lang', 'Document language', prop.language || cfg.language, { options: LANGUAGES });
168
- ask('client', 'Client name, as it must appear in the document', prop.client || cfg.client.name);
169
- ask('short', 'Client short name (file names, references)', cfg.client.short !== 'CLIENT' ? cfg.client.short : String(prop.client || cfg.client.name).replace(/[^A-Za-z0-9]/g, '').slice(0, 8).toUpperCase());
170
- ask('integrator', 'Integrator, if any (blank: the document only names the client)', cfg.integrator.name || '', { optional: true });
171
- ask('author', 'Author of the document (blank: no author shown)', cfg.document.author || '', { optional: true });
172
- ask('title', 'Document title', cfg.document.title);
173
- ask('scope', 'Part of the org to document — name patterns such as NWB_*,Loan* (blank: the whole org)', (cfg.scope.include || []).join(','), { optional: true });
174
- ask('logo', `Client logo — a file or a URL${cfg.client.logo ? ` (current: ${cfg.client.logo})` : ' (blank: searched in the org, then a badge with initials)'}`, cfg.client.logo || '', { optional: true });
175
- }
176
- if (!cfg.setup.specsAsked && !(cfg.inputs.specs || []).length) ask('specs', 'Functional specifications or other project documents (PDF, DOCX, MD) — comma-separated paths (blank: none)', '', { optional: true });
177
- if (!cfg.source.mode) {
178
- ask('source', 'Metadata to document', alias ? 'org' : 'local', { options: ['org', 'local', 'both'], help: 'org: retrieve every metadata type from the org (recommended — the org is the truth); local: the force-app sources already here' + (hasLocal ? '' : ' (none found)') + '; both: org first, local sources added' });
179
- }
180
- return q;
181
- }
182
-
183
171
  /** Systems to name and exclusions to confirm, from facts/questions.json. */
184
172
  function factQuestions(ctx) {
185
173
  const p = ctx.p('facts', 'questions.json');
186
174
  if (!exists(p) || ctx.cfg.setup.factsReviewed) return [];
187
175
  const qs = readJson(p);
188
176
  const out = [];
189
- for (const s of qs.systems || []) out.push({ id: `system:${s.key}`, kind: 'system', question: `Which system is ${s.key}? (name as the client calls it; blank: to be confirmed)`, endpoints: s.endpoints, writeTo: 'knowledge/glossary.yml › systems' });
177
+ for (const s of qs.systems || []) out.push({ id: `system:${s.key}`, kind: 'system', question: `Which system is ${s.key}? (name as the client calls it)`, recommended: '', endpoints: s.endpoints, writeTo: 'knowledge/glossary.yml › systems' });
190
178
  for (const g of (qs.exclusions && qs.exclusions.groups) || []) out.push({ id: `exclude:${g.reason}`, kind: 'exclusion', question: `${g.count} component(s): ${g.reason}. Exclude from the document? (all / none / list of names)`, recommended: 'none', items: g.items, writeTo: 'knowledge/exclusions.yml › exclude' });
191
179
  out.push({ id: 'features', kind: 'features', question: 'Any feature decommissioned or switched off that the document must not describe? (blank: none)', recommended: '', writeTo: 'knowledge/exclusions.yml › features' });
192
180
  return out;
@@ -203,6 +191,10 @@ function editYaml(file, key, items) {
203
191
  /** Interactive answers to the facts questions, written to the knowledge files. */
204
192
  async function askFacts(ctx, rl, questions) {
205
193
  const systems = [], exclude = [], features = [];
194
+ log.step('What the metadata cannot tell:');
195
+ questions.forEach((q, i) => console.log(` ${i + 1}. ${q.question}`));
196
+ console.log(` ${c.dim('recommended: systems « to be confirmed » in the document, nothing excluded, no decommissioned feature')}`);
197
+ if (yes((await rl.question(' Accept the recommended answers? [Y/n] ')).trim())) return 0;
206
198
  for (const q of questions) {
207
199
  const a = (await rl.question(` ${q.question}${q.items ? '\n ' + c.dim(q.items.slice(0, 12).join(', ') + (q.items.length > 12 ? ` … +${q.items.length - 12}` : '')) : ''}${q.endpoints ? '\n ' + c.dim(q.endpoints.join(', ')) : ''}\n > `)).trim();
208
200
  if (!a) continue;
@@ -219,140 +211,320 @@ async function askFacts(ctx, rl, questions) {
219
211
  return systems.length + exclude.length + features.length;
220
212
  }
221
213
 
214
+ /** Framing questions (5 to 6), each with a recommended answer. Title and short code are derived. */
215
+ function framingQuestions(cfg, prop) {
216
+ if (cfg.setup.confirmed) return [];
217
+ const q = [];
218
+ const ask = (id, question, recommended, extra = {}) => q.push({ id, question, recommended, ...extra });
219
+ ask('lang', `Document language (${LANGUAGES.join(', ')} built in, or any code: es, de, it, pt, ar…)`, prop.language || cfg.language, { options: [...LANGUAGES, 'other'] });
220
+ ask('client', 'Client name, as it must appear in the document', prop.client || (cfg.client.name !== 'Client' ? cfg.client.name : ''));
221
+ ask('integrator', 'Integrator (blank: the document only names the client)', cfg.integrator.name || '', { optional: true });
222
+ ask('author', 'Author of the document (blank: no author shown)', cfg.document.author || '', { optional: true });
223
+ ask('specs', 'Functional specifications to use, comma-separated paths (blank: none)', (cfg.inputs.specs || []).join(','), { optional: true });
224
+ ask('scope', 'Part of the org to document, name patterns such as NWB_* (blank: the whole org)', (cfg.scope.include || []).join(','), { optional: true });
225
+ return q;
226
+ }
227
+
228
+ /** Interactive batch: the list with its recommendations, then « accept all » or one by one. */
229
+ async function askBatch(rl, title, questions) {
230
+ log.step(title);
231
+ questions.forEach((q, i) => console.log(` ${i + 1}. ${q.question}\n ${c.dim('recommended:')} ${q.recommended === '' || q.recommended == null ? c.dim('(blank)') : c.bold(String(q.recommended))}`));
232
+ const all = (await rl.question(' Accept all the recommended answers? [Y/n] ')).trim();
233
+ const answers = {};
234
+ if (/^(|y|o|yes|oui)$/i.test(all)) { for (const q of questions) answers[q.id] = q.recommended ?? ''; return answers; }
235
+ for (const q of questions) {
236
+ const a = (await rl.question(` ${q.question}${q.options ? ` (${q.options.join('/')})` : ''}${q.recommended ? ` [${q.recommended}]` : ''}: `)).trim();
237
+ answers[q.id] = a || q.recommended || '';
238
+ if (q.id === 'lang' && /^(other|autre)$/i.test(a)) answers.lang = (await rl.question(' Language code (es, de, it, pt, nl, ar, ja…): ')).trim() || 'en';
239
+ }
240
+ return answers;
241
+ }
242
+
243
+ const yes = (a, def = true) => (a === '' ? def : /^(y|o|yes|oui)$/i.test(a));
244
+
245
+ /** Salesforce DX projects in this folder's sub-folders and next to it (two levels), for the user to pick from. */
246
+ function findProjects(start) {
247
+ const out = new Set();
248
+ const visit = (dir, depth) => {
249
+ let entries = [];
250
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
251
+ if (entries.some((e) => e.isFile() && e.name === 'sfdx-project.json')) out.add(dir);
252
+ if (depth <= 0) return;
253
+ for (const e of entries) if (e.isDirectory() && !/^(node_modules|\.|force-app$)/.test(e.name)) visit(path.join(dir, e.name), depth - 1);
254
+ };
255
+ visit(start, 2);
256
+ visit(path.dirname(start), 1);
257
+ out.delete(start);
258
+ return [...out].slice(0, 9);
259
+ }
260
+
261
+ const hasLocalSources = (project) => (readJson(path.join(project, 'sfdx-project.json')).packageDirectories || [])
262
+ .some((d) => exists(path.join(project, d.path)) && fs.readdirSync(path.join(project, d.path), { recursive: true }).some((f) => String(f).endsWith('-meta.xml')));
263
+
264
+ /**
265
+ * Step 2 — which Salesforce project. Never creates anything without a yes. Returns the project folder,
266
+ * or { stop } with the question to hand back.
267
+ */
268
+ async function resolveProject(start, args, rl) {
269
+ const ws = findWorkspace(start);
270
+ const confirmed = ws && defaultConfig(readJson(path.join(ws, CONFIG_FILE))).setup.projectConfirmed;
271
+ if (exists(path.join(start, 'sfdx-project.json'))) {
272
+ if (confirmed || args.project === 'yes' || args.yes) return start;
273
+ const name = readJson(path.join(start, 'sfdx-project.json')).name || path.basename(start);
274
+ if (!rl) return { stop: { id: 'project', question: `Generate the TAD for the Salesforce project "${name}" (${start})?`, recommended: 'yes', options: ['yes', '<path of another project>'] } };
275
+ if (yes((await rl.question(` Salesforce project found: ${c.bold(name)} (${start}).\n Generate the TAD for this project? [Y/n] `)).trim())) return start;
276
+ const other = (await rl.question(' Path of the project to document (blank: stop): ')).trim();
277
+ return other ? resolveProject(path.resolve(other), { ...args, project: 'yes' }, rl) : { stop: null };
278
+ }
279
+ if (args.project === 'create') return start;
280
+ if (args.project && args.project !== 'yes') return resolveProject(path.resolve(String(args.project)), { ...args, project: 'yes' }, rl);
281
+ const found = findProjects(start);
282
+ if (!rl) return { stop: { id: 'project', question: `No Salesforce DX project in ${start}. Which project should the TAD document? A path, or "create" to create a new project here.`, recommended: found[0] || '', options: [...found, 'create'] } };
283
+ console.log(` ${c.yellow('⚠')} ${start} is not a Salesforce DX project (no sfdx-project.json).`);
284
+ found.forEach((p, i) => console.log(` ${i + 1}. ${p}`));
285
+ console.log(` ${c.dim('or a path · c = create a new project here · Enter = stop')}`);
286
+ const a = (await rl.question(` Project to document${found.length ? ' [1]' : ''}: `)).trim();
287
+ if (!a && found.length) return found[0];
288
+ if (/^\d+$/.test(a) && found[Number(a) - 1]) return found[Number(a) - 1];
289
+ if (/^c$/i.test(a)) return start;
290
+ if (a) return resolveProject(path.resolve(a), { ...args, project: 'yes' }, rl);
291
+ console.log(' Open your Salesforce project folder (cd <path>), then run tadforce doctor again.');
292
+ return { stop: null };
293
+ }
294
+
295
+ /**
296
+ * Step 3 — which org. The default org is proposed, never taken silently; « no » opens the Salesforce
297
+ * login and asks again once an org is connected. Returns { ok } or { stop } with the question to hand back.
298
+ */
299
+ async function confirmOrg(project, ctx, args, rl) {
300
+ const cfg = ctx.cfg;
301
+ if (cfg.source.mode === 'local') return { ok: true };
302
+ const explicit = typeof args.org === 'string' ? args.org : null;
303
+ let alias = explicit || (cfg.setup.orgConfirmed ? cfg.org.alias : await resolveAlias(project, {}, { org: {} }));
304
+ let status = alias ? await orgStatus(alias) : { ok: false, reason: 'none' };
305
+ const keep = () => { cfg.org.alias = alias; cfg.setup.orgConfirmed = true; ctx.save(cfg); return { ok: true }; };
306
+ if (status.ok && (explicit || cfg.setup.orgConfirmed)) return keep();
307
+ const describe = () => `${c.bold(alias)} (${status.username}, ${status.instanceUrl})`;
308
+ if (!rl) {
309
+ return { stop: alias && status.ok
310
+ ? { id: 'org', question: `Generate the TAD from the org "${alias}" (${status.username}, ${status.instanceUrl})?`, recommended: alias, options: [alias, 'another org: the human runs `sf org login web --alias <a>` (opens the browser), then --org <a>', 'local: document the local sources only (--source local)'] }
311
+ : { id: 'org', question: alias ? `The org "${alias}" is not connected (session expired). The human logs in again: \`sf org login web --alias ${alias}\`, then --org ${alias}.` : 'No org connected. The human logs in: `sf org login web --alias <a>` (opens the browser), then --org <a>; or --source local.', recommended: alias || null, human: true } };
312
+ }
313
+ for (let attempt = 0; attempt < 4; attempt++) {
314
+ if (alias && status.ok) {
315
+ const a = (await rl.question(` ${attempt ? 'Connected' : 'Default org'}: ${describe()}\n Generate the TAD from this org? [Y/n/l = local sources only] `)).trim();
316
+ if (yes(a)) return keep();
317
+ if (/^l/i.test(a)) { cfg.source.mode = 'local'; ctx.save(cfg); return { ok: true }; }
318
+ } else if (alias) console.log(` ${c.yellow('⚠')} The org "${alias}" is not connected (session expired): log in again.`);
319
+ // « no », or no usable org: open the Salesforce CLI login and wait for it.
320
+ const def = alias && !status.ok ? alias : 'tad-org';
321
+ const next = (await rl.question(` Alias for the org to connect [${def}]: `)).trim() || def;
322
+ const sandbox = yes((await rl.question(' Is it a sandbox? [y/N] ')).trim(), false);
323
+ log.human('a browser window opens: log in to the org, then click « Allow ». Waiting for the login…');
324
+ if (!sfInteractive(['org', 'login', 'web', '--alias', next, ...(sandbox ? ['--instance-url', 'https://test.salesforce.com'] : [])])) { log.warn('Login did not complete.'); continue; }
325
+ alias = next; status = await orgStatus(alias);
326
+ }
327
+ return { stop: null };
328
+ }
329
+
330
+ /** Step 5 — which metadata, and the go before a long retrieve. Returns { mode } or { stop }. */
331
+ async function chooseSource(project, ctx, args, rl) {
332
+ const cfg = ctx.cfg;
333
+ const local = hasLocalSources(project);
334
+ // local sources need no go: there is no long retrieve
335
+ if (cfg.source.mode === 'local' || (cfg.source.mode && cfg.setup.sourceGo)) return { mode: cfg.source.mode };
336
+ if (typeof args.source === 'string') { cfg.source.mode = args.source === 'self' ? 'local' : args.source; cfg.setup.sourceGo = args.source !== 'self'; ctx.save(cfg); if (args.source !== 'self') return { mode: cfg.source.mode }; }
337
+ const org = cfg.source.mode !== 'local' && cfg.org.alias;
338
+ const options = [
339
+ ...(org ? [{ id: 'org', text: `retrieve all the metadata of ${cfg.org.alias}, read-only (several minutes)` }] : []),
340
+ ...(local ? [{ id: 'local', text: 'use only the metadata already in this project (force-app…)' }] : []),
341
+ { id: 'self', text: 'you retrieve the metadata yourself first, then give the go' },
342
+ ];
343
+ const recommended = org ? 'org' : local ? 'local' : 'self';
344
+ if (!rl) return { stop: { id: 'source', question: 'Which metadata should the TAD be written from? (--source answers it and is the go: the retrieve starts right away)', recommended, options: options.map((o) => `${o.id}: ${o.text}`) } };
345
+ log.step('Metadata for the document:');
346
+ options.forEach((o, i) => console.log(` ${i + 1}. ${o.text}${o.id === recommended ? c.dim(' (recommended)') : ''}`));
347
+ const a = (await rl.question(` Your choice [${options.findIndex((o) => o.id === recommended) + 1}]: `)).trim();
348
+ const pick = options[Number(a) - 1] || options.find((o) => o.id === recommended);
349
+ if (pick.id === 'self') {
350
+ console.log(` Retrieve the metadata into this project, for example:\n ${c.cyan('sf project retrieve start --manifest manifest/package.xml')}`);
351
+ const done = (await rl.question(' Press Enter when your retrieve is done (s = stop for now): ')).trim();
352
+ if (/^s/i.test(done)) return { stop: null };
353
+ cfg.source.mode = 'local'; cfg.setup.sourceGo = true; ctx.save(cfg);
354
+ return { mode: 'local' };
355
+ }
356
+ if (pick.id === 'org' && !yes((await rl.question(' The retrieve takes several minutes, read-only. Start now? [Y/n] ')).trim())) {
357
+ console.log(' Run tadforce doctor again when you are ready: your answers are kept.');
358
+ return { stop: null };
359
+ }
360
+ cfg.source.mode = pick.id; cfg.setup.sourceGo = true; ctx.save(cfg);
361
+ return { mode: pick.id };
362
+ }
363
+
364
+ /** Step 6a — logo and colours found from evidence, confirmed in one question. */
365
+ async function confirmBranding(ctx, args, rl, say) {
366
+ for (const n of await brand(ctx)) say(() => log.ok(n));
367
+ const cfg = loadWorkspace(ctx.project).cfg;
368
+ if (cfg.setup.brandingConfirmed) return null;
369
+ const logo = cfg.client.logo || 'none (a badge with the initials)';
370
+ const colours = cfg.brand.source ? `${cfg.brand.gradient[0]} (from the ${cfg.brand.source})` : 'TADForce default colours';
371
+ if (!rl) return { id: 'branding', question: `Branding: logo ${logo}; colours ${colours}. Keep it? (--branding keep, or --logo <file|url>)`, recommended: 'keep' };
372
+ const a = (await rl.question(` Logo: ${c.bold(logo)} · colours: ${c.bold(colours)}\n Keep it? [Y / path or URL of another logo] `)).trim();
373
+ const c2 = loadWorkspace(ctx.project);
374
+ if (a && !yes(a, false) && !/^n/i.test(a)) {
375
+ try { c2.cfg.client.logo = await importAsset(c2, a, 'client-logo'); c2.save(c2.cfg); for (const n of await brand(loadWorkspace(ctx.project))) say(() => log.ok(n)); } catch (e) { log.warn(e.message); }
376
+ }
377
+ const c3 = loadWorkspace(ctx.project); c3.cfg.setup.brandingConfirmed = true; c3.save(c3.cfg);
378
+ return null;
379
+ }
380
+
222
381
  /** One pass: where the workspace stands, and what remains. */
223
382
  async function evaluate(project, args) {
224
383
  const tools = await checks(project);
225
384
  const ws = findWorkspace(project);
226
385
  const cfg = ws ? defaultConfig(readJson(path.join(ws, CONFIG_FILE))) : defaultConfig();
227
- const alias = tools.find((t) => t.id === 'sf-cli').ok ? await resolveAlias(project, args, cfg) : null;
228
- const status = alias ? await orgStatus(alias) : { ok: false, reason: 'none' };
229
386
  const list = [...tools];
230
387
  const add = (id, ok, message, fix = null, human = false, level = ok ? 'ok' : 'error') => list.push({ id, ok, level, message, fix, human });
231
- add('workspace', !!ws, ws ? `Workspace: ${path.relative(project, ws) || '.'}` : 'No TADForce workspace yet', ws ? null : 'created by doctor', false, ws ? 'ok' : 'warn');
232
- const wantOrg = cfg.source.mode !== 'local';
233
- if (wantOrg) {
234
- if (status.ok) add('org', true, `Org: ${alias} (${status.instanceUrl})`);
235
- else if (status.reason === 'none') add('org', false, 'No org connected to this project', 'log in once in the browser (doctor opens it), or `--source local` to document the local sources only', true);
236
- else add('org', false, `Org "${alias}" is not connected (${status.detail || 'session expired'})`, 'log in again in the browser (doctor opens it)', true);
388
+ if (ws) add('workspace', true, `Workspace: ${path.relative(project, ws) || '.'}`);
389
+ if (cfg.source.mode !== 'local' && cfg.setup.orgConfirmed && cfg.org.alias) {
390
+ const status = await orgStatus(cfg.org.alias);
391
+ add('org', status.ok, status.ok ? `Org: ${cfg.org.alias} (${status.instanceUrl})` : `Org "${cfg.org.alias}" is not connected`, status.ok ? null : 'log in again (doctor opens the browser)', !status.ok);
237
392
  }
238
- let facts = false, snapshot = false;
393
+ let facts = false;
239
394
  if (ws) {
240
395
  const ctx = loadWorkspace(project);
241
- snapshot = exists(ctx.p(ctx.cfg.source.dir));
242
396
  facts = exists(ctx.p('facts', 'facts.json'));
243
- add('metadata', facts, facts ? `Metadata: ${snapshot ? 'retrieved from the org' : 'local sources'} → facts/facts.json` : 'Metadata not collected yet', facts ? null : 'doctor retrieves it once the questions are answered');
397
+ const lang = normalizeLang(ctx.cfg.language);
398
+ const own = ctx.p('i18n', `${lang}.json`);
399
+ if (!LANGUAGES.includes(lang) && exists(own) && readJson(own)._translate) add('language', true, `Document language "${lang}": headings and labels still to translate (once)`, `the agent translates the headings of src/*/*.md (keep the {#anchors}) and the values of tadforce/i18n/${lang}.json, then removes its "_translate" line`, false, 'warn');
400
+ if (facts) add('metadata', true, `Metadata: ${exists(ctx.p(ctx.cfg.source.dir)) && ctx.cfg.source.mode !== 'local' ? 'retrieved from the org' : 'local sources'} → facts/facts.json`);
244
401
  }
245
- return { tools, ws, cfg, alias, status, list, facts, snapshot };
402
+ return { tools, ws, cfg, list, facts };
246
403
  }
247
404
 
248
405
  export async function run(args) {
249
- const project = path.resolve(args.cwd || process.cwd());
406
+ const start = path.resolve(args.cwd || process.cwd());
250
407
  const interactive = process.stdin.isTTY && !args.json && !args.yes;
251
408
  const rl = interactive ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;
252
409
  const say = (fn) => { if (!args.json) fn(); };
253
410
  if (args.json) setQuiet(true);
254
411
  say(() => log.banner('doctor'));
255
412
  try {
256
- for (let round = 1; round <= 8; round++) {
257
- let st = await evaluate(project, args);
258
- // 1. Tools: TADForce installs what it may, the rest is a human action.
259
- const sfMissing = st.tools.find((t) => t.id === 'sf-cli' && !t.ok);
260
- if (sfMissing && rl && /^(y|o|yes|oui|)$/i.test((await rl.question(` Salesforce CLI missing. Install it now with npm? [Y/n] `)).trim())) {
261
- spawnSync('npm', ['install', '--global', '@salesforce/cli'], { stdio: 'inherit', shell: process.platform === 'win32' });
262
- continue;
263
- }
264
- const blocking = st.tools.filter((t) => t.level === 'error');
265
- if (blocking.length) return finish(args, st, [], [], 'install the missing tools, then run tadforce doctor again');
266
-
267
- // 2. Workspace (with the skills for the agents of this project)
268
- if (!st.ws) {
269
- await quietly(args.json, () => { ensureWorkspace(project, defaultConfig({ org: { alias: st.alias || null } })); installSkills(project, null); });
270
- say(() => log.ok('Workspace created: tadforce/ (skills installed for your agents)'));
271
- st = await evaluate(project, args);
272
- }
273
- const ctx = loadWorkspace(project);
274
- ctx.cfg.setup = ctx.cfg.setup || {};
275
- const hasLocal = (readJson(path.join(project, 'sfdx-project.json')).packageDirectories || []).some((d) => exists(path.join(project, d.path)) && fs.readdirSync(path.join(project, d.path), { recursive: true }).some((f) => String(f).endsWith('-meta.xml')));
276
- await applyAnswers(ctx, args);
277
-
278
- // 3. Org connection: reuse the project's target-org, log in only when needed.
279
- if (ctx.cfg.source.mode !== 'local' && !st.status.ok) {
280
- if (!rl) return finish(args, st, [], [], 'the human logs in: run `tadforce doctor` in a terminal (it opens the browser), or `sf org login web --alias <alias> --set-default`');
281
- if (hasLocal && /^l/i.test((await rl.question(' No org connected. Log in now, or document the local sources only? [Login/local] ')).trim())) { ctx.cfg.source.mode = 'local'; ctx.save(ctx.cfg); continue; }
282
- const def = st.alias || 'tadforce-org';
283
- const alias = (await rl.question(` Org alias to log in [${def}]: `)).trim() || def;
284
- const sandbox = /^(y|o|yes|oui)$/i.test((await rl.question(' Is it a sandbox? [y/N] ')).trim());
285
- log.human('a browser window opens: log in to the org, then click « Allow ».');
286
- if (sfInteractive(['org', 'login', 'web', '--alias', alias, '--set-default', ...(sandbox ? ['--instance-url', 'https://test.salesforce.com'] : [])])) { ctx.cfg.org.alias = alias; ctx.save(ctx.cfg); }
287
- continue;
288
- }
289
- if (st.alias && ctx.cfg.org.alias !== st.alias && st.status.ok) { ctx.cfg.org.alias = st.alias; ctx.save(ctx.cfg); }
290
-
291
- // 4. Framing questions (one batch, each with a recommendation)
292
- const prop = st.status.ok ? await proposals(st.alias) : {};
293
- const framing = framingQuestions(ctx.cfg, prop, st.status.ok ? st.alias : null, hasLocal);
294
- if (framing.length) {
295
- if (!rl) return finish(args, st, framing, [], 'ask the human these questions in one message, then run `tadforce doctor --json --confirm --<id> "<answer>"…`');
296
- log.step('A few facts only you can confirm (Enter keeps the recommendation):');
297
- const answers = {};
298
- for (const q of framing) {
299
- const a = (await rl.question(` ${q.question}${q.options ? ` (${q.options.join('/')})` : ''}${q.recommended ? ` [${q.recommended}]` : ''}: `)).trim();
300
- answers[q.id] = a || q.recommended || (q.optional ? '' : undefined);
301
- }
302
- const flags = { confirm: true, specs: answers.specs || false };
303
- for (const [k, v] of Object.entries(answers)) if (v !== undefined) flags[k === 'logo' ? 'logo' : k] = v === '' && ['integrator', 'author'].includes(k) ? 'none' : v === '' ? undefined : v;
304
- await applyAnswers(loadWorkspace(project), flags);
305
- continue;
306
- }
413
+ // 1. Tools: TADForce installs what it may, the rest is a human action.
414
+ let st = await evaluate(start, args);
415
+ if (st.tools.find((t) => t.id === 'sf-cli' && !t.ok) && rl && yes((await rl.question(' Salesforce CLI missing. Install it now with npm? [Y/n] ')).trim())) {
416
+ spawnSync('npm', ['install', '--global', '@salesforce/cli'], { stdio: 'inherit', shell: process.platform === 'win32' });
417
+ st = await evaluate(start, args);
418
+ }
419
+ if (st.tools.some((t) => t.level === 'error' && t.id !== 'sfdx-project')) return finish(args, st, [], 'install the missing tools, then run tadforce doctor again');
307
420
 
308
- // 5. Metadata: retrieve from the org (curated `*` manifest) and/or read the local sources, then facts.
309
- const mode = ctx.cfg.source.mode || 'org';
310
- if (!st.facts || args.refresh) {
311
- const a = { ...args, cwd: project, org: mode === 'local' ? false : ctx.cfg.org.alias, quiet: args.json };
312
- if (mode !== 'local') {
313
- say(() => log.step('Retrieving the metadata from the org (read-only, into tadforce/.snapshot)…'));
314
- for (const fn of [org.manifest, org.retrieve]) { const code = await quietly(args.json, () => fn(a)); if (code) return finish(args, st, [], [], 'retrieve failed: see the messages above'); }
315
- }
316
- await quietly(args.json, () => docs.facts(a));
317
- await quietly(args.json, () => docs.scaffold(a));
318
- args.refresh = false;
319
- continue;
320
- }
421
+ // 2. The Salesforce project the TAD is about (confirmed, never created without a yes).
422
+ const p = await resolveProject(start, args, rl);
423
+ if (typeof p !== 'string') return p.stop ? finish(args, st, [p.stop], 'ask the human, then run `tadforce doctor --json --project <yes|path|create>`') : 1;
424
+ const project = p;
425
+ if (project !== start) say(() => log.ok(`Project: ${project}`));
426
+ if (!findWorkspace(project)) {
427
+ await quietly(args.json, () => { ensureWorkspace(project, defaultConfig()); installSkills(project, null); });
428
+ say(() => log.ok('Workspace created: tadforce/ (skills installed for your agents)'));
429
+ }
430
+ let ctx = loadWorkspace(project);
431
+ ctx.cfg.setup.projectConfirmed = true; ctx.save(ctx.cfg);
432
+ await applyAnswers(ctx, args);
433
+ ctx = loadWorkspace(project);
434
+
435
+ // 3. The org: the default one is proposed; « no » opens the login and asks again.
436
+ const o = await confirmOrg(project, ctx, args, rl);
437
+ if (!o.ok) return o.stop ? finish(args, await evaluate(project, args), [o.stop], 'ask the human, then run `tadforce doctor --json --org <alias>` (or --source local)') : 1;
438
+ ctx = loadWorkspace(project);
321
439
 
322
- // 6. Branding from evidence
323
- for (const n of await brand(loadWorkspace(project))) say(() => log.ok(n));
324
-
325
- // 7. Facts questions (systems, exclusions, decommissioned features)
326
- const fq = factQuestions(loadWorkspace(project));
327
- if (fq.length) {
328
- if (!rl) return finish(args, st, [], fq, 'ask the human, write the answers to the knowledge files, then run `tadforce doctor --json --facts-reviewed`');
329
- log.step('What the metadata cannot tell (Enter: skip / to be confirmed):');
330
- const changed = await askFacts(loadWorkspace(project), rl, fq);
331
- const c2 = loadWorkspace(project); c2.cfg.setup.factsReviewed = true; c2.save(c2.cfg);
332
- if (changed) await quietly(args.json, () => docs.facts({ cwd: project, org: c2.cfg.source.mode === 'local' ? false : c2.cfg.org.alias }));
333
- continue;
440
+ // 4. Framing: 6 questions, each with a recommended answer; « accept all » in one keystroke.
441
+ const prop = ctx.cfg.org.alias && ctx.cfg.source.mode !== 'local' ? await proposals(ctx.cfg.org.alias) : {};
442
+ const framing = framingQuestions(ctx.cfg, prop);
443
+ if (framing.length) {
444
+ if (!rl) return finish(args, await evaluate(project, args), framing, 'ask the human in one message (recommended answers shown; « accept all » is fine), then run `tadforce doctor --json --confirm --lang … --client "…" --integrator …|none --author …|none [--specs …|--no-specs] [--scope …]`');
445
+ const a = await askBatch(rl, 'A few facts only you can confirm:', framing);
446
+ await applyAnswers(loadWorkspace(project), {
447
+ confirm: true, lang: a.lang, client: a.client || undefined,
448
+ integrator: a.integrator || 'none', author: a.author || 'none',
449
+ specs: a.specs || false, scope: a.scope || undefined,
450
+ });
451
+ ctx = loadWorkspace(project);
452
+ }
453
+
454
+ // 5. Metadata: org, local or retrieved by the user — and a go before a long retrieve.
455
+ if (!exists(ctx.p('facts', 'facts.json')) || args.refresh) {
456
+ const s = await chooseSource(project, ctx, args, rl);
457
+ if (!s.mode) return s.stop ? finish(args, await evaluate(project, args), [s.stop], 'ask the human; the answer is the go: `tadforce doctor --json --source org|local|self`') : 1;
458
+ const a = { ...args, cwd: project, org: s.mode === 'local' ? false : ctx.cfg.org.alias, quiet: args.json };
459
+ if (s.mode !== 'local') {
460
+ say(() => log.step('Retrieving the metadata from the org (read-only, into tadforce/.snapshot)…'));
461
+ for (const fn of [org.manifest, org.retrieve]) { const code = await quietly(args.json, () => fn(a)); if (code) return finish(args, await evaluate(project, args), [], 'the retrieve failed: see the messages above'); }
334
462
  }
463
+ await quietly(args.json, () => docs.facts(a));
464
+ await quietly(args.json, () => docs.scaffold(a));
465
+ ctx = loadWorkspace(project);
466
+ }
335
467
 
336
- st = await evaluate(project, args);
337
- return finish(args, st, [], [], null);
468
+ // 6. Branding (logo, colours) and what the metadata cannot tell — recommended answers again.
469
+ const b = await confirmBranding(ctx, args, rl, say);
470
+ if (b) return finish(args, await evaluate(project, args), [b], 'ask the human, then run `tadforce doctor --json --branding keep` (or --logo <file|url>)');
471
+ const fq = factQuestions(loadWorkspace(project));
472
+ if (fq.length) {
473
+ if (!rl) return finish(args, await evaluate(project, args), fq, 'ask the human in one message (recommended answers shown), write the answers to the knowledge files, then run `tadforce doctor --json --facts-reviewed`');
474
+ const changed = await askFacts(loadWorkspace(project), rl, fq);
475
+ const c2 = loadWorkspace(project); c2.cfg.setup.factsReviewed = true; c2.save(c2.cfg);
476
+ if (changed) await quietly(args.json, () => docs.facts({ cwd: project, org: c2.cfg.source.mode === 'local' ? false : c2.cfg.org.alias }));
338
477
  }
339
- log.fail('doctor stopped after 8 rounds: run it again.');
340
- return 1;
478
+
479
+ // 7. All green: say it, and offer to start the writing.
480
+ const code = finish(args, await evaluate(project, args), [], null);
481
+ // CI, agents (--json) and agent sessions stop here; a person in a terminal is offered the writing.
482
+ if (code || args.json || args.launch === false || !process.stdin.isTTY || insideAgent()) return code;
483
+ return handOver(project, args, rl);
341
484
  } finally {
342
485
  if (rl) rl.close();
343
486
  }
344
487
  }
345
488
 
489
+ /**
490
+ * Everything is green: offer to start the AI agent on the writing, in this terminal. The agent is chosen
491
+ * once (--agent claude|codex|gemini|cursor) and remembered; --no-launch stops at the prerequisites.
492
+ */
493
+ async function handOver(project, args, rl) {
494
+ const ctx = loadWorkspace(project);
495
+ const found = installedAgents();
496
+ const wanted = typeof args.agent === 'string' ? args.agent : ctx.cfg.setup.agent;
497
+ let agent = found.find((a) => a.id === wanted) || (wanted ? null : found[0]);
498
+ if (!agent) {
499
+ log.human(found.length ? `agent "${wanted}" not found; installed: ${found.map((a) => a.id).join(', ')} (--agent <id>).` : 'open your AI agent (Claude Code, Codex, Cursor, Copilot…) in this folder and type /tadforce to write the document.');
500
+ return 0;
501
+ }
502
+ if (rl) {
503
+ if (!yes((await rl.question(` Launch the writing of the document now with ${agent.name}? [Y/n] `)).trim())) {
504
+ log.info(`Later: open your agent in ${project} and type /tadforce.`);
505
+ rl.close(); return 0;
506
+ }
507
+ if (found.length > 1 && !wanted) {
508
+ const pick = (await rl.question(` Agent (${found.map((a) => a.id).join('/')}) [${agent.id}]: `)).trim();
509
+ agent = found.find((a) => a.id === pick) || agent;
510
+ }
511
+ rl.close();
512
+ }
513
+ ctx.cfg.setup.agent = agent.id; ctx.save(ctx.cfg);
514
+ log.step(`Handing over to ${agent.name}: it writes the document, then builds and validates it…`);
515
+ return launchAgent(agent, project);
516
+ }
517
+
346
518
  async function quietly(json, fn) {
347
519
  if (!json) return fn();
348
520
  const orig = console.log; console.log = () => {};
349
521
  try { return await fn(); } finally { console.log = orig; }
350
522
  }
351
523
 
352
- function finish(args, st, framing, facts, next) {
524
+ function finish(args, st, questions, next) {
353
525
  const ok = !next && st.list.every((x) => x.level !== 'error');
354
526
  if (args.json) {
355
- console.log(JSON.stringify({ ok, checks: st.list, questions: [...framing, ...facts], next: next || 'tadforce build' }, null, 2));
527
+ console.log(JSON.stringify({ ok, checks: st.list, questions, next: next || 'tadforce build' }, null, 2));
356
528
  return ok ? 0 : 1;
357
529
  }
358
530
  for (const x of st.list) {
@@ -4,7 +4,7 @@ import readline from 'node:readline/promises';
4
4
  import { exists, writeText, writeJson, copyDir, readJson } from '../util/fsx.js';
5
5
  import { defaultConfig, findWorkspace } from '../util/config.js';
6
6
  import { WORKSPACE_DIR, CONFIG_FILE, SKILLS } from '../util/paths.js';
7
- import { LANGUAGES } from '../util/i18n.js';
7
+ import { LANGUAGES, normalizeLang } from '../util/i18n.js';
8
8
  import { log } from '../util/log.js';
9
9
  import { run as doctor } from './doctor.js';
10
10
 
@@ -100,7 +100,7 @@ export async function run(args) {
100
100
  const prev = existing ? readJson(path.join(ws, CONFIG_FILE)) : {};
101
101
  const base = defaultConfig(prev);
102
102
  if (interactive) log.step('A few facts only you can confirm (Enter keeps the default):');
103
- const language = args.lang || (await ask(rl, `Document language (${LANGUAGES.join('/')})`, base.language));
103
+ const language = args.lang || (await ask(rl, `Document language (${LANGUAGES.join('/')} built in, or any code: es, de, it, pt, ar…)`, base.language));
104
104
  const clientName = args.client || (await ask(rl, 'Client name', prev.client?.name || base.client.name));
105
105
  const short = args.short || (await ask(rl, 'Client short name (used in file names)', prev.client?.short || clientName.replace(/[^A-Za-z0-9]/g, '').slice(0, 8).toUpperCase()));
106
106
  const integrator = args.integrator || (await ask(rl, 'Integrator (blank if none)', base.integrator.name || ''));
@@ -109,7 +109,7 @@ export async function run(args) {
109
109
  const alias = args.org || (await ask(rl, 'Salesforce org alias (blank: decide later)', base.org.alias || ''));
110
110
  const cfg = defaultConfig({
111
111
  ...prev,
112
- language: LANGUAGES.includes(language) ? language : 'en',
112
+ language: normalizeLang(language),
113
113
  client: { ...base.client, name: clientName, short },
114
114
  integrator: { ...base.integrator, name: integrator || null },
115
115
  document: { ...base.document, author: author || null, title, reference: prev.document?.reference || `TAD-${short}-CRM`, environment: args.environment || base.document.environment, status: language === 'fr' && !prev.document?.status ? 'Pour revue' : base.document.status, classification: language === 'fr' && !prev.document?.classification ? 'Confidentiel' : base.document.classification },
@@ -7,6 +7,7 @@ import { AREAS, fingerprints } from '../facts/fingerprints.js';
7
7
  import { loadWorkspace, sourceDirs } from '../util/config.js';
8
8
  import { TEMPLATES } from '../util/paths.js';
9
9
  import { log, c } from '../util/log.js';
10
+ import { templateLang } from '../util/i18n.js';
10
11
  import * as org from './org.js';
11
12
  import * as docs from './docs.js';
12
13
 
@@ -121,7 +122,7 @@ export async function run(args) {
121
122
  for (const m of modulesAfter.filter((x) => !modulesBefore.includes(x))) note('modules', `new product detected: ${m}`);
122
123
  for (const m of modulesBefore.filter((x) => !modulesAfter.includes(x))) note('modules', `product no longer detected: ${m}`);
123
124
  const specs = readYaml(path.join(TEMPLATES, 'specs', 'sections.yml'), {}).sections || {};
124
- const sections = affectedSections(areas, specs, sectionLocations(ctx2.cfg.language === 'fr' ? 'fr' : 'en'));
125
+ const sections = affectedSections(areas, specs, sectionLocations(templateLang(ctx2.cfg.language)));
125
126
 
126
127
  // 4. New version in the document control.
127
128
  const cfg = ctx2.cfg;
@@ -0,0 +1,35 @@
1
+ // AI agents TADForce can hand over to once `doctor` is green: the agent writes the document.
2
+ import { spawnSync } from 'node:child_process';
3
+
4
+ const RUN = '/tadforce';
5
+ const PROMPT = 'Use the tadforce skill: the prerequisites are green (tadforce doctor). Write every section of the Technical Architecture Document and the Improvement Axes, then build and validate them.';
6
+
7
+ /** Command-line agents, in order of preference: the argument list that starts an interactive session on the task. */
8
+ export const AGENTS = [
9
+ { id: 'claude', name: 'Claude Code', bin: 'claude', args: [RUN] },
10
+ { id: 'codex', name: 'Codex', bin: 'codex', args: [PROMPT] },
11
+ { id: 'gemini', name: 'Gemini CLI', bin: 'gemini', args: ['-i', PROMPT] },
12
+ { id: 'cursor', name: 'Cursor Agent', bin: 'cursor-agent', args: [PROMPT] },
13
+ ];
14
+
15
+ const onPath = (bin) => spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], { encoding: 'utf8' }).status === 0;
16
+
17
+ /** Agents installed on this machine. */
18
+ export function installedAgents() {
19
+ return AGENTS.filter((a) => onPath(a.bin));
20
+ }
21
+
22
+ /** True when TADForce already runs inside an agent session: never start another agent from there. */
23
+ export function insideAgent() {
24
+ const e = process.env;
25
+ return !!(e.CLAUDECODE || e.CLAUDE_CODE_ENTRYPOINT || e.CODEX_SANDBOX || e.CODEX_HOME && e.CODEX_SESSION || e.GEMINI_CLI || e.CURSOR_AGENT || e.TADFORCE_NO_LAUNCH);
26
+ }
27
+
28
+ /** Start the agent in this terminal, in the project folder. Returns its exit code. */
29
+ export function launchAgent(agent, cwd) {
30
+ const quote = (a) => (/^[\w./-]+$/.test(a) ? a : `"${a.replace(/"/g, '\\"')}"`);
31
+ const r = process.platform === 'win32'
32
+ ? spawnSync([agent.bin, ...agent.args].map(quote).join(' '), [], { cwd, stdio: 'inherit', shell: true })
33
+ : spawnSync(agent.bin, agent.args, { cwd, stdio: 'inherit' });
34
+ return r.status ?? 1;
35
+ }
package/src/util/i18n.js CHANGED
@@ -2,15 +2,30 @@ import path from 'node:path';
2
2
  import { readJson, exists } from './fsx.js';
3
3
  import { TEMPLATES } from './paths.js';
4
4
 
5
+ /** Languages shipped with skeletons and interface labels. Any other language works from the English ones. */
5
6
  export const LANGUAGES = ['en', 'fr'];
6
7
 
7
- export function loadI18n(lang) {
8
- const l = LANGUAGES.includes(lang) ? lang : 'en';
9
- const base = readJson(path.join(TEMPLATES, 'i18n', 'en.json'));
10
- if (l === 'en') return { lang: 'en', dict: base };
11
- const p = path.join(TEMPLATES, 'i18n', `${l}.json`);
12
- const loc = exists(p) ? readJson(p) : {};
13
- return { lang: l, dict: merge(base, loc) };
8
+ /** A language code as written in the config: en, fr, es, de, pt-BR… (anything else falls back to en). */
9
+ export function normalizeLang(lang) {
10
+ const m = /^([a-z]{2,3})(?:[-_]([a-z]{2}))?$/i.exec(String(lang || '').trim());
11
+ return m ? m[1].toLowerCase() + (m[2] ? '-' + m[2].toUpperCase() : '') : 'en';
12
+ }
13
+
14
+ /** Skeletons and specs come in the built-in languages; other languages start from English. */
15
+ export const templateLang = (lang) => (LANGUAGES.includes(normalizeLang(lang)) ? normalizeLang(lang) : 'en');
16
+
17
+ /**
18
+ * Interface and document labels: English, then the built-in translation, then the workspace's own
19
+ * file (tadforce/i18n/<lang>.json) — the translation the agent writes for any other language.
20
+ */
21
+ export function loadI18n(lang, ws = null) {
22
+ const l = normalizeLang(lang);
23
+ let dict = readJson(path.join(TEMPLATES, 'i18n', 'en.json'));
24
+ const builtIn = path.join(TEMPLATES, 'i18n', `${l}.json`);
25
+ if (l !== 'en' && exists(builtIn)) dict = merge(dict, readJson(builtIn));
26
+ const own = ws && path.join(ws, 'i18n', `${l}.json`);
27
+ if (own && exists(own)) dict = merge(dict, readJson(own));
28
+ return { lang: l, dict };
14
29
  }
15
30
 
16
31
  function merge(a, b) {
@@ -29,5 +44,6 @@ export function t(dict, key, vars = {}) {
29
44
  export function formatNumber(n, lang) {
30
45
  if (typeof n !== 'number' || !Number.isFinite(n)) return String(n);
31
46
  // fr-FR uses a narrow no-break space as thousands separator, as in the reference document.
32
- return new Intl.NumberFormat(lang === 'fr' ? 'fr-FR' : 'en-US').format(n);
47
+ const locale = lang === 'fr' ? 'fr-FR' : lang === 'en' || !lang ? 'en-US' : lang;
48
+ try { return new Intl.NumberFormat(locale).format(n); } catch { return new Intl.NumberFormat('en-US').format(n); }
33
49
  }
package/README.fr.md DELETED
@@ -1,125 +0,0 @@
1
- <div align="center">
2
-
3
- <img src="assets/banner.svg" alt="TADForce" width="100%">
4
-
5
- ### Le scribe de l'architecte pour les orgs Salesforce.
6
-
7
- **Quatre commandes. Une org. Un document d'architecture technique fondé sur des preuves, reproductible, relu dans le navigateur.**
8
-
9
- [English](README.md) · [**Démo en ligne**](https://azamiji.github.io/TADForce/)
10
-
11
- </div>
12
-
13
- <img src="assets/divider.svg" width="100%" alt="">
14
-
15
- ## Pourquoi TADForce
16
-
17
- Rédiger le document d'architecture technique d'une org Salesforce prend des semaines, et il est périmé à la release suivante. Un chat IA générique l'écrit en une heure, puis on passe des jours à vérifier ce qu'il a inventé.
18
-
19
- TADForce lit l'org comme le ferait un architecte Salesforce senior — chaque classe, Flow, objet, Permission Set Group, Named Credential et OmniScript — et votre agent IA rédige le document à partir de ces preuves. Une seule règle : **pas de preuve, pas de phrase.** Si les métadonnées ne le prouvent pas, l'agent vous pose la question ou écrit « à confirmer ». Il ne devine jamais le nom d'un système, ne documente jamais une fonction décommissionnée et ne tape jamais un chiffre à la main.
20
-
21
- <div align="center">
22
- <img src="assets/gifs/document.gif" alt="Navigation dans un document généré par TADForce" width="92%">
23
- </div>
24
-
25
- ## ⚡ Démarrage
26
-
27
- **Prérequis** : Node.js 20+, Salesforce CLI (`sf`), Chrome ou Edge, un agent IA. `doctor` vérifie tout et installe ce qu'il peut.
28
-
29
- À la racine de votre projet Salesforce DX (ou dans un dossier vide) :
30
-
31
- ```bash
32
- npx tadforce doctor
33
- ```
34
-
35
- Puis, dans votre agent IA (Claude Code, Codex, Cursor, Copilot…) :
36
-
37
- ```text
38
- /tadforce
39
- ```
40
-
41
- | Chemin | Commandes |
42
- |---|---|
43
- | Plugin Claude Code | `/plugin marketplace add azamiji/TADForce` puis `/plugin install tadforce@tadforce` |
44
- | Tout agent (Agent Skills) | `npx tadforce doctor` installe les skills dans `.claude/`, `.agents/`, `.cursor/`, `.codex/`… |
45
- | Déterministe / CI | `npx tadforce prepare --org prod`, puis `npx tadforce build --pdf --docx`, puis `npx tadforce validate` |
46
-
47
- ## 🧭 Quatre commandes
48
-
49
- | Commande | Ce qu'elle fait | Votre part |
50
- |---|---|---|
51
- | `npx tadforce doctor` | Tout ce qui précède la rédaction, en boucle jusqu'à ce que tout soit vert : outils, projet SFDX, connexion à l'org (réutilise la `target-org` du projet, n'ouvre le navigateur que si nécessaire), métadonnées (récupérées depuis l'org avec un manifest `*`, ou vos sources locales), logo et couleurs du client, puis les questions dont seul un humain connaît la réponse | Vous connecter une fois ; valider les réponses recommandées ; nommer les systèmes externes ; confirmer ce qui est inutilisé ou décommissionné |
52
- | `npx tadforce build` | Votre agent rédige chaque section à partir de sa fiche et des faits, puis le document est produit : **HTML** toujours, **PDF** et **Word** à la demande, et validé | — |
53
- | `npx tadforce review` | Ouvre le document pour le commenter. **Enregistrer écrit dans le même fichier** : un seul fichier par document, pas de copies, aucun commentaire perdu. Puis `/tadforce-review` transforme chaque commentaire en correction | Commenter un passage ou capturer une zone, Enregistrer |
54
- | `npx tadforce update` | Quelques mois plus tard : relit l'org, archive les faits précédents, liste les sections touchées par ce qui a changé (une classe Apex ajoutée, un Flow modifié, de nouveaux chiffres…), incrémente la version. `/tadforce-update` ne réécrit que ces sections | — |
55
-
56
- <div align="center">
57
- <img src="assets/gifs/doctor.gif" alt="tadforce doctor pose les questions dont seul un humain connaît la réponse" width="80%">
58
- </div>
59
-
60
- Les commandes fines (`manifest`, `retrieve`, `facts`, `next`, `validate`…) restent disponibles pour la CI et les experts : `npx tadforce help --all`.
61
-
62
- ## 📦 Ce que vous obtenez
63
-
64
- - **Document d'architecture technique**, destiné au client :
65
- - en HTML : navigable, modifiable en place, avec commentaire d'un passage ou capture d'une zone, enregistrement dans le même fichier et export PDF ou Word ;
66
- - en PDF A4 et en DOCX, la version dans le nom du fichier.
67
- - **Axes d'amélioration**, document interne à l'intégrateur : signaux mesurés par pilier Well-Architected, registre `AX-nn` et feuille de route. Ils restent hors du document client.
68
- - **`facts.json`** : l'inventaire déterministe de l'org, identique octet pour octet d'une exécution à l'autre.
69
-
70
- Le document porte **le logo et les couleurs du client** : le logo est cherché dans l'org (static resources, content assets, assets des sites) ou fourni par vous ; les couleurs viennent du thème Lightning de l'org, sinon du logo, toujours lisibles (contraste WCAG vérifié). L'intégrateur et l'auteur sont facultatifs : laissés vides, le document ne nomme que le client.
71
-
72
- <div align="center">
73
- <img src="assets/gifs/review.gif" alt="Commenter un passage dans le HTML et l'enregistrer dans le même fichier" width="92%">
74
- </div>
75
-
76
- ## 🧩 Tous les types de projets Salesforce
77
-
78
- | Votre situation | Comment TADForce s'y adapte |
79
- |---|---|
80
- | **Une org complète**, état existant | `doctor` récupère tout avec un manifest `*` maîtrisé (130 types, rapports et tableaux de bord dans leurs dossiers, objets standard) |
81
- | **Un projet dans une org partagée** (plusieurs BU, plusieurs intégrateurs) | Périmètre : `--scope "NWB_*,Loan*"` ou `--package-dirs`. Les composants hors périmètre sortent des faits, des chiffres et des tableaux ; les objets standard et les réglages globaux restent |
82
- | **Avant ou pendant le build** | Fournissez vos spécifications fonctionnelles (`--specs SFD.pdf,…`) : elles deviennent des preuves (niveau P2/P3) à côté des métadonnées |
83
- | **Sans accès à l'org**, sources seules | `--source local` documente les package directories du projet |
84
- | **Packages unlocked ou managés** | Namespaces et packages installés sont détectés et documentés avec leurs preuves |
85
- | **Tous les clouds, toutes les industries** | Une section ne s'active que si le produit est détecté, avec sa fiche de rédaction |
86
-
87
- - **Clouds** : Sales, Service, Field Service, Experience, Marketing, Commerce, CPQ et Revenue ;
88
- - **Industries** : Financial Services, Insurance, Health, Life Sciences, Manufacturing, Automotive, Consumer Goods, Communications, Public Sector, Education, Nonprofit, Loyalty, Net Zero ;
89
- - **Plateforme** : OmniStudio, Data Cloud, Agentforce, CRM Analytics, Platform Events, Shield, MuleSoft, Heroku et Slack.
90
-
91
- ## 🔒 Lecture seule par construction
92
-
93
- - **Liste blanche des commandes `sf`** : pas de deploy, pas de suppression, pas d'exécution d'Apex.
94
- - **Garde-fou SOQL** : seuls les objets de configuration de l'org et les agrégats sont lus, jamais les données métier.
95
- - **`force-app` jamais modifié** : la récupération se fait dans `tadforce/.snapshot`.
96
- - **Aucun secret dans le texte** : la validation bloque les secrets et les identifiants d'org.
97
-
98
- **Ce qui sort de votre poste** : le CLI TADForce n'envoie rien nulle part — ni télémétrie, ni serveur, ni compte. Il parle à votre org via votre propre Salesforce CLI, et au web seulement quand **vous** lui donnez l'URL d'un logo. Votre agent IA lit les métadonnées, les faits et les spécifications que vous fournissez pour rédiger, exactement comme lorsque vous lui faites lire votre code. Utilisez l'agent et les réglages de confidentialité validés par votre entreprise pour les travaux client.
99
-
100
- ## 🎯 Reproductible
101
-
102
- - **Les chiffres sont des placeholders**, par exemple `{{n:apex.classes}}`.
103
- - **Les tableaux, les ERD, la hiérarchie des rôles et les annexes sont générés.**
104
- - **84 fiches de section** cadrent la rédaction de n'importe quel agent.
105
- - **`npx tadforce validate`** bloque tout ce qui ne doit pas être livré.
106
-
107
- ## 👤 Auteur
108
-
109
- <table>
110
- <tr>
111
- <td width="110"><img src="https://github.com/azamiji.png" width="96" alt="Jihad AZAMI HASSANI"></td>
112
- <td>
113
-
114
- **Jihad AZAMI HASSANI**<br>
115
- Salesforce Technical Architect, 10+ ans d'expérience · Claude Certified Architect · Leader du Salesforce Architect Community Group de Casablanca<br>
116
- [![Claude Certified Architect](https://img.shields.io/badge/Claude-Certified%20Architect-D97757?logo=claude&logoColor=white)](https://www.linkedin.com/in/jihadazami/)<br>
117
- [LinkedIn](https://www.linkedin.com/in/jihadazami/) · [GitHub](https://github.com/azamiji)
118
-
119
- </td>
120
- </tr>
121
- </table>
122
-
123
- ## ⚖️ Licence
124
-
125
- [Apache-2.0](LICENSE). Salesforce et les noms de ses produits sont des marques de Salesforce, Inc. TADForce est un projet communautaire indépendant, ni affilié à Salesforce ni approuvé par elle. L'organisation de l'exemple (Northwind Bank, Acme Integrators) est fictive.