ac-framework 1.3.0 → 1.5.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.
@@ -1,3 +1,16 @@
1
+ /**
2
+ * init.js
3
+ * ──────────────────────────────────────────────────────────────────
4
+ * `acfm init` — Interactive wizard that installs AC Framework
5
+ * modules into the user's project.
6
+ *
7
+ * Flags:
8
+ * --latest Download the latest framework from GitHub
9
+ * instead of using the bundled npm version.
10
+ * --branch <name> GitHub branch to pull (implies --latest).
11
+ * ──────────────────────────────────────────────────────────────────
12
+ */
13
+
1
14
  import chalk from 'chalk';
2
15
  import gradient from 'gradient-string';
3
16
  import inquirer from 'inquirer';
@@ -11,7 +24,9 @@ import {
11
24
  existsInTarget,
12
25
  copyModule,
13
26
  copyMdFile,
27
+ FRAMEWORK_PATH,
14
28
  } from '../services/installer.js';
29
+ import { downloadWithSpinner, cleanupTempDir } from '../services/github-sync.js';
15
30
  import {
16
31
  matrixRain,
17
32
  scanAnimation,
@@ -25,6 +40,8 @@ import {
25
40
 
26
41
  const acGradient = gradient(['#6C5CE7', '#00CEC9', '#0984E3']);
27
42
 
43
+ // ── Helpers ──────────────────────────────────────────────────────
44
+
28
45
  function buildChoices(folders) {
29
46
  const choices = [];
30
47
 
@@ -160,215 +177,264 @@ async function selectMdFiles(selected, targetDir) {
160
177
  return requiredMd;
161
178
  }
162
179
 
163
- export async function initCommand() {
164
- const targetDir = process.cwd();
180
+ // ── Main command ─────────────────────────────────────────────────
165
181
 
166
- // ── Step 1/4: Scan ──────────────────────────────────────────────
167
- await stepHeader(1, 4, 'Scanning framework modules');
168
- await scanAnimation('Indexing available modules', 1000);
169
- console.log();
182
+ export async function initCommand(options = {}) {
183
+ const targetDir = process.cwd();
170
184
 
171
- await matrixRain(1800);
185
+ // --branch implies --latest
186
+ const useLatest = !!(options.latest || options.branch);
172
187
 
173
- let folders;
174
- try {
175
- folders = await getSelectableModules();
176
- } catch {
177
- console.log(chalk.hex('#D63031')(' ✗ Error: Could not read framework directory.'));
178
- console.log(chalk.hex('#636E72')(' Make sure ac-framework is installed correctly.'));
179
- process.exit(1);
180
- }
188
+ // Dynamic step counting: +1 step when downloading from GitHub
189
+ const stepOffset = useLatest ? 1 : 0;
190
+ const totalSteps = 4 + stepOffset;
181
191
 
182
- if (folders.length === 0) {
183
- console.log(chalk.hex('#FDCB6E')(' No modules found in framework directory.'));
184
- process.exit(0);
185
- }
192
+ // Framework source: bundled by default, overridden by --latest
193
+ let frameworkPath = FRAMEWORK_PATH;
194
+ let tempDir = null;
186
195
 
187
- const countBadge = chalk.hex('#2D3436').bgHex('#00CEC9').bold(` ${folders.length} `);
188
- const autoBadge = chalk.hex('#2D3436').bgHex('#6C5CE7').bold(' +openspec ');
189
- console.log(` ${countBadge} ${chalk.hex('#B2BEC3')('assistant modules found')} ${autoBadge} ${chalk.hex('#636E72')('auto-included')}`);
190
- console.log();
191
- await animatedSeparator(60);
192
- console.log();
193
-
194
- // ── Step 2/4: Select ────────────────────────────────────────────
195
- await stepHeader(2, 4, 'Select your assistants');
196
-
197
- const key = (k) => chalk.hex('#2D3436').bgHex('#636E72')(` ${k} `);
198
- console.log(
199
- ` ${key('↑↓')} ${chalk.hex('#636E72')('navigate')} ` +
200
- `${key('Space')} ${chalk.hex('#636E72')('toggle')} ` +
201
- `${key('Enter')} ${chalk.hex('#636E72')('confirm')}`
202
- );
203
- console.log();
204
-
205
- const choices = buildChoices(folders);
206
-
207
- const { selected } = await inquirer.prompt([
208
- {
209
- type: 'checkbox',
210
- name: 'selected',
211
- message: acGradient('Choose modules to install:'),
212
- choices,
213
- pageSize: 15,
214
- loop: false,
215
- validate(answer) {
216
- if (answer.length === 0) {
217
- return chalk.hex('#D63031')('Select at least one module. Use Space to toggle.');
196
+ try {
197
+ // ── Download (only with --latest / --branch) ──────────────────
198
+ if (useLatest) {
199
+ await stepHeader(1, totalSteps, 'Downloading latest framework');
200
+
201
+ const branchLabel = options.branch || 'main';
202
+ const branchBadge = chalk.hex('#2D3436').bgHex('#6C5CE7').bold(` ${branchLabel} `);
203
+ console.log(` ${chalk.hex('#636E72')('Branch:')} ${branchBadge}`);
204
+ console.log();
205
+
206
+ try {
207
+ const result = await downloadWithSpinner({
208
+ branch: options.branch,
209
+ });
210
+ tempDir = result.tempDir;
211
+ frameworkPath = result.tempDir;
212
+
213
+ if (result.commitSha) {
214
+ const shaBadge = chalk.hex('#2D3436').bgHex('#00CEC9').bold(` ${result.commitSha.slice(0, 7)} `);
215
+ console.log(` ${shaBadge} ${chalk.hex('#636E72')('latest commit')}`);
218
216
  }
219
- return true;
220
- },
221
- },
222
- ]);
217
+ console.log();
218
+ } catch (err) {
219
+ console.log();
220
+ console.log(chalk.hex('#FDCB6E')(` ⚠ ${err.message}`));
221
+ console.log(chalk.hex('#636E72')(' Falling back to bundled version...\n'));
222
+ frameworkPath = FRAMEWORK_PATH;
223
+ }
224
+ }
225
+
226
+ // ── Step: Scan ────────────────────────────────────────────────
227
+ await stepHeader(1 + stepOffset, totalSteps, 'Scanning framework modules');
228
+ await scanAnimation('Indexing available modules', 1000);
229
+ console.log();
223
230
 
224
- console.log();
231
+ await matrixRain(1800);
225
232
 
226
- // ── Check module conflicts ──────────────────────────────────────
227
- const bundledForCheck = [];
228
- for (const folder of selected) {
229
- if (BUNDLED[folder]) {
230
- bundledForCheck.push(...BUNDLED[folder]);
233
+ let folders;
234
+ try {
235
+ folders = await getSelectableModules(frameworkPath);
236
+ } catch {
237
+ console.log(chalk.hex('#D63031')(' ✗ Error: Could not read framework directory.'));
238
+ console.log(chalk.hex('#636E72')(' Make sure ac-framework is installed correctly.'));
239
+ process.exit(1);
231
240
  }
232
- }
233
- const allForCheck = [...selected, ...bundledForCheck, ...ALWAYS_INSTALL];
234
- const existing = [];
235
- for (const folder of allForCheck) {
236
- if (await existsInTarget(targetDir, folder)) {
237
- existing.push(folder);
241
+
242
+ if (folders.length === 0) {
243
+ console.log(chalk.hex('#FDCB6E')(' No modules found in framework directory.'));
244
+ process.exit(0);
238
245
  }
239
- }
240
246
 
241
- if (existing.length > 0) {
247
+ const countBadge = chalk.hex('#2D3436').bgHex('#00CEC9').bold(` ${folders.length} `);
248
+ const autoBadge = chalk.hex('#2D3436').bgHex('#6C5CE7').bold(' +openspec ');
249
+ console.log(` ${countBadge} ${chalk.hex('#B2BEC3')('assistant modules found')} ${autoBadge} ${chalk.hex('#636E72')('auto-included')}`);
250
+ console.log();
251
+ await animatedSeparator(60);
252
+ console.log();
253
+
254
+ // ── Step: Select ──────────────────────────────────────────────
255
+ await stepHeader(2 + stepOffset, totalSteps, 'Select your assistants');
256
+
257
+ const key = (k) => chalk.hex('#2D3436').bgHex('#636E72')(` ${k} `);
242
258
  console.log(
243
- chalk.hex('#FDCB6E')(' ⚠ These modules already exist in your project:\n')
259
+ ` ${key('↑↓')} ${chalk.hex('#636E72')('navigate')} ` +
260
+ `${key('Space')} ${chalk.hex('#636E72')('toggle')} ` +
261
+ `${key('Enter')} ${chalk.hex('#636E72')('confirm')}`
244
262
  );
245
- for (const folder of existing) {
246
- console.log(
247
- chalk.hex('#FDCB6E')(' ▸ ') +
248
- chalk.hex('#DFE6E9')(formatFolderName(folder)) +
249
- chalk.hex('#636E72')(` (${folder})`)
250
- );
251
- }
252
263
  console.log();
253
264
 
254
- const { overwrite } = await inquirer.prompt([
265
+ const choices = buildChoices(folders);
266
+
267
+ const { selected } = await inquirer.prompt([
255
268
  {
256
- type: 'confirm',
257
- name: 'overwrite',
258
- message: chalk.hex('#FDCB6E')('Overwrite existing modules?'),
259
- default: false,
269
+ type: 'checkbox',
270
+ name: 'selected',
271
+ message: acGradient('Choose modules to install:'),
272
+ choices,
273
+ pageSize: 15,
274
+ loop: false,
275
+ validate(answer) {
276
+ if (answer.length === 0) {
277
+ return chalk.hex('#D63031')('Select at least one module. Use Space to toggle.');
278
+ }
279
+ return true;
280
+ },
260
281
  },
261
282
  ]);
262
283
 
263
- if (!overwrite) {
264
- const filtered = selected.filter((f) => !existing.includes(f));
265
- const autoFiltered = ALWAYS_INSTALL.filter((f) => !existing.includes(f));
266
- if (filtered.length === 0 && autoFiltered.length === 0) {
267
- console.log(chalk.hex('#636E72')('\n Nothing new to install. Exiting.\n'));
268
- process.exit(0);
284
+ console.log();
285
+
286
+ // ── Check module conflicts ────────────────────────────────────
287
+ const bundledForCheck = [];
288
+ for (const folder of selected) {
289
+ if (BUNDLED[folder]) {
290
+ bundledForCheck.push(...BUNDLED[folder]);
291
+ }
292
+ }
293
+ const allForCheck = [...selected, ...bundledForCheck, ...ALWAYS_INSTALL];
294
+ const existing = [];
295
+ for (const folder of allForCheck) {
296
+ if (await existsInTarget(targetDir, folder)) {
297
+ existing.push(folder);
269
298
  }
270
- selected.length = 0;
271
- selected.push(...filtered);
272
- const newCount = chalk.hex('#00CEC9').bold(filtered.length + autoFiltered.length);
299
+ }
300
+
301
+ if (existing.length > 0) {
273
302
  console.log(
274
- '\n ' + chalk.hex('#B2BEC3')('Continuing with ') + newCount + chalk.hex('#B2BEC3')(' new module(s)...') + '\n'
303
+ chalk.hex('#FDCB6E')(' ⚠ These modules already exist in your project:\n')
275
304
  );
305
+ for (const folder of existing) {
306
+ console.log(
307
+ chalk.hex('#FDCB6E')(' ▸ ') +
308
+ chalk.hex('#DFE6E9')(formatFolderName(folder)) +
309
+ chalk.hex('#636E72')(` (${folder})`)
310
+ );
311
+ }
312
+ console.log();
313
+
314
+ const { overwrite } = await inquirer.prompt([
315
+ {
316
+ type: 'confirm',
317
+ name: 'overwrite',
318
+ message: chalk.hex('#FDCB6E')('Overwrite existing modules?'),
319
+ default: false,
320
+ },
321
+ ]);
322
+
323
+ if (!overwrite) {
324
+ const filtered = selected.filter((f) => !existing.includes(f));
325
+ const autoFiltered = ALWAYS_INSTALL.filter((f) => !existing.includes(f));
326
+ if (filtered.length === 0 && autoFiltered.length === 0) {
327
+ console.log(chalk.hex('#636E72')('\n Nothing new to install. Exiting.\n'));
328
+ process.exit(0);
329
+ }
330
+ selected.length = 0;
331
+ selected.push(...filtered);
332
+ const newCount = chalk.hex('#00CEC9').bold(filtered.length + autoFiltered.length);
333
+ console.log(
334
+ '\n ' + chalk.hex('#B2BEC3')('Continuing with ') + newCount + chalk.hex('#B2BEC3')(' new module(s)...') + '\n'
335
+ );
336
+ }
276
337
  }
277
- }
278
-
279
- // ── Reveal selection ────────────────────────────────────────────
280
- console.log(chalk.hex('#B2BEC3')(' Selected modules:\n'));
281
-
282
- const selectedItems = selected.map((folder) => {
283
- const desc = DESCRIPTIONS[folder] || '';
284
- return chalk.hex('#DFE6E9').bold(formatFolderName(folder)) +
285
- (desc ? chalk.hex('#636E72')(` · ${desc}`) : '');
286
- });
287
- selectedItems.push(
288
- chalk.hex('#DFE6E9').bold('Openspec') +
289
- chalk.hex('#636E72')(` · ${DESCRIPTIONS['openspec']}`) +
290
- chalk.hex('#6C5CE7').italic(' (auto)')
291
- );
292
338
 
293
- await revealList(selectedItems, { prefix: '◆', color: '#00CEC9', delay: 40 });
339
+ // ── Reveal selection ──────────────────────────────────────────
340
+ console.log(chalk.hex('#B2BEC3')(' Selected modules:\n'));
294
341
 
295
- console.log();
342
+ const selectedItems = selected.map((folder) => {
343
+ const desc = DESCRIPTIONS[folder] || '';
344
+ return chalk.hex('#DFE6E9').bold(formatFolderName(folder)) +
345
+ (desc ? chalk.hex('#636E72')(` · ${desc}`) : '');
346
+ });
347
+ selectedItems.push(
348
+ chalk.hex('#DFE6E9').bold('Openspec') +
349
+ chalk.hex('#636E72')(` · ${DESCRIPTIONS['openspec']}`) +
350
+ chalk.hex('#6C5CE7').italic(' (auto)')
351
+ );
296
352
 
297
- // ── Step 3/4: Instruction Files ─────────────────────────────────
298
- await animatedSeparator(60);
299
- console.log();
300
- await stepHeader(3, 4, 'Instruction files');
353
+ await revealList(selectedItems, { prefix: '◆', color: '#00CEC9', delay: 40 });
301
354
 
302
- const mdFiles = await selectMdFiles(selected, targetDir);
355
+ console.log();
303
356
 
304
- // Show combined summary if there are .md files
305
- if (mdFiles.length > 0) {
306
- console.log(chalk.hex('#B2BEC3')(' Instruction files to install:\n'));
307
- const mdItems = mdFiles.map((md) => {
308
- const desc = MD_DESCRIPTIONS[md] || '';
309
- return chalk.hex('#DFE6E9').bold(md) +
310
- (desc ? chalk.hex('#636E72')(` · ${desc}`) : '');
311
- });
312
- await revealList(mdItems, { prefix: '◆', color: '#6C5CE7', delay: 40 });
357
+ // ── Step: Instruction Files ───────────────────────────────────
358
+ await animatedSeparator(60);
313
359
  console.log();
314
- }
360
+ await stepHeader(3 + stepOffset, totalSteps, 'Instruction files');
315
361
 
316
- // ── Final confirmation ──────────────────────────────────────────
317
- const { confirm } = await inquirer.prompt([
318
- {
319
- type: 'confirm',
320
- name: 'confirm',
321
- message: chalk.hex('#B2BEC3')('Proceed with installation?'),
322
- default: true,
323
- },
324
- ]);
325
-
326
- if (!confirm) {
327
- console.log(chalk.hex('#636E72')('\n Installation cancelled.\n'));
328
- process.exit(0);
329
- }
362
+ const mdFiles = await selectMdFiles(selected, targetDir);
363
+
364
+ // Show combined summary if there are .md files
365
+ if (mdFiles.length > 0) {
366
+ console.log(chalk.hex('#B2BEC3')(' Instruction files to install:\n'));
367
+ const mdItems = mdFiles.map((md) => {
368
+ const desc = MD_DESCRIPTIONS[md] || '';
369
+ return chalk.hex('#DFE6E9').bold(md) +
370
+ (desc ? chalk.hex('#636E72')(` · ${desc}`) : '');
371
+ });
372
+ await revealList(mdItems, { prefix: '◆', color: '#6C5CE7', delay: 40 });
373
+ console.log();
374
+ }
330
375
 
331
- // ── Step 4/4: Install ───────────────────────────────────────────
332
- console.log();
333
- await animatedSeparator(60);
334
- console.log();
335
- await stepHeader(4, 4, 'Installing modules');
376
+ // ── Final confirmation ────────────────────────────────────────
377
+ const { confirm } = await inquirer.prompt([
378
+ {
379
+ type: 'confirm',
380
+ name: 'confirm',
381
+ message: chalk.hex('#B2BEC3')('Proceed with installation?'),
382
+ default: true,
383
+ },
384
+ ]);
336
385
 
337
- const allToInstall = expandWithBundled(selected);
338
- let installed = 0;
339
- const errors = [];
386
+ if (!confirm) {
387
+ console.log(chalk.hex('#636E72')('\n Installation cancelled.\n'));
388
+ process.exit(0);
389
+ }
340
390
 
341
- // Install module folders
342
- for (const folder of allToInstall) {
343
- const displayName = formatFolderName(folder);
344
- try {
345
- await installWithAnimation(displayName, async () => {
346
- await copyModule(folder, targetDir);
347
- });
348
- installed++;
349
- } catch (err) {
350
- errors.push({ folder, error: err.message });
391
+ // ── Step: Install ─────────────────────────────────────────────
392
+ console.log();
393
+ await animatedSeparator(60);
394
+ console.log();
395
+ await stepHeader(4 + stepOffset, totalSteps, 'Installing modules');
396
+
397
+ const allToInstall = expandWithBundled(selected);
398
+ let installed = 0;
399
+ const errors = [];
400
+
401
+ // Install module folders
402
+ for (const folder of allToInstall) {
403
+ const displayName = formatFolderName(folder);
404
+ try {
405
+ await installWithAnimation(displayName, async () => {
406
+ await copyModule(folder, targetDir, frameworkPath);
407
+ });
408
+ installed++;
409
+ } catch (err) {
410
+ errors.push({ folder, error: err.message });
411
+ }
412
+ await sleep(80);
351
413
  }
352
- await sleep(80);
353
- }
354
414
 
355
- // Install .md instruction files
356
- for (const md of mdFiles) {
357
- try {
358
- await installWithAnimation(md, async () => {
359
- await copyMdFile(md, targetDir);
360
- });
361
- installed++;
362
- } catch (err) {
363
- errors.push({ folder: md, error: err.message });
415
+ // Install .md instruction files
416
+ for (const md of mdFiles) {
417
+ try {
418
+ await installWithAnimation(md, async () => {
419
+ await copyMdFile(md, targetDir, frameworkPath);
420
+ });
421
+ installed++;
422
+ } catch (err) {
423
+ errors.push({ folder: md, error: err.message });
424
+ }
425
+ await sleep(80);
364
426
  }
365
- await sleep(80);
366
- }
367
427
 
368
- // ── Final result ────────────────────────────────────────────────
369
- if (errors.length === 0) {
370
- await celebrateSuccess(installed, targetDir);
371
- } else {
372
- await showFailureSummary(installed, errors);
428
+ // ── Final result ──────────────────────────────────────────────
429
+ if (errors.length === 0) {
430
+ await celebrateSuccess(installed, targetDir);
431
+ } else {
432
+ await showFailureSummary(installed, errors);
433
+ }
434
+ } finally {
435
+ // Always clean up the temp directory if we downloaded from GitHub
436
+ if (tempDir) {
437
+ await cleanupTempDir(tempDir);
438
+ }
373
439
  }
374
440
  }