@artefact-ci/sdd 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,7 +49,7 @@ L'installeur enregistre la version de la méthode installée et sa source dans `
49
49
 
50
50
  ```json
51
51
  {
52
- "version": "0.2.0",
52
+ "version": "0.2.1",
53
53
  "source": "release"
54
54
  }
55
55
  ```
@@ -64,4 +64,4 @@ Un projet installé avant ce fichier (phase 4, avec `--source`) a déjà les com
64
64
 
65
65
  ## Ce que `update` touche
66
66
 
67
- Il réinstalle l'extension et le preset, puis régénère les fichiers d'agent et réapplique les hooks. Sans `--source`, il les prend dans les zips de la release de sa version : `extension add --from <url du zip> --force` et `preset update artefact-sdd --from <url du zip>`. Avec `--source`, il les prend dans le clone en `--dev` : `extension add --dev … --force` et `preset update artefact-sdd --dev …` (soit un retrait puis un ajout). Si un téléchargement échoue (réseau coupé, version sans release), l'installeur s'arrête en citant l'URL du zip : relancer la même commande une fois le réseau rétabli. `installed_at` change dans les registres de `.specify/` : c'est attendu. Il ne touche jamais `stack.yml` ni la constitution, et s'arrête si `.specify/` est absent.
67
+ Il réinstalle l'extension et le preset, puis régénère les fichiers d'agent et réapplique les hooks. Quand `.specify/integration.json` liste plusieurs outils, il lance `specify integration use` pour chaque outil autre que celui par défaut, puis pour l'outil par défaut : Spec Kit n'enregistre les commandes de l'extension et du preset que pour l'outil sélectionné. L'outil par défaut ne change pas, même si un `use` échoue, et les scripts de `.specify/scripts/` gardent leurs droits. Sans `--source`, il les prend dans les zips de la release de sa version : `extension add --from <url du zip> --force` et `preset update artefact-sdd --from <url du zip>`. Avec `--source`, il les prend dans le clone en `--dev` : `extension add --dev … --force` et `preset update artefact-sdd --dev …` (soit un retrait puis un ajout). Si un téléchargement échoue (réseau coupé, version sans release), l'installeur s'arrête en citant l'URL du zip : relancer la même commande une fois le réseau rétabli. `installed_at` change dans les registres de `.specify/` : c'est attendu. Il ne touche jamais `stack.yml` ni la constitution, et s'arrête si `.specify/` est absent.
package/dist/agents.mjs CHANGED
@@ -24,6 +24,14 @@ export function readIntegrations(root) {
24
24
  return json.installed_integrations;
25
25
  return json.integration ? [json.integration] : [];
26
26
  }
27
+ /** Intégration par défaut de `.specify/integration.json` (`default_integration`, sinon `integration`) ; null si aucune. */
28
+ export function readDefaultIntegration(root) {
29
+ const file = join(root, '.specify', 'integration.json');
30
+ if (!existsSync(file))
31
+ return null;
32
+ const json = JSON.parse(readFileSync(file, 'utf8'));
33
+ return json.default_integration ?? json.integration ?? null;
34
+ }
27
35
  function body(spec) {
28
36
  const lines = [
29
37
  `Lis \`${INSTRUCTIONS_DIR}/${spec.name}.md\` et applique-le à la lettre : méthode, grille et format du rapport.`,
package/dist/install.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
1
+ import { chmodSync, existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
2
  import { basename, join } from 'node:path';
3
- import { agentFiles, readAgentSpecs, readIntegrations, skippedAgents } from "./agents.mjs";
3
+ import { agentFiles, readAgentSpecs, readDefaultIntegration, readIntegrations, skippedAgents } from "./agents.mjs";
4
4
  import { compareVersions, readInstalled, writeInstalled } from "./artefact.mjs";
5
5
  import { baseConstitution, isBlankConstitution } from "./constitution.mjs";
6
6
  import { applyGitHooks } from "./hooks.mjs";
@@ -62,6 +62,62 @@ async function branches(o) {
62
62
  const defaultBranch = head?.startsWith('refs/remotes/origin/') ? head.slice('refs/remotes/origin/'.length) : undefined;
63
63
  return { names: [...new Set(refs)], ...(defaultBranch ? { defaultBranch } : {}) };
64
64
  }
65
+ const SCRIPTS = '.specify/scripts';
66
+ /** Droits de chaque fichier sous `.specify/scripts/` (chemin absolu → mode). */
67
+ function scriptModes(root) {
68
+ const modes = new Map();
69
+ const walk = (dir) => {
70
+ if (!existsSync(dir))
71
+ return;
72
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
73
+ const p = join(dir, e.name);
74
+ if (e.isDirectory())
75
+ walk(p);
76
+ else if (e.isFile())
77
+ modes.set(p, statSync(p).mode & 0o777);
78
+ }
79
+ };
80
+ walk(join(root, SCRIPTS));
81
+ return modes;
82
+ }
83
+ /**
84
+ * Spec Kit 1.3.2 n'enregistre les commandes d'une extension ou d'un preset que pour l'intégration
85
+ * par défaut ; une autre ne les reçoit qu'à `specify integration use`. On sélectionne donc chaque
86
+ * intégration secondaire, puis on revient toujours à celle par défaut, même après un échec.
87
+ * `use` réécrit les scripts partagés sans leur droit d'exécution : on remet les droits d'avant.
88
+ */
89
+ async function registerSecondary(o, integrations) {
90
+ const fallback = readDefaultIntegration(o.root);
91
+ if (fallback === null || !integrations.includes(fallback))
92
+ return [];
93
+ const others = integrations.filter((i) => i !== fallback);
94
+ if (others.length === 0)
95
+ return [];
96
+ const modes = scriptModes(o.root);
97
+ let failed;
98
+ for (const integration of others) {
99
+ try {
100
+ await specify(o.runner, o.root, ['integration', 'use', integration]);
101
+ }
102
+ catch (e) {
103
+ failed = { integration, error: e };
104
+ break;
105
+ }
106
+ }
107
+ try {
108
+ await specify(o.runner, o.root, ['integration', 'use', fallback]);
109
+ }
110
+ finally {
111
+ for (const [file, mode] of modes) {
112
+ if (existsSync(file) && (statSync(file).mode & 0o777) !== mode)
113
+ chmodSync(file, mode);
114
+ }
115
+ }
116
+ if (failed) {
117
+ throw new SetupError(`impossible d'enregistrer les commandes de la méthode pour ${failed.integration} : ${failed.error.message} ; ${fallback} reste l'outil par défaut, relance update`);
118
+ }
119
+ return others;
120
+ }
65
121
  async function isDirty(o) {
66
122
  return ((await git(o, ['status', '--porcelain'])) ?? '').trim() !== '';
67
123
  }
@@ -101,6 +157,8 @@ async function common(o, mode) {
101
157
  catch (e) {
102
158
  throw new SetupError(`.specify/integration.json illisible : ${e.message}`);
103
159
  }
160
+ // Seulement quand un composant vient d'être (ré)installé : un second init ne lance rien.
161
+ const registeredFor = installed.length > 0 ? await registerSecondary(o, integrations) : [];
104
162
  const specs = readAgentSpecs(root);
105
163
  const agents = [];
106
164
  const agentsSkipped = [];
@@ -127,6 +185,7 @@ async function common(o, mode) {
127
185
  agents,
128
186
  agentsSkipped,
129
187
  noAgentGenerator,
188
+ registeredFor,
130
189
  legacy: legacyAgents(root),
131
190
  };
132
191
  }
@@ -141,7 +200,8 @@ export async function init(o) {
141
200
  }
142
201
  const noneInstalled = !registered(root, 'presets', 'artefact-sdd') && !registered(root, 'extensions', 'artefact-product');
143
202
  const dirty = await isDirty(o);
144
- if (!existsSync(join(root, '.specify'))) {
203
+ const hadSpecify = existsSync(join(root, '.specify'));
204
+ if (!hadSpecify) {
145
205
  const args = ['init', '--here', '--script', 'sh', '--non-interactive', '--ignore-agent-tools'];
146
206
  args.push('--integration', o.integration ?? 'claude');
147
207
  // Spec Kit 1.3.2 compte `.git` comme contenu : sans --force, il refuse un dossier non vide.
@@ -169,7 +229,22 @@ export async function init(o) {
169
229
  writeIfChanged(constFile, baseConstitution(readFileSync(template, 'utf8'), basename(root)));
170
230
  constitution = 'written';
171
231
  }
172
- return { mode: 'init', ...done, stack, constitution, dirty, version: { installed: record ? current : null, previous, downgrade: false } };
232
+ // Spec Kit n'installe une intégration qu'à son init : sur un projet déjà initialisé, --integration ne fait rien.
233
+ let integrationIgnored;
234
+ if (hadSpecify && o.integration !== undefined) {
235
+ const installed = readIntegrations(root);
236
+ if (!installed.includes(o.integration))
237
+ integrationIgnored = { requested: o.integration, installed };
238
+ }
239
+ return {
240
+ mode: 'init',
241
+ ...done,
242
+ stack,
243
+ constitution,
244
+ dirty,
245
+ ...(integrationIgnored ? { integrationIgnored } : {}),
246
+ version: { installed: record ? current : null, previous, downgrade: false }
247
+ };
173
248
  }
174
249
  export async function update(o) {
175
250
  if (!(await hasUv(o.runner)))
package/dist/report.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { SPECIFY_VERSION } from "./specify.mjs";
1
2
  const LEGACY_STATE = {
2
3
  unchanged: 'identique au boilerplate, tu peux le supprimer',
3
4
  /** Variante quand des fichiers citent encore docs/agents/. */
@@ -17,6 +18,9 @@ export function formatReport(r) {
17
18
  }
18
19
  if (r.skipped.length > 0)
19
20
  out.push(` Déjà installé : ${list(r.skipped)}`);
21
+ if (r.registeredFor.length > 0) {
22
+ out.push(` Commandes de la méthode enregistrées aussi pour : ${list(r.registeredFor)} (outil par défaut inchangé)`);
23
+ }
20
24
  const { installed, previous, downgrade } = r.version;
21
25
  if (installed === null) {
22
26
  out.push(' Version installée inconnue : lance update pour aligner tous les composants');
@@ -50,7 +54,7 @@ export function formatReport(r) {
50
54
  out.push(` ${name} : pas de générateur de fichiers d'agent, la relecture se fait en contexte neuf`);
51
55
  }
52
56
  for (const { integration, names } of r.agentsSkipped) {
53
- out.push(` ${integration}, ${list(names)} : pas de fichier d'agent pour cet outil : la commande fera la relecture en nouvelle session`);
57
+ out.push(` ${integration} : pas de fichier d'agent ${list(names)} pour cet outil : la commande fera ce travail en nouvelle session`);
54
58
  }
55
59
  }
56
60
  if (r.stack) {
@@ -74,6 +78,12 @@ export function formatReport(r) {
74
78
  out.push(` Citent encore docs/agents/ : ${list(r.legacy.mentions)}`);
75
79
  }
76
80
  }
81
+ if (r.integrationIgnored) {
82
+ const { requested, installed } = r.integrationIgnored;
83
+ const current = installed.length > 0 ? list(installed) : 'aucune intégration';
84
+ const specify = `uvx --from specify-cli==${SPECIFY_VERSION} specify integration`;
85
+ out.push('', `Attention : --integration ${requested} ignoré : Spec Kit était déjà installé ici avec ${current}.`, ` pour garder ${current} et ajouter ${requested} : ${specify} install ${requested} --script sh --force`, ` pour remplacer ${current} par ${requested} : ${specify} switch ${requested}`, " puis : npx @artefact-ci/sdd update (voir « Changer ou ajouter un outil » dans le README de speckit-artefact)");
86
+ }
77
87
  if (r.dirty) {
78
88
  out.push('', "Attention : le dépôt avait des changements non commités avant l'installation ; relis le diff avant de commiter.");
79
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artefact-ci/sdd",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Installeur de la méthode Artefact CI pour Spec Kit (preset artefact-sdd, extension artefact-product)",
5
5
  "license": "MIT",
6
6
  "repository": {