@factorialco/fcode-cli 3.0.0 → 3.1.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.
- package/README.md +8 -7
- package/dist/commands/team/clone.js +10 -11
- package/dist/commands/team/pull.js +5 -6
- package/dist/commands/team/status.js +11 -7
- package/dist/config/dev-team.d.ts +16 -0
- package/dist/config/dev-team.js +18 -4
- package/dist/services/dashboard-client.d.ts +12 -0
- package/dist/services/dashboard-client.js +9 -0
- package/dist/services/dev-team-service.d.ts +29 -15
- package/dist/services/dev-team-service.js +79 -20
- package/oclif.manifest.json +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ $ npm install -g @factorialco/fcode-cli
|
|
|
20
20
|
$ fcode COMMAND
|
|
21
21
|
running command...
|
|
22
22
|
$ fcode (--version)
|
|
23
|
-
@factorialco/fcode-cli/3.
|
|
23
|
+
@factorialco/fcode-cli/3.1.0 linux-x64 node-v22.22.3
|
|
24
24
|
$ fcode --help [COMMAND]
|
|
25
25
|
USAGE
|
|
26
26
|
$ fcode COMMAND
|
|
@@ -1247,7 +1247,7 @@ EXAMPLES
|
|
|
1247
1247
|
|
|
1248
1248
|
## `fcode team:clone [TEAMID]`
|
|
1249
1249
|
|
|
1250
|
-
Clone
|
|
1250
|
+
Clone a development team into one folder — an App folder each, holding the App's development workspace, plus the team's global workspaces under global-workspaces/
|
|
1251
1251
|
|
|
1252
1252
|
```
|
|
1253
1253
|
USAGE
|
|
@@ -1260,7 +1260,8 @@ FLAGS
|
|
|
1260
1260
|
--skipSkillsSetup Skip installation of the Factorial Code agent skills
|
|
1261
1261
|
|
|
1262
1262
|
DESCRIPTION
|
|
1263
|
-
Clone
|
|
1263
|
+
Clone a development team into one folder — an App folder each, holding the App's development workspace, plus the
|
|
1264
|
+
team's global workspaces under global-workspaces/
|
|
1264
1265
|
|
|
1265
1266
|
EXAMPLES
|
|
1266
1267
|
$ fcode team:clone
|
|
@@ -1270,14 +1271,14 @@ EXAMPLES
|
|
|
1270
1271
|
|
|
1271
1272
|
## `fcode team:pull`
|
|
1272
1273
|
|
|
1273
|
-
Pull
|
|
1274
|
+
Pull the development team folder: clones Apps and global workspaces added since the last run and pulls every workspace
|
|
1274
1275
|
|
|
1275
1276
|
```
|
|
1276
1277
|
USAGE
|
|
1277
1278
|
$ fcode team:pull
|
|
1278
1279
|
|
|
1279
1280
|
DESCRIPTION
|
|
1280
|
-
Pull
|
|
1281
|
+
Pull the development team folder: clones Apps and global workspaces added since the last run and pulls every workspace
|
|
1281
1282
|
|
|
1282
1283
|
EXAMPLES
|
|
1283
1284
|
$ fcode team:pull
|
|
@@ -1285,14 +1286,14 @@ EXAMPLES
|
|
|
1285
1286
|
|
|
1286
1287
|
## `fcode team:status`
|
|
1287
1288
|
|
|
1288
|
-
Show what differs from the cloud in every
|
|
1289
|
+
Show what differs from the cloud in every workspace of the development team folder
|
|
1289
1290
|
|
|
1290
1291
|
```
|
|
1291
1292
|
USAGE
|
|
1292
1293
|
$ fcode team:status
|
|
1293
1294
|
|
|
1294
1295
|
DESCRIPTION
|
|
1295
|
-
Show what differs from the cloud in every
|
|
1296
|
+
Show what differs from the cloud in every workspace of the development team folder
|
|
1296
1297
|
|
|
1297
1298
|
EXAMPLES
|
|
1298
1299
|
$ fcode team:status
|
|
@@ -26,9 +26,9 @@ class TeamCloneCommand extends core_1.Command {
|
|
|
26
26
|
if (!team) {
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
|
-
const
|
|
30
|
-
if (apps.length === 0) {
|
|
31
|
-
cli_1.default.log(`ℹ️ '${team.name}' has no Apps yet.`);
|
|
29
|
+
const remote = await (0, dev_team_service_1.fetchTeamRemote)(client, team.id);
|
|
30
|
+
if (remote.apps.length === 0 && remote.globalWorkspaces.length === 0) {
|
|
31
|
+
cli_1.default.log(`ℹ️ '${team.name}' has no Apps or global workspaces yet.`);
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
const rootDir = path.resolve((0, slugify_1.slugifyOr)(team.name, team.id));
|
|
@@ -39,26 +39,25 @@ class TeamCloneCommand extends core_1.Command {
|
|
|
39
39
|
suggestions: [`cd ${path.basename(rootDir)}`, 'fcode team:pull'],
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
cli_1.default.log(color_1.default.cyan(`\n🏢 cloning ${apps.length} App(s) of '${team.name}'...`));
|
|
42
|
+
cli_1.default.log(color_1.default.cyan(`\n🏢 cloning ${remote.apps.length} App(s) and ${remote.globalWorkspaces.length} global workspace(s) of '${team.name}'...`));
|
|
43
43
|
const metadata = {
|
|
44
44
|
developmentTeam: { id: team.id, name: team.name },
|
|
45
45
|
cloudUrl,
|
|
46
46
|
apps: {},
|
|
47
|
+
globalWorkspaces: {},
|
|
47
48
|
};
|
|
48
49
|
// Written before anything is cloned, so a run interrupted halfway still leaves a folder
|
|
49
50
|
// `fcode team:pull` recognises and can finish.
|
|
50
51
|
const config = new dev_team_1.default(rootDir);
|
|
51
52
|
config.write(metadata);
|
|
52
|
-
// Skills next: each
|
|
53
|
-
//
|
|
53
|
+
// Skills next: each workspace links to them as it lands, so an install that fails leaves
|
|
54
|
+
// everything cloned and only the links missing.
|
|
54
55
|
if (!(await this.skipSkills(flags.skipSkillsSetup))) {
|
|
55
56
|
await (0, skills_installer_1.installSkillsAt)(rootDir);
|
|
56
57
|
}
|
|
57
|
-
const
|
|
58
|
-
const outcomes = await (0, dev_team_service_1.syncApps)(rootDir, metadata, plan);
|
|
58
|
+
const result = await (0, dev_team_service_1.syncTeam)(rootDir, metadata, remote);
|
|
59
59
|
config.write(metadata);
|
|
60
|
-
|
|
61
|
-
if (failed) {
|
|
60
|
+
if ((0, dev_team_service_1.reportOutcomes)(result)) {
|
|
62
61
|
process.exitCode = 1;
|
|
63
62
|
cli_1.default.log(`\n⚠️ '${team.name}' cloned with errors — rerun 'fcode team:pull' inside the folder to retry.\n`);
|
|
64
63
|
}
|
|
@@ -74,7 +73,7 @@ class TeamCloneCommand extends core_1.Command {
|
|
|
74
73
|
return (await new global_1.default().getSetting(settings_1.SettingKeys.SKIP_SKILLS_SETUP)) === 'true';
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
|
-
TeamCloneCommand.description = "Clone
|
|
76
|
+
TeamCloneCommand.description = "Clone a development team into one folder — an App folder each, holding the App's development workspace, plus the team's global workspaces under global-workspaces/";
|
|
78
77
|
TeamCloneCommand.examples = ['$ fcode team:clone', '$ fcode team:clone 0f9c1a2b-3d4e-5f60-8a1b-2c3d4e5f6071'];
|
|
79
78
|
TeamCloneCommand.args = {
|
|
80
79
|
teamId: core_1.Args.string({
|
|
@@ -20,17 +20,16 @@ class TeamPullCommand extends core_1.Command {
|
|
|
20
20
|
const metadata = config.read();
|
|
21
21
|
const { developmentTeam } = metadata;
|
|
22
22
|
cli_1.default.log(color_1.default.cyan(`\n🏢 pulling '${developmentTeam.name}'...`));
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
// Written even when an App failed, so the ones that did land are recorded and a rerun
|
|
23
|
+
const remote = await (0, dev_team_service_1.fetchTeamRemote)(new dashboard_client_1.default(metadata.cloudUrl), developmentTeam.id);
|
|
24
|
+
const result = await (0, dev_team_service_1.syncTeam)(rootDir, metadata, remote);
|
|
25
|
+
// Written even when a workspace failed, so the ones that did land are recorded and a rerun
|
|
27
26
|
// only retries what is still missing.
|
|
28
27
|
config.write(metadata);
|
|
29
|
-
if ((0, dev_team_service_1.reportOutcomes)(
|
|
28
|
+
if ((0, dev_team_service_1.reportOutcomes)(result)) {
|
|
30
29
|
process.exitCode = 1;
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
|
-
TeamPullCommand.description =
|
|
33
|
+
TeamPullCommand.description = 'Pull the development team folder: clones Apps and global workspaces added since the last run and pulls every workspace';
|
|
35
34
|
TeamPullCommand.examples = ['$ fcode team:pull'];
|
|
36
35
|
exports.default = TeamPullCommand;
|
|
@@ -19,13 +19,17 @@ class TeamStatusCommand extends core_1.Command {
|
|
|
19
19
|
await this.parse(TeamStatusCommand);
|
|
20
20
|
const rootDir = process.cwd();
|
|
21
21
|
const metadata = new dev_team_1.default(rootDir).read();
|
|
22
|
+
const checkouts = [
|
|
23
|
+
...Object.values(metadata.globalWorkspaces).map(({ name, slug }) => ({ name, folder: (0, dev_team_1.globalWorkspaceFolder)(slug) })),
|
|
24
|
+
...Object.values(metadata.apps).map(({ name, folder }) => ({ name, folder: path.join(folder, dev_team_1.APP_WORKSPACE_DIR) })),
|
|
25
|
+
];
|
|
22
26
|
const failed = [];
|
|
23
|
-
for (const
|
|
24
|
-
cli_1.default.log(color_1.default.cyan(`\n━━ ${
|
|
25
|
-
const workspaceDir = path.join(rootDir,
|
|
27
|
+
for (const { name, folder } of checkouts) {
|
|
28
|
+
cli_1.default.log(color_1.default.cyan(`\n━━ ${name} (${folder}) ━━`));
|
|
29
|
+
const workspaceDir = path.join(rootDir, folder);
|
|
26
30
|
if (!fs.pathExistsSync(workspaceDir)) {
|
|
27
31
|
cli_1.default.log(color_1.default.yellow('⚠️ not cloned yet — run fcode team:pull'));
|
|
28
|
-
failed.push(
|
|
32
|
+
failed.push(folder);
|
|
29
33
|
continue;
|
|
30
34
|
}
|
|
31
35
|
try {
|
|
@@ -33,15 +37,15 @@ class TeamStatusCommand extends core_1.Command {
|
|
|
33
37
|
}
|
|
34
38
|
catch (error) {
|
|
35
39
|
cli_1.default.log(color_1.default.red(`❌ ${error.message}`));
|
|
36
|
-
failed.push(
|
|
40
|
+
failed.push(folder);
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
if (failed.length > 0) {
|
|
40
44
|
process.exitCode = 1;
|
|
41
|
-
cli_1.default.log(color_1.default.red(`\n${failed.length}
|
|
45
|
+
cli_1.default.log(color_1.default.red(`\n${failed.length} workspace(s) could not be checked: ${failed.join(', ')}`));
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
|
-
TeamStatusCommand.description =
|
|
49
|
+
TeamStatusCommand.description = 'Show what differs from the cloud in every workspace of the development team folder';
|
|
46
50
|
TeamStatusCommand.examples = ['$ fcode team:status'];
|
|
47
51
|
exports.default = TeamStatusCommand;
|
|
@@ -5,6 +5,12 @@ import { CLIError } from '../core/errors';
|
|
|
5
5
|
* it at another workspace with `fcode remote:add` (dev → prod) must not make its name a lie.
|
|
6
6
|
*/
|
|
7
7
|
export declare const APP_WORKSPACE_DIR = "app";
|
|
8
|
+
/**
|
|
9
|
+
* The global workspaces the team owns sit together under this folder, one checkout per workspace
|
|
10
|
+
* named after its slug. A global workspace is one platform team with a fixed slug, so — unlike an
|
|
11
|
+
* App — there is no name to derive a folder from and nothing to re-point the checkout at.
|
|
12
|
+
*/
|
|
13
|
+
export declare const GLOBAL_WORKSPACES_DIR = "global-workspaces";
|
|
8
14
|
export interface DevTeamApp {
|
|
9
15
|
name: string;
|
|
10
16
|
description: string | null;
|
|
@@ -13,6 +19,12 @@ export interface DevTeamApp {
|
|
|
13
19
|
/** Workspace the checkout was cloned from. The directory itself is always `APP_WORKSPACE_DIR`. */
|
|
14
20
|
devWorkspaceSlug: string;
|
|
15
21
|
}
|
|
22
|
+
export interface DevTeamGlobalWorkspace {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
/** Platform team slug; also the checkout's folder name under `GLOBAL_WORKSPACES_DIR`. */
|
|
26
|
+
slug: string;
|
|
27
|
+
}
|
|
16
28
|
export interface DevTeamMetadata {
|
|
17
29
|
developmentTeam: {
|
|
18
30
|
id: string;
|
|
@@ -21,7 +33,11 @@ export interface DevTeamMetadata {
|
|
|
21
33
|
cloudUrl: string;
|
|
22
34
|
/** Keyed by App id, so a renamed App keeps its folder. */
|
|
23
35
|
apps: Record<string, DevTeamApp>;
|
|
36
|
+
/** Keyed by global workspace id. */
|
|
37
|
+
globalWorkspaces: Record<string, DevTeamGlobalWorkspace>;
|
|
24
38
|
}
|
|
39
|
+
/** Where a global workspace is checked out, relative to the team root. */
|
|
40
|
+
export declare function globalWorkspaceFolder(slug: string): string;
|
|
25
41
|
export declare class NotDevTeamFolder extends CLIError {
|
|
26
42
|
constructor();
|
|
27
43
|
}
|
package/dist/config/dev-team.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.APP_SETTINGS_FILENAME = exports.NotDevTeamFolder = exports.APP_WORKSPACE_DIR = void 0;
|
|
3
|
+
exports.APP_SETTINGS_FILENAME = exports.NotDevTeamFolder = exports.GLOBAL_WORKSPACES_DIR = exports.APP_WORKSPACE_DIR = void 0;
|
|
4
|
+
exports.globalWorkspaceFolder = globalWorkspaceFolder;
|
|
4
5
|
exports.writeAppSettings = writeAppSettings;
|
|
5
6
|
const fs = require("fs-extra");
|
|
6
7
|
const path = require("node:path");
|
|
@@ -10,8 +11,8 @@ const errors_1 = require("../core/errors");
|
|
|
10
11
|
* and what `team:pull` / `team:status` recognise the folder by.
|
|
11
12
|
*
|
|
12
13
|
* Not to be confused with a workspace's `settings.json` (the file that used to be called
|
|
13
|
-
* `team.json`): this one describes the *development team* — the humans
|
|
14
|
-
* and lives one level above the workspaces, inside `.fcode/`.
|
|
14
|
+
* `team.json`): this one describes the *development team* — the humans, the Apps they own and the
|
|
15
|
+
* global workspaces they maintain — and lives one level above the workspaces, inside `.fcode/`.
|
|
15
16
|
*/
|
|
16
17
|
const DEV_TEAM_CONFIG_PATH = path.join('.fcode', 'team.json');
|
|
17
18
|
/**
|
|
@@ -20,6 +21,16 @@ const DEV_TEAM_CONFIG_PATH = path.join('.fcode', 'team.json');
|
|
|
20
21
|
* it at another workspace with `fcode remote:add` (dev → prod) must not make its name a lie.
|
|
21
22
|
*/
|
|
22
23
|
exports.APP_WORKSPACE_DIR = 'app';
|
|
24
|
+
/**
|
|
25
|
+
* The global workspaces the team owns sit together under this folder, one checkout per workspace
|
|
26
|
+
* named after its slug. A global workspace is one platform team with a fixed slug, so — unlike an
|
|
27
|
+
* App — there is no name to derive a folder from and nothing to re-point the checkout at.
|
|
28
|
+
*/
|
|
29
|
+
exports.GLOBAL_WORKSPACES_DIR = 'global-workspaces';
|
|
30
|
+
/** Where a global workspace is checked out, relative to the team root. */
|
|
31
|
+
function globalWorkspaceFolder(slug) {
|
|
32
|
+
return path.join(exports.GLOBAL_WORKSPACES_DIR, slug);
|
|
33
|
+
}
|
|
23
34
|
class NotDevTeamFolder extends errors_1.CLIError {
|
|
24
35
|
constructor() {
|
|
25
36
|
super('⚠️ Current directory is not a Factorial Code development team folder.', {
|
|
@@ -44,7 +55,10 @@ class DevTeamConfig {
|
|
|
44
55
|
if (!this.exists()) {
|
|
45
56
|
throw new NotDevTeamFolder();
|
|
46
57
|
}
|
|
47
|
-
|
|
58
|
+
const metadata = fs.readJsonSync(this.configPath);
|
|
59
|
+
// A team folder cloned before global workspaces were part of it has no entry; the next
|
|
60
|
+
// team:pull then clones them the way it clones Apps added since the last run.
|
|
61
|
+
return { ...metadata, globalWorkspaces: metadata.globalWorkspaces ?? {} };
|
|
48
62
|
}
|
|
49
63
|
write(metadata) {
|
|
50
64
|
fs.outputJsonSync(this.configPath, metadata, { spaces: 2 });
|
|
@@ -8,6 +8,16 @@ export interface DevelopmentTeamApp {
|
|
|
8
8
|
name: string;
|
|
9
9
|
description: string | null;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* A base workspace the team owns (`base-app` and the like), as opposed to an App's `dev-` workspace.
|
|
13
|
+
* The slug is the platform team slug itself: fixed, filesystem-safe, and unique.
|
|
14
|
+
*/
|
|
15
|
+
export interface DevelopmentTeamGlobalWorkspace {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
slug: string;
|
|
19
|
+
description: string;
|
|
20
|
+
}
|
|
11
21
|
/**
|
|
12
22
|
* `https://code.factorialhr.com/platform` → `https://code.factorialhr.com/dashboard/api/v1`.
|
|
13
23
|
* The dashboard is served under `/dashboard` on the same origin as the platform; environments
|
|
@@ -24,6 +34,8 @@ export default class DashboardClient {
|
|
|
24
34
|
listDevelopmentTeams(): Promise<DevelopmentTeam[]>;
|
|
25
35
|
/** The Apps owned by a development team, narrowed by the API to the ones the session can see. */
|
|
26
36
|
listTeamApps(teamId: string): Promise<DevelopmentTeamApp[]>;
|
|
37
|
+
/** The global workspaces owned by a development team. */
|
|
38
|
+
listTeamGlobalWorkspaces(teamId: string): Promise<DevelopmentTeamGlobalWorkspace[]>;
|
|
27
39
|
private getAllPages;
|
|
28
40
|
private get;
|
|
29
41
|
private headers;
|
|
@@ -45,6 +45,15 @@ class DashboardClient {
|
|
|
45
45
|
});
|
|
46
46
|
return items.map(({ id, name, description }) => ({ id, name, description }));
|
|
47
47
|
}
|
|
48
|
+
/** The global workspaces owned by a development team. */
|
|
49
|
+
async listTeamGlobalWorkspaces(teamId) {
|
|
50
|
+
const items = await this.getAllPages('/global-workspaces', {
|
|
51
|
+
owner_development_team_id: teamId,
|
|
52
|
+
sort_by: 'slug',
|
|
53
|
+
sort_dir: 'asc',
|
|
54
|
+
});
|
|
55
|
+
return items.map(({ id, name, slug, description }) => ({ id, name, slug, description }));
|
|
56
|
+
}
|
|
48
57
|
async getAllPages(path, query = {}) {
|
|
49
58
|
const items = [];
|
|
50
59
|
let offset = 0;
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
import DashboardClient, { DevelopmentTeam, DevelopmentTeamApp } from './dashboard-client';
|
|
2
|
-
import {
|
|
3
|
-
/** What
|
|
4
|
-
export interface
|
|
1
|
+
import DashboardClient, { DevelopmentTeam, DevelopmentTeamApp, DevelopmentTeamGlobalWorkspace } from './dashboard-client';
|
|
2
|
+
import { DevTeamMetadata } from '../config/dev-team';
|
|
3
|
+
/** What the dashboard currently says the team holds. */
|
|
4
|
+
export interface DevTeamRemote {
|
|
5
|
+
apps: DevelopmentTeamApp[];
|
|
6
|
+
globalWorkspaces: DevelopmentTeamGlobalWorkspace[];
|
|
7
|
+
}
|
|
8
|
+
export declare function fetchTeamRemote(client: DashboardClient, teamId: string): Promise<DevTeamRemote>;
|
|
9
|
+
/** What a team folder has to do to make one kind of entry match the dashboard. */
|
|
10
|
+
export interface SyncPlan<Remote, Recorded> {
|
|
5
11
|
/** In the team, with no folder yet — cloned. */
|
|
6
|
-
toClone:
|
|
12
|
+
toClone: Remote[];
|
|
7
13
|
/** In the team and already cloned — refreshed and pulled. */
|
|
8
|
-
toRefresh:
|
|
14
|
+
toRefresh: Remote[];
|
|
9
15
|
/** Recorded locally but no longer in the team — reported, never deleted. */
|
|
10
16
|
removed: Array<{
|
|
11
17
|
id: string;
|
|
12
|
-
|
|
18
|
+
recorded: Recorded;
|
|
13
19
|
}>;
|
|
14
20
|
}
|
|
15
|
-
export declare function
|
|
21
|
+
export declare function planSync<Remote extends {
|
|
22
|
+
id: string;
|
|
23
|
+
}, Recorded>(recorded: Record<string, Recorded>, remote: Remote[]): SyncPlan<Remote, Recorded>;
|
|
16
24
|
/**
|
|
17
25
|
* The folder an App gets inside the team root. Names are free text and two Apps may well slugify
|
|
18
26
|
* the same, so the first claimant keeps the plain slug and the next ones are suffixed. Only ever
|
|
@@ -20,20 +28,26 @@ export declare function planAppSync(metadata: DevTeamMetadata, apps: Development
|
|
|
20
28
|
* a renamed App keeps the folder it was cloned into.
|
|
21
29
|
*/
|
|
22
30
|
export declare function assignFolder(app: DevelopmentTeamApp, metadata: DevTeamMetadata): string;
|
|
23
|
-
export interface
|
|
31
|
+
export interface TeamSyncOutcome {
|
|
24
32
|
name: string;
|
|
25
33
|
folder: string;
|
|
26
34
|
action: 'cloned' | 'pulled';
|
|
27
35
|
error?: Error;
|
|
28
36
|
}
|
|
37
|
+
export interface TeamSyncResult {
|
|
38
|
+
outcomes: TeamSyncOutcome[];
|
|
39
|
+
/** Folders recorded locally whose App or global workspace has left the team. */
|
|
40
|
+
removedFolders: string[];
|
|
41
|
+
}
|
|
29
42
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
43
|
+
* Brings the team folder in line with the dashboard — global workspaces first, since the Apps
|
|
44
|
+
* inherit from them, then the Apps — one workspace at a time, recording the metadata of each one
|
|
45
|
+
* that lands. A failing workspace is reported and the run carries on: with a dozen Apps, one bad
|
|
46
|
+
* workspace must not cost the other eleven.
|
|
33
47
|
*/
|
|
34
|
-
export declare function
|
|
35
|
-
/** Prints the per-
|
|
36
|
-
export declare function reportOutcomes(outcomes
|
|
48
|
+
export declare function syncTeam(rootDir: string, metadata: DevTeamMetadata, remote: DevTeamRemote): Promise<TeamSyncResult>;
|
|
49
|
+
/** Prints the per-workspace result and reports whether anything failed. */
|
|
50
|
+
export declare function reportOutcomes({ outcomes, removedFolders }: TeamSyncResult): boolean;
|
|
37
51
|
/**
|
|
38
52
|
* The team to operate on: the one named, the only one the session belongs to, or — when there are
|
|
39
53
|
* several — none, after listing them so the user can pass an id.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.fetchTeamRemote = fetchTeamRemote;
|
|
4
|
+
exports.planSync = planSync;
|
|
4
5
|
exports.assignFolder = assignFolder;
|
|
5
|
-
exports.
|
|
6
|
+
exports.syncTeam = syncTeam;
|
|
6
7
|
exports.reportOutcomes = reportOutcomes;
|
|
7
8
|
exports.resolveTeam = resolveTeam;
|
|
8
9
|
const fs = require("fs-extra");
|
|
@@ -15,14 +16,18 @@ const dev_slug_1 = require("../utils/dev-slug");
|
|
|
15
16
|
const workspace_cloner_1 = require("./workspace-cloner");
|
|
16
17
|
const slugify_1 = require("../utils/slugify");
|
|
17
18
|
const cli_1 = require("../utils/cli");
|
|
18
|
-
function
|
|
19
|
-
const
|
|
19
|
+
async function fetchTeamRemote(client, teamId) {
|
|
20
|
+
const [apps, globalWorkspaces] = await Promise.all([client.listTeamApps(teamId), client.listTeamGlobalWorkspaces(teamId)]);
|
|
21
|
+
return { apps, globalWorkspaces };
|
|
22
|
+
}
|
|
23
|
+
function planSync(recorded, remote) {
|
|
24
|
+
const remoteIds = new Set(remote.map(({ id }) => id));
|
|
20
25
|
return {
|
|
21
|
-
toClone:
|
|
22
|
-
toRefresh:
|
|
23
|
-
removed: Object.entries(
|
|
26
|
+
toClone: remote.filter(({ id }) => recorded[id] === undefined),
|
|
27
|
+
toRefresh: remote.filter(({ id }) => recorded[id] !== undefined),
|
|
28
|
+
removed: Object.entries(recorded)
|
|
24
29
|
.filter(([id]) => !remoteIds.has(id))
|
|
25
|
-
.map(([id,
|
|
30
|
+
.map(([id, entry]) => ({ id, recorded: entry })),
|
|
26
31
|
};
|
|
27
32
|
}
|
|
28
33
|
/**
|
|
@@ -44,9 +49,12 @@ function linkSkills(rootDir, appDir, workspaceDir) {
|
|
|
44
49
|
(0, skills_installer_1.linkSkillsInto)(appDir, rootDir);
|
|
45
50
|
(0, skills_installer_1.linkSkillsInto)(workspaceDir, rootDir);
|
|
46
51
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
/**
|
|
53
|
+
* A committed .gitignore keeping the skill links out of the repository: one in each App folder,
|
|
54
|
+
* since each is meant to become its own repository, and one over the global workspaces.
|
|
55
|
+
*/
|
|
56
|
+
function writeSkillsGitIgnore(dir) {
|
|
57
|
+
const gitIgnorePath = path.join(dir, '.gitignore');
|
|
50
58
|
if (fs.pathExistsSync(gitIgnorePath)) {
|
|
51
59
|
return;
|
|
52
60
|
}
|
|
@@ -65,7 +73,7 @@ async function cloneApp(rootDir, app, folder, metadata) {
|
|
|
65
73
|
await (0, workspace_cloner_1.scaffoldWorkspace)(workspaceDir, { url: metadata.cloudUrl, slug });
|
|
66
74
|
metadata.apps[app.id] = { name: app.name, description: app.description, folder, devWorkspaceSlug: slug };
|
|
67
75
|
(0, dev_team_1.writeAppSettings)(appDir, { id: app.id, name: app.name, description: app.description });
|
|
68
|
-
|
|
76
|
+
writeSkillsGitIgnore(appDir);
|
|
69
77
|
linkSkills(rootDir, appDir, workspaceDir);
|
|
70
78
|
await (0, workspace_cloner_1.runInWorkspace)(workspaceDir, () => pull_1.default.run([]));
|
|
71
79
|
}
|
|
@@ -81,17 +89,30 @@ async function refreshApp(rootDir, cloudUrl, app, recorded) {
|
|
|
81
89
|
// Name and description live in the dashboard; the local copy is a mirror refreshed on every
|
|
82
90
|
// pull. There is deliberately no team:push — pushing happens inside a workspace.
|
|
83
91
|
(0, dev_team_1.writeAppSettings)(appDir, { id: app.id, name: app.name, description: app.description });
|
|
84
|
-
|
|
92
|
+
writeSkillsGitIgnore(appDir);
|
|
85
93
|
recorded.name = app.name;
|
|
86
94
|
recorded.description = app.description;
|
|
87
95
|
linkSkills(rootDir, appDir, workspaceDir);
|
|
88
96
|
await (0, workspace_cloner_1.runInWorkspace)(workspaceDir, () => pull_1.default.run([]));
|
|
89
97
|
}
|
|
90
98
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
99
|
+
* Clones or refreshes one global workspace. Its slug is the platform team slug — fixed, unique and
|
|
100
|
+
* filesystem-safe — so the checkout goes straight to `global-workspaces/{slug}` with no folder to
|
|
101
|
+
* assign and no App-level settings to mirror. Recorded before the pull for the same reason as an
|
|
102
|
+
* App: a half-cloned checkout must read as cloned on the retry.
|
|
94
103
|
*/
|
|
104
|
+
async function syncGlobalWorkspace(rootDir, workspace, metadata) {
|
|
105
|
+
const folder = (0, dev_team_1.globalWorkspaceFolder)(workspace.slug);
|
|
106
|
+
const workspaceDir = path.join(rootDir, folder);
|
|
107
|
+
cli_1.default.log(color_1.default.cyan(`\n🌐 ${workspace.name} → ${folder}`));
|
|
108
|
+
if (!fs.pathExistsSync(path.join(workspaceDir, '.fcode'))) {
|
|
109
|
+
await (0, workspace_cloner_1.scaffoldWorkspace)(workspaceDir, { url: metadata.cloudUrl, slug: workspace.slug });
|
|
110
|
+
}
|
|
111
|
+
metadata.globalWorkspaces[workspace.id] = { name: workspace.name, description: workspace.description, slug: workspace.slug };
|
|
112
|
+
writeSkillsGitIgnore(path.join(rootDir, dev_team_1.GLOBAL_WORKSPACES_DIR));
|
|
113
|
+
(0, skills_installer_1.linkSkillsInto)(workspaceDir, rootDir);
|
|
114
|
+
await (0, workspace_cloner_1.runInWorkspace)(workspaceDir, () => pull_1.default.run([]));
|
|
115
|
+
}
|
|
95
116
|
async function syncApps(rootDir, metadata, plan) {
|
|
96
117
|
const outcomes = [];
|
|
97
118
|
for (const app of plan.toClone) {
|
|
@@ -116,8 +137,46 @@ async function syncApps(rootDir, metadata, plan) {
|
|
|
116
137
|
}
|
|
117
138
|
return outcomes;
|
|
118
139
|
}
|
|
119
|
-
|
|
120
|
-
|
|
140
|
+
async function syncGlobalWorkspaces(rootDir, metadata, plan) {
|
|
141
|
+
const outcomes = [];
|
|
142
|
+
for (const [action, workspaces] of [
|
|
143
|
+
['cloned', plan.toClone],
|
|
144
|
+
['pulled', plan.toRefresh],
|
|
145
|
+
]) {
|
|
146
|
+
for (const workspace of workspaces) {
|
|
147
|
+
const folder = (0, dev_team_1.globalWorkspaceFolder)(workspace.slug);
|
|
148
|
+
try {
|
|
149
|
+
await syncGlobalWorkspace(rootDir, workspace, metadata);
|
|
150
|
+
outcomes.push({ name: workspace.name, folder, action });
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
outcomes.push({ name: workspace.name, folder, action, error: error });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return outcomes;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Brings the team folder in line with the dashboard — global workspaces first, since the Apps
|
|
161
|
+
* inherit from them, then the Apps — one workspace at a time, recording the metadata of each one
|
|
162
|
+
* that lands. A failing workspace is reported and the run carries on: with a dozen Apps, one bad
|
|
163
|
+
* workspace must not cost the other eleven.
|
|
164
|
+
*/
|
|
165
|
+
async function syncTeam(rootDir, metadata, remote) {
|
|
166
|
+
const globalWorkspacePlan = planSync(metadata.globalWorkspaces, remote.globalWorkspaces);
|
|
167
|
+
const appPlan = planSync(metadata.apps, remote.apps);
|
|
168
|
+
const outcomes = [
|
|
169
|
+
...(await syncGlobalWorkspaces(rootDir, metadata, globalWorkspacePlan)),
|
|
170
|
+
...(await syncApps(rootDir, metadata, appPlan)),
|
|
171
|
+
];
|
|
172
|
+
const removedFolders = [
|
|
173
|
+
...globalWorkspacePlan.removed.map(({ recorded }) => (0, dev_team_1.globalWorkspaceFolder)(recorded.slug)),
|
|
174
|
+
...appPlan.removed.map(({ recorded }) => recorded.folder),
|
|
175
|
+
];
|
|
176
|
+
return { outcomes, removedFolders };
|
|
177
|
+
}
|
|
178
|
+
/** Prints the per-workspace result and reports whether anything failed. */
|
|
179
|
+
function reportOutcomes({ outcomes, removedFolders }) {
|
|
121
180
|
cli_1.default.log('');
|
|
122
181
|
for (const outcome of outcomes) {
|
|
123
182
|
if (outcome.error) {
|
|
@@ -127,8 +186,8 @@ function reportOutcomes(outcomes, removed) {
|
|
|
127
186
|
cli_1.default.log(color_1.default.green(`✅ ${outcome.folder} (${outcome.action})`));
|
|
128
187
|
}
|
|
129
188
|
}
|
|
130
|
-
for (const
|
|
131
|
-
cli_1.default.log(color_1.default.yellow(`⚠️ ${
|
|
189
|
+
for (const folder of removedFolders) {
|
|
190
|
+
cli_1.default.log(color_1.default.yellow(`⚠️ ${folder} is no longer in the team — the folder was kept, remove it yourself`));
|
|
132
191
|
}
|
|
133
192
|
return outcomes.some(({ error }) => error !== undefined);
|
|
134
193
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -1956,7 +1956,7 @@
|
|
|
1956
1956
|
"required": false
|
|
1957
1957
|
}
|
|
1958
1958
|
},
|
|
1959
|
-
"description": "Clone
|
|
1959
|
+
"description": "Clone a development team into one folder — an App folder each, holding the App's development workspace, plus the team's global workspaces under global-workspaces/",
|
|
1960
1960
|
"examples": [
|
|
1961
1961
|
"$ fcode team:clone",
|
|
1962
1962
|
"$ fcode team:clone 0f9c1a2b-3d4e-5f60-8a1b-2c3d4e5f6071"
|
|
@@ -1988,7 +1988,7 @@
|
|
|
1988
1988
|
"team:pull": {
|
|
1989
1989
|
"aliases": [],
|
|
1990
1990
|
"args": {},
|
|
1991
|
-
"description": "Pull
|
|
1991
|
+
"description": "Pull the development team folder: clones Apps and global workspaces added since the last run and pulls every workspace",
|
|
1992
1992
|
"examples": [
|
|
1993
1993
|
"$ fcode team:pull"
|
|
1994
1994
|
],
|
|
@@ -2012,7 +2012,7 @@
|
|
|
2012
2012
|
"team:status": {
|
|
2013
2013
|
"aliases": [],
|
|
2014
2014
|
"args": {},
|
|
2015
|
-
"description": "Show what differs from the cloud in every
|
|
2015
|
+
"description": "Show what differs from the cloud in every workspace of the development team folder",
|
|
2016
2016
|
"examples": [
|
|
2017
2017
|
"$ fcode team:status"
|
|
2018
2018
|
],
|
|
@@ -2483,5 +2483,5 @@
|
|
|
2483
2483
|
]
|
|
2484
2484
|
}
|
|
2485
2485
|
},
|
|
2486
|
-
"version": "3.
|
|
2486
|
+
"version": "3.1.0"
|
|
2487
2487
|
}
|